diff --git a/adk/middleware/aiosandbox-backend/.example.env b/adk/middleware/aiosandbox-backend/.example.env new file mode 100644 index 00000000..282f859b --- /dev/null +++ b/adk/middleware/aiosandbox-backend/.example.env @@ -0,0 +1,25 @@ +# === Mode 1: Direct (use existing sandbox) === +# Data plane URL with faasInstanceName query parameter +AIO_SANDBOX_BASE_URL=https://xxx.apigateway-cn-beijing.volceapi.com?faasInstanceName=your-instance-name +# Optional: Bearer token authentication +# AIO_SANDBOX_TOKEN=your-token + +# === Mode 2: Managed (auto create/kill sandbox) === +# Volcengine credentials for sandbox lifecycle management +# VOLC_ACCESSKEY=your-access-key +# VOLC_SECRETKEY=your-secret-key +# VEFAAS_FUNCTION_ID=your-function-id +# VEFAAS_GATEWAY_URL=https://xxx.apigateway-cn-beijing.volceapi.com + +# === Model configuration (choose one provider) === + +# Option 1: OpenAI compatible +OPENAI_API_KEY=your-api-key +OPENAI_MODEL=gpt-4 +OPENAI_BASE_URL=https://api.openai.com/v1 + +# Option 2: Ark (set MODEL_TYPE=ark) +# MODEL_TYPE=ark +# ARK_API_KEY=your-ark-api-key +# ARK_MODEL=your-model-name +# ARK_BASE_URL=https://ark.cn-beijing.volces.com/api/v3 diff --git a/adk/middleware/aiosandbox-backend/README.md b/adk/middleware/aiosandbox-backend/README.md new file mode 100644 index 00000000..abff1465 --- /dev/null +++ b/adk/middleware/aiosandbox-backend/README.md @@ -0,0 +1,164 @@ +# AIO Sandbox Filesystem Middleware Example + +This example demonstrates how to use the Deep Agent with [AIO Sandbox](https://github.com/agent-infra/sandbox) + +You can access AIO Sandbox through [Volcano Engine veFaaS Sandbox](https://www.volcengine.com/docs/6662/1802770) to quickly get a secure isolated code execution environment. + +This example demonstrates how to implement a custom `filesystem.Backend` and use it with `filesystem.NewMiddleware` to provide file system tools to an agent running in a remote AIO Sandbox environment. + +## Overview + +The `filesystem.Backend` interface allows you to plug in any file system implementation. This example shows how to: + +1. Implement the `filesystem.Backend` interface using AIO Sandbox SDK (data plane) +2. Manage sandbox lifecycle via veFaaS API (control plane) +3. Create a filesystem middleware with the custom backend +4. Use the middleware with a deep agent + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Deep Agent │ +├─────────────────────────────────────────────────────────────────┤ +│ Filesystem Middleware │ +│ (Auto-registers: ls, read_file, write_file, edit_file, │ +│ glob, grep, execute tools) │ +├─────────────────────────────────────────────────────────────────┤ +│ AIOSandboxBackend │ +│ (implements filesystem.Backend + Shell) │ +├──────────────────────┬──────────────────────────────────────────┤ +│ Control Plane │ Data Plane │ +│ veFaaS SDK │ AIO Sandbox SDK │ +│ (Create/Kill/ │ (File/Shell operations via │ +│ Describe sandbox) │ faasInstanceName query param) │ +└──────────────────────┴──────────────────────────────────────────┘ +``` + +## Backend Interface Mapping + +| filesystem.Backend Method | AIO Sandbox SDK API | +|---------------------------|-------------------------------| +| LsInfo | File.ListPath | +| Read | File.ReadFile | +| Write | File.WriteFile | +| Edit | File.ReplaceInFile | +| GrepRaw | Ripgrep | +| GlobInfo | File.FindFiles | +| Execute (Shell) | Shell.ExecCommand | + +## Prerequisites + +The example supports two modes: + +### Mode 1: Direct (use existing sandbox) + +```bash +# Data plane URL with faasInstanceName query parameter +AIO_SANDBOX_BASE_URL=https://xxx.apigateway-cn-beijing.volceapi.com?faasInstanceName=your-instance-name + +# Optional: Bearer token authentication +# AIO_SANDBOX_TOKEN=your-token +``` + +### Mode 2: Managed (auto create/kill sandbox) + +```bash +# Volcengine credentials for sandbox lifecycle management +VOLC_ACCESSKEY=your-access-key +VOLC_SECRETKEY=your-secret-key +VEFAAS_FUNCTION_ID=your-function-id +VEFAAS_GATEWAY_URL=https://xxx.apigateway-cn-beijing.volceapi.com +``` + +### Model configuration + +```bash +# Option 1: OpenAI compatible +OPENAI_API_KEY=your-api-key +OPENAI_MODEL=gpt-4 +OPENAI_BASE_URL=https://api.openai.com/v1 + +# Option 2: Ark (set MODEL_TYPE=ark) +# MODEL_TYPE=ark +# ARK_API_KEY=your-ark-api-key +# ARK_MODEL=your-model-name +# ARK_BASE_URL=https://ark.cn-beijing.volces.com/api/v3 +``` + +## Usage + +### Direct mode + +```go +// Use an existing sandbox via data plane URL +backend, err := NewAIOSandboxBackend(ctx, &AIOSandboxBackendConfig{ + BaseURL: "https://xxx.apigateway-cn-beijing.volceapi.com?faasInstanceName=your-instance-name", + WorkDir: "/tmp", +}) +``` + +### Managed mode + +```go +// Create sandbox via control plane +mgr, _ := NewSandboxManager(&SandboxManagerConfig{ + AccessKey: "your-ak", + SecretKey: "your-sk", + FunctionID: "your-function-id", +}) + +sandboxID, _ := mgr.CreateSandbox() +defer mgr.KillSandbox(sandboxID) + +// Connect data plane +baseURL := mgr.DataPlaneBaseURL("https://xxx.apigateway-cn-beijing.volceapi.com", sandboxID) +backend, err := NewAIOSandboxBackend(ctx, &AIOSandboxBackendConfig{ + BaseURL: baseURL, + WorkDir: "/tmp", +}) +``` + +### Use with agent + +```go +fsMW, _ := filesystem.NewMiddleware(ctx, &filesystem.Config{ + Backend: backend, + Shell: backend, +}) + +agent, _ := deep.New(ctx, &deep.Config{ + ChatModel: chatModel, + Middlewares: []adk.AgentMiddleware{fsMW}, +}) +``` + +## Run the Example + +```bash +cd adk/middleware/aiosandbox-backend +go run . +``` + +## Implementing Your Own Backend + +To implement a custom filesystem backend, implement the `filesystem.Backend` interface: + +```go +type Backend interface { + LsInfo(ctx context.Context, req *LsInfoRequest) ([]FileInfo, error) + Read(ctx context.Context, req *ReadRequest) (*FileContent, error) + GrepRaw(ctx context.Context, req *GrepRequest) ([]GrepMatch, error) + GlobInfo(ctx context.Context, req *GlobInfoRequest) ([]FileInfo, error) + Write(ctx context.Context, req *WriteRequest) error + Edit(ctx context.Context, req *EditRequest) error +} +``` + +Optionally implement the `filesystem.Shell` interface to provide the `execute` tool: + +```go +type Shell interface { + Execute(ctx context.Context, input *ExecuteRequest) (*ExecuteResponse, error) +} +``` diff --git a/adk/middleware/aiosandbox-backend/README_zh.md b/adk/middleware/aiosandbox-backend/README_zh.md new file mode 100644 index 00000000..b0bf84c4 --- /dev/null +++ b/adk/middleware/aiosandbox-backend/README_zh.md @@ -0,0 +1,164 @@ +# AIO Sandbox 文件系统中间件示例 + +本示例演示如何将 Deep Agent 与 [AIO Sandbox](https://github.com/agent-infra/sandbox) 集成 + +您可以通过 [火山引擎 veFaaS Sandbox](https://www.volcengine.com/docs/6662/1802770) 快速获得一个安全隔离的代码执行环境。 + +本示例演示如何实现自定义 `filesystem.Backend`,并将其与 `filesystem.NewMiddleware` 配合使用,为运行在远程 AIO Sandbox 环境中的 Agent 提供文件系统工具。 + +## 概述 + +`filesystem.Backend` 接口允许你接入任意文件系统实现。本示例展示了如何: + +1. 使用 AIO Sandbox SDK 实现 `filesystem.Backend` 接口(数据面) +2. 通过 veFaaS API 管理沙箱生命周期(控制面) +3. 使用自定义后端创建文件系统中间件 +4. 将中间件与 Deep Agent 配合使用 + +## 架构 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Deep Agent │ +├─────────────────────────────────────────────────────────────────┤ +│ Filesystem Middleware │ +│ (自动注册: ls, read_file, write_file, edit_file, │ +│ glob, grep, execute 工具) │ +├─────────────────────────────────────────────────────────────────┤ +│ AIOSandboxBackend │ +│ (实现 filesystem.Backend + Shell) │ +├──────────────────────┬──────────────────────────────────────────┤ +│ 控制面 │ 数据面 │ +│ veFaaS SDK │ AIO Sandbox SDK │ +│ (创建/销毁/ │ (文件/Shell 操作,通过 │ +│ 查询沙箱) │ faasInstanceName 查询参数) │ +└──────────────────────┴──────────────────────────────────────────┘ +``` + +## 后端接口映射 + +| filesystem.Backend 方法 | AIO Sandbox SDK API | +|---------------------------|-------------------------------| +| LsInfo | File.ListPath | +| Read | File.ReadFile | +| Write | File.WriteFile | +| Edit | File.ReplaceInFile | +| GrepRaw | Ripgrep | +| GlobInfo | File.FindFiles | +| Execute (Shell) | Shell.ExecCommand | + +## 前置条件 + +示例支持两种模式: + +### 模式一:直连(使用已有沙箱) + +```bash +# 数据面 URL,通过 faasInstanceName 查询参数指定沙箱实例 +AIO_SANDBOX_BASE_URL=https://xxx.apigateway-cn-beijing.volceapi.com?faasInstanceName=your-instance-name + +# 可选:Bearer Token 认证 +# AIO_SANDBOX_TOKEN=your-token +``` + +### 模式二:托管(自动创建/销毁沙箱) + +```bash +# 火山引擎凭证,用于沙箱生命周期管理 +VOLC_ACCESSKEY=your-access-key +VOLC_SECRETKEY=your-secret-key +VEFAAS_FUNCTION_ID=your-function-id +VEFAAS_GATEWAY_URL=https://xxx.apigateway-cn-beijing.volceapi.com +``` + +### 模型配置 + +```bash +# 方式一:OpenAI 兼容接口 +OPENAI_API_KEY=your-api-key +OPENAI_MODEL=gpt-4 +OPENAI_BASE_URL=https://api.openai.com/v1 + +# 方式二:Ark(设置 MODEL_TYPE=ark) +# MODEL_TYPE=ark +# ARK_API_KEY=your-ark-api-key +# ARK_MODEL=your-model-name +# ARK_BASE_URL=https://ark.cn-beijing.volces.com/api/v3 +``` + +## 使用方法 + +### 直连模式 + +```go +// 通过数据面 URL 使用已有沙箱 +backend, err := NewAIOSandboxBackend(ctx, &AIOSandboxBackendConfig{ + BaseURL: "https://xxx.apigateway-cn-beijing.volceapi.com?faasInstanceName=your-instance-name", + WorkDir: "/tmp", +}) +``` + +### 托管模式 + +```go +// 通过控制面创建沙箱 +mgr, _ := NewSandboxManager(&SandboxManagerConfig{ + AccessKey: "your-ak", + SecretKey: "your-sk", + FunctionID: "your-function-id", +}) + +sandboxID, _ := mgr.CreateSandbox() +defer mgr.KillSandbox(sandboxID) + +// 连接数据面 +baseURL := mgr.DataPlaneBaseURL("https://xxx.apigateway-cn-beijing.volceapi.com", sandboxID) +backend, err := NewAIOSandboxBackend(ctx, &AIOSandboxBackendConfig{ + BaseURL: baseURL, + WorkDir: "/tmp", +}) +``` + +### 与 Agent 配合使用 + +```go +fsMW, _ := filesystem.NewMiddleware(ctx, &filesystem.Config{ + Backend: backend, + Shell: backend, +}) + +agent, _ := deep.New(ctx, &deep.Config{ + ChatModel: chatModel, + Middlewares: []adk.AgentMiddleware{fsMW}, +}) +``` + +## 运行示例 + +```bash +cd adk/middleware/aiosandbox-backend +go run . +``` + +## 实现自定义后端 + +要实现自定义文件系统后端,需要实现 `filesystem.Backend` 接口: + +```go +type Backend interface { + LsInfo(ctx context.Context, req *LsInfoRequest) ([]FileInfo, error) + Read(ctx context.Context, req *ReadRequest) (*FileContent, error) + GrepRaw(ctx context.Context, req *GrepRequest) ([]GrepMatch, error) + GlobInfo(ctx context.Context, req *GlobInfoRequest) ([]FileInfo, error) + Write(ctx context.Context, req *WriteRequest) error + Edit(ctx context.Context, req *EditRequest) error +} +``` + +可选实现 `filesystem.Shell` 接口以提供 `execute` 工具: + +```go +type Shell interface { + Execute(ctx context.Context, input *ExecuteRequest) (*ExecuteResponse, error) +} +``` diff --git a/adk/middleware/aiosandbox-backend/backend.go b/adk/middleware/aiosandbox-backend/backend.go new file mode 100644 index 00000000..623cb497 --- /dev/null +++ b/adk/middleware/aiosandbox-backend/backend.go @@ -0,0 +1,408 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package main + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "regexp" + "strconv" + "strings" + "time" + + sandboxsdk "github.com/agent-infra/sandbox-sdk-go" + "github.com/agent-infra/sandbox-sdk-go/client" + "github.com/agent-infra/sandbox-sdk-go/option" + "github.com/cloudwego/eino/adk/filesystem" +) + +// AIOSandboxBackendConfig is the configuration for AIO Sandbox backend. +type AIOSandboxBackendConfig struct { + // BaseURL is the AIO Sandbox API endpoint. Required. + BaseURL string + + // Token is the authentication token for AIO Sandbox API. Optional. + Token string + + // Headers is additional HTTP headers to include in requests. Optional. + Headers map[string]string + + // WorkDir is the working directory inside the sandbox. Default: "/tmp" + WorkDir string +} + +func (c *AIOSandboxBackendConfig) setDefaults() { + if c.WorkDir == "" { + c.WorkDir = "/tmp" + } +} + +// AIOSandboxBackend implements filesystem.Backend interface using AIO Sandbox API. +type AIOSandboxBackend struct { + config *AIOSandboxBackendConfig + client *client.Client +} + +// NewAIOSandboxBackend creates a new AIO Sandbox backend that implements filesystem.Backend. +func NewAIOSandboxBackend(ctx context.Context, config *AIOSandboxBackendConfig) (*AIOSandboxBackend, error) { + if config == nil { + return nil, fmt.Errorf("config is required") + } + if config.BaseURL == "" { + return nil, fmt.Errorf("BaseURL is required") + } + + cfg := *config + cfg.setDefaults() + + parsedURL, err := url.Parse(cfg.BaseURL) + if err != nil { + return nil, fmt.Errorf("invalid BaseURL: %w", err) + } + + if parsedURL.Scheme == "" || parsedURL.Host == "" { + return nil, fmt.Errorf("invalid BaseURL: scheme and host are required") + } + + if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { + return nil, fmt.Errorf("invalid BaseURL: only http and https schemes are supported") + } + + baseURL := fmt.Sprintf("%s://%s%s", parsedURL.Scheme, parsedURL.Host, strings.TrimRight(parsedURL.Path, "/")) + + opts := []option.RequestOption{ + option.WithBaseURL(baseURL), + } + + if len(parsedURL.RawQuery) > 0 { + opts = append(opts, option.WithQueryParameters(parsedURL.Query())) + } + + customHeader := http.Header{} + if cfg.Token != "" { + customHeader.Set("Authorization", "Bearer "+cfg.Token) + } + for k, v := range cfg.Headers { + customHeader.Set(k, v) + } + if len(customHeader) > 0 { + opts = append(opts, option.WithHTTPHeader(customHeader)) + } + + c := client.NewClient(opts...) + + return &AIOSandboxBackend{ + config: &cfg, + client: c, + }, nil +} + +// LsInfo lists file information under the given path. +func (b *AIOSandboxBackend) LsInfo(ctx context.Context, req *filesystem.LsInfoRequest) ([]filesystem.FileInfo, error) { + p := req.Path + if p == "" { + p = b.config.WorkDir + } + + resp, err := b.client.File.ListPath(ctx, &sandboxsdk.FileListRequest{ + Path: p, + Recursive: sandboxsdk.Bool(false), + IncludeSize: sandboxsdk.Bool(true), + }) + if err != nil { + return nil, fmt.Errorf("list path failed: %w", err) + } + + data := resp.GetData() + if data == nil { + return nil, fmt.Errorf("empty response data") + } + + files := data.GetFiles() + result := make([]filesystem.FileInfo, 0, len(files)) + for _, f := range files { + info := filesystem.FileInfo{ + Path: f.GetPath(), + IsDir: f.GetIsDirectory(), + } + if f.Size != nil { + info.Size = int64(*f.Size) + } + if f.ModifiedTime != nil { + info.ModifiedAt = unixToISO8601(*f.ModifiedTime) + } + result = append(result, info) + } + + return result, nil +} + +// Read reads file content with support for line-based offset and limit. +func (b *AIOSandboxBackend) Read(ctx context.Context, req *filesystem.ReadRequest) (*filesystem.FileContent, error) { + sdkReq := &sandboxsdk.FileReadRequest{ + File: req.FilePath, + } + + // eino Offset is 1-based, SDK StartLine is 0-based + if req.Offset > 0 { + sdkReq.StartLine = sandboxsdk.Int(req.Offset - 1) + } + if req.Limit > 0 { + offset := req.Offset + if offset < 1 { + offset = 1 + } + sdkReq.EndLine = sandboxsdk.Int(offset - 1 + req.Limit) + } + + resp, err := b.client.File.ReadFile(ctx, sdkReq) + if err != nil { + return nil, fmt.Errorf("read file failed: %w", err) + } + + data := resp.GetData() + if data == nil { + return nil, fmt.Errorf("empty response data") + } + + return &filesystem.FileContent{Content: data.GetContent()}, nil +} + +// escapeShellArg escapes single quotes for shell arguments: ' -> '\'' +func escapeShellArg(s string) string { + return strings.ReplaceAll(s, "'", "'\\''") +} + +// rgJSONMatch represents the ripgrep JSON output for a match. +type rgJSONMatch struct { + Type string `json:"type"` + Data struct { + Path struct { + Text string `json:"text"` + } `json:"path"` + Lines struct { + Text string `json:"text"` + } `json:"lines"` + LineNumber int `json:"line_number"` + } `json:"data"` +} + +var validFileType = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`) + +// buildGrepCommand builds ripgrep command from GrepRequest. +func buildGrepCommand(req *filesystem.GrepRequest, searchPath string) string { + var args []string + args = append(args, "rg", "--json") + + if req.CaseInsensitive { + args = append(args, "-i") + } + if req.EnableMultiline { + args = append(args, "-U", "--multiline-dotall") + } + if req.AfterLines > 0 { + args = append(args, fmt.Sprintf("-A %d", req.AfterLines)) + } + if req.BeforeLines > 0 { + args = append(args, fmt.Sprintf("-B %d", req.BeforeLines)) + } + if req.Glob != "" { + args = append(args, fmt.Sprintf("-g '%s'", escapeShellArg(req.Glob))) + } + if req.FileType != "" && validFileType.MatchString(req.FileType) { + args = append(args, fmt.Sprintf("-t %s", req.FileType)) + } + + args = append(args, fmt.Sprintf("'%s'", escapeShellArg(req.Pattern))) + args = append(args, fmt.Sprintf("'%s'", escapeShellArg(searchPath))) + + return strings.Join(args, " ") + " 2>/dev/null || true" +} + +// GrepRaw searches for content matching the specified pattern in files. +// Uses ripgrep (rg) with JSON output for reliable parsing. +func (b *AIOSandboxBackend) GrepRaw(ctx context.Context, req *filesystem.GrepRequest) ([]filesystem.GrepMatch, error) { + if req.Pattern == "" { + return nil, nil + } + + searchPath := req.Path + if searchPath == "" { + searchPath = b.config.WorkDir + } + + cmd := buildGrepCommand(req, searchPath) + + resp, err := b.client.Shell.ExecCommand(ctx, &sandboxsdk.ShellExecRequest{ + Command: cmd, + ExecDir: &b.config.WorkDir, + }) + if err != nil { + return nil, fmt.Errorf("rg command failed: %w", err) + } + + data := resp.GetData() + if data == nil || data.Output == nil || *data.Output == "" { + return nil, nil + } + + // Parse rg --json output: each line is a JSON object + var matches []filesystem.GrepMatch + lines := strings.Split(*data.Output, "\n") + for _, line := range lines { + if line == "" { + continue + } + var m rgJSONMatch + if err := json.Unmarshal([]byte(line), &m); err != nil { + continue + } + if m.Type != "match" { + continue + } + matches = append(matches, filesystem.GrepMatch{ + Path: m.Data.Path.Text, + Line: m.Data.LineNumber, + Content: strings.TrimSuffix(m.Data.Lines.Text, "\n"), + }) + } + + return matches, nil +} + +// GlobInfo returns file information matching the glob pattern. +func (b *AIOSandboxBackend) GlobInfo(ctx context.Context, req *filesystem.GlobInfoRequest) ([]filesystem.FileInfo, error) { + searchPath := req.Path + if searchPath == "" { + searchPath = b.config.WorkDir + } + + resp, err := b.client.File.FindFiles(ctx, &sandboxsdk.FileFindRequest{ + Path: searchPath, + Glob: req.Pattern, + }) + if err != nil { + return nil, fmt.Errorf("find files failed: %w", err) + } + + data := resp.GetData() + if data == nil { + return nil, fmt.Errorf("empty response data") + } + + files := data.GetFiles() + result := make([]filesystem.FileInfo, 0, len(files)) + for _, f := range files { + result = append(result, filesystem.FileInfo{ + Path: f, + }) + } + + return result, nil +} + +// Write creates or updates file content. +func (b *AIOSandboxBackend) Write(ctx context.Context, req *filesystem.WriteRequest) error { + _, err := b.client.File.WriteFile(ctx, &sandboxsdk.FileWriteRequest{ + File: req.FilePath, + Content: req.Content, + }) + if err != nil { + return fmt.Errorf("write file failed: %w", err) + } + + return nil +} + +// Edit replaces string occurrences in a file using the sandbox's str_replace_editor. +func (b *AIOSandboxBackend) Edit(ctx context.Context, req *filesystem.EditRequest) error { + if req.OldString == "" { + return fmt.Errorf("old_string cannot be empty") + } + if req.OldString == req.NewString { + return fmt.Errorf("old_string and new_string must be different") + } + + editorReq := &sandboxsdk.StrReplaceEditorRequest{ + Command: sandboxsdk.CommandStrReplace, + Path: req.FilePath, + OldStr: &req.OldString, + NewStr: &req.NewString, + } + + if req.ReplaceAll { + mode := sandboxsdk.StrReplaceEditorRequestReplaceModeAll + editorReq.ReplaceMode = &mode + } + + _, err := b.client.File.StrReplaceEditor(ctx, editorReq) + if err != nil { + return fmt.Errorf("edit file failed: %w", err) + } + + return nil +} + +// unixToISO8601 converts a unix timestamp string to ISO 8601 format. +// Falls back to returning the original string if parsing fails. +func unixToISO8601(s string) string { + ts, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return s + } + return time.Unix(ts, 0).UTC().Format(time.RFC3339) +} + +// Execute runs a shell command in the sandbox. +// This implements the filesystem.Shell interface. +func (b *AIOSandboxBackend) Execute(ctx context.Context, req *filesystem.ExecuteRequest) (*filesystem.ExecuteResponse, error) { + sdkReq := &sandboxsdk.ShellExecRequest{ + Command: req.Command, + ExecDir: &b.config.WorkDir, + } + + resp, err := b.client.Shell.ExecCommand(ctx, sdkReq) + if err != nil { + return nil, fmt.Errorf("execute command failed: %w", err) + } + + data := resp.GetData() + if data == nil { + return nil, fmt.Errorf("empty response data") + } + + var output string + if data.Output != nil { + output = *data.Output + } + + result := &filesystem.ExecuteResponse{ + Output: output, + ExitCode: data.ExitCode, + } + + return result, nil +} + +// Compile-time check that AIOSandboxBackend implements filesystem.Backend interface +var _ filesystem.Backend = (*AIOSandboxBackend)(nil) + +// Compile-time check that AIOSandboxBackend implements filesystem.Shell interface +var _ filesystem.Shell = (*AIOSandboxBackend)(nil) diff --git a/adk/middleware/aiosandbox-backend/main.go b/adk/middleware/aiosandbox-backend/main.go new file mode 100644 index 00000000..0cc6c7e8 --- /dev/null +++ b/adk/middleware/aiosandbox-backend/main.go @@ -0,0 +1,165 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// This example demonstrates how to implement a custom filesystem.Backend +// and use it with filesystem.NewMiddleware to provide file system tools +// to an agent running in a remote AIO Sandbox environment. +// +// It supports two modes: +// 1. Direct mode: Set AIO_SANDBOX_BASE_URL with faasInstanceName to use an existing sandbox. +// 2. Managed mode: Set VOLC_ACCESSKEY, VOLC_SECRETKEY, and VEFAAS_FUNCTION_ID +// to automatically create and cleanup a sandbox via veFaaS API. +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/adk/middlewares/filesystem" + "github.com/cloudwego/eino/adk/prebuilt/deep" + "github.com/cloudwego/eino/schema" + + "github.com/cloudwego/eino-examples/adk/common/model" + "github.com/cloudwego/eino-examples/adk/common/prints" +) + +func main() { + ctx := context.Background() + + baseURL, cleanup, err := resolveBaseURL() + if err != nil { + log.Fatal(err) + } + if cleanup != nil { + defer cleanup() + } + + token := os.Getenv("AIO_SANDBOX_TOKEN") // optional + + // Create custom filesystem backend using AIO Sandbox + backend, err := NewAIOSandboxBackend(ctx, &AIOSandboxBackendConfig{ + BaseURL: baseURL, + Token: token, + WorkDir: "/tmp", + }) + if err != nil { + log.Fatal(err) + } + + // Create filesystem middleware with custom backend + // This automatically provides: ls, read_file, write_file, edit_file, glob, grep, execute tools + fsMW, err := filesystem.NewMiddleware(ctx, &filesystem.Config{ + Backend: backend, + Shell: backend, + }) + if err != nil { + log.Fatal(err) + } + + // Create chat model + cm := model.NewChatModel() + + // Create agent with the filesystem middleware + agent, err := deep.New(ctx, &deep.Config{ + Name: "FileAgent", + Description: "An agent that can work with files in a remote sandbox", + ChatModel: cm, + Middlewares: []adk.AgentMiddleware{fsMW}, + }) + if err != nil { + log.Fatal(err) + } + + // Run the agent + runner := adk.NewRunner(ctx, adk.RunnerConfig{ + Agent: agent, + EnableStreaming: true, + }) + + // Example: Test all filesystem tools (ls, read_file, write_file, edit_file, glob, grep, execute) + query := schema.UserMessage(`Please test all filesystem tools: +1. execute: run "echo Hello" +2. write_file: create /tmp/test.txt with "Hello World" +3. read_file: read /tmp/test.txt +4. edit_file: replace "Hello" with "Hi" in /tmp/test.txt +5. ls: list /tmp +6. glob: find *.txt in /tmp +7. grep: search "World" in /tmp/*.txt (use glob parameter "*.txt" to limit search scope) +8. execute: run "cat /tmp/test.txt"`) + + fmt.Println("Query:", query.Content) + fmt.Println() + + iter := runner.Run(ctx, []*schema.Message{query}) + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Err != nil { + log.Fatal(event.Err) + } + prints.Event(event) + } +} + +// resolveBaseURL determines the sandbox base URL and optional cleanup function. +// In managed mode, it creates a sandbox via veFaaS API and returns a cleanup function +// that kills the sandbox when done. +func resolveBaseURL() (baseURL string, cleanup func(), err error) { + // Direct mode: use provided base URL (with faasInstanceName query param) + if u := os.Getenv("AIO_SANDBOX_BASE_URL"); u != "" { + return u, nil, nil + } + + // Managed mode: create sandbox via veFaaS control plane + ak := os.Getenv("VOLC_ACCESSKEY") + sk := os.Getenv("VOLC_SECRETKEY") + functionID := os.Getenv("VEFAAS_FUNCTION_ID") + gatewayURL := os.Getenv("VEFAAS_GATEWAY_URL") + + if ak == "" || sk == "" || functionID == "" || gatewayURL == "" { + return "", nil, fmt.Errorf("either AIO_SANDBOX_BASE_URL, or VOLC_ACCESSKEY + VOLC_SECRETKEY + VEFAAS_FUNCTION_ID + VEFAAS_GATEWAY_URL must be set") + } + + mgr, err := NewSandboxManager(&SandboxManagerConfig{ + AccessKey: ak, + SecretKey: sk, + FunctionID: functionID, + }) + if err != nil { + return "", nil, fmt.Errorf("failed to create sandbox manager: %w", err) + } + + sandboxID, err := mgr.CreateSandbox() + if err != nil { + return "", nil, fmt.Errorf("failed to create sandbox: %w", err) + } + fmt.Printf("Created sandbox: %s\n", sandboxID) + + baseURL = mgr.DataPlaneBaseURL(gatewayURL, sandboxID) + cleanup = func() { + fmt.Printf("Killing sandbox: %s\n", sandboxID) + if err := mgr.KillSandbox(sandboxID); err != nil { + log.Printf("Warning: failed to kill sandbox: %v", err) + } + } + + return baseURL, cleanup, nil +} diff --git a/adk/middleware/aiosandbox-backend/sandbox.go b/adk/middleware/aiosandbox-backend/sandbox.go new file mode 100644 index 00000000..1f1ccfa3 --- /dev/null +++ b/adk/middleware/aiosandbox-backend/sandbox.go @@ -0,0 +1,127 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package main + +import ( + "fmt" + "net/url" + + "github.com/volcengine/volcengine-go-sdk/service/vefaas" + "github.com/volcengine/volcengine-go-sdk/volcengine" + "github.com/volcengine/volcengine-go-sdk/volcengine/credentials" + "github.com/volcengine/volcengine-go-sdk/volcengine/session" +) + +// SandboxManagerConfig is the configuration for the veFaaS sandbox lifecycle manager. +type SandboxManagerConfig struct { + // AccessKey is the Volcengine AK. Required. + AccessKey string + // SecretKey is the Volcengine SK. Required. + SecretKey string + // Region is the Volcengine region. Default: "cn-beijing". + Region string + // FunctionID is the veFaaS function ID. Required. + FunctionID string +} + +// SandboxManager manages sandbox lifecycle via the Volcengine veFaaS control plane API. +type SandboxManager struct { + client *vefaas.VEFAAS + functionID string +} + +// NewSandboxManager creates a new sandbox lifecycle manager. +func NewSandboxManager(config *SandboxManagerConfig) (*SandboxManager, error) { + if config == nil { + return nil, fmt.Errorf("config is required") + } + if config.AccessKey == "" || config.SecretKey == "" { + return nil, fmt.Errorf("AccessKey and SecretKey are required") + } + if config.FunctionID == "" { + return nil, fmt.Errorf("FunctionID is required") + } + + region := config.Region + if region == "" { + region = "cn-beijing" + } + + sess, err := session.NewSession(volcengine.NewConfig(). + WithCredentials(credentials.NewStaticCredentials(config.AccessKey, config.SecretKey, "")). + WithRegion(region). + WithMaxRetries(0)) + if err != nil { + return nil, fmt.Errorf("failed to create session: %w", err) + } + + return &SandboxManager{ + client: vefaas.New(sess), + functionID: config.FunctionID, + }, nil +} + +// CreateSandbox creates a new sandbox instance and returns its ID. +func (m *SandboxManager) CreateSandbox() (string, error) { + resp, err := m.client.CreateSandbox(&vefaas.CreateSandboxInput{ + FunctionId: volcengine.String(m.functionID), + }) + if err != nil { + return "", fmt.Errorf("create sandbox failed: %w", err) + } + if resp.SandboxId == nil { + return "", fmt.Errorf("create sandbox returned empty ID") + } + return *resp.SandboxId, nil +} + +// DescribeSandbox returns the status of the specified sandbox. +func (m *SandboxManager) DescribeSandbox(sandboxID string) (*vefaas.DescribeSandboxOutput, error) { + resp, err := m.client.DescribeSandbox(&vefaas.DescribeSandboxInput{ + FunctionId: volcengine.String(m.functionID), + SandboxId: volcengine.String(sandboxID), + }) + if err != nil { + return nil, fmt.Errorf("describe sandbox failed: %w", err) + } + return resp, nil +} + +// KillSandbox terminates the specified sandbox. +func (m *SandboxManager) KillSandbox(sandboxID string) error { + _, err := m.client.KillSandbox(&vefaas.KillSandboxInput{ + FunctionId: volcengine.String(m.functionID), + SandboxId: volcengine.String(sandboxID), + }) + if err != nil { + return fmt.Errorf("kill sandbox failed: %w", err) + } + return nil +} + +// DataPlaneBaseURL returns the data plane base URL for the given sandbox, +// which can be passed to NewAIOSandboxBackend as BaseURL. +func (m *SandboxManager) DataPlaneBaseURL(gatewayURL, sandboxID string) string { + u, err := url.Parse(gatewayURL) + if err != nil { + return fmt.Sprintf("%s?faasInstanceName=%s", gatewayURL, url.QueryEscape(sandboxID)) + } + q := u.Query() + q.Set("faasInstanceName", sandboxID) + u.RawQuery = q.Encode() + return u.String() +} diff --git a/go.mod b/go.mod index 8a28a85d..24c24a10 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,13 @@ module github.com/cloudwego/eino-examples go 1.24.7 require ( + github.com/agent-infra/sandbox-sdk-go v0.0.3 github.com/alicebob/miniredis/v2 v2.35.0 github.com/bytedance/sonic v1.15.0 github.com/chromedp/chromedp v0.9.5 - github.com/cloudwego/eino v0.8.0 - github.com/cloudwego/eino-ext/adk/backend/local v0.2.1 - github.com/cloudwego/eino-ext/callbacks/cozeloop v0.2.0 + github.com/cloudwego/eino v0.8.1 + github.com/cloudwego/eino-ext/adk/backend/local v0.2.3 + github.com/cloudwego/eino-ext/callbacks/cozeloop v0.1.6 github.com/cloudwego/eino-ext/components/document/parser/html v0.0.0-20251117090452-bd6375a0b3cf github.com/cloudwego/eino-ext/components/document/parser/pdf v0.0.0-20251117090452-bd6375a0b3cf github.com/cloudwego/eino-ext/components/model/ark v0.1.65 @@ -16,7 +17,7 @@ require ( github.com/cloudwego/eino-ext/components/model/ollama v0.1.8 github.com/cloudwego/eino-ext/components/model/openai v0.1.8 github.com/cloudwego/eino-ext/components/retriever/volc_vikingdb v0.0.0-20251120060928-25485ef519b5 - github.com/cloudwego/eino-ext/components/tool/commandline v0.0.0-20251117090452-bd6375a0b3cf + github.com/cloudwego/eino-ext/components/tool/commandline v0.0.0-20260313050455-88e279b3b32f github.com/cloudwego/eino-ext/components/tool/duckduckgo/v2 v2.0.0-20251117090452-bd6375a0b3cf github.com/cloudwego/eino-ext/components/tool/mcp/officialmcp v0.1.0 github.com/cloudwego/eino-ext/devops v0.1.8 diff --git a/go.sum b/go.sum index 06e01b30..0541febf 100644 --- a/go.sum +++ b/go.sum @@ -49,6 +49,8 @@ github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMx github.com/Shopify/toxiproxy/v2 v2.1.6-0.20210914104332-15ea381dcdae/go.mod h1:/cvHQkZ1fst0EmZnA5dFtiQdWCNCFYzb+uE2vqVgvx0= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= +github.com/agent-infra/sandbox-sdk-go v0.0.3 h1:RUDol4LX2txAIhp1iVaMsvRybW/Xi90lIlyqTysN+3Q= +github.com/agent-infra/sandbox-sdk-go v0.0.3/go.mod h1:LcR1ZvwCSafPmNj1+RA6myQRMHWs1DZL7HoPMFJGSDo= github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -129,12 +131,12 @@ github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5P github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= -github.com/cloudwego/eino v0.8.0 h1:DLbrgEAloA+l7aR2qim7qQocQB48DjPrb8LzG3PYMHY= -github.com/cloudwego/eino v0.8.0/go.mod h1:+2N4nsMPxA6kGBHpH+75JuTfEcGprAMTdsZESrShKpU= -github.com/cloudwego/eino-ext/adk/backend/local v0.2.1 h1:sZ4f21SFzygzVXI6ppkkZom6JOibAjvS+YT2GZMqIy0= -github.com/cloudwego/eino-ext/adk/backend/local v0.2.1/go.mod h1:os5Tq5FuSoz/MLqAdZER3ip49Oef9prc0kVsKsPYO48= -github.com/cloudwego/eino-ext/callbacks/cozeloop v0.2.0 h1:KZ4HuOG/7xbx4bifxUL4zADTkeGwZ4vhqfUsIlFn12c= -github.com/cloudwego/eino-ext/callbacks/cozeloop v0.2.0/go.mod h1:nJf/6LvrW3pJlqa1Qk9wrh6SYIjjrZACoWknH8CNj1s= +github.com/cloudwego/eino v0.8.1 h1:ahwA/5KwCLdwuUn6qC5g1uoTNs6cP1G8iaYLoNEAd4Y= +github.com/cloudwego/eino v0.8.1/go.mod h1:+2N4nsMPxA6kGBHpH+75JuTfEcGprAMTdsZESrShKpU= +github.com/cloudwego/eino-ext/adk/backend/local v0.2.3 h1:kuyWFI6VJFP8xmYrTUNzMrIubDA7Ds79dLMJUXqeUcI= +github.com/cloudwego/eino-ext/adk/backend/local v0.2.3/go.mod h1:os5Tq5FuSoz/MLqAdZER3ip49Oef9prc0kVsKsPYO48= +github.com/cloudwego/eino-ext/callbacks/cozeloop v0.1.6 h1:gS4nAOpQQC5WItt1k32yjZt9O2UWMpnbgF6vkMQAWhg= +github.com/cloudwego/eino-ext/callbacks/cozeloop v0.1.6/go.mod h1:ZniRkgN+9FUFxtN60X7yzD6UOruqrKQusjrOiGcH4I8= github.com/cloudwego/eino-ext/components/document/parser/html v0.0.0-20251117090452-bd6375a0b3cf h1:Uwh3VT+xPrfDjM677dj1pSidCzBFoTrYlC274kEci5w= github.com/cloudwego/eino-ext/components/document/parser/html v0.0.0-20251117090452-bd6375a0b3cf/go.mod h1:DBwsPrdNxPeE3HNr5XyjjFreG4ypqCUjN0T2C2JSy6k= github.com/cloudwego/eino-ext/components/document/parser/pdf v0.0.0-20251117090452-bd6375a0b3cf h1:0KFSxuvFqs9dJ3Pu9Lk+4+Stt43I2u9eu3L4gDNcZ4A= @@ -149,8 +151,8 @@ github.com/cloudwego/eino-ext/components/model/openai v0.1.8 h1:uVCE8nNvbhD37xGF github.com/cloudwego/eino-ext/components/model/openai v0.1.8/go.mod h1:K6g2VgULehhJC5dgFdPW3u7gZNZ1p6DhnfA5UhkRpNY= github.com/cloudwego/eino-ext/components/retriever/volc_vikingdb v0.0.0-20251120060928-25485ef519b5 h1:HoXqcYm3x4eXk6i2HLBklvx8WeFW7/MxijpCsEBQLSE= github.com/cloudwego/eino-ext/components/retriever/volc_vikingdb v0.0.0-20251120060928-25485ef519b5/go.mod h1:He7AHJpLTs0MXKPx5JpI8MdYtLUcKhUAZwCsgNtaq6k= -github.com/cloudwego/eino-ext/components/tool/commandline v0.0.0-20251117090452-bd6375a0b3cf h1:LLyVGanJpHpSSiqsbRXkuSpdpc1UmuaUIEs/BiOQkH4= -github.com/cloudwego/eino-ext/components/tool/commandline v0.0.0-20251117090452-bd6375a0b3cf/go.mod h1:cMZb1KM71kM+2hTJwxLwXT+HC66HimZirDDA5Oo64Hw= +github.com/cloudwego/eino-ext/components/tool/commandline v0.0.0-20260313050455-88e279b3b32f h1:9yj8p4aRD3N9yaxzZ9dNOyDygWHVkFa/gbQhyEIsuWg= +github.com/cloudwego/eino-ext/components/tool/commandline v0.0.0-20260313050455-88e279b3b32f/go.mod h1:cMZb1KM71kM+2hTJwxLwXT+HC66HimZirDDA5Oo64Hw= github.com/cloudwego/eino-ext/components/tool/duckduckgo/v2 v2.0.0-20251117090452-bd6375a0b3cf h1:54xcNETtXP1gYieDuNyOIvzE4Mk/jOxjlqCERr4cxTk= github.com/cloudwego/eino-ext/components/tool/duckduckgo/v2 v2.0.0-20251117090452-bd6375a0b3cf/go.mod h1:Np0BXy/9hPRu3wCgn+ij6L7YsjFcybVzg1k7uYOXh0M= github.com/cloudwego/eino-ext/components/tool/mcp/officialmcp v0.1.0 h1:4haF2hnv7+t9JUc4E+6SHo7cRgpi+6hux+YeteT5CW8=