Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 862f47c46e | |||
| 5572cd3a0d | |||
| 6426850714 |
27
.github/workflows/release.yml
vendored
Normal file
27
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Create Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
generate_release_notes: true
|
||||
body: |
|
||||
See the [full release notes on Gitea](https://somegit.dev/vikingowl/vessel/releases) for detailed information.
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -33,3 +33,6 @@ backend/server
|
||||
# Docker
|
||||
*.pid
|
||||
docker-compose.override.yml
|
||||
|
||||
# Claude Code project instructions (local only)
|
||||
CLAUDE.md
|
||||
|
||||
@@ -18,8 +18,8 @@ RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server ./cmd/serv
|
||||
# Final stage
|
||||
FROM alpine:latest
|
||||
|
||||
# curl for web fetching, ca-certificates for HTTPS
|
||||
RUN apk --no-cache add ca-certificates curl
|
||||
# curl for web fetching, ca-certificates for HTTPS, python3 for tool execution
|
||||
RUN apk --no-cache add ca-certificates curl python3
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
)
|
||||
|
||||
// Version is set at build time via -ldflags, or defaults to dev
|
||||
var Version = "0.3.0"
|
||||
var Version = "0.4.0"
|
||||
|
||||
func getEnvOrDefault(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
|
||||
@@ -66,6 +66,9 @@ func SetupRoutes(r *gin.Engine, db *sql.DB, ollamaURL string, appVersion string)
|
||||
// IP-based geolocation (fallback when browser geolocation fails)
|
||||
v1.GET("/location", IPGeolocationHandler())
|
||||
|
||||
// Tool execution (for Python tools)
|
||||
v1.POST("/tools/execute", ExecuteToolHandler())
|
||||
|
||||
// Model registry routes (cached models from ollama.com)
|
||||
models := v1.Group("/models")
|
||||
{
|
||||
|
||||
174
backend/internal/api/tools.go
Normal file
174
backend/internal/api/tools.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ExecuteToolRequest represents a tool execution request
|
||||
type ExecuteToolRequest struct {
|
||||
Language string `json:"language" binding:"required,oneof=python javascript"`
|
||||
Code string `json:"code" binding:"required"`
|
||||
Args map[string]interface{} `json:"args"`
|
||||
Timeout int `json:"timeout"` // seconds, default 30
|
||||
}
|
||||
|
||||
// ExecuteToolResponse represents the tool execution response
|
||||
type ExecuteToolResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Result interface{} `json:"result,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Stdout string `json:"stdout,omitempty"`
|
||||
Stderr string `json:"stderr,omitempty"`
|
||||
}
|
||||
|
||||
// MaxOutputSize is the maximum size of tool output (100KB)
|
||||
const MaxOutputSize = 100 * 1024
|
||||
|
||||
// ExecuteToolHandler handles tool execution requests
|
||||
func ExecuteToolHandler() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req ExecuteToolRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, ExecuteToolResponse{
|
||||
Success: false,
|
||||
Error: "Invalid request: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Default timeout
|
||||
timeout := req.Timeout
|
||||
if timeout <= 0 || timeout > 60 {
|
||||
timeout = 30
|
||||
}
|
||||
|
||||
var resp ExecuteToolResponse
|
||||
|
||||
switch req.Language {
|
||||
case "python":
|
||||
resp = executePython(req.Code, req.Args, timeout)
|
||||
case "javascript":
|
||||
// JavaScript execution not supported on backend (runs in browser)
|
||||
resp = ExecuteToolResponse{
|
||||
Success: false,
|
||||
Error: "JavaScript tools should be executed in the browser",
|
||||
}
|
||||
default:
|
||||
resp = ExecuteToolResponse{
|
||||
Success: false,
|
||||
Error: "Unsupported language: " + req.Language,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
}
|
||||
|
||||
// executePython executes Python code with the given arguments
|
||||
func executePython(code string, args map[string]interface{}, timeout int) ExecuteToolResponse {
|
||||
// Create a wrapper script that reads args from stdin
|
||||
wrapperScript := `
|
||||
import json
|
||||
import sys
|
||||
|
||||
# Read args from stdin
|
||||
args = json.loads(sys.stdin.read())
|
||||
|
||||
# Execute user code
|
||||
` + code
|
||||
|
||||
// Create temp file for the script
|
||||
tmpFile, err := os.CreateTemp("", "tool-*.py")
|
||||
if err != nil {
|
||||
return ExecuteToolResponse{
|
||||
Success: false,
|
||||
Error: "Failed to create temp file: " + err.Error(),
|
||||
}
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
|
||||
if _, err := tmpFile.WriteString(wrapperScript); err != nil {
|
||||
return ExecuteToolResponse{
|
||||
Success: false,
|
||||
Error: "Failed to write script: " + err.Error(),
|
||||
}
|
||||
}
|
||||
tmpFile.Close()
|
||||
|
||||
// Marshal args to JSON for stdin
|
||||
argsJSON, err := json.Marshal(args)
|
||||
if err != nil {
|
||||
return ExecuteToolResponse{
|
||||
Success: false,
|
||||
Error: "Failed to serialize args: " + err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
// Create context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Execute Python (using exec.Command, not shell)
|
||||
cmd := exec.CommandContext(ctx, "python3", tmpFile.Name())
|
||||
cmd.Stdin = bytes.NewReader(argsJSON)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
err = cmd.Run()
|
||||
|
||||
// Check for timeout
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return ExecuteToolResponse{
|
||||
Success: false,
|
||||
Error: "Execution timed out after " + string(rune(timeout)) + " seconds",
|
||||
Stderr: truncateOutput(stderr.String()),
|
||||
}
|
||||
}
|
||||
|
||||
// Truncate output if needed
|
||||
stdoutStr := truncateOutput(stdout.String())
|
||||
stderrStr := truncateOutput(stderr.String())
|
||||
|
||||
if err != nil {
|
||||
return ExecuteToolResponse{
|
||||
Success: false,
|
||||
Error: "Execution failed: " + err.Error(),
|
||||
Stdout: stdoutStr,
|
||||
Stderr: stderrStr,
|
||||
}
|
||||
}
|
||||
|
||||
// Try to parse stdout as JSON
|
||||
var result interface{}
|
||||
if stdoutStr != "" {
|
||||
if err := json.Unmarshal([]byte(stdoutStr), &result); err != nil {
|
||||
// If not valid JSON, return as string
|
||||
result = stdoutStr
|
||||
}
|
||||
}
|
||||
|
||||
return ExecuteToolResponse{
|
||||
Success: true,
|
||||
Result: result,
|
||||
Stdout: stdoutStr,
|
||||
Stderr: stderrStr,
|
||||
}
|
||||
}
|
||||
|
||||
// truncateOutput truncates output to MaxOutputSize
|
||||
func truncateOutput(s string) string {
|
||||
if len(s) > MaxOutputSize {
|
||||
return s[:MaxOutputSize] + "\n... (output truncated)"
|
||||
}
|
||||
return s
|
||||
}
|
||||
236
frontend/package-lock.json
generated
236
frontend/package-lock.json
generated
@@ -1,17 +1,22 @@
|
||||
{
|
||||
"name": "vessel",
|
||||
"version": "0.1.0",
|
||||
"version": "0.3.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "vessel",
|
||||
"version": "0.1.0",
|
||||
"version": "0.3.0",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-javascript": "^6.2.3",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-python": "^6.1.7",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@skeletonlabs/skeleton": "^2.10.0",
|
||||
"@skeletonlabs/tw-plugin": "^0.4.0",
|
||||
"@sveltejs/adapter-node": "^5.4.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"codemirror": "^6.0.1",
|
||||
"dexie": "^4.0.10",
|
||||
"dompurify": "^3.2.0",
|
||||
"marked": "^15.0.0",
|
||||
@@ -110,6 +115,137 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/autocomplete": {
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz",
|
||||
"integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/commands": {
|
||||
"version": "6.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.1.tgz",
|
||||
"integrity": "sha512-uWDWFypNdQmz2y1LaNJzK7fL7TYKLeUAU0npEC685OKTF3KcQ2Vu3klIM78D7I6wGhktme0lh3CuQLv0ZCrD9Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.4.0",
|
||||
"@codemirror/view": "^6.27.0",
|
||||
"@lezer/common": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-javascript": {
|
||||
"version": "6.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz",
|
||||
"integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.6.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/javascript": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-json": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz",
|
||||
"integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@lezer/json": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-python": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.2.1.tgz",
|
||||
"integrity": "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.3.2",
|
||||
"@codemirror/language": "^6.8.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@lezer/common": "^1.2.1",
|
||||
"@lezer/python": "^1.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/language": {
|
||||
"version": "6.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz",
|
||||
"integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.23.0",
|
||||
"@lezer/common": "^1.5.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0",
|
||||
"style-mod": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lint": {
|
||||
"version": "6.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.2.tgz",
|
||||
"integrity": "sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.35.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/search": {
|
||||
"version": "6.5.11",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz",
|
||||
"integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.3.tgz",
|
||||
"integrity": "sha512-MerMzJzlXogk2fxWFU1nKp36bY5orBG59HnPiz0G9nLRebWa0zXuv2siH6PLIHBvv5TH8CkQRqjBs0MlxCZu+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/theme-one-dark": {
|
||||
"version": "6.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
|
||||
"integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/highlight": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.39.8",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.8.tgz",
|
||||
"integrity": "sha512-1rASYd9Z/mE3tkbC9wInRlCNyCkSn+nLsiQKZhEDUUJiUfs/5FHDpCUDaQpoTIaNGeDc6/bhaEAyLmeEucEFPw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.5.0",
|
||||
"crelt": "^1.0.6",
|
||||
"style-mod": "^4.1.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/color-helpers": {
|
||||
"version": "5.1.0",
|
||||
"dev": true,
|
||||
@@ -698,6 +834,69 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/common": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.0.tgz",
|
||||
"integrity": "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@lezer/highlight": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
|
||||
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/javascript": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz",
|
||||
"integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.1.3",
|
||||
"@lezer/lr": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/json": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz",
|
||||
"integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/lr": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.5.tgz",
|
||||
"integrity": "sha512-/YTRKP5yPPSo1xImYQk7AZZMAgap0kegzqCSYHjAL9x1AZ0ZQW+IpcEzMKagCsbTsLnVeWkxYrCNeXG8xEPrjg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/python": {
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.18.tgz",
|
||||
"integrity": "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@marijn/find-cluster-break": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
|
||||
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@napi-rs/canvas": {
|
||||
"version": "0.1.88",
|
||||
"license": "MIT",
|
||||
@@ -2049,6 +2248,21 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/codemirror": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
|
||||
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/commands": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/search": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/comma-separated-tokens": {
|
||||
"version": "2.0.3",
|
||||
"license": "MIT",
|
||||
@@ -2075,6 +2289,12 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/css-tree": {
|
||||
"version": "3.1.0",
|
||||
"dev": true,
|
||||
@@ -3362,6 +3582,12 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/style-mod": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sucrase": {
|
||||
"version": "3.35.1",
|
||||
"license": "MIT",
|
||||
@@ -3961,6 +4187,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/w3c-xmlserializer": {
|
||||
"version": "5.0.0",
|
||||
"dev": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vessel",
|
||||
"version": "0.3.0",
|
||||
"version": "0.4.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -32,7 +32,12 @@
|
||||
"vitest": "^4.0.16"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-javascript": "^6.2.3",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-python": "^6.1.7",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@skeletonlabs/skeleton": "^2.10.0",
|
||||
"codemirror": "^6.0.1",
|
||||
"@skeletonlabs/tw-plugin": "^0.4.0",
|
||||
"@sveltejs/adapter-node": "^5.4.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
|
||||
127
frontend/src/lib/components/tools/CodeEditor.svelte
Normal file
127
frontend/src/lib/components/tools/CodeEditor.svelte
Normal file
@@ -0,0 +1,127 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { EditorView, basicSetup } from 'codemirror';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { python } from '@codemirror/lang-python';
|
||||
import { json } from '@codemirror/lang-json';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { EditorState, Compartment } from '@codemirror/state';
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
language?: 'javascript' | 'python' | 'json';
|
||||
readonly?: boolean;
|
||||
placeholder?: string;
|
||||
minHeight?: string;
|
||||
onchange?: (value: string) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
value = $bindable(''),
|
||||
language = 'javascript',
|
||||
readonly = false,
|
||||
placeholder = '',
|
||||
minHeight = '200px',
|
||||
onchange
|
||||
}: Props = $props();
|
||||
|
||||
let editorContainer: HTMLDivElement;
|
||||
let editorView: EditorView | null = null;
|
||||
const languageCompartment = new Compartment();
|
||||
const readonlyCompartment = new Compartment();
|
||||
|
||||
function getLanguageExtension(lang: string) {
|
||||
switch (lang) {
|
||||
case 'python':
|
||||
return python();
|
||||
case 'json':
|
||||
return json();
|
||||
case 'javascript':
|
||||
default:
|
||||
return javascript();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const updateListener = EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged) {
|
||||
const newValue = update.state.doc.toString();
|
||||
if (newValue !== value) {
|
||||
value = newValue;
|
||||
onchange?.(newValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: value,
|
||||
extensions: [
|
||||
basicSetup,
|
||||
languageCompartment.of(getLanguageExtension(language)),
|
||||
readonlyCompartment.of(EditorState.readOnly.of(readonly)),
|
||||
oneDark,
|
||||
updateListener,
|
||||
EditorView.theme({
|
||||
'&': { minHeight },
|
||||
'.cm-scroller': { overflow: 'auto' },
|
||||
'.cm-content': { minHeight },
|
||||
'&.cm-focused': { outline: 'none' }
|
||||
}),
|
||||
placeholder ? EditorView.contentAttributes.of({ 'aria-placeholder': placeholder }) : []
|
||||
]
|
||||
});
|
||||
|
||||
editorView = new EditorView({
|
||||
state,
|
||||
parent: editorContainer
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
editorView?.destroy();
|
||||
});
|
||||
|
||||
// Update editor when value changes externally
|
||||
$effect(() => {
|
||||
if (editorView && editorView.state.doc.toString() !== value) {
|
||||
editorView.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: editorView.state.doc.length,
|
||||
insert: value
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Update language when it changes
|
||||
$effect(() => {
|
||||
if (editorView) {
|
||||
editorView.dispatch({
|
||||
effects: languageCompartment.reconfigure(getLanguageExtension(language))
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Update readonly when it changes
|
||||
$effect(() => {
|
||||
if (editorView) {
|
||||
editorView.dispatch({
|
||||
effects: readonlyCompartment.reconfigure(EditorState.readOnly.of(readonly))
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="code-editor rounded-md overflow-hidden border border-surface-500/30" bind:this={editorContainer}></div>
|
||||
|
||||
<style>
|
||||
.code-editor :global(.cm-editor) {
|
||||
font-size: 14px;
|
||||
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
|
||||
}
|
||||
|
||||
.code-editor :global(.cm-gutters) {
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
</style>
|
||||
117
frontend/src/lib/components/tools/ToolDocs.svelte
Normal file
117
frontend/src/lib/components/tools/ToolDocs.svelte
Normal file
@@ -0,0 +1,117 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* ToolDocs - Inline documentation panel for tool creation
|
||||
*/
|
||||
|
||||
interface Props {
|
||||
language: 'javascript' | 'python';
|
||||
isOpen?: boolean;
|
||||
onclose?: () => void;
|
||||
}
|
||||
|
||||
const { language, isOpen = false, onclose }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<div class="rounded-lg border border-theme-subtle bg-theme-tertiary/50 p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h4 class="text-sm font-medium text-theme-primary">
|
||||
{language === 'javascript' ? 'JavaScript' : 'Python'} Tool Guide
|
||||
</h4>
|
||||
{#if onclose}
|
||||
<button
|
||||
type="button"
|
||||
onclick={onclose}
|
||||
class="text-theme-muted hover:text-theme-primary"
|
||||
aria-label="Close documentation"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 text-sm text-theme-secondary">
|
||||
{#if language === 'javascript'}
|
||||
<!-- JavaScript Documentation -->
|
||||
<div>
|
||||
<h5 class="font-medium text-theme-primary mb-1">Arguments</h5>
|
||||
<p>Access parameters via the <code class="bg-theme-primary/30 px-1 rounded text-xs">args</code> object:</p>
|
||||
<pre class="mt-1 p-2 rounded bg-theme-primary/20 text-xs overflow-x-auto"><code>const name = args.name;
|
||||
const count = args.count || 10;</code></pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h5 class="font-medium text-theme-primary mb-1">Return Value</h5>
|
||||
<p>Return any JSON-serializable value:</p>
|
||||
<pre class="mt-1 p-2 rounded bg-theme-primary/20 text-xs overflow-x-auto"><code>return {'{'}
|
||||
success: true,
|
||||
data: result
|
||||
{'}'};</code></pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h5 class="font-medium text-theme-primary mb-1">Async/Await</h5>
|
||||
<p>Full async support - use await for API calls:</p>
|
||||
<pre class="mt-1 p-2 rounded bg-theme-primary/20 text-xs overflow-x-auto"><code>const res = await fetch(args.url);
|
||||
const data = await res.json();
|
||||
return data;</code></pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h5 class="font-medium text-theme-primary mb-1">Error Handling</h5>
|
||||
<p>Throw errors to signal failures:</p>
|
||||
<pre class="mt-1 p-2 rounded bg-theme-primary/20 text-xs overflow-x-auto"><code>if (!args.required_param) {'{'}
|
||||
throw new Error('Missing required param');
|
||||
{'}'}</code></pre>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Python Documentation -->
|
||||
<div>
|
||||
<h5 class="font-medium text-theme-primary mb-1">Arguments</h5>
|
||||
<p>Access parameters via the <code class="bg-theme-primary/30 px-1 rounded text-xs">args</code> dict:</p>
|
||||
<pre class="mt-1 p-2 rounded bg-theme-primary/20 text-xs overflow-x-auto"><code>name = args.get('name')
|
||||
count = args.get('count', 10)</code></pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h5 class="font-medium text-theme-primary mb-1">Return Value</h5>
|
||||
<p>Print JSON to stdout (import json first):</p>
|
||||
<pre class="mt-1 p-2 rounded bg-theme-primary/20 text-xs overflow-x-auto"><code>import json
|
||||
|
||||
result = {'{'}'success': True, 'data': data{'}'}
|
||||
print(json.dumps(result))</code></pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h5 class="font-medium text-theme-primary mb-1">Available Modules</h5>
|
||||
<p>Python standard library is available:</p>
|
||||
<pre class="mt-1 p-2 rounded bg-theme-primary/20 text-xs overflow-x-auto"><code>import json, math, re
|
||||
import hashlib, base64
|
||||
import urllib.request
|
||||
from collections import Counter</code></pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h5 class="font-medium text-theme-primary mb-1">Error Handling</h5>
|
||||
<p>Print error JSON or raise exceptions:</p>
|
||||
<pre class="mt-1 p-2 rounded bg-theme-primary/20 text-xs overflow-x-auto"><code>try:
|
||||
# risky operation
|
||||
except Exception as e:
|
||||
print(json.dumps({'{'}'error': str(e){'}'})</code></pre>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="pt-2 border-t border-theme-subtle">
|
||||
<h5 class="font-medium text-theme-primary mb-1">Tips</h5>
|
||||
<ul class="list-disc list-inside space-y-1 text-xs text-theme-muted">
|
||||
<li>Tools run with a 30-second timeout</li>
|
||||
<li>Large outputs are truncated at 100KB</li>
|
||||
<li>Network requests are allowed</li>
|
||||
<li>Use descriptive error messages for debugging</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -5,6 +5,10 @@
|
||||
|
||||
import { toolsState } from '$lib/stores';
|
||||
import type { CustomTool, JSONSchema, JSONSchemaProperty, ToolImplementation } from '$lib/tools';
|
||||
import { getTemplatesByLanguage, type ToolTemplate } from '$lib/tools';
|
||||
import CodeEditor from './CodeEditor.svelte';
|
||||
import ToolDocs from './ToolDocs.svelte';
|
||||
import ToolTester from './ToolTester.svelte';
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
@@ -15,21 +19,62 @@
|
||||
|
||||
const { isOpen, editingTool = null, onClose, onSave }: Props = $props();
|
||||
|
||||
// Default code templates
|
||||
const defaultJsCode = `// Arguments are available as \`args\` object
|
||||
// Return the result
|
||||
return { message: "Hello from custom tool!" };`;
|
||||
|
||||
const defaultPythonCode = `# Arguments available as \`args\` dict
|
||||
# Print JSON result to stdout
|
||||
import json
|
||||
|
||||
result = {"message": f"Hello, {args.get('name', 'World')}!"}
|
||||
print(json.dumps(result))`;
|
||||
|
||||
// Form state
|
||||
let name = $state('');
|
||||
let description = $state('');
|
||||
let implementation = $state<ToolImplementation>('javascript');
|
||||
let code = $state('// Arguments are available as `args` object\n// Return the result\nreturn { message: "Hello from custom tool!" };');
|
||||
let code = $state(defaultJsCode);
|
||||
let endpoint = $state('');
|
||||
let httpMethod = $state<'GET' | 'POST'>('POST');
|
||||
let enabled = $state(true);
|
||||
|
||||
// Track previous implementation for code switching
|
||||
let prevImplementation = $state<ToolImplementation>('javascript');
|
||||
|
||||
// Parameters state (simplified - array of parameter definitions)
|
||||
let parameters = $state<Array<{ name: string; type: string; description: string; required: boolean }>>([]);
|
||||
|
||||
// Validation
|
||||
let errors = $state<Record<string, string>>({});
|
||||
|
||||
// UI state
|
||||
let showDocs = $state(false);
|
||||
let showTemplates = $state(false);
|
||||
let showTest = $state(false);
|
||||
|
||||
// Get templates for current language
|
||||
const currentTemplates = $derived(
|
||||
implementation === 'javascript' || implementation === 'python'
|
||||
? getTemplatesByLanguage(implementation)
|
||||
: []
|
||||
);
|
||||
|
||||
function applyTemplate(template: ToolTemplate): void {
|
||||
name = template.name.toLowerCase().replace(/[^a-z0-9]+/g, '_');
|
||||
description = template.description;
|
||||
code = template.code;
|
||||
// Convert parameters from template
|
||||
parameters = Object.entries(template.parameters.properties ?? {}).map(([paramName, prop]) => ({
|
||||
name: paramName,
|
||||
type: prop.type,
|
||||
description: prop.description ?? '',
|
||||
required: template.parameters.required?.includes(paramName) ?? false
|
||||
}));
|
||||
showTemplates = false;
|
||||
}
|
||||
|
||||
// Reset form when modal opens or editing tool changes
|
||||
$effect(() => {
|
||||
if (isOpen) {
|
||||
@@ -59,13 +104,32 @@
|
||||
name = '';
|
||||
description = '';
|
||||
implementation = 'javascript';
|
||||
code = '// Arguments are available as `args` object\n// Return the result\nreturn { message: "Hello from custom tool!" };';
|
||||
prevImplementation = 'javascript';
|
||||
code = defaultJsCode;
|
||||
endpoint = '';
|
||||
httpMethod = 'POST';
|
||||
enabled = true;
|
||||
parameters = [];
|
||||
}
|
||||
|
||||
// Switch to default code when implementation changes (unless editing)
|
||||
$effect(() => {
|
||||
if (!editingTool && implementation !== prevImplementation) {
|
||||
// Only switch code if it's still the default for the previous type
|
||||
const isDefaultJs = code === defaultJsCode || code.trim() === '';
|
||||
const isDefaultPy = code === defaultPythonCode;
|
||||
|
||||
if (isDefaultJs || isDefaultPy || code.trim() === '') {
|
||||
if (implementation === 'python') {
|
||||
code = defaultPythonCode;
|
||||
} else if (implementation === 'javascript') {
|
||||
code = defaultJsCode;
|
||||
}
|
||||
}
|
||||
prevImplementation = implementation;
|
||||
}
|
||||
});
|
||||
|
||||
function addParameter(): void {
|
||||
parameters = [...parameters, { name: '', type: 'string', description: '', required: false }];
|
||||
}
|
||||
@@ -93,8 +157,8 @@
|
||||
newErrors.description = 'Description is required';
|
||||
}
|
||||
|
||||
if (implementation === 'javascript' && !code.trim()) {
|
||||
newErrors.code = 'JavaScript code is required';
|
||||
if ((implementation === 'javascript' || implementation === 'python') && !code.trim()) {
|
||||
newErrors.code = `${implementation === 'javascript' ? 'JavaScript' : 'Python'} code is required`;
|
||||
}
|
||||
|
||||
if (implementation === 'http' && !endpoint.trim()) {
|
||||
@@ -144,7 +208,7 @@
|
||||
description: description.trim(),
|
||||
parameters: buildParameterSchema(),
|
||||
implementation,
|
||||
code: implementation === 'javascript' ? code : undefined,
|
||||
code: (implementation === 'javascript' || implementation === 'python') ? code : undefined,
|
||||
endpoint: implementation === 'http' ? endpoint : undefined,
|
||||
httpMethod: implementation === 'http' ? httpMethod : undefined,
|
||||
enabled,
|
||||
@@ -290,7 +354,7 @@
|
||||
<!-- Implementation Type -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-theme-secondary">Implementation</label>
|
||||
<div class="mt-2 flex gap-4">
|
||||
<div class="mt-2 flex flex-wrap gap-4">
|
||||
<label class="flex items-center gap-2 text-theme-secondary">
|
||||
<input
|
||||
type="radio"
|
||||
@@ -300,6 +364,15 @@
|
||||
/>
|
||||
JavaScript
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-theme-secondary">
|
||||
<input
|
||||
type="radio"
|
||||
bind:group={implementation}
|
||||
value="python"
|
||||
class="text-blue-500"
|
||||
/>
|
||||
Python
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-theme-secondary">
|
||||
<input
|
||||
type="radio"
|
||||
@@ -312,22 +385,102 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript Code -->
|
||||
{#if implementation === 'javascript'}
|
||||
<!-- Code Editor (JavaScript or Python) -->
|
||||
{#if implementation === 'javascript' || implementation === 'python'}
|
||||
<div>
|
||||
<label for="tool-code" class="block text-sm font-medium text-theme-secondary">JavaScript Code</label>
|
||||
<p class="mt-1 text-xs text-theme-muted">
|
||||
Arguments are passed as an <code class="bg-theme-tertiary px-1 rounded">args</code> object. Return the result.
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<label class="block text-sm font-medium text-theme-secondary">
|
||||
{implementation === 'javascript' ? 'JavaScript' : 'Python'} Code
|
||||
</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Templates dropdown -->
|
||||
<div class="relative">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => showTemplates = !showTemplates}
|
||||
class="flex items-center gap-1 text-xs text-blue-400 hover:text-blue-300"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M9 4.804A7.968 7.968 0 005.5 4c-1.255 0-2.443.29-3.5.804v10A7.969 7.969 0 015.5 14c1.669 0 3.218.51 4.5 1.385A7.962 7.962 0 0114.5 14c1.255 0 2.443.29 3.5.804v-10A7.968 7.968 0 0014.5 4c-1.255 0-2.443.29-3.5.804V12a1 1 0 11-2 0V4.804z" />
|
||||
</svg>
|
||||
Templates
|
||||
</button>
|
||||
{#if showTemplates && currentTemplates.length > 0}
|
||||
<div class="absolute right-0 top-full mt-1 w-64 rounded-lg border border-theme-subtle bg-theme-secondary shadow-lg z-10">
|
||||
<div class="p-2 space-y-1 max-h-48 overflow-y-auto">
|
||||
{#each currentTemplates as template (template.id)}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => applyTemplate(template)}
|
||||
class="w-full text-left px-3 py-2 rounded hover:bg-theme-tertiary"
|
||||
>
|
||||
<div class="text-sm text-theme-primary">{template.name}</div>
|
||||
<div class="text-xs text-theme-muted truncate">{template.description}</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Docs toggle -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => showDocs = !showDocs}
|
||||
class="flex items-center gap-1 text-xs {showDocs ? 'text-emerald-400' : 'text-theme-muted hover:text-theme-secondary'}"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Docs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-theme-muted mb-2">
|
||||
{#if implementation === 'javascript'}
|
||||
Arguments are passed as an <code class="bg-theme-tertiary px-1 rounded">args</code> object. Return the result.
|
||||
{:else}
|
||||
Arguments are available as <code class="bg-theme-tertiary px-1 rounded">args</code> dict. Print JSON result to stdout.
|
||||
{/if}
|
||||
</p>
|
||||
<textarea
|
||||
id="tool-code"
|
||||
bind:value={code}
|
||||
rows="8"
|
||||
class="mt-2 w-full rounded-lg border border-theme-subtle bg-theme-primary px-3 py-2 font-mono text-sm text-theme-primary placeholder-theme-muted focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
></textarea>
|
||||
|
||||
<!-- Documentation panel -->
|
||||
<ToolDocs
|
||||
language={implementation}
|
||||
isOpen={showDocs}
|
||||
onclose={() => showDocs = false}
|
||||
/>
|
||||
|
||||
<div class="mt-2">
|
||||
<CodeEditor
|
||||
bind:value={code}
|
||||
language={implementation === 'python' ? 'python' : 'javascript'}
|
||||
minHeight="200px"
|
||||
/>
|
||||
</div>
|
||||
{#if errors.code}
|
||||
<p class="mt-1 text-sm text-red-400">{errors.code}</p>
|
||||
{/if}
|
||||
|
||||
<!-- Test button -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => showTest = !showTest}
|
||||
class="mt-3 flex items-center gap-2 text-sm {showTest ? 'text-emerald-400' : 'text-theme-muted hover:text-theme-secondary'}"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
{showTest ? 'Hide Test Panel' : 'Test Tool'}
|
||||
</button>
|
||||
|
||||
<!-- Tool tester -->
|
||||
<ToolTester
|
||||
{implementation}
|
||||
{code}
|
||||
parameters={buildParameterSchema()}
|
||||
isOpen={showTest}
|
||||
onclose={() => showTest = false}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
220
frontend/src/lib/components/tools/ToolTester.svelte
Normal file
220
frontend/src/lib/components/tools/ToolTester.svelte
Normal file
@@ -0,0 +1,220 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* ToolTester - Test panel for running tools with sample inputs
|
||||
*/
|
||||
|
||||
import type { JSONSchema, ToolImplementation } from '$lib/tools';
|
||||
import CodeEditor from './CodeEditor.svelte';
|
||||
|
||||
interface Props {
|
||||
implementation: ToolImplementation;
|
||||
code: string;
|
||||
parameters: JSONSchema;
|
||||
isOpen?: boolean;
|
||||
onclose?: () => void;
|
||||
}
|
||||
|
||||
const { implementation, code, parameters, isOpen = false, onclose }: Props = $props();
|
||||
|
||||
let testInput = $state('{}');
|
||||
let testResult = $state<{ success: boolean; result?: unknown; error?: string } | null>(null);
|
||||
let isRunning = $state(false);
|
||||
|
||||
// Generate example input from parameters
|
||||
$effect(() => {
|
||||
if (isOpen && testInput === '{}' && parameters.properties) {
|
||||
const example: Record<string, unknown> = {};
|
||||
for (const [name, prop] of Object.entries(parameters.properties)) {
|
||||
switch (prop.type) {
|
||||
case 'string':
|
||||
example[name] = prop.description ? `example_${name}` : '';
|
||||
break;
|
||||
case 'number':
|
||||
example[name] = 0;
|
||||
break;
|
||||
case 'boolean':
|
||||
example[name] = false;
|
||||
break;
|
||||
case 'array':
|
||||
example[name] = [];
|
||||
break;
|
||||
case 'object':
|
||||
example[name] = {};
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (Object.keys(example).length > 0) {
|
||||
testInput = JSON.stringify(example, null, 2);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function runTest(): Promise<void> {
|
||||
if (isRunning) return;
|
||||
|
||||
isRunning = true;
|
||||
testResult = null;
|
||||
|
||||
try {
|
||||
// Parse the input
|
||||
let args: Record<string, unknown>;
|
||||
try {
|
||||
args = JSON.parse(testInput);
|
||||
} catch {
|
||||
testResult = { success: false, error: 'Invalid JSON input' };
|
||||
isRunning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (implementation === 'javascript') {
|
||||
// Execute JavaScript directly in browser
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
||||
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
||||
const fn = new AsyncFunction(
|
||||
'args',
|
||||
`
|
||||
"use strict";
|
||||
${code}
|
||||
`
|
||||
);
|
||||
const result = await fn(args);
|
||||
testResult = { success: true, result };
|
||||
} catch (error) {
|
||||
testResult = {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
};
|
||||
}
|
||||
} else if (implementation === 'python') {
|
||||
// Python requires backend execution
|
||||
try {
|
||||
const response = await fetch('/api/v1/tools/execute', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
language: 'python',
|
||||
code,
|
||||
args,
|
||||
timeout: 30
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server error: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
testResult = { success: true, result: data.result };
|
||||
} else {
|
||||
testResult = { success: false, error: data.error || 'Unknown error' };
|
||||
}
|
||||
} catch (error) {
|
||||
testResult = {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
};
|
||||
}
|
||||
} else {
|
||||
testResult = { success: false, error: 'HTTP tools cannot be tested in the editor' };
|
||||
}
|
||||
} finally {
|
||||
isRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
function formatResult(result: unknown): string {
|
||||
if (result === undefined) return 'undefined';
|
||||
if (result === null) return 'null';
|
||||
try {
|
||||
return JSON.stringify(result, null, 2);
|
||||
} catch {
|
||||
return String(result);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<div class="rounded-lg border border-theme-subtle bg-theme-tertiary/50 p-4 mt-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h4 class="text-sm font-medium text-theme-primary flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Test Tool
|
||||
</h4>
|
||||
{#if onclose}
|
||||
<button
|
||||
type="button"
|
||||
onclick={onclose}
|
||||
class="text-theme-muted hover:text-theme-primary"
|
||||
aria-label="Close test panel"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Input -->
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-theme-secondary mb-1">Input Arguments (JSON)</label>
|
||||
<CodeEditor bind:value={testInput} language="json" minHeight="80px" />
|
||||
</div>
|
||||
|
||||
<!-- Run button -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={runTest}
|
||||
disabled={isRunning || !code.trim()}
|
||||
class="flex items-center gap-2 px-3 py-1.5 text-sm font-medium rounded-lg bg-emerald-600 text-white hover:bg-emerald-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{#if isRunning}
|
||||
<svg class="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Running...
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Run Test
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<!-- Result -->
|
||||
{#if testResult}
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-theme-secondary mb-1">Result</label>
|
||||
<div
|
||||
class="rounded-lg p-3 text-sm font-mono overflow-x-auto {testResult.success
|
||||
? 'bg-emerald-900/30 border border-emerald-500/30'
|
||||
: 'bg-red-900/30 border border-red-500/30'}"
|
||||
>
|
||||
{#if testResult.success}
|
||||
<div class="flex items-center gap-2 text-emerald-400 mb-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Success
|
||||
</div>
|
||||
<pre class="text-theme-primary whitespace-pre-wrap">{formatResult(testResult.result)}</pre>
|
||||
{:else}
|
||||
<div class="flex items-center gap-2 text-red-400 mb-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Error
|
||||
</div>
|
||||
<pre class="text-red-300 whitespace-pre-wrap">{testResult.error}</pre>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -2,4 +2,7 @@
|
||||
* Tools components exports
|
||||
*/
|
||||
|
||||
export { default as CodeEditor } from './CodeEditor.svelte';
|
||||
export { default as ToolDocs } from './ToolDocs.svelte';
|
||||
export { default as ToolEditor } from './ToolEditor.svelte';
|
||||
export { default as ToolTester } from './ToolTester.svelte';
|
||||
|
||||
@@ -41,6 +41,45 @@ async function executeJavaScriptTool(tool: CustomTool, args: Record<string, unkn
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a custom Python tool via backend API
|
||||
*
|
||||
* SECURITY NOTE: This sends user-provided Python code to the backend for execution.
|
||||
* This is by design - users create custom tools with their own code.
|
||||
* The backend executes code in a subprocess with timeout protection.
|
||||
*/
|
||||
async function executePythonTool(tool: CustomTool, args: Record<string, unknown>): Promise<unknown> {
|
||||
if (!tool.code) {
|
||||
throw new Error('Python tool has no code');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/tools/execute', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
language: 'python',
|
||||
code: tool.code,
|
||||
args,
|
||||
timeout: 30
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Backend error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'Python execution failed');
|
||||
}
|
||||
|
||||
return data.result;
|
||||
} catch (error) {
|
||||
throw new Error(`Python tool error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a custom HTTP tool
|
||||
*/
|
||||
@@ -168,6 +207,8 @@ export async function executeCustomTool(
|
||||
switch (tool.implementation) {
|
||||
case 'javascript':
|
||||
return executeJavaScriptTool(tool, args);
|
||||
case 'python':
|
||||
return executePythonTool(tool, args);
|
||||
case 'http':
|
||||
return executeHttpTool(tool, args);
|
||||
default:
|
||||
|
||||
@@ -21,3 +21,10 @@ export {
|
||||
defaultToolConfig,
|
||||
type ToolConfig
|
||||
} from './config.js';
|
||||
export {
|
||||
toolTemplates,
|
||||
getTemplatesByLanguage,
|
||||
getTemplatesByCategory,
|
||||
getTemplateById,
|
||||
type ToolTemplate
|
||||
} from './templates.js';
|
||||
|
||||
352
frontend/src/lib/tools/templates.ts
Normal file
352
frontend/src/lib/tools/templates.ts
Normal file
@@ -0,0 +1,352 @@
|
||||
/**
|
||||
* Tool templates - Starter templates for custom tools
|
||||
*/
|
||||
|
||||
import type { JSONSchema, ToolImplementation } from './types';
|
||||
|
||||
export interface ToolTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: 'api' | 'data' | 'utility' | 'integration';
|
||||
language: ToolImplementation;
|
||||
code: string;
|
||||
parameters: JSONSchema;
|
||||
}
|
||||
|
||||
export const toolTemplates: ToolTemplate[] = [
|
||||
// JavaScript Templates
|
||||
{
|
||||
id: 'js-api-fetch',
|
||||
name: 'API Request',
|
||||
description: 'Fetch data from an external REST API',
|
||||
category: 'api',
|
||||
language: 'javascript',
|
||||
code: `// Fetch data from an API endpoint
|
||||
const response = await fetch(args.url, {
|
||||
method: args.method || 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(args.headers || {})
|
||||
},
|
||||
...(args.body ? { body: JSON.stringify(args.body) } : {})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(\`HTTP \${response.status}: \${response.statusText}\`);
|
||||
}
|
||||
|
||||
return await response.json();`,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string', description: 'The API endpoint URL' },
|
||||
method: { type: 'string', description: 'HTTP method (GET, POST, etc.)' },
|
||||
headers: { type: 'object', description: 'Additional headers' },
|
||||
body: { type: 'object', description: 'Request body for POST/PUT' }
|
||||
},
|
||||
required: ['url']
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'js-json-transform',
|
||||
name: 'JSON Transform',
|
||||
description: 'Transform and filter JSON data',
|
||||
category: 'data',
|
||||
language: 'javascript',
|
||||
code: `// Transform JSON data
|
||||
const data = args.data;
|
||||
const fields = args.fields || Object.keys(data[0] || data);
|
||||
|
||||
// Handle both arrays and single objects
|
||||
const items = Array.isArray(data) ? data : [data];
|
||||
|
||||
const result = items.map(item => {
|
||||
const filtered = {};
|
||||
for (const field of fields) {
|
||||
if (field in item) {
|
||||
filtered[field] = item[field];
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
});
|
||||
|
||||
return Array.isArray(data) ? result : result[0];`,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
data: { type: 'object', description: 'JSON data to transform' },
|
||||
fields: { type: 'array', description: 'Fields to keep (optional)' }
|
||||
},
|
||||
required: ['data']
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'js-string-utils',
|
||||
name: 'String Utilities',
|
||||
description: 'Common string manipulation operations',
|
||||
category: 'utility',
|
||||
language: 'javascript',
|
||||
code: `// String manipulation utilities
|
||||
const text = args.text;
|
||||
const operation = args.operation;
|
||||
|
||||
switch (operation) {
|
||||
case 'uppercase':
|
||||
return text.toUpperCase();
|
||||
case 'lowercase':
|
||||
return text.toLowerCase();
|
||||
case 'capitalize':
|
||||
return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
|
||||
case 'reverse':
|
||||
return text.split('').reverse().join('');
|
||||
case 'word_count':
|
||||
return { count: text.split(/\\s+/).filter(w => w).length };
|
||||
case 'char_count':
|
||||
return { count: text.length };
|
||||
case 'slug':
|
||||
return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
||||
default:
|
||||
return { text, error: 'Unknown operation' };
|
||||
}`,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
text: { type: 'string', description: 'Input text to process' },
|
||||
operation: { type: 'string', description: 'Operation: uppercase, lowercase, capitalize, reverse, word_count, char_count, slug' }
|
||||
},
|
||||
required: ['text', 'operation']
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'js-date-utils',
|
||||
name: 'Date Utilities',
|
||||
description: 'Date formatting and calculations',
|
||||
category: 'utility',
|
||||
language: 'javascript',
|
||||
code: `// Date utilities
|
||||
const date = args.date ? new Date(args.date) : new Date();
|
||||
const format = args.format || 'iso';
|
||||
|
||||
const formatDate = (d, fmt) => {
|
||||
const pad = n => String(n).padStart(2, '0');
|
||||
|
||||
switch (fmt) {
|
||||
case 'iso':
|
||||
return d.toISOString();
|
||||
case 'date':
|
||||
return d.toLocaleDateString();
|
||||
case 'time':
|
||||
return d.toLocaleTimeString();
|
||||
case 'unix':
|
||||
return Math.floor(d.getTime() / 1000);
|
||||
case 'relative':
|
||||
const diff = Date.now() - d.getTime();
|
||||
const mins = Math.floor(diff / 60000);
|
||||
if (mins < 60) return \`\${mins} minutes ago\`;
|
||||
const hours = Math.floor(mins / 60);
|
||||
if (hours < 24) return \`\${hours} hours ago\`;
|
||||
return \`\${Math.floor(hours / 24)} days ago\`;
|
||||
default:
|
||||
return d.toISOString();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
formatted: formatDate(date, format),
|
||||
timestamp: date.getTime(),
|
||||
iso: date.toISOString()
|
||||
};`,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
date: { type: 'string', description: 'Date string or timestamp (default: now)' },
|
||||
format: { type: 'string', description: 'Format: iso, date, time, unix, relative' }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Python Templates
|
||||
{
|
||||
id: 'py-api-fetch',
|
||||
name: 'API Request (Python)',
|
||||
description: 'Fetch data from an external REST API using Python',
|
||||
category: 'api',
|
||||
language: 'python',
|
||||
code: `# Fetch data from an API endpoint
|
||||
import json
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
url = args.get('url')
|
||||
method = args.get('method', 'GET')
|
||||
headers = args.get('headers', {})
|
||||
body = args.get('body')
|
||||
|
||||
req = urllib.request.Request(url, method=method)
|
||||
req.add_header('Content-Type', 'application/json')
|
||||
for key, value in headers.items():
|
||||
req.add_header(key, value)
|
||||
|
||||
data = json.dumps(body).encode() if body else None
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, data=data) as response:
|
||||
result = json.loads(response.read().decode())
|
||||
print(json.dumps(result))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": f"HTTP {e.code}: {e.reason}"}))`,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string', description: 'The API endpoint URL' },
|
||||
method: { type: 'string', description: 'HTTP method (GET, POST, etc.)' },
|
||||
headers: { type: 'object', description: 'Additional headers' },
|
||||
body: { type: 'object', description: 'Request body for POST/PUT' }
|
||||
},
|
||||
required: ['url']
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'py-data-analysis',
|
||||
name: 'Data Analysis (Python)',
|
||||
description: 'Basic statistical analysis of numeric data',
|
||||
category: 'data',
|
||||
language: 'python',
|
||||
code: `# Basic data analysis
|
||||
import json
|
||||
import math
|
||||
|
||||
data = args.get('data', [])
|
||||
if not data:
|
||||
print(json.dumps({"error": "No data provided"}))
|
||||
else:
|
||||
n = len(data)
|
||||
total = sum(data)
|
||||
mean = total / n
|
||||
|
||||
sorted_data = sorted(data)
|
||||
mid = n // 2
|
||||
median = sorted_data[mid] if n % 2 else (sorted_data[mid-1] + sorted_data[mid]) / 2
|
||||
|
||||
variance = sum((x - mean) ** 2 for x in data) / n
|
||||
std_dev = math.sqrt(variance)
|
||||
|
||||
result = {
|
||||
"count": n,
|
||||
"sum": total,
|
||||
"mean": round(mean, 4),
|
||||
"median": median,
|
||||
"min": min(data),
|
||||
"max": max(data),
|
||||
"std_dev": round(std_dev, 4),
|
||||
"variance": round(variance, 4)
|
||||
}
|
||||
print(json.dumps(result))`,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
data: { type: 'array', description: 'Array of numbers to analyze' }
|
||||
},
|
||||
required: ['data']
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'py-text-analysis',
|
||||
name: 'Text Analysis (Python)',
|
||||
description: 'Analyze text for word frequency, sentiment indicators',
|
||||
category: 'data',
|
||||
language: 'python',
|
||||
code: `# Text analysis
|
||||
import json
|
||||
import re
|
||||
from collections import Counter
|
||||
|
||||
text = args.get('text', '')
|
||||
top_n = args.get('top_n', 10)
|
||||
|
||||
# Tokenize and count
|
||||
words = re.findall(r'\\b\\w+\\b', text.lower())
|
||||
word_freq = Counter(words)
|
||||
|
||||
# Basic stats
|
||||
sentences = re.split(r'[.!?]+', text)
|
||||
sentences = [s.strip() for s in sentences if s.strip()]
|
||||
|
||||
result = {
|
||||
"word_count": len(words),
|
||||
"unique_words": len(word_freq),
|
||||
"sentence_count": len(sentences),
|
||||
"avg_word_length": round(sum(len(w) for w in words) / len(words), 2) if words else 0,
|
||||
"top_words": dict(word_freq.most_common(top_n)),
|
||||
"char_count": len(text),
|
||||
"char_count_no_spaces": len(text.replace(' ', ''))
|
||||
}
|
||||
print(json.dumps(result))`,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
text: { type: 'string', description: 'Text to analyze' },
|
||||
top_n: { type: 'number', description: 'Number of top words to return (default: 10)' }
|
||||
},
|
||||
required: ['text']
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'py-hash-encode',
|
||||
name: 'Hash & Encode (Python)',
|
||||
description: 'Hash strings and encode/decode base64',
|
||||
category: 'utility',
|
||||
language: 'python',
|
||||
code: `# Hash and encoding utilities
|
||||
import json
|
||||
import hashlib
|
||||
import base64
|
||||
|
||||
text = args.get('text', '')
|
||||
operation = args.get('operation', 'md5')
|
||||
|
||||
result = {}
|
||||
|
||||
if operation == 'md5':
|
||||
result['hash'] = hashlib.md5(text.encode()).hexdigest()
|
||||
elif operation == 'sha256':
|
||||
result['hash'] = hashlib.sha256(text.encode()).hexdigest()
|
||||
elif operation == 'sha512':
|
||||
result['hash'] = hashlib.sha512(text.encode()).hexdigest()
|
||||
elif operation == 'base64_encode':
|
||||
result['encoded'] = base64.b64encode(text.encode()).decode()
|
||||
elif operation == 'base64_decode':
|
||||
try:
|
||||
result['decoded'] = base64.b64decode(text.encode()).decode()
|
||||
except Exception as e:
|
||||
result['error'] = str(e)
|
||||
else:
|
||||
result['error'] = f'Unknown operation: {operation}'
|
||||
|
||||
result['operation'] = operation
|
||||
result['input_length'] = len(text)
|
||||
|
||||
print(json.dumps(result))`,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
text: { type: 'string', description: 'Text to process' },
|
||||
operation: { type: 'string', description: 'Operation: md5, sha256, sha512, base64_encode, base64_decode' }
|
||||
},
|
||||
required: ['text', 'operation']
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export function getTemplatesByLanguage(language: ToolImplementation): ToolTemplate[] {
|
||||
return toolTemplates.filter(t => t.language === language);
|
||||
}
|
||||
|
||||
export function getTemplatesByCategory(category: ToolTemplate['category']): ToolTemplate[] {
|
||||
return toolTemplates.filter(t => t.category === category);
|
||||
}
|
||||
|
||||
export function getTemplateById(id: string): ToolTemplate | undefined {
|
||||
return toolTemplates.find(t => t.id === id);
|
||||
}
|
||||
@@ -54,7 +54,7 @@ export interface ToolResult {
|
||||
}
|
||||
|
||||
/** Tool implementation type */
|
||||
export type ToolImplementation = 'builtin' | 'javascript' | 'http';
|
||||
export type ToolImplementation = 'builtin' | 'javascript' | 'python' | 'http';
|
||||
|
||||
/** Custom tool configuration */
|
||||
export interface CustomTool {
|
||||
@@ -63,7 +63,7 @@ export interface CustomTool {
|
||||
description: string;
|
||||
parameters: JSONSchema;
|
||||
implementation: ToolImplementation;
|
||||
/** JavaScript code for 'javascript' implementation */
|
||||
/** Code for 'javascript' or 'python' implementation */
|
||||
code?: string;
|
||||
/** HTTP endpoint for 'http' implementation */
|
||||
endpoint?: string;
|
||||
|
||||
Reference in New Issue
Block a user