diff --git a/client-sdks/python-bridge/aster.py b/client-sdks/python-bridge/aster.py
new file mode 100644
index 0000000..0740f41
--- /dev/null
+++ b/client-sdks/python-bridge/aster.py
@@ -0,0 +1,341 @@
+"""
+Aster Bridge - Python SDK for Programmatic Tool Calling
+
+This module provides async functions to call Aster tools from Python code.
+It's designed to be injected into code execution environments automatically.
+
+Usage:
+ # Automatically injected by RuntimeManager
+ content = await Read(path="file.txt")
+ await Write(path="output.txt", content=content.upper())
+"""
+
+import aiohttp
+import asyncio
+import os
+import time
+from typing import Any, Dict, Optional
+
+
+class AsterBridgeError(Exception):
+ """Aster 桥接错误基类"""
+ pass
+
+
+class ToolExecutionError(AsterBridgeError):
+ """工具执行错误"""
+ def __init__(self, tool_name: str, error_msg: str):
+ self.tool_name = tool_name
+ self.error_msg = error_msg
+ super().__init__(f"Tool {tool_name} failed: {error_msg}")
+
+
+class NetworkError(AsterBridgeError):
+ """网络错误"""
+ pass
+
+
+class AsterBridge:
+ """
+ Aster 工具桥接客户端
+
+ 通过 HTTP API 调用 Go 侧的 Aster 工具
+ """
+
+ def __init__(
+ self,
+ base_url: Optional[str] = None,
+ max_retries: int = 3,
+ retry_delay: float = 0.5,
+ ):
+ """
+ 初始化桥接客户端
+
+ Args:
+ base_url: HTTP 桥接服务器地址,默认从环境变量 ASTER_BRIDGE_URL 获取
+ max_retries: 最大重试次数(默认3次)
+ retry_delay: 重试延迟秒数(默认0.5秒,指数退避)
+ """
+ self.base_url = base_url or os.environ.get(
+ "ASTER_BRIDGE_URL", "http://localhost:8080"
+ )
+ self.max_retries = max_retries
+ self.retry_delay = retry_delay
+ self._session: Optional[aiohttp.ClientSession] = None
+
+ async def _get_session(self) -> aiohttp.ClientSession:
+ """获取或创建 HTTP 会话"""
+ if self._session is None or self._session.closed:
+ self._session = aiohttp.ClientSession()
+ return self._session
+
+ async def call_tool(self, name: str, **kwargs) -> Any:
+ """
+ 调用工具(带重试逻辑)
+
+ Args:
+ name: 工具名称
+ **kwargs: 工具输入参数
+
+ Returns:
+ 工具执行结果
+
+ Raises:
+ ToolExecutionError: 工具执行失败
+ NetworkError: 网络错误(重试后仍失败)
+ """
+ last_error = None
+
+ for attempt in range(self.max_retries):
+ try:
+ session = await self._get_session()
+
+ async with session.post(
+ f"{self.base_url}/tools/call",
+ json={"tool": name, "input": kwargs},
+ timeout=aiohttp.ClientTimeout(total=60),
+ ) as resp:
+ # 检查 HTTP 状态码
+ if resp.status >= 500:
+ # 服务器错误,可以重试
+ error_text = await resp.text()
+ last_error = NetworkError(
+ f"Server error (HTTP {resp.status}): {error_text}"
+ )
+ if attempt < self.max_retries - 1:
+ await asyncio.sleep(self.retry_delay * (2 ** attempt))
+ continue
+ raise last_error
+
+ if resp.status >= 400:
+ # 客户端错误,不重试
+ error_text = await resp.text()
+ raise NetworkError(
+ f"Client error (HTTP {resp.status}): {error_text}"
+ )
+
+ # 解析响应
+ result = await resp.json()
+
+ # 检查工具执行结果
+ if not result.get("success"):
+ error_msg = result.get("error", "Unknown error")
+ raise ToolExecutionError(name, error_msg)
+
+ return result.get("result")
+
+ except aiohttp.ClientConnectorError as e:
+ # 连接错误,可以重试
+ last_error = NetworkError(
+ f"Connection error: {str(e)}. "
+ f"Is the bridge server running at {self.base_url}?"
+ )
+ if attempt < self.max_retries - 1:
+ await asyncio.sleep(self.retry_delay * (2 ** attempt))
+ continue
+ raise last_error
+
+ except aiohttp.ClientError as e:
+ # 其他网络错误,可以重试
+ last_error = NetworkError(f"Network error: {str(e)}")
+ if attempt < self.max_retries - 1:
+ await asyncio.sleep(self.retry_delay * (2 ** attempt))
+ continue
+ raise last_error
+
+ except ToolExecutionError:
+ # 工具执行错误,不重试,直接抛出
+ raise
+
+ except asyncio.TimeoutError:
+ # 超时错误,可以重试
+ last_error = NetworkError(f"Tool {name} timed out after 60 seconds")
+ if attempt < self.max_retries - 1:
+ await asyncio.sleep(self.retry_delay * (2 ** attempt))
+ continue
+ raise last_error
+
+ # 所有重试都失败
+ if last_error:
+ raise last_error
+ raise NetworkError(f"Failed to call tool {name} after {self.max_retries} attempts")
+
+ async def list_tools(self) -> list:
+ """
+ 列出所有可用工具
+
+ Returns:
+ 工具名称列表
+ """
+ session = await self._get_session()
+
+ async with session.get(f"{self.base_url}/tools/list") as resp:
+ data = await resp.json()
+ return data.get("tools", [])
+
+ async def get_tool_schema(self, name: str) -> Dict[str, Any]:
+ """
+ 获取工具的 Schema
+
+ Args:
+ name: 工具名称
+
+ Returns:
+ 工具 Schema 定义
+ """
+ session = await self._get_session()
+
+ async with session.get(
+ f"{self.base_url}/tools/schema",
+ params={"name": name},
+ ) as resp:
+ return await resp.json()
+
+ async def close(self):
+ """关闭 HTTP 会话"""
+ if self._session and not self._session.closed:
+ await self._session.close()
+
+ async def __aenter__(self):
+ """支持 async with 语法"""
+ return self
+
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
+ """支持 async with 语法"""
+ await self.close()
+
+
+# ========== 全局桥接实例 ==========
+# 在代码执行环境中,这个实例会被自动创建
+_bridge: Optional[AsterBridge] = None
+
+
+def _init_bridge(base_url: Optional[str] = None) -> AsterBridge:
+ """初始化全局桥接实例"""
+ global _bridge
+ if _bridge is None:
+ _bridge = AsterBridge(base_url)
+ return _bridge
+
+
+# ========== 工具函数 (会被动态生成) ==========
+# 以下函数会在代码注入时动态生成,这里提供类型提示
+
+
+async def Read(path: str) -> str:
+ """
+ 读取文件内容
+
+ Args:
+ path: 文件路径
+
+ Returns:
+ 文件内容
+ """
+ return await _bridge.call_tool("Read", path=path)
+
+
+async def Write(path: str, content: str) -> None:
+ """
+ 写入文件内容
+
+ Args:
+ path: 文件路径
+ content: 要写入的内容
+ """
+ await _bridge.call_tool("Write", path=path, content=content)
+
+
+async def Glob(pattern: str, path: str = ".") -> list:
+ """
+ 文件模式匹配
+
+ Args:
+ pattern: Glob 模式
+ path: 搜索路径
+
+ Returns:
+ 匹配的文件路径列表
+ """
+ return await _bridge.call_tool("Glob", pattern=pattern, path=path)
+
+
+async def Grep(pattern: str, path: str = ".", glob: Optional[str] = None) -> Any:
+ """
+ 文件内容搜索
+
+ Args:
+ pattern: 搜索模式 (正则表达式)
+ path: 搜索路径
+ glob: 文件过滤模式
+
+ Returns:
+ 搜索结果
+ """
+ params = {"pattern": pattern, "path": path}
+ if glob:
+ params["glob"] = glob
+ return await _bridge.call_tool("Grep", **params)
+
+
+async def Bash(command: str, timeout: Optional[int] = None) -> Dict[str, Any]:
+ """
+ 执行 Bash 命令
+
+ Args:
+ command: 要执行的命令
+ timeout: 超时时间(秒)
+
+ Returns:
+ 执行结果 (包含 stdout, stderr, exit_code)
+ """
+ params = {"command": command}
+ if timeout is not None:
+ params["timeout"] = timeout
+ return await _bridge.call_tool("Bash", **params)
+
+
+# ========== 工具函数生成器 ==========
+def _generate_tool_function(tool_name: str):
+ """
+ 动态生成工具函数
+
+ Args:
+ tool_name: 工具名称
+
+ Returns:
+ async 工具函数
+ """
+ async def tool_func(**kwargs):
+ return await _bridge.call_tool(tool_name, **kwargs)
+
+ tool_func.__name__ = tool_name
+ tool_func.__doc__ = f"Call {tool_name} tool"
+ return tool_func
+
+
+def inject_tools(tools: list, base_url: Optional[str] = None) -> Dict[str, Any]:
+ """
+ 注入工具到全局命名空间
+
+ 这个函数会在代码执行前被 RuntimeManager 调用,
+ 将所有可用的工具注入到 globals() 中
+
+ Args:
+ tools: 工具名称列表
+ base_url: 桥接服务器地址
+
+ Returns:
+ 注入的工具函数字典
+ """
+ # 初始化桥接
+ global _bridge
+ _bridge = _init_bridge(base_url)
+
+ # 生成工具函数
+ injected = {}
+ for tool_name in tools:
+ func = _generate_tool_function(tool_name)
+ injected[tool_name] = func
+
+ return injected
diff --git a/docs/content/05.tools/5.ptc/.navigation.yml b/docs/content/05.tools/5.ptc/.navigation.yml
new file mode 100644
index 0000000..6efaa4e
--- /dev/null
+++ b/docs/content/05.tools/5.ptc/.navigation.yml
@@ -0,0 +1,2 @@
+title: Programmatic Tool Calling
+icon: i-lucide-code
diff --git a/docs/content/05.tools/5.ptc/1.quickstart.md b/docs/content/05.tools/5.ptc/1.quickstart.md
new file mode 100644
index 0000000..a80b0f5
--- /dev/null
+++ b/docs/content/05.tools/5.ptc/1.quickstart.md
@@ -0,0 +1,170 @@
+---
+title: 快速开始
+description: 5分钟学会使用 Programmatic Tool Calling
+---
+
+# PTC 快速开始
+
+## 什么是 PTC?
+
+Programmatic Tool Calling (PTC) 让 LLM 生成的 Python 代码可以直接调用 Aster 工具,实现更强大的编程能力。
+
+::code-group
+```python [LLM 生成的代码]
+# 搜索文件
+files = await Glob(pattern="*.go", path=".")
+
+# 读取文件
+content = await Read(path=files[0])
+
+# 处理并写入
+result = content.upper()
+await Write(path="output.txt", content=result)
+```
+::
+
+## 前置条件
+
+::steps
+### 安装 Python 依赖
+
+```bash
+pip install aiohttp
+```
+
+### 设置 API Key
+
+```bash
+export ANTHROPIC_API_KEY="your-api-key"
+```
+::
+
+## 基础示例
+
+```go
+package main
+
+import (
+ "context"
+ "github.com/astercloud/aster/pkg/provider"
+ "github.com/astercloud/aster/pkg/tools"
+ "github.com/astercloud/aster/pkg/tools/bridge"
+ "github.com/astercloud/aster/pkg/tools/builtin"
+)
+
+func main() {
+ // 1. 创建工具生态系统
+ registry := tools.NewRegistry()
+ builtin.RegisterAll(registry)
+
+ toolBridge := bridge.NewToolBridge(registry)
+ codeExec := builtin.NewCodeExecuteToolWithBridge(toolBridge)
+
+ // 2. 配置 PTC 工具
+ tools := []provider.ToolSchema{
+ {
+ Name: "CodeExecute",
+ Description: codeExec.Description(),
+ InputSchema: codeExec.InputSchema(),
+ AllowedCallers: []string{"direct"},
+ },
+ {
+ Name: "Read",
+ Description: "读取文件内容",
+ InputSchema: map[string]any{/*...*/},
+ // 关键: 允许在 Python 代码中调用
+ AllowedCallers: []string{"direct", "code_execution_20250825"},
+ },
+ }
+
+ // 3. 使用 Provider
+ provider, _ := provider.NewAnthropicProvider(&types.ModelConfig{
+ Provider: "anthropic",
+ Model: "claude-3-5-sonnet-20241022",
+ APIKey: os.Getenv("ANTHROPIC_API_KEY"),
+ })
+
+ // 4. 执行任务
+ response, _ := provider.Complete(context.Background(),
+ []types.Message{{
+ Role: types.RoleUser,
+ Content: "用 Python 读取 README.md 并统计单词数",
+ }},
+ &provider.StreamOptions{Tools: tools},
+ )
+}
+```
+
+## 配置 AllowedCallers
+
+::alert{type="info"}
+`AllowedCallers` 字段控制工具在哪些上下文中可以被调用。
+::
+
+::code-group
+```go [仅 LLM 调用]
+AllowedCallers: []string{"direct"}
+```
+
+```go [仅 Python 调用]
+AllowedCallers: []string{"code_execution_20250825"}
+```
+
+```go [两者都允许 (推荐)]
+AllowedCallers: []string{"direct", "code_execution_20250825"}
+```
+::
+
+## 可用工具
+
+| 工具 | 功能 | Python 示例 |
+|------|------|-------------|
+| `Read` | 读取文件 | `await Read(path="file.txt")` |
+| `Write` | 写入文件 | `await Write(path="out.txt", content="data")` |
+| `Glob` | 文件搜索 | `await Glob(pattern="*.go")` |
+| `Grep` | 内容搜索 | `await Grep(pattern="TODO")` |
+| `Bash` | 执行命令 | `await Bash(command="ls -la")` |
+
+## 本地测试
+
+无需 API Key,直接测试 PTC 基础设施:
+
+```bash
+cd examples/ptc/local-test
+go run main.go
+```
+
+::alert{type="success"}
+✅ 如果看到测试通过,说明 PTC 配置正确!
+::
+
+## 下一步
+
+::card-group
+::card
+---
+title: 完整文档
+icon: i-lucide-book-open
+to: /tools/ptc
+---
+深入了解 PTC 架构和高级用法
+::
+
+::card
+---
+title: 示例程序
+icon: i-lucide-code
+to: /examples
+---
+查看更多实战示例
+::
+
+::card
+---
+title: 故障排除
+icon: i-lucide-help-circle
+to: /tools/ptc#常见问题
+---
+解决常见问题
+::
+::
diff --git a/docs/content/05.tools/5.ptc/2.advanced.md b/docs/content/05.tools/5.ptc/2.advanced.md
new file mode 100644
index 0000000..aa21323
--- /dev/null
+++ b/docs/content/05.tools/5.ptc/2.advanced.md
@@ -0,0 +1,412 @@
+---
+title: 高级用法
+description: PTC 的高级特性和最佳实践
+---
+
+# PTC 高级用法
+
+## 错误处理
+
+### Python 代码中的异常处理
+
+```python
+import asyncio
+
+async def main():
+ try:
+ # 尝试读取文件
+ content = await Read(path="config.json")
+ except Exception as e:
+ print(f"读取失败: {e}")
+ # 使用默认配置
+ content = '{"default": true}'
+
+ # 继续处理
+ data = json.loads(content)
+ print(f"配置: {data}")
+
+asyncio.run(main())
+```
+
+### 自定义重试逻辑
+
+```python
+async def retry_read(path, max_retries=3):
+ for attempt in range(max_retries):
+ try:
+ return await Read(path=path)
+ except Exception as e:
+ if attempt == max_retries - 1:
+ raise
+ await asyncio.sleep(0.5 * (2 ** attempt))
+
+content = await retry_read("important.txt")
+```
+
+## 性能优化
+
+### 并发调用
+
+::code-group
+```python [串行执行 (慢)]
+# 耗时: 3秒 (1秒 × 3)
+file1 = await Read(path="a.txt")
+file2 = await Read(path="b.txt")
+file3 = await Read(path="c.txt")
+```
+
+```python [并发执行 (快)]
+# 耗时: ~1秒
+import asyncio
+
+files = await asyncio.gather(
+ Read(path="a.txt"),
+ Read(path="b.txt"),
+ Read(path="c.txt"),
+)
+```
+::
+
+### 批量处理
+
+```python
+async def process_files(pattern):
+ # 1. 查找所有文件
+ files = await Glob(pattern=pattern)
+
+ # 2. 并发读取 (限制并发数)
+ sem = asyncio.Semaphore(10) # 最多10个并发
+
+ async def read_with_limit(path):
+ async with sem:
+ return await Read(path=path)
+
+ contents = await asyncio.gather(
+ *[read_with_limit(f) for f in files]
+ )
+
+ # 3. 处理结果
+ return contents
+
+# 使用
+results = await process_files("*.go")
+```
+
+### 结果缓存
+
+```python
+_cache = {}
+
+async def cached_read(path):
+ """带缓存的文件读取"""
+ if path not in _cache:
+ _cache[path] = await Read(path=path)
+ return _cache[path]
+
+# 多次调用只会读取一次
+content1 = await cached_read("data.json")
+content2 = await cached_read("data.json") # 从缓存返回
+```
+
+## 复杂数据流
+
+### Pipeline 模式
+
+```python
+async def pipeline():
+ # 1. 数据源
+ files = await Glob(pattern="*.log")
+
+ # 2. 转换
+ async def extract_errors(file):
+ content = await Read(path=file)
+ return [
+ line for line in content.split('\n')
+ if 'ERROR' in line
+ ]
+
+ # 3. 聚合
+ all_errors = []
+ for file in files:
+ errors = await extract_errors(file)
+ all_errors.extend(errors)
+
+ # 4. 输出
+ report = '\n'.join(all_errors)
+ await Write(path="error_report.txt", content=report)
+
+ return len(all_errors)
+
+error_count = await pipeline()
+print(f"发现 {error_count} 个错误")
+```
+
+### MapReduce 模式
+
+```python
+async def mapreduce():
+ # Map 阶段: 并发处理每个文件
+ files = await Glob(pattern="data/*.csv")
+
+ async def map_file(path):
+ content = await Read(path=path)
+ lines = content.split('\n')
+ return len([l for l in lines if l.strip()])
+
+ line_counts = await asyncio.gather(
+ *[map_file(f) for f in files]
+ )
+
+ # Reduce 阶段: 聚合结果
+ total_lines = sum(line_counts)
+
+ return {
+ 'files': len(files),
+ 'total_lines': total_lines,
+ 'avg_lines': total_lines / len(files) if files else 0
+ }
+```
+
+## 工具组合
+
+### 跨工具协作
+
+```python
+async def analyze_codebase():
+ # 1. 使用 Glob 找到所有文件
+ go_files = await Glob(pattern="**/*.go", path=".")
+
+ # 2. 使用 Grep 搜索 TODO 注释
+ todos = await Grep(
+ pattern="TODO|FIXME",
+ path=".",
+ glob="*.go"
+ )
+
+ # 3. 使用 Read 读取关键文件
+ main_content = await Read(path="main.go")
+
+ # 4. 使用 Bash 获取 Git 信息
+ git_info = await Bash(command="git log -1 --oneline")
+
+ # 5. 组合结果
+ report = f"""
+# 代码库分析报告
+
+- Go 文件数: {len(go_files)}
+- 待办事项: {len(todos)} 个
+- 最新提交: {git_info}
+- 主文件大小: {len(main_content)} 字节
+"""
+
+ # 6. 使用 Write 保存报告
+ await Write(path="ANALYSIS.md", content=report)
+```
+
+## 自定义工具支持
+
+### 注册自定义工具
+
+```go
+// Go 侧: 注册自定义工具
+type MyCustomTool struct{}
+
+func (t *MyCustomTool) Name() string {
+ return "CustomAnalyze"
+}
+
+func (t *MyCustomTool) Execute(ctx context.Context, input map[string]any, tc *tools.ToolContext) (any, error) {
+ // 自定义逻辑
+ data := input["data"].(string)
+ result := analyze(data)
+ return map[string]any{
+ "result": result,
+ }, nil
+}
+
+// 注册到 registry
+registry.Register("CustomAnalyze", func(config map[string]any) (tools.Tool, error) {
+ return &MyCustomTool{}, nil
+})
+
+// 配置 AllowedCallers
+toolSchema := provider.ToolSchema{
+ Name: "CustomAnalyze",
+ AllowedCallers: []string{"direct", "code_execution_20250825"},
+ // ...
+}
+```
+
+```python
+# Python 侧: 使用自定义工具
+result = await CustomAnalyze(data="sample input")
+print(result)
+```
+
+## 调试技巧
+
+### 启用详细日志
+
+```go
+import "log"
+
+// 在 Go 程序中
+log.SetFlags(log.LstdFlags | log.Lshortfile)
+
+// 会输出:
+// - HTTP 服务器启动信息
+// - 工具调用详情
+// - Python 执行日志
+```
+
+### Python 代码调试
+
+```python
+import sys
+
+async def debug_wrapper():
+ print("=== 调试信息 ===", file=sys.stderr)
+
+ # 打印可用工具
+ print(f"可用工具: {globals().keys()}", file=sys.stderr)
+
+ # 执行主逻辑
+ try:
+ result = await Read(path="test.txt")
+ print(f"读取成功: {len(result)} 字节", file=sys.stderr)
+ return result
+ except Exception as e:
+ print(f"错误: {e}", file=sys.stderr)
+ raise
+
+await debug_wrapper()
+```
+
+### 测试 HTTP 桥接
+
+```bash
+# 测试服务器健康
+curl http://localhost:8080/health
+
+# 列出可用工具
+curl http://localhost:8080/tools/list
+
+# 获取工具 Schema
+curl "http://localhost:8080/tools/schema?name=Read"
+
+# 调用工具
+curl -X POST http://localhost:8080/tools/call \
+ -H "Content-Type: application/json" \
+ -d '{"tool": "Read", "input": {"path": "README.md"}}'
+```
+
+## 安全最佳实践
+
+### 限制可调用工具
+
+```go
+// 仅允许安全的只读工具在 Python 中调用
+safeTools := []string{"Read", "Glob", "Grep"}
+
+for _, toolName := range safeTools {
+ tool, _ := registry.Create(toolName, nil)
+ toolSchemas = append(toolSchemas, provider.ToolSchema{
+ Name: toolName,
+ AllowedCallers: []string{"direct", "code_execution_20250825"},
+ // ...
+ })
+}
+
+// 危险工具只允许 LLM 直接调用
+dangerousTools := []string{"Write", "Bash"}
+for _, toolName := range dangerousTools {
+ tool, _ := registry.Create(toolName, nil)
+ toolSchemas = append(toolSchemas, provider.ToolSchema{
+ Name: toolName,
+ AllowedCallers: []string{"direct"}, // 不能在 Python 中调用
+ // ...
+ })
+}
+```
+
+### 沙箱隔离
+
+```go
+// 在 Docker 容器中运行 Python 代码
+config := &bridge.RuntimeConfig{
+ WorkDir: "/tmp/sandbox",
+ Env: map[string]string{
+ "PYTHONPATH": "/app",
+ },
+}
+
+runtime := bridge.NewPythonRuntime(config)
+```
+
+### 超时保护
+
+```go
+// 设置严格的超时
+config := &bridge.RuntimeConfig{
+ Timeout: 10 * time.Second, // Python 执行超时
+}
+
+// HTTP 调用也有 60秒 超时(SDK 内置)
+```
+
+## 性能监控
+
+### 自定义指标
+
+```go
+type metrics struct {
+ toolCalls int64
+ totalLatency time.Duration
+ mu sync.Mutex
+}
+
+var m metrics
+
+// 在工具调用前后记录
+start := time.Now()
+result, _ := tool.Execute(ctx, input, tc)
+latency := time.Since(start)
+
+m.mu.Lock()
+m.toolCalls++
+m.totalLatency += latency
+m.mu.Unlock()
+
+log.Printf("平均延迟: %v", m.totalLatency/time.Duration(m.toolCalls))
+```
+
+## 参考资料
+
+::card-group
+::card
+---
+title: Anthropic PTC 文档
+icon: i-lucide-external-link
+to: https://docs.anthropic.com/en/docs/build-with-claude/tool-use#programmatic-tool-use-beta
+---
+官方协议规范
+::
+
+::card
+---
+title: Python asyncio 文档
+icon: i-lucide-external-link
+to: https://docs.python.org/3/library/asyncio.html
+---
+异步编程指南
+::
+
+::card
+---
+title: aiohttp 文档
+icon: i-lucide-external-link
+to: https://docs.aiohttp.org/
+---
+HTTP 客户端库
+::
+::
diff --git a/docs/content/05.tools/5.ptc/3.architecture.md b/docs/content/05.tools/5.ptc/3.architecture.md
new file mode 100644
index 0000000..2176270
--- /dev/null
+++ b/docs/content/05.tools/5.ptc/3.architecture.md
@@ -0,0 +1,469 @@
+---
+title: 架构设计
+description: PTC 的技术架构和实现原理
+---
+
+# PTC 架构设计
+
+## 整体架构
+
+```mermaid
+graph TB
+ User[用户程序] --> Provider[Anthropic Provider]
+ Provider --> LLM[Claude LLM]
+ LLM --> CodeGen[生成 Python 代码]
+ CodeGen --> CodeExec[CodeExecute 工具]
+ CodeExec --> Bridge[HTTP Bridge Server]
+ Bridge --> Tools[Aster 工具]
+
+ CodeExec --> Runtime[Python Runtime]
+ Runtime --> SDK[注入 SDK]
+ SDK --> HTTP[HTTP 调用]
+ HTTP --> Bridge
+
+ style LLM fill:#f9f,stroke:#333
+ style Bridge fill:#bbf,stroke:#333
+ style Tools fill:#bfb,stroke:#333
+```
+
+## 核心组件
+
+### 1. 协议扩展
+
+#### ToolSchema 扩展
+
+```go
+type ToolSchema struct {
+ Name string
+ Description string
+ InputSchema map[string]any
+
+ // PTC 扩展
+ AllowedCallers []string `json:"allowed_callers,omitempty"`
+}
+```
+
+**字段说明:**
+- `AllowedCallers`: 指定工具可调用的上下文
+ - `"direct"`: LLM 直接调用
+ - `"code_execution_20250825"`: Python 代码中调用
+
+#### ToolUseBlock 扩展
+
+```go
+type ToolUseBlock struct {
+ ID string
+ Name string
+ Input map[string]any
+
+ // PTC 扩展
+ Caller *ToolCaller `json:"caller,omitempty"`
+}
+
+type ToolCaller struct {
+ Type string // "direct" | "code_execution_20250825"
+ ToolID string // CodeExecute 工具的 ID
+}
+```
+
+### 2. HTTP 桥接服务器
+
+::code-group
+```go [服务器实现]
+type HTTPBridgeServer struct {
+ bridge *ToolBridge
+ server *http.Server
+
+ // 性能优化
+ schemaCache *schemaCache
+}
+
+func NewHTTPBridgeServer(bridge *ToolBridge, addr string) *HTTPBridgeServer {
+ return &HTTPBridgeServer{
+ bridge: bridge,
+ server: &http.Server{
+ Addr: addr,
+ ReadTimeout: 30 * time.Second,
+ WriteTimeout: 30 * time.Second,
+ IdleTimeout: 120 * time.Second, // Keep-Alive
+ },
+ schemaCache: newSchemaCache(5 * time.Minute),
+ }
+}
+```
+
+```go [端点实现]
+// POST /tools/call
+func (s *HTTPBridgeServer) handleToolCall(w http.ResponseWriter, r *http.Request) {
+ var req ToolCallRequest
+ json.NewDecoder(r.Body).Decode(&req)
+
+ // 获取工具上下文
+ tc := s.getToolContext()
+
+ // 调用工具
+ result, _ := s.bridge.CallTool(ctx, req.Tool, req.Input, tc)
+
+ // 返回结果
+ s.sendJSON(w, &ToolCallResponse{
+ Success: result.Success,
+ Result: result.Result,
+ Error: result.Error,
+ })
+}
+```
+::
+
+**API 端点:**
+
+| 端点 | 方法 | 功能 | 缓存 |
+|------|------|------|------|
+| `/tools/call` | POST | 调用工具 | ❌ |
+| `/tools/list` | GET | 列出工具 | ✅ |
+| `/tools/schema` | GET | 获取 Schema | ✅ 5分钟 |
+| `/health` | GET | 健康检查 | ❌ |
+
+### 3. Python SDK
+
+#### 核心类设计
+
+```python
+class AsterBridge:
+ def __init__(self, base_url, max_retries=3, retry_delay=0.5):
+ self.base_url = base_url
+ self.max_retries = max_retries
+ self.retry_delay = retry_delay
+ self._session = None
+
+ async def call_tool(self, name: str, **kwargs) -> Any:
+ # 重试逻辑
+ for attempt in range(self.max_retries):
+ try:
+ # HTTP 调用
+ async with session.post(...) as resp:
+ result = await resp.json()
+ return result["result"]
+ except aiohttp.ClientConnectorError:
+ # 网络错误: 重试
+ if attempt < self.max_retries - 1:
+ await asyncio.sleep(self.retry_delay * (2 ** attempt))
+ continue
+ raise
+ except ToolExecutionError:
+ # 工具错误: 不重试
+ raise
+```
+
+#### 错误层次
+
+```mermaid
+graph TD
+ AsterBridgeError[AsterBridgeError
基类]
+ AsterBridgeError --> ToolError[ToolExecutionError
工具执行错误]
+ AsterBridgeError --> NetError[NetworkError
网络错误]
+
+ ToolError --> NoRetry[不重试
直接抛出]
+ NetError --> Retry[自动重试
指数退避]
+
+ style NoRetry fill:#fcc
+ style Retry fill:#cfc
+```
+
+### 4. 运行时集成
+
+#### 代码注入流程
+
+```mermaid
+sequenceDiagram
+ participant RT as RuntimeManager
+ participant PR as PythonRuntime
+ participant Code as 用户代码
+
+ RT->>PR: Execute(code, input)
+ PR->>PR: wrapCode()
+
+ alt 无工具模式
+ PR->>Code: 简单包装
+ else PTC 模式
+ PR->>PR: 内联 SDK (100行)
+ PR->>PR: 生成工具函数
+ PR->>Code: async def _user_main()
+ PR->>Code: 包装用户代码
+ end
+
+ PR->>PR: 写入临时文件
+ PR->>PR: python3 temp.py
+ PR-->>RT: 返回结果
+```
+
+#### 生成代码结构
+
+```python
+# 1. 导入 (5行)
+import json, asyncio, aiohttp, sys, os
+
+# 2. 异常类 (10行)
+class _ToolExecutionError(Exception): pass
+class _NetworkError(Exception): pass
+
+# 3. 桥接类 (80行)
+class _AsterBridge:
+ # 完整实现包含重试逻辑
+
+# 4. 初始化 (5行)
+_bridge = _AsterBridge("http://localhost:8080")
+
+# 5. 工具函数生成 (10行)
+for _tool_name in ["Read", "Write", ...]:
+ globals()[_tool_name] = _create_tool_function(_bridge, _tool_name)
+
+# 6. 用户代码包装 (5行 + 用户代码)
+async def _user_main():
+ # [用户代码]
+ files = await Glob(pattern="*.go")
+
+# 7. 运行 (5行)
+if __name__ == "__main__":
+ asyncio.run(_user_main())
+ asyncio.run(_bridge.close())
+```
+
+**代码大小分析:**
+- SDK 基础代码: ~100 行
+- 用户代码: 可变
+- 总代码: ~100 + 用户代码行数
+
+### 5. 数据流
+
+#### 完整调用链
+
+```mermaid
+sequenceDiagram
+ autonumber
+
+ User->>Provider: Complete(messages)
+ Provider->>Anthropic: POST /v1/messages
+ Note over Provider,Anthropic: Tools + AllowedCallers
+
+ Anthropic-->>Provider: ToolUseBlock(CodeExecute)
+ Provider-->>User: Response
+
+ User->>CodeExec: Execute(language=python, code=...)
+ CodeExec->>CodeExec: ensureBridgeServer()
+
+ alt First Call
+ CodeExec->>Bridge: StartAsync()
+ Note over Bridge: 启动 HTTP 服务器
+ end
+
+ CodeExec->>Runtime: Execute(code)
+ Runtime->>Runtime: wrapCode() + 注入 SDK
+ Runtime->>Python: python3 temp.py
+
+ Python->>Bridge: POST /tools/call (Read)
+ Bridge->>ToolRegistry: CallTool("Read")
+ ToolRegistry-->>Bridge: Result
+ Bridge-->>Python: JSON Response
+
+ Python-->>Runtime: stdout/stderr
+ Runtime-->>CodeExec: ExecutionResult
+ CodeExec-->>User: Result
+```
+
+## 性能优化
+
+### 1. 连接复用
+
+```go
+// HTTP 服务器配置
+server := &http.Server{
+ IdleTimeout: 120 * time.Second, // Keep-Alive 2分钟
+}
+
+// Python SDK
+class AsterBridge:
+ async def _get_session(self):
+ # 复用同一个 aiohttp.ClientSession
+ if self._session is None or self._session.closed:
+ self._session = aiohttp.ClientSession()
+ return self._session
+```
+
+**优势:**
+- 减少 TCP 握手开销
+- 降低延迟 (1-2ms → 0.5ms)
+- 提升吞吐量 (2x)
+
+### 2. Schema 缓存
+
+```go
+type schemaCache struct {
+ entries map[string]*cacheEntry
+ ttl time.Duration
+ mu sync.RWMutex
+}
+
+func (c *schemaCache) get(key string) (any, bool) {
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+
+ entry, ok := c.entries[key]
+ if !ok || time.Since(entry.timestamp) > c.ttl {
+ return nil, false
+ }
+ return entry.data, true
+}
+```
+
+**性能提升:**
+- Schema 查询: O(n) → O(1)
+- 延迟降低: 10ms → 0.1ms
+- CPU 使用: 减少 90%
+
+### 3. 并发控制
+
+```python
+# 限制并发数避免资源耗尽
+sem = asyncio.Semaphore(10)
+
+async def call_with_limit(tool, **kwargs):
+ async with sem:
+ return await tool(**kwargs)
+
+# 批量调用
+results = await asyncio.gather(
+ *[call_with_limit(Read, path=f) for f in files]
+)
+```
+
+## 安全设计
+
+### 1. 沙箱隔离
+
+```go
+// 代码执行在受控环境中
+config := &RuntimeConfig{
+ WorkDir: "/tmp/sandbox", // 隔离目录
+ MaxOutput: 1 * 1024 * 1024, // 限制输出 1MB
+ Timeout: 30 * time.Second, // 超时保护
+}
+```
+
+### 2. 网络隔离
+
+```go
+// HTTP 服务器仅监听本地
+server := &http.Server{
+ Addr: "localhost:8080", // 不对外暴露
+}
+```
+
+### 3. 权限控制
+
+```go
+// AllowedCallers 精确控制
+toolSchemas := []ToolSchema{
+ {
+ Name: "Read",
+ AllowedCallers: []string{"direct", "code_execution_20250825"},
+ },
+ {
+ Name: "Bash",
+ AllowedCallers: []string{"direct"}, // 危险工具禁止 PTC
+ },
+}
+```
+
+## 扩展性
+
+### 多语言支持
+
+当前架构可以轻松扩展支持其他语言:
+
+```go
+// Node.js Runtime (未来)
+type NodeJSRuntime struct {
+ config *RuntimeConfig
+}
+
+func (r *NodeJSRuntime) wrapCode(code string) string {
+ return fmt.Sprintf(`
+const axios = require('axios');
+
+async function Read(params) {
+ const res = await axios.post('http://localhost:8080/tools/call', {
+ tool: 'Read',
+ input: params
+ });
+ return res.data.result;
+}
+
+%s
+`, code)
+}
+```
+
+### 分布式部署
+
+```mermaid
+graph LR
+ A[Agent 1] --> B[Bridge Server 1]
+ C[Agent 2] --> D[Bridge Server 2]
+ E[Agent 3] --> F[Bridge Server 3]
+
+ B --> G[Tool Registry]
+ D --> G
+ F --> G
+
+ G --> H[(Shared Tools)]
+```
+
+## 监控指标
+
+### 关键指标
+
+| 指标 | 说明 | 目标值 |
+|------|------|--------|
+| 服务器启动时间 | HTTP 服务器就绪 | < 100ms |
+| 工具调用延迟 | 单次调用耗时 | < 10ms |
+| Python 冷启动 | 首次执行耗时 | < 100ms |
+| Python 热执行 | 后续执行耗时 | < 20ms |
+| 内存占用 | 空闲状态 | < 10MB |
+| QPS | 并发吞吐量 | > 20/s |
+
+### 监控实现
+
+```go
+type Metrics struct {
+ ToolCalls prometheus.Counter
+ CallLatency prometheus.Histogram
+ ActiveSessions prometheus.Gauge
+}
+
+// 记录指标
+start := time.Now()
+result, _ := bridge.CallTool(ctx, name, input, tc)
+metrics.CallLatency.Observe(time.Since(start).Seconds())
+metrics.ToolCalls.Inc()
+```
+
+## 参考实现
+
+完整源代码位置:
+
+```
+pkg/tools/bridge/
+├── http_server.go # HTTP 桥接服务器
+├── runtime.go # Python 运行时
+├── tool_bridge.go # 工具桥接
+└── ptc_integration_test.go # 集成测试
+
+client-sdks/python-bridge/
+└── aster.py # Python SDK
+
+pkg/tools/builtin/
+└── codeexecute.go # CodeExecute 工具
+
+pkg/provider/
+└── anthropic.go # Anthropic 适配
+```
diff --git a/docs/content/05.tools/5.ptc/4.troubleshooting.md b/docs/content/05.tools/5.ptc/4.troubleshooting.md
new file mode 100644
index 0000000..550f234
--- /dev/null
+++ b/docs/content/05.tools/5.ptc/4.troubleshooting.md
@@ -0,0 +1,541 @@
+---
+title: 故障排除
+description: 常见问题及解决方案
+---
+
+# 故障排除
+
+## 常见错误
+
+### aiohttp 依赖缺失
+
+::alert{type="error"}
+**错误信息:**
+```
+Error: aiohttp is required. Install it with: pip install aiohttp
+```
+::
+
+**原因:** Python 环境缺少 `aiohttp` 库
+
+**解决方案:**
+
+::code-group
+```bash [pip]
+pip install aiohttp
+```
+
+```bash [conda]
+conda install -c conda-forge aiohttp
+```
+
+```bash [poetry]
+poetry add aiohttp
+```
+::
+
+**验证安装:**
+```bash
+python3 -c "import aiohttp; print(aiohttp.__version__)"
+```
+
+---
+
+### 桥接服务器未启动
+
+::alert{type="error"}
+**错误信息:**
+```
+Connection error: Cannot connect to host localhost:8080
+```
+::
+
+**原因:** HTTP 桥接服务器未启动
+
+**诊断步骤:**
+
+1. 检查服务器是否运行
+```bash
+curl http://localhost:8080/health
+```
+
+2. 检查端口占用
+```bash
+lsof -i :8080
+```
+
+3. 查看进程日志
+```go
+// 在 Go 代码中添加日志
+if err := server.StartAsync(); err != nil {
+ log.Printf("服务器启动失败: %v", err)
+}
+```
+
+**解决方案:**
+
+::code-group
+```go [修改端口]
+// 使用不同端口
+codeExec.SetBridgeURL("http://localhost:9000")
+server := bridge.NewHTTPBridgeServer(toolBridge, "localhost:9000")
+```
+
+```go [手动启动]
+// 确保服务器已启动
+server := bridge.NewHTTPBridgeServer(toolBridge, "localhost:8080")
+if err := server.StartAsync(); err != nil {
+ log.Fatal(err)
+}
+time.Sleep(200 * time.Millisecond) // 等待启动
+```
+::
+
+---
+
+### Python 执行超时
+
+::alert{type="error"}
+**错误信息:**
+```
+execution timeout
+```
+::
+
+**原因:** 代码执行时间超过配置的超时限制
+
+**解决方案:**
+
+```go
+// 增加超时时间
+config := &bridge.RuntimeConfig{
+ Timeout: 60 * time.Second, // 从 30s 增加到 60s
+}
+
+runtime := bridge.NewPythonRuntime(config)
+```
+
+**优化建议:**
+- 使用并发减少执行时间
+- 避免阻塞操作
+- 检查是否有死循环
+
+---
+
+### 工具调用失败
+
+::alert{type="error"}
+**错误信息:**
+```
+Tool Read failed: file not found
+```
+::
+
+**原因:** 工具执行失败(业务逻辑错误)
+
+**调试步骤:**
+
+1. 直接测试工具
+```bash
+# 测试 HTTP 接口
+curl -X POST http://localhost:8080/tools/call \
+ -H "Content-Type: application/json" \
+ -d '{
+ "tool": "Read",
+ "input": {"path": "test.txt"}
+ }'
+```
+
+2. 检查输入参数
+```python
+# 在 Python 代码中打印参数
+import sys
+print(f"调用 Read, path={path}", file=sys.stderr)
+result = await Read(path=path)
+```
+
+3. 捕获异常
+```python
+try:
+ result = await Read(path="nonexistent.txt")
+except Exception as e:
+ print(f"详细错误: {type(e).__name__}: {e}")
+ # 使用默认值
+ result = ""
+```
+
+---
+
+### AllowedCallers 配置错误
+
+::alert{type="error"}
+**错误信息:**
+```
+Tool XXX is not allowed in code_execution context
+```
+::
+
+**原因:** 工具的 `AllowedCallers` 未包含 `code_execution_20250825`
+
+**解决方案:**
+
+```go
+// 修改工具配置
+toolSchema := provider.ToolSchema{
+ Name: "Read",
+ Description: "...",
+ InputSchema: {...},
+ // 添加 code_execution 权限
+ AllowedCallers: []string{"direct", "code_execution_20250825"},
+}
+```
+
+**检查清单:**
+- ✅ 工具在 ToolSchema 中定义
+- ✅ AllowedCallers 包含正确值
+- ✅ 工具已注册到 Registry
+- ✅ CodeExecute 工具能访问 Registry
+
+---
+
+## 性能问题
+
+### 调用延迟过高
+
+**症状:** 每次工具调用需要 100ms+
+
+**诊断:**
+
+```go
+// 添加性能日志
+start := time.Now()
+result, _ := tool.Execute(ctx, input, tc)
+latency := time.Since(start)
+log.Printf("Tool %s latency: %v", name, latency)
+```
+
+**可能原因和解决方案:**
+
+::code-group
+```go [网络延迟]
+// 使用本地桥接服务器
+codeExec.SetBridgeURL("http://localhost:8080") // ✅
+// 而不是
+codeExec.SetBridgeURL("http://remote-server:8080") // ❌
+```
+
+```python [连接复用]
+# 确保使用 session 复用
+# SDK 已内置,无需额外配置
+```
+
+```python [并发优化]
+# 使用并发而不是串行
+import asyncio
+
+# ❌ 串行: 3秒
+r1 = await Read(path="a.txt")
+r2 = await Read(path="b.txt")
+r3 = await Read(path="c.txt")
+
+# ✅ 并发: ~1秒
+results = await asyncio.gather(
+ Read(path="a.txt"),
+ Read(path="b.txt"),
+ Read(path="c.txt"),
+)
+```
+::
+
+---
+
+### 内存占用过高
+
+**症状:** Go 进程内存持续增长
+
+**诊断:**
+
+```go
+import "runtime"
+
+// 打印内存统计
+var m runtime.MemStats
+runtime.ReadMemStats(&m)
+log.Printf("Alloc = %v MB, TotalAlloc = %v MB, Sys = %v MB",
+ m.Alloc/1024/1024, m.TotalAlloc/1024/1024, m.Sys/1024/1024)
+```
+
+**可能原因:**
+
+1. **Schema 缓存未清理**
+ - 缓存有 TTL,会自动过期
+ - 检查缓存大小是否合理
+
+2. **HTTP 连接泄漏**
+ - 确保 Python 会话正确关闭
+ - SDK 已在 `__aexit__` 中实现清理
+
+3. **临时文件未删除**
+ - Runtime 会自动删除
+ - 检查 `/tmp/aster_*.py` 文件
+
+---
+
+## 调试技巧
+
+### 启用详细日志
+
+```go
+import "log"
+
+func main() {
+ // 设置日志格式
+ log.SetFlags(log.LstdFlags | log.Lshortfile)
+
+ // 会输出:
+ // 2025/01/30 10:30:45 http_server.go:179: HTTP Bridge Server listening on localhost:8080
+ // 2025/01/30 10:30:46 tool_bridge.go:45: Calling tool Read with input map[path:test.txt]
+}
+```
+
+### Python 代码调试
+
+```python
+import sys
+
+# 输出到 stderr 避免干扰结果
+print("=== 调试信息 ===", file=sys.stderr)
+
+# 打印可用的全局变量
+import pprint
+pprint.pprint({k: v for k, v in globals().items() if not k.startswith('_')},
+ stream=sys.stderr)
+
+# 执行并捕获详细错误
+try:
+ result = await Read(path="test.txt")
+ print(f"✅ 成功: {len(result)} 字节", file=sys.stderr)
+except Exception as e:
+ import traceback
+ print("❌ 错误:", file=sys.stderr)
+ traceback.print_exc(file=sys.stderr)
+ raise
+```
+
+### 查看生成的 Python 代码
+
+```go
+// 在 runtime.go 中临时修改
+func (r *PythonRuntime) Execute(ctx context.Context, code string, input map[string]any) (*ExecutionResult, error) {
+ wrappedCode := r.wrapCode(code, input)
+
+ // 打印生成的代码
+ log.Printf("=== Generated Python Code ===\n%s\n", wrappedCode)
+
+ // ... 继续执行
+}
+```
+
+### 测试 HTTP 端点
+
+```bash
+# 1. 健康检查
+curl http://localhost:8080/health
+
+# 2. 列出工具
+curl http://localhost:8080/tools/list | jq
+
+# 3. 获取 Schema
+curl "http://localhost:8080/tools/schema?name=Read" | jq
+
+# 4. 调用工具
+curl -X POST http://localhost:8080/tools/call \
+ -H "Content-Type: application/json" \
+ -d '{
+ "tool": "Read",
+ "input": {"path": "README.md"}
+ }' | jq
+
+# 5. 测试错误处理
+curl -X POST http://localhost:8080/tools/call \
+ -H "Content-Type: application/json" \
+ -d '{
+ "tool": "Read",
+ "input": {"path": "/nonexistent/file.txt"}
+ }' | jq
+```
+
+### 性能分析
+
+```go
+import _ "net/http/pprof"
+
+func main() {
+ // 启动 pprof 服务器
+ go func() {
+ log.Println(http.ListenAndServe("localhost:6060", nil))
+ }()
+
+ // ... 正常逻辑
+}
+```
+
+```bash
+# CPU 分析
+go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
+
+# 内存分析
+go tool pprof http://localhost:6060/debug/pprof/heap
+
+# Goroutine 分析
+go tool pprof http://localhost:6060/debug/pprof/goroutine
+```
+
+---
+
+## 环境问题
+
+### Python 版本不兼容
+
+::alert{type="warning"}
+**要求:** Python 3.7+
+::
+
+**检查版本:**
+```bash
+python3 --version
+```
+
+**解决方案:**
+```bash
+# macOS
+brew install python@3.11
+
+# Ubuntu
+sudo apt install python3.11
+
+# 使用 pyenv
+pyenv install 3.11.0
+pyenv global 3.11.0
+```
+
+---
+
+### 依赖冲突
+
+**症状:** aiohttp 安装失败或版本冲突
+
+**解决方案:**
+
+::code-group
+```bash [使用虚拟环境]
+python3 -m venv venv
+source venv/bin/activate
+pip install aiohttp
+```
+
+```bash [指定版本]
+pip install 'aiohttp>=3.8.0,<4.0.0'
+```
+
+```bash [清理缓存]
+pip cache purge
+pip install --no-cache-dir aiohttp
+```
+::
+
+---
+
+## 生产环境建议
+
+### 监控配置
+
+```go
+// 添加健康检查端点监控
+go func() {
+ ticker := time.NewTicker(30 * time.Second)
+ for range ticker.C {
+ resp, err := http.Get("http://localhost:8080/health")
+ if err != nil || resp.StatusCode != 200 {
+ log.Printf("⚠️ Bridge server unhealthy")
+ // 发送告警
+ }
+ }
+}()
+```
+
+### 错误处理
+
+```go
+// 生产环境应该捕获所有错误
+result, err := tool.Execute(ctx, input, tc)
+if err != nil {
+ // 记录详细错误
+ log.Printf("Tool execution failed: %v, input: %+v", err, input)
+
+ // 返回友好错误
+ return map[string]any{
+ "success": false,
+ "error": "Internal server error",
+ }, nil
+}
+```
+
+### 资源限制
+
+```go
+// 限制并发执行数
+var sem = make(chan struct{}, 10) // 最多10个并发
+
+func executeWithLimit(ctx context.Context, code string) error {
+ select {
+ case sem <- struct{}{}:
+ defer func() { <-sem }()
+ // 执行代码
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+}
+```
+
+---
+
+## 获取帮助
+
+如果以上方案都无法解决问题:
+
+::card-group
+::card
+---
+title: GitHub Issues
+icon: i-lucide-github
+to: https://github.com/astercloud/aster/issues
+---
+报告 Bug 或提交功能请求
+::
+
+::card
+---
+title: 讨论区
+icon: i-lucide-message-circle
+to: https://github.com/astercloud/aster/discussions
+---
+提问和讨论
+::
+
+::card
+---
+title: 文档
+icon: i-lucide-book
+to: /tools/ptc
+---
+查阅完整文档
+::
+::
+
+**报告问题时请提供:**
+1. ✅ 错误信息和堆栈跟踪
+2. ✅ Go 版本和 Python 版本
+3. ✅ 最小可复现示例
+4. ✅ 相关配置文件
+5. ✅ 日志输出
diff --git a/docs/content/05.tools/5.ptc/index.md b/docs/content/05.tools/5.ptc/index.md
new file mode 100644
index 0000000..e6ab73a
--- /dev/null
+++ b/docs/content/05.tools/5.ptc/index.md
@@ -0,0 +1,338 @@
+# Programmatic Tool Calling (PTC)
+
+Programmatic Tool Calling (PTC) 是 Aster 实现的 Anthropic 协议扩展,允许 LLM 生成的 Python 代码直接调用 Aster 工具,实现更强大的编程能力。
+
+## 概述
+
+传统的工具调用流程:
+```
+LLM → 工具调用请求 → Aster 执行工具 → 返回结果 → LLM
+```
+
+PTC 流程:
+```
+LLM → 生成 Python 代码 → CodeExecute 工具执行代码 →
+代码中调用 Aster 工具 → 返回结果 → LLM
+```
+
+### 优势
+
+1. **组合能力**: Python 代码可以组合多个工具调用,实现复杂逻辑
+2. **控制流**: 支持条件判断、循环等控制结构
+3. **数据处理**: 利用 Python 生态处理复杂数据转换
+4. **错误处理**: 代码中可以捕获和处理工具调用错误
+
+## 架构
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ LLM (Anthropic) │
+│ 生成 Python 代码,调用 Read/Write/Glob/Grep 等工具 │
+└────────────────────────┬────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ CodeExecute 工具 │
+│ - 启动 HTTP 桥接服务器 │
+│ - 注入工具 SDK 到 Python 代码 │
+│ - 执行 Python 代码 │
+└────────────────────────┬────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ HTTP Bridge Server │
+│ 端点: │
+│ - POST /tools/call - 调用工具 │
+│ - GET /tools/list - 列出可用工具 │
+│ - GET /tools/schema - 获取工具 Schema │
+│ - GET /health - 健康检查 │
+└────────────────────────┬────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ ToolBridge │
+│ 调用 Go 侧的工具实现 │
+└─────────────────────────────────────────────────────────────┘
+```
+
+## 快速开始
+
+### 1. 基本使用
+
+创建一个带 PTC 支持的 Agent:
+
+```go
+package main
+
+import (
+ "context"
+ "log"
+
+ "github.com/astercloud/aster/pkg/agent"
+ "github.com/astercloud/aster/pkg/provider"
+ "github.com/astercloud/aster/pkg/tools"
+ "github.com/astercloud/aster/pkg/tools/bridge"
+ "github.com/astercloud/aster/pkg/tools/builtin"
+)
+
+func main() {
+ // 1. 创建工具注册表
+ registry := tools.NewRegistry()
+ builtin.RegisterAll(registry)
+
+ // 2. 创建 ToolBridge
+ toolBridge := bridge.NewToolBridge(registry)
+
+ // 3. 创建 CodeExecute 工具(带 PTC 支持)
+ codeExecTool := builtin.NewCodeExecuteToolWithBridge(toolBridge)
+
+ // 4. 创建 Provider
+ providerConfig := &types.ModelConfig{
+ Provider: "anthropic",
+ Model: "claude-3-5-sonnet-20241022",
+ APIKey: os.Getenv("ANTHROPIC_API_KEY"),
+ }
+ provider, _ := provider.NewAnthropicProvider(providerConfig)
+
+ // 5. 创建 Agent
+ ag := agent.NewAgent("ptc-demo", provider, &agent.Dependencies{
+ ToolRegistry: registry,
+ })
+
+ // 6. 注册 CodeExecute 工具
+ ag.AddTool(codeExecTool)
+
+ // 7. 运行任务
+ ctx := context.Background()
+ ag.Run(ctx, "请用 Python 代码读取当前目录下所有 .go 文件,统计总行数")
+}
+```
+
+### 2. LLM 生成的 Python 代码示例
+
+```python
+# LLM 会生成类似这样的代码
+import asyncio
+
+async def main():
+ # 搜索所有 .go 文件
+ go_files = await Glob(pattern="*.go", path=".")
+
+ total_lines = 0
+ for file_path in go_files:
+ # 读取文件内容
+ content = await Read(path=file_path)
+ lines = len(content.split('\n'))
+ total_lines += lines
+ print(f"{file_path}: {lines} 行")
+
+ print(f"总计: {total_lines} 行")
+
+asyncio.run(main())
+```
+
+### 3. 工具配置 AllowedCallers
+
+默认情况下,所有工具仅支持 LLM 直接调用。要允许工具在 Python 代码中被调用,需要配置 `AllowedCallers`:
+
+```go
+// 创建工具时指定 AllowedCallers
+type ReadTool struct{}
+
+func (t *ReadTool) Schema() provider.ToolSchema {
+ return provider.ToolSchema{
+ Name: "Read",
+ Description: "读取文件内容",
+ InputSchema: map[string]any{
+ "type": "object",
+ "properties": map[string]any{
+ "path": map[string]any{
+ "type": "string",
+ "description": "文件路径",
+ },
+ },
+ "required": []string{"path"},
+ },
+ // PTC 配置: 允许 LLM 直接调用 + Python 代码调用
+ AllowedCallers: []string{"direct", "code_execution_20250825"},
+ }
+}
+```
+
+AllowedCallers 可选值:
+- `"direct"`: 允许 LLM 直接调用(默认)
+- `"code_execution_20250825"`: 允许在 CodeExecute 生成的代码中调用
+
+## 可用工具
+
+以下内置工具默认支持 PTC:
+
+| 工具名 | 功能 | 示例 |
+|--------|------|------|
+| `Read` | 读取文件 | `await Read(path="file.txt")` |
+| `Write` | 写入文件 | `await Write(path="out.txt", content="data")` |
+| `Glob` | 文件模式匹配 | `await Glob(pattern="*.py")` |
+| `Grep` | 内容搜索 | `await Grep(pattern="TODO", path=".")` |
+| `Bash` | 执行命令 | `await Bash(command="ls -la")` |
+
+## 高级用法
+
+### 1. 自定义桥接 URL
+
+```go
+codeExecTool := builtin.NewCodeExecuteToolWithBridge(toolBridge)
+codeExecTool.SetBridgeURL("http://localhost:9000")
+```
+
+### 2. 工具上下文传递
+
+HTTP 桥接服务器支持工具上下文工厂,可以为每次工具调用提供不同的上下文:
+
+```go
+httpServer := bridge.NewHTTPBridgeServer(toolBridge, "localhost:8080")
+
+httpServer.SetContextFactory(func() *tools.ToolContext {
+ return &tools.ToolContext{
+ AgentID: "agent-123",
+ Services: map[string]any{
+ "database": db,
+ "cache": cache,
+ },
+ }
+})
+```
+
+### 3. 错误处理
+
+Python 代码中可以捕获工具调用错误:
+
+```python
+async def main():
+ try:
+ content = await Read(path="nonexistent.txt")
+ except Exception as e:
+ print(f"读取失败: {e}")
+ # 使用备用方案
+ content = await Read(path="default.txt")
+```
+
+### 4. 批量工具调用
+
+```python
+async def main():
+ # 并发读取多个文件
+ files = ["a.txt", "b.txt", "c.txt"]
+ tasks = [Read(path=f) for f in files]
+
+ import asyncio
+ results = await asyncio.gather(*tasks)
+
+ for file, content in zip(files, results):
+ print(f"{file}: {len(content)} 字节")
+```
+
+## 限制和注意事项
+
+### 1. Python 依赖
+
+生成的 Python 代码依赖 `aiohttp` 库。如果执行环境没有安装,会报错:
+
+```
+Error: aiohttp is required. Install it with: pip install aiohttp
+```
+
+解决方案:
+```bash
+pip install aiohttp
+```
+
+### 2. 超时设置
+
+- HTTP 请求超时: 60秒
+- Python 代码执行超时: 30秒(可配置)
+
+```go
+config := &bridge.RuntimeConfig{
+ Timeout: 60 * time.Second,
+}
+runtime := bridge.NewPythonRuntime(config)
+```
+
+### 3. 安全考虑
+
+- CodeExecute 工具会执行 LLM 生成的任意 Python 代码,存在安全风险
+- 建议在沙箱环境中运行,或使用白名单限制可调用的工具
+- HTTP 桥接服务器默认监听 localhost,不对外暴露
+
+### 4. 性能影响
+
+- 首次调用 CodeExecute 时会启动 HTTP 服务器(约 100ms)
+- 每次工具调用都是 HTTP 请求,有网络开销(约 1-5ms)
+- Python 解释器启动有开销(约 50-100ms)
+
+## 调试
+
+### 启用详细日志
+
+```go
+import "log"
+
+// Anthropic Provider 会输出工具调用详情
+log.SetFlags(log.LstdFlags | log.Lshortfile)
+```
+
+### 查看生成的 Python 代码
+
+CodeExecute 工具执行的完整 Python 代码会包含:
+1. SDK 注入代码
+2. 工具函数生成
+3. 用户代码包装
+
+可以在 stderr 中查看执行错误。
+
+### HTTP 请求日志
+
+HTTP 桥接服务器会输出:
+```
+HTTP Bridge Server listening on localhost:8080
+```
+
+## 示例项目
+
+参考 `examples/ptc/` 目录下的完整示例:
+
+- `basic/`: 基础 PTC 使用
+- `file-processor/`: 文件批处理
+- `code-analyzer/`: 代码分析工具
+
+## 常见问题
+
+### Q: PTC 和普通工具调用有什么区别?
+
+A:
+- 普通工具调用: LLM 一次调用一个工具,适合简单场景
+- PTC: LLM 生成 Python 代码,可以组合多个工具,适合复杂逻辑
+
+### Q: 为什么只支持 Python?
+
+A: Python 是 AI 领域的标准语言,大部分 LLM 对 Python 支持最好。Anthropic、OpenAI、Manus 等平台都使用 Python。
+
+### Q: 如何调试 Python 代码错误?
+
+A: CodeExecute 工具会返回完整的 stdout 和 stderr,包含 Python 异常堆栈。
+
+### Q: 能在 Python 代码中调用自定义工具吗?
+
+A: 可以!只要工具在 ToolRegistry 中注册,并设置了正确的 AllowedCallers,就可以在 Python 中调用。
+
+## 参考资料
+
+- [Anthropic PTC 文档](https://docs.anthropic.com/en/docs/build-with-claude/tool-use#programmatic-tool-use-beta)
+- [Aster 工具系统](./tools.md)
+- [CodeExecute 工具文档](./tools/code-execute.md)
+
+## 更新日志
+
+- 2025-01-30: 初始版本,支持 Python PTC
+- 2025-01-30: 添加 HTTP 桥接服务器
+- 2025-01-30: 集成 Anthropic Provider
diff --git a/docs/content/05.tools/index.md b/docs/content/05.tools/index.md
index 1dfe24c..c531bcf 100644
--- a/docs/content/05.tools/index.md
+++ b/docs/content/05.tools/index.md
@@ -27,6 +27,13 @@ aster 提供了强大的工具系统,让 Agent 能够与外部世界交互。
- 工具注册
- 工具生命周期
+### [Programmatic Tool Calling](/tools/ptc) ::badge{type="success"}新::
+- 让 LLM 生成的 Python 代码直接调用工具
+- HTTP 桥接服务器
+- 异步 Python SDK
+- 错误处理和重试
+- 性能优化
+
## 🚀 快速开始
```go
diff --git a/examples/ptc/README.md b/examples/ptc/README.md
new file mode 100644
index 0000000..5bd9aca
--- /dev/null
+++ b/examples/ptc/README.md
@@ -0,0 +1,287 @@
+# PTC (Programmatic Tool Calling) 示例
+
+本目录包含 Aster Programmatic Tool Calling 功能的示例程序。
+
+## 前置条件
+
+1. **Anthropic API Key**
+ ```bash
+ export ANTHROPIC_API_KEY="your-api-key-here"
+ ```
+
+2. **Python 环境** (用于执行生成的代码)
+ ```bash
+ python3 --version # 需要 Python 3.7+
+ pip install aiohttp # 必需依赖
+ ```
+
+## 示例列表
+
+### 1. basic - 基础 PTC 使用
+
+最简单的 PTC 示例,演示如何让 LLM 生成 Python 代码并调用 Aster 工具。
+
+**运行:**
+```bash
+cd basic
+go run main.go
+```
+
+**功能:**
+- 使用 Glob 查找所有 .go 文件
+- 使用 Read 读取文件内容
+- 统计代码行数和字符数
+
+**预期输出:**
+```
+正在调用 LLM 生成 Python 代码...
+
+=== LLM 响应 ===
+工具调用: CodeExecute
+参数: map[language:python code:import asyncio...]
+
+执行结果: map[success:true output:...]
+
+=== Token 使用 ===
+输入: 1234 tokens
+输出: 567 tokens
+总计: 1801 tokens
+```
+
+### 2. file-processor - 文件批处理
+
+演示如何使用 PTC 进行批量文件处理。
+
+**运行:**
+```bash
+cd file-processor
+go run main.go
+```
+
+**功能:**
+- 批量读取多个文件
+- 数据转换和处理
+- 并发执行提升性能
+
+### 3. code-analyzer - 代码分析
+
+演示复杂的代码分析任务。
+
+**运行:**
+```bash
+cd code-analyzer
+go run main.go
+```
+
+**功能:**
+- 使用 Grep 搜索特定模式
+- 统计代码复杂度
+- 生成分析报告
+
+## 工作原理
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ 1. Go 程序创建 ToolBridge 和 CodeExecute 工具 │
+└────────────────────────┬────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ 2. 调用 LLM,提供工具列表(带 AllowedCallers 配置) │
+└────────────────────────┬────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ 3. LLM 生成 Python 代码,调用 Read/Write/Glob 等工具 │
+└────────────────────────┬────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ 4. CodeExecute 工具: │
+│ - 启动 HTTP 桥接服务器 (localhost:8080) │
+│ - 注入 Python SDK 到生成的代码 │
+│ - 执行 Python 代码 │
+└────────────────────────┬────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ 5. Python 代码通过 HTTP 调用 Go 侧工具 │
+│ POST http://localhost:8080/tools/call │
+│ {"tool": "Read", "input": {"path": "file.txt"}} │
+└────────────────────────┬────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ 6. 返回结果给 Python 代码 │
+└────────────────────────┬────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ 7. Python 代码完成执行,返回结果 │
+└─────────────────────────────────────────────────────────────┘
+```
+
+## 关键配置
+
+### AllowedCallers 字段
+
+控制工具在哪些上下文中可以被调用:
+
+```go
+toolSchemas := []provider.ToolSchema{
+ {
+ Name: "Read",
+ Description: "读取文件",
+ InputSchema: readTool.InputSchema(),
+ // 允许 LLM 直接调用 + Python 代码调用
+ AllowedCallers: []string{"direct", "code_execution_20250825"},
+ },
+ {
+ Name: "CodeExecute",
+ Description: "执行代码",
+ InputSchema: codeExecTool.InputSchema(),
+ // 仅允许 LLM 直接调用,不能在 Python 中递归调用
+ AllowedCallers: []string{"direct"},
+ },
+}
+```
+
+可选值:
+- `"direct"`: LLM 直接调用
+- `"code_execution_20250825"`: Python 代码中调用
+
+### Caller 字段
+
+工具调用时会包含调用者信息:
+
+```go
+toolUseBlock := &types.ToolUseBlock{
+ ID: "call_123",
+ Name: "Read",
+ Input: map[string]any{"path": "file.txt"},
+ Caller: &types.ToolCaller{
+ Type: "code_execution_20250825", // 从代码中调用
+ ToolID: "call_456", // CodeExecute 工具的 ID
+ },
+}
+```
+
+## 调试技巧
+
+### 1. 查看生成的 Python 代码
+
+CodeExecute 工具会输出完整的 Python 代码到临时文件,可以通过修改代码保留临时文件:
+
+```go
+// 在 runtime.go 中注释掉临时文件删除
+// defer os.Remove(tmpFile.Name())
+fmt.Printf("临时文件: %s\n", tmpFile.Name())
+```
+
+### 2. 启用详细日志
+
+```go
+import "log"
+
+log.SetFlags(log.LstdFlags | log.Lshortfile)
+```
+
+会输出:
+- HTTP 桥接服务器启动信息
+- 工具调用详情
+- Provider API 请求详情
+
+### 3. 测试单个工具
+
+```bash
+# 测试 HTTP 桥接服务器
+curl -X POST http://localhost:8080/tools/call \
+ -H "Content-Type: application/json" \
+ -d '{"tool": "Read", "input": {"path": "README.md"}}'
+
+# 列出可用工具
+curl http://localhost:8080/tools/list
+
+# 获取工具 Schema
+curl "http://localhost:8080/tools/schema?name=Read"
+```
+
+## 常见问题
+
+### Q: 报错 "aiohttp is required"
+
+A: 安装 Python 依赖:
+```bash
+pip install aiohttp
+```
+
+### Q: HTTP 桥接服务器启动失败
+
+A: 检查端口占用:
+```bash
+lsof -i :8080
+```
+
+修改端口:
+```go
+codeExecTool.SetBridgeURL("http://localhost:9000")
+```
+
+### Q: Python 代码执行超时
+
+A: 增加超时时间:
+```go
+config := &bridge.RuntimeConfig{
+ Timeout: 60 * time.Second,
+}
+runtime := bridge.NewPythonRuntime(config)
+```
+
+### Q: 如何限制可调用的工具?
+
+A: 只为需要的工具设置 `AllowedCallers`:
+```go
+// 仅允许 Read 和 Glob 在 Python 中调用
+toolSchemas := []provider.ToolSchema{
+ {Name: "Read", AllowedCallers: []string{"direct", "code_execution_20250825"}},
+ {Name: "Glob", AllowedCallers: []string{"direct", "code_execution_20250825"}},
+ {Name: "Write", AllowedCallers: []string{"direct"}}, // 不能在 Python 中调用
+}
+```
+
+## 性能优化
+
+### 1. 复用 HTTP 服务器
+
+HTTP 桥接服务器在首次调用 CodeExecute 时启动,后续调用会复用同一个服务器实例。
+
+### 2. 批量工具调用
+
+在 Python 代码中使用 `asyncio.gather` 并发调用:
+
+```python
+import asyncio
+
+results = await asyncio.gather(
+ Read(path="a.txt"),
+ Read(path="b.txt"),
+ Read(path="c.txt"),
+)
+```
+
+### 3. 缓存工具结果
+
+```python
+_cache = {}
+
+async def cached_read(path):
+ if path not in _cache:
+ _cache[path] = await Read(path=path)
+ return _cache[path]
+```
+
+## 更多资源
+
+- [PTC 完整文档](../../docs/programmatic-tool-calling.md)
+- [Anthropic PTC 官方文档](https://docs.anthropic.com/en/docs/build-with-claude/tool-use#programmatic-tool-use-beta)
+- [Aster 工具系统](../../docs/tools.md)
diff --git a/examples/ptc/basic/main.go b/examples/ptc/basic/main.go
new file mode 100644
index 0000000..c34c293
--- /dev/null
+++ b/examples/ptc/basic/main.go
@@ -0,0 +1,168 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "os"
+
+ "github.com/astercloud/aster/pkg/provider"
+ "github.com/astercloud/aster/pkg/tools"
+ "github.com/astercloud/aster/pkg/tools/bridge"
+ "github.com/astercloud/aster/pkg/tools/builtin"
+ "github.com/astercloud/aster/pkg/types"
+)
+
+// PTC 基础示例
+// 演示如何使用 Programmatic Tool Calling 让 LLM 生成 Python 代码并调用工具
+func main() {
+ // 检查 API Key
+ apiKey := os.Getenv("ANTHROPIC_API_KEY")
+ if apiKey == "" {
+ log.Fatal("请设置环境变量 ANTHROPIC_API_KEY")
+ }
+
+ // 1. 创建工具注册表并注册内置工具
+ registry := tools.NewRegistry()
+ builtin.RegisterAll(registry)
+
+ // 2. 创建 ToolBridge 用于程序化工具调用
+ toolBridge := bridge.NewToolBridge(registry)
+
+ // 3. 创建 CodeExecute 工具并启用 PTC 支持
+ codeExecTool := builtin.NewCodeExecuteToolWithBridge(toolBridge)
+
+ // 可选: 自定义桥接服务器地址
+ // codeExecTool.SetBridgeURL("http://localhost:9000")
+
+ // 4. 创建工具实例
+ readTool, _ := registry.Create("Read", nil)
+ writeTool, _ := registry.Create("Write", nil)
+ globTool, _ := registry.Create("Glob", nil)
+ bashTool, _ := registry.Create("Bash", nil)
+
+ // 5. 转换为 Provider ToolSchema (添加 AllowedCallers 支持)
+ toolSchemas := []provider.ToolSchema{
+ {
+ Name: "CodeExecute",
+ Description: codeExecTool.Description(),
+ InputSchema: codeExecTool.InputSchema(),
+ // CodeExecute 只能被 LLM 直接调用
+ AllowedCallers: []string{"direct"},
+ },
+ {
+ Name: "Read",
+ Description: readTool.Description(),
+ InputSchema: readTool.InputSchema(),
+ // Read 可以被 LLM 直接调用,也可以在 Python 代码中调用
+ AllowedCallers: []string{"direct", "code_execution_20250825"},
+ },
+ {
+ Name: "Write",
+ Description: writeTool.Description(),
+ InputSchema: writeTool.InputSchema(),
+ AllowedCallers: []string{"direct", "code_execution_20250825"},
+ },
+ {
+ Name: "Glob",
+ Description: globTool.Description(),
+ InputSchema: globTool.InputSchema(),
+ AllowedCallers: []string{"direct", "code_execution_20250825"},
+ },
+ {
+ Name: "Bash",
+ Description: bashTool.Description(),
+ InputSchema: bashTool.InputSchema(),
+ AllowedCallers: []string{"direct", "code_execution_20250825"},
+ },
+ }
+
+ // 6. 创建 Anthropic Provider
+ providerConfig := &types.ModelConfig{
+ Provider: "anthropic",
+ Model: "claude-3-5-sonnet-20241022",
+ APIKey: apiKey,
+ }
+
+ anthropicProvider, err := provider.NewAnthropicProvider(providerConfig)
+ if err != nil {
+ log.Fatalf("创建 Provider 失败: %v", err)
+ }
+
+ // 7. 准备消息
+ messages := []types.Message{
+ {
+ Role: types.RoleUser,
+ Content: "请用 Python 代码完成以下任务:\n1. 使用 Glob 查找当前目录下所有 .go 文件\n2. 使用 Read 读取每个文件\n3. 统计总行数和总字符数\n4. 输出统计结果",
+ },
+ }
+
+ // 8. 调用 LLM (非流式)
+ ctx := context.Background()
+ opts := &provider.StreamOptions{
+ Tools: toolSchemas,
+ MaxTokens: 4096,
+ Temperature: 0.7,
+ System: "你是一个编程助手,擅长使用 Python 代码调用工具完成任务。",
+ }
+
+ fmt.Println("正在调用 LLM 生成 Python 代码...")
+ response, err := anthropicProvider.Complete(ctx, messages, opts)
+ if err != nil {
+ log.Fatalf("LLM 调用失败: %v", err)
+ }
+
+ // 9. 处理响应
+ fmt.Println("\n=== LLM 响应 ===")
+ for _, block := range response.Message.ContentBlocks {
+ switch b := block.(type) {
+ case *types.TextBlock:
+ fmt.Printf("文本: %s\n", b.Text)
+
+ case *types.ToolUseBlock:
+ fmt.Printf("\n工具调用: %s\n", b.Name)
+ fmt.Printf("参数: %+v\n", b.Input)
+
+ // PTC 支持: 检查 Caller 信息
+ if b.Caller != nil {
+ fmt.Printf("调用者类型: %s\n", b.Caller.Type)
+ if b.Caller.ToolID != "" {
+ fmt.Printf("调用者工具ID: %s\n", b.Caller.ToolID)
+ }
+ }
+
+ // 执行工具
+ var tool tools.Tool
+ var err error
+
+ switch b.Name {
+ case "CodeExecute":
+ tool = codeExecTool
+ default:
+ tool, err = registry.Create(b.Name, nil)
+ }
+
+ if err != nil {
+ fmt.Printf("工具创建失败: %v\n", err)
+ continue
+ }
+
+ // 执行工具
+ result, err := tool.Execute(ctx, b.Input, &tools.ToolContext{})
+ if err != nil {
+ fmt.Printf("工具执行失败: %v\n", err)
+ continue
+ }
+
+ fmt.Printf("执行结果: %+v\n", result)
+ }
+ }
+
+ // 输出 Token 使用情况
+ if response.Usage != nil {
+ fmt.Printf("\n=== Token 使用 ===\n")
+ fmt.Printf("输入: %d tokens\n", response.Usage.InputTokens)
+ fmt.Printf("输出: %d tokens\n", response.Usage.OutputTokens)
+ fmt.Printf("总计: %d tokens\n", response.Usage.InputTokens+response.Usage.OutputTokens)
+ }
+}
diff --git a/examples/ptc/file-processor/main.go b/examples/ptc/file-processor/main.go
new file mode 100644
index 0000000..39d3f61
--- /dev/null
+++ b/examples/ptc/file-processor/main.go
@@ -0,0 +1,174 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "os"
+
+ "github.com/astercloud/aster/pkg/provider"
+ "github.com/astercloud/aster/pkg/tools"
+ "github.com/astercloud/aster/pkg/tools/bridge"
+ "github.com/astercloud/aster/pkg/tools/builtin"
+ "github.com/astercloud/aster/pkg/types"
+)
+
+// 文件批处理示例
+// 演示如何使用 PTC 进行复杂的文件处理任务
+func main() {
+ // 检查 API Key
+ apiKey := os.Getenv("ANTHROPIC_API_KEY")
+ if apiKey == "" {
+ log.Fatal("请设置环境变量 ANTHROPIC_API_KEY")
+ }
+
+ fmt.Println("=== Aster PTC 文件处理示例 ===")
+
+ // 1. 创建工具生态系统
+ registry := tools.NewRegistry()
+ builtin.RegisterAll(registry)
+
+ toolBridge := bridge.NewToolBridge(registry)
+ codeExecTool := builtin.NewCodeExecuteToolWithBridge(toolBridge)
+
+ // 2. 准备工具列表(启用 PTC)
+ toolSchemas := []provider.ToolSchema{
+ {
+ Name: "CodeExecute",
+ Description: codeExecTool.Description(),
+ InputSchema: codeExecTool.InputSchema(),
+ AllowedCallers: []string{"direct"},
+ },
+ }
+
+ // 添加可在 Python 中调用的工具
+ allowedTools := []string{"Read", "Write", "Glob", "Grep", "Bash"}
+ for _, toolName := range allowedTools {
+ tool, _ := registry.Create(toolName, nil)
+ toolSchemas = append(toolSchemas, provider.ToolSchema{
+ Name: toolName,
+ Description: tool.Description(),
+ InputSchema: tool.InputSchema(),
+ AllowedCallers: []string{"direct", "code_execution_20250825"},
+ })
+ }
+
+ // 3. 创建 Anthropic Provider
+ providerConfig := &types.ModelConfig{
+ Provider: "anthropic",
+ Model: "claude-3-5-sonnet-20241022",
+ APIKey: apiKey,
+ }
+
+ anthropicProvider, err := provider.NewAnthropicProvider(providerConfig)
+ if err != nil {
+ log.Fatalf("创建 Provider 失败: %v", err)
+ }
+
+ // 4. 定义复杂任务
+ task := `请编写 Python 代码完成以下文件处理任务:
+
+1. 使用 Glob 查找当前目录下所有 .go 文件
+2. 使用 Grep 在这些文件中搜索包含 "TODO" 或 "FIXME" 的注释
+3. 统计每个文件中待办事项的数量
+4. 生成一份 Markdown 格式的报告,包含:
+ - 总待办事项数量
+ - 每个文件的待办事项列表
+ - 按优先级分类(FIXME > TODO)
+5. 使用 Write 将报告保存到 TODO_REPORT.md
+
+要求:
+- 使用异步编程提升性能
+- 对每个待办事项包含行号和内容
+- 报告格式清晰美观`
+
+ fmt.Printf("任务: %s\n\n", task)
+
+ messages := []types.Message{
+ {
+ Role: types.RoleUser,
+ Content: task,
+ },
+ }
+
+ // 5. 调用 LLM
+ opts := &provider.StreamOptions{
+ Tools: toolSchemas,
+ MaxTokens: 4096,
+ Temperature: 0.7,
+ System: "你是一个专业的 Python 开发者,擅长使用工具完成文件处理任务。代码要简洁高效,充分利用 asyncio 并发能力。",
+ }
+
+ fmt.Println("正在调用 LLM 生成代码...")
+ ctx := context.Background()
+
+ response, err := anthropicProvider.Complete(ctx, messages, opts)
+ if err != nil {
+ log.Fatalf("LLM 调用失败: %v", err)
+ }
+
+ // 6. 处理响应并执行工具
+ fmt.Println("=== LLM 响应 ===")
+
+ for _, block := range response.Message.ContentBlocks {
+ switch b := block.(type) {
+ case *types.TextBlock:
+ fmt.Printf("\n[文本]\n%s\n", b.Text)
+
+ case *types.ToolUseBlock:
+ fmt.Printf("\n[工具调用: %s]\n", b.Name)
+
+ // 打印 Caller 信息
+ if b.Caller != nil {
+ fmt.Printf("调用方式: %s\n", b.Caller.Type)
+ } else {
+ fmt.Printf("调用方式: direct (LLM 直接调用)\n")
+ }
+
+ // 执行工具
+ var tool tools.Tool
+ if b.Name == "CodeExecute" {
+ tool = codeExecTool
+ } else {
+ tool, _ = registry.Create(b.Name, nil)
+ }
+
+ fmt.Println("执行中...")
+ result, err := tool.Execute(ctx, b.Input, &tools.ToolContext{})
+ if err != nil {
+ fmt.Printf("❌ 执行失败: %v\n", err)
+ continue
+ }
+
+ // 打印结果
+ if resultMap, ok := result.(map[string]any); ok {
+ if success, ok := resultMap["success"].(bool); ok && success {
+ fmt.Println("✅ 执行成功")
+
+ // 如果是 CodeExecute,打印输出
+ if b.Name == "CodeExecute" {
+ if stdout, ok := resultMap["stdout"].(string); ok && stdout != "" {
+ fmt.Printf("\n输出:\n%s\n", stdout)
+ }
+ if stderr, ok := resultMap["stderr"].(string); ok && stderr != "" {
+ fmt.Printf("\n错误输出:\n%s\n", stderr)
+ }
+ }
+ } else {
+ fmt.Printf("❌ 执行失败: %v\n", resultMap["error"])
+ }
+ }
+ }
+ }
+
+ // 7. 显示统计信息
+ if response.Usage != nil {
+ fmt.Printf("\n\n=== 统计信息 ===\n")
+ fmt.Printf("输入 Tokens: %d\n", response.Usage.InputTokens)
+ fmt.Printf("输出 Tokens: %d\n", response.Usage.OutputTokens)
+ fmt.Printf("总计 Tokens: %d\n", response.Usage.InputTokens+response.Usage.OutputTokens)
+ }
+
+ fmt.Println("\n\n=== 完成 ===")
+ fmt.Println("如果执行成功,请查看 TODO_REPORT.md 文件")
+}
diff --git a/examples/ptc/local-test/main.go b/examples/ptc/local-test/main.go
new file mode 100644
index 0000000..1bf4cbd
--- /dev/null
+++ b/examples/ptc/local-test/main.go
@@ -0,0 +1,205 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "time"
+
+ "github.com/astercloud/aster/pkg/tools"
+ "github.com/astercloud/aster/pkg/tools/bridge"
+ "github.com/astercloud/aster/pkg/tools/builtin"
+)
+
+// 本地测试示例
+// 不需要 API Key,直接测试 PTC 基础设施
+func main() {
+ fmt.Println("=== Aster PTC 本地测试 ===")
+
+ // 1. 创建工具注册表
+ registry := tools.NewRegistry()
+ builtin.RegisterAll(registry)
+
+ // 2. 创建 ToolBridge
+ toolBridge := bridge.NewToolBridge(registry)
+
+ // 3. 启动 HTTP 桥接服务器
+ server := bridge.NewHTTPBridgeServer(toolBridge, "localhost:18080")
+
+ fmt.Println("启动 HTTP 桥接服务器...")
+ if err := server.StartAsync(); err != nil {
+ log.Fatalf("服务器启动失败: %v", err)
+ }
+ defer func() {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ _ = server.Shutdown(ctx)
+ }()
+
+ // 等待服务器完全启动
+ time.Sleep(200 * time.Millisecond)
+ fmt.Println("✅ 服务器启动成功: http://localhost:18080")
+
+ // 4. 创建 Python 运行时
+ runtime := bridge.NewPythonRuntime(nil)
+ runtime.SetTools([]string{"Read", "Write", "Glob"})
+ runtime.SetBridgeURL("http://localhost:18080")
+
+ // 5. 测试 Python 代码执行
+ testCases := []struct {
+ name string
+ code string
+ desc string
+ }{
+ {
+ name: "基础工具调用",
+ desc: "测试单个工具调用",
+ code: `
+import asyncio
+
+async def main():
+ # 调用 Glob 工具查找 Go 文件
+ files = await Glob(pattern="*.go", path=".")
+ print(f"找到 {len(files)} 个 Go 文件")
+ for f in files[:5]: # 只显示前5个
+ print(f" - {f}")
+
+asyncio.run(main())
+`,
+ },
+ {
+ name: "并发调用",
+ desc: "测试并发调用多个工具",
+ code: `
+import asyncio
+
+async def main():
+ # 并发查找不同类型的文件
+ tasks = [
+ Glob(pattern="*.go", path="."),
+ Glob(pattern="*.md", path="."),
+ Glob(pattern="*.json", path="."),
+ ]
+
+ results = await asyncio.gather(*tasks)
+
+ print("文件统计:")
+ print(f" Go 文件: {len(results[0])}")
+ print(f" Markdown 文件: {len(results[1])}")
+ print(f" JSON 文件: {len(results[2])}")
+
+asyncio.run(main())
+`,
+ },
+ {
+ name: "错误处理",
+ desc: "测试错误处理机制",
+ code: `
+import asyncio
+
+async def main():
+ try:
+ # 尝试读取不存在的文件
+ content = await Read(path="nonexistent_file_12345.txt")
+ print(f"内容: {content}")
+ except Exception as e:
+ print(f"✅ 捕获到预期错误: {e}")
+
+ # 继续执行其他任务
+ files = await Glob(pattern="*.go", path=".")
+ print(f"✅ 错误后继续执行成功,找到 {len(files)} 个文件")
+
+asyncio.run(main())
+`,
+ },
+ {
+ name: "复杂数据处理",
+ desc: "测试复杂的数据处理逻辑",
+ code: `
+import asyncio
+
+async def main():
+ # 获取所有 Go 文件
+ go_files = await Glob(pattern="*.go", path=".")
+
+ # 按目录分组
+ by_dir = {}
+ for f in go_files:
+ parts = f.split("/")
+ if len(parts) > 1:
+ dir_name = "/".join(parts[:-1])
+ else:
+ dir_name = "."
+
+ if dir_name not in by_dir:
+ by_dir[dir_name] = []
+ by_dir[dir_name].append(parts[-1])
+
+ # 输出统计
+ print(f"文件分布:")
+ for dir_name, files in sorted(by_dir.items()):
+ print(f" {dir_name}: {len(files)} 个文件")
+
+asyncio.run(main())
+`,
+ },
+ }
+
+ // 6. 执行测试用例
+ ctx := context.Background()
+
+ for i, tc := range testCases {
+ fmt.Printf("\n[测试 %d/%d] %s\n", i+1, len(testCases), tc.name)
+ fmt.Printf("说明: %s\n", tc.desc)
+ fmt.Println("执行中...")
+
+ start := time.Now()
+ result, err := runtime.Execute(ctx, tc.code, map[string]any{})
+ duration := time.Since(start)
+
+ if err != nil {
+ fmt.Printf("❌ 执行失败: %v\n", err)
+ continue
+ }
+
+ if !result.Success {
+ fmt.Printf("❌ 代码执行失败: %s\n", result.Error)
+ if result.Stderr != "" {
+ fmt.Printf("错误输出:\n%s\n", result.Stderr)
+ }
+ continue
+ }
+
+ fmt.Printf("✅ 执行成功 (耗时: %v)\n", duration)
+ if result.Stdout != "" {
+ fmt.Printf("\n输出:\n%s\n", result.Stdout)
+ }
+ }
+
+ // 7. 性能测试
+ fmt.Println("\n=== 性能测试 ===")
+ fmt.Println("测试 10 次连续调用的平均延迟...")
+
+ totalDuration := time.Duration(0)
+ iterations := 10
+ simpleCode := `
+import asyncio
+async def main():
+ files = await Glob(pattern="*.go", path=".")
+ print(len(files))
+asyncio.run(main())
+`
+
+ for i := 0; i < iterations; i++ {
+ start := time.Now()
+ _, _ = runtime.Execute(ctx, simpleCode, map[string]any{})
+ totalDuration += time.Since(start)
+ }
+
+ avgDuration := totalDuration / time.Duration(iterations)
+ fmt.Printf("平均延迟: %v\n", avgDuration)
+ fmt.Printf("QPS: %.2f 次/秒\n", 1000.0/float64(avgDuration.Milliseconds()))
+
+ fmt.Println("\n=== 测试完成 ===")
+ fmt.Println("PTC 基础设施工作正常!")
+}
diff --git a/pkg/provider/anthropic.go b/pkg/provider/anthropic.go
index b1b854a..d45daa9 100644
--- a/pkg/provider/anthropic.go
+++ b/pkg/provider/anthropic.go
@@ -247,6 +247,10 @@ func (ap *AnthropicProvider) buildRequest(messages []types.Message, opts *Stream
}
toolMap["input_examples"] = examples
}
+ // PTC 支持: 添加 AllowedCallers 字段
+ if len(tool.AllowedCallers) > 0 {
+ toolMap["allowed_callers"] = tool.AllowedCallers
+ }
tools = append(tools, toolMap)
}
req["tools"] = tools
@@ -316,12 +320,22 @@ func (ap *AnthropicProvider) convertMessages(messages []types.Message) []map[str
"text": b.Text,
})
case *types.ToolUseBlock:
- blocks = append(blocks, map[string]any{
+ toolUse := map[string]any{
"type": "tool_use",
"id": b.ID,
"name": b.Name,
"input": b.Input,
- })
+ }
+ // PTC 支持: 添加 Caller 字段
+ if b.Caller != nil {
+ toolUse["caller"] = map[string]any{
+ "type": b.Caller.Type,
+ }
+ if b.Caller.ToolID != "" {
+ toolUse["caller"].(map[string]any)["tool_id"] = b.Caller.ToolID
+ }
+ }
+ blocks = append(blocks, toolUse)
case *types.ToolResultBlock:
blocks = append(blocks, map[string]any{
"type": "tool_result",
@@ -512,10 +526,22 @@ func (ap *AnthropicProvider) parseCompleteResponse(apiResp map[string]any) (type
input = make(map[string]any)
}
+ // PTC 支持: 解析 Caller 字段
+ var caller *types.ToolCaller
+ if callerData, ok := block["caller"].(map[string]any); ok {
+ callerType, _ := callerData["type"].(string)
+ toolID, _ := callerData["tool_id"].(string)
+ caller = &types.ToolCaller{
+ Type: callerType,
+ ToolID: toolID,
+ }
+ }
+
assistantContent = append(assistantContent, &types.ToolUseBlock{
- ID: toolID,
- Name: toolName,
- Input: input,
+ ID: toolID,
+ Name: toolName,
+ Input: input,
+ Caller: caller,
})
}
}
diff --git a/pkg/provider/interface.go b/pkg/provider/interface.go
index b3bb218..1885b11 100644
--- a/pkg/provider/interface.go
+++ b/pkg/provider/interface.go
@@ -129,20 +129,25 @@ type CompleteResponse struct {
// ToolExample 工具使用示例(与 tools.ToolExample 保持一致)
type ToolExample struct {
- Description string `json:"description"`
+ Description string `json:"description"`
Input map[string]any `json:"input"`
Output any `json:"output,omitempty"`
}
// ToolSchema 工具Schema
type ToolSchema struct {
- Name string `json:"name"`
- Description string `json:"description"`
+ Name string `json:"name"`
+ Description string `json:"description"`
InputSchema map[string]any `json:"input_schema"`
// InputExamples 工具使用示例,帮助 LLM 更准确地调用工具
// 参考 Anthropic 的 Tool Use Examples 功能
InputExamples []ToolExample `json:"input_examples,omitempty"`
+
+ // AllowedCallers 指定哪些上下文可以调用此工具 (PTC 支持)
+ // 可选值: ["direct"], ["code_execution_20250825"], 或两者组合
+ // 默认: nil 或 ["direct"] - 仅 LLM 直接调用
+ AllowedCallers []string `json:"allowed_callers,omitempty"`
}
// ProviderCapabilities 模型能力(扩展版本)
diff --git a/pkg/tools/bridge/http_server.go b/pkg/tools/bridge/http_server.go
new file mode 100644
index 0000000..2ed3c34
--- /dev/null
+++ b/pkg/tools/bridge/http_server.go
@@ -0,0 +1,274 @@
+package bridge
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "os"
+ "sync"
+ "time"
+
+ "github.com/astercloud/aster/pkg/tools"
+)
+
+// 性能优化: Schema 缓存
+type schemaCache struct {
+ schemas map[string]any
+ mu sync.RWMutex
+ ttl time.Duration
+ entries map[string]*cacheEntry
+}
+
+type cacheEntry struct {
+ data any
+ timestamp time.Time
+}
+
+func newSchemaCache(ttl time.Duration) *schemaCache {
+ return &schemaCache{
+ schemas: make(map[string]any),
+ entries: make(map[string]*cacheEntry),
+ ttl: ttl,
+ }
+}
+
+func (c *schemaCache) get(key string) (any, bool) {
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+
+ entry, ok := c.entries[key]
+ if !ok {
+ return nil, false
+ }
+
+ // 检查是否过期
+ if time.Since(entry.timestamp) > c.ttl {
+ return nil, false
+ }
+
+ return entry.data, true
+}
+
+func (c *schemaCache) set(key string, value any) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ c.entries[key] = &cacheEntry{
+ data: value,
+ timestamp: time.Now(),
+ }
+}
+
+// HTTPBridgeServer HTTP 桥接服务器
+// 提供 HTTP API 供 Python/Node.js 代码调用 Go 侧的工具
+type HTTPBridgeServer struct {
+ bridge *ToolBridge
+ server *http.Server
+ mu sync.RWMutex
+
+ // 工具上下文工厂
+ contextFactory func() *tools.ToolContext
+
+ // 性能优化: Schema 缓存
+ schemaCache *schemaCache
+}
+
+// NewHTTPBridgeServer 创建 HTTP 桥接服务器
+func NewHTTPBridgeServer(bridge *ToolBridge, addr string) *HTTPBridgeServer {
+ s := &HTTPBridgeServer{
+ bridge: bridge,
+ server: &http.Server{
+ Addr: addr,
+ ReadTimeout: 30 * time.Second,
+ WriteTimeout: 30 * time.Second,
+ // 性能优化: 配置更大的缓冲区
+ ReadHeaderTimeout: 5 * time.Second,
+ MaxHeaderBytes: 1 << 20, // 1MB
+ // 启用 keep-alive 连接复用
+ IdleTimeout: 120 * time.Second,
+ },
+ // 初始化 Schema 缓存(5分钟TTL)
+ schemaCache: newSchemaCache(5 * time.Minute),
+ }
+
+ // 设置路由
+ mux := http.NewServeMux()
+ mux.HandleFunc("/tools/call", s.handleToolCall)
+ mux.HandleFunc("/tools/list", s.handleToolList)
+ mux.HandleFunc("/tools/schema", s.handleToolSchema)
+ mux.HandleFunc("/health", s.handleHealth)
+
+ s.server.Handler = mux
+ return s
+}
+
+// SetContextFactory 设置工具上下文工厂
+func (s *HTTPBridgeServer) SetContextFactory(factory func() *tools.ToolContext) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ s.contextFactory = factory
+}
+
+// ToolCallRequest 工具调用请求
+type ToolCallRequest struct {
+ Tool string `json:"tool"`
+ Input map[string]any `json:"input"`
+}
+
+// ToolCallResponse 工具调用响应
+type ToolCallResponse struct {
+ Success bool `json:"success"`
+ Result any `json:"result,omitempty"`
+ Error string `json:"error,omitempty"`
+}
+
+// handleToolCall 处理工具调用请求
+func (s *HTTPBridgeServer) handleToolCall(w http.ResponseWriter, r *http.Request) {
+ // 仅支持 POST
+ if r.Method != http.MethodPost {
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ // 解析请求
+ var req ToolCallRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ s.sendError(w, "Invalid JSON request", http.StatusBadRequest)
+ return
+ }
+
+ // 验证参数
+ if req.Tool == "" {
+ s.sendError(w, "Tool name is required", http.StatusBadRequest)
+ return
+ }
+
+ // 获取工具上下文
+ tc := s.getToolContext()
+
+ // 调用工具
+ ctx := r.Context()
+ result, _ := s.bridge.CallTool(ctx, req.Tool, req.Input, tc)
+
+ // 返回响应
+ s.sendJSON(w, &ToolCallResponse{
+ Success: result.Success,
+ Result: result.Result,
+ Error: result.Error,
+ })
+}
+
+// handleToolList 处理工具列表请求
+func (s *HTTPBridgeServer) handleToolList(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet {
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ tools := s.bridge.ListAvailableTools()
+ s.sendJSON(w, map[string]any{
+ "tools": tools,
+ })
+}
+
+// handleToolSchema 处理工具 Schema 请求(带缓存)
+func (s *HTTPBridgeServer) handleToolSchema(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet {
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ toolName := r.URL.Query().Get("name")
+ if toolName == "" {
+ s.sendError(w, "Tool name is required", http.StatusBadRequest)
+ return
+ }
+
+ // 尝试从缓存获取
+ if cachedSchema, ok := s.schemaCache.get(toolName); ok {
+ s.sendJSON(w, cachedSchema)
+ return
+ }
+
+ // 缓存未命中,从 bridge 获取
+ schema, err := s.bridge.GetToolSchema(toolName)
+ if err != nil {
+ s.sendError(w, err.Error(), http.StatusNotFound)
+ return
+ }
+
+ // 存入缓存
+ s.schemaCache.set(toolName, schema)
+
+ s.sendJSON(w, schema)
+}
+
+// handleHealth 健康检查
+func (s *HTTPBridgeServer) handleHealth(w http.ResponseWriter, r *http.Request) {
+ s.sendJSON(w, map[string]string{
+ "status": "ok",
+ })
+}
+
+// getToolContext 获取工具上下文
+func (s *HTTPBridgeServer) getToolContext() *tools.ToolContext {
+ s.mu.RLock()
+ factory := s.contextFactory
+ s.mu.RUnlock()
+
+ if factory != nil {
+ return factory()
+ }
+
+ // 默认返回空上下文
+ return &tools.ToolContext{
+ Services: make(map[string]any),
+ }
+}
+
+// sendJSON 发送 JSON 响应
+func (s *HTTPBridgeServer) sendJSON(w http.ResponseWriter, data any) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ if err := json.NewEncoder(w).Encode(data); err != nil {
+ // 无法发送错误响应给客户端,仅记录日志
+ fmt.Fprintf(os.Stderr, "Failed to encode JSON response: %v\n", err)
+ }
+}
+
+// sendError 发送错误响应
+func (s *HTTPBridgeServer) sendError(w http.ResponseWriter, message string, statusCode int) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(statusCode)
+ if err := json.NewEncoder(w).Encode(map[string]string{
+ "error": message,
+ }); err != nil {
+ // 无法发送错误响应给客户端,仅记录日志
+ fmt.Fprintf(os.Stderr, "Failed to encode error response: %v\n", err)
+ }
+}
+
+// Start 启动服务器
+func (s *HTTPBridgeServer) Start() error {
+ fmt.Printf("HTTP Bridge Server listening on %s\n", s.server.Addr)
+ return s.server.ListenAndServe()
+}
+
+// StartAsync 异步启动服务器
+func (s *HTTPBridgeServer) StartAsync() error {
+ go func() {
+ if err := s.Start(); err != nil && err != http.ErrServerClosed {
+ fmt.Printf("HTTP Bridge Server error: %v\n", err)
+ }
+ }()
+
+ // 等待服务器启动
+ time.Sleep(100 * time.Millisecond)
+ return nil
+}
+
+// Shutdown 关闭服务器
+func (s *HTTPBridgeServer) Shutdown(ctx context.Context) error {
+ return s.server.Shutdown(ctx)
+}
diff --git a/pkg/tools/bridge/ptc_integration_test.go b/pkg/tools/bridge/ptc_integration_test.go
new file mode 100644
index 0000000..577ced4
--- /dev/null
+++ b/pkg/tools/bridge/ptc_integration_test.go
@@ -0,0 +1,240 @@
+package bridge
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/astercloud/aster/pkg/tools"
+)
+
+// MockTool 模拟工具用于测试
+type MockTool struct{}
+
+func (m *MockTool) Name() string {
+ return "MockTool"
+}
+
+func (m *MockTool) Description() string {
+ return "A mock tool for testing"
+}
+
+func (m *MockTool) InputSchema() map[string]any {
+ return map[string]any{
+ "type": "object",
+ "properties": map[string]any{
+ "message": map[string]any{
+ "type": "string",
+ "description": "A test message",
+ },
+ },
+ "required": []string{"message"},
+ }
+}
+
+func (m *MockTool) Execute(ctx context.Context, input map[string]any, tc *tools.ToolContext) (any, error) {
+ message, _ := input["message"].(string)
+ return map[string]any{
+ "echo": message,
+ "time": time.Now().Unix(),
+ }, nil
+}
+
+func (m *MockTool) Prompt() string {
+ return "Mock tool for testing"
+}
+
+// NewMockTool 创建 MockTool 实例
+func NewMockTool(config map[string]any) (tools.Tool, error) {
+ return &MockTool{}, nil
+}
+
+// TestPTCIntegration 测试完整的 PTC 流程
+func TestPTCIntegration(t *testing.T) {
+ // 检查 Python 和 aiohttp 是否可用
+ if !checkPythonAiohttp() {
+ t.Skip("Skipping test: Python with aiohttp is not available")
+ }
+
+ // 1. 创建工具注册表
+ registry := tools.NewRegistry()
+ registry.Register("MockTool", NewMockTool)
+
+ // 2. 创建 ToolBridge
+ toolBridge := NewToolBridge(registry)
+
+ // 3. 创建 HTTP 桥接服务器
+ server := NewHTTPBridgeServer(toolBridge, "localhost:18080")
+
+ // 4. 启动服务器
+ if err := server.StartAsync(); err != nil {
+ t.Fatalf("Failed to start server: %v", err)
+ }
+ defer func() {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ _ = server.Shutdown(ctx)
+ }()
+
+ // 等待服务器启动
+ time.Sleep(200 * time.Millisecond)
+
+ // 5. 创建 PythonRuntime
+ runtime := NewPythonRuntime(nil)
+ runtime.SetTools([]string{"MockTool"})
+ runtime.SetBridgeURL("http://localhost:18080")
+
+ // 6. 测试代码执行 - 调用工具
+ pythonCode := `
+import asyncio
+
+async def main():
+ result = await MockTool(message="Hello from Python!")
+ print(result)
+
+asyncio.run(main())
+`
+
+ ctx := context.Background()
+ result, err := runtime.Execute(ctx, pythonCode, map[string]any{})
+
+ // 7. 验证结果
+ if err != nil {
+ t.Fatalf("Failed to execute Python code: %v", err)
+ }
+
+ if !result.Success {
+ t.Errorf("Execution failed: %s", result.Error)
+ }
+
+ if result.Stdout == "" {
+ t.Error("Expected stdout output, got empty string")
+ }
+
+ t.Logf("Test passed! Output: %s", result.Stdout)
+}
+
+// TestHTTPBridgeServerEndpoints 测试 HTTP 服务器端点
+func TestHTTPBridgeServerEndpoints(t *testing.T) {
+ // 创建工具注册表和桥接
+ registry := tools.NewRegistry()
+ registry.Register("MockTool", NewMockTool)
+ toolBridge := NewToolBridge(registry)
+
+ // 创建并启动服务器
+ server := NewHTTPBridgeServer(toolBridge, "localhost:18081")
+ if err := server.StartAsync(); err != nil {
+ t.Fatalf("Failed to start server: %v", err)
+ }
+ defer func() {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ _ = server.Shutdown(ctx)
+ }()
+
+ // 等待服务器启动
+ time.Sleep(200 * time.Millisecond)
+
+ // 测试各个端点
+ t.Run("ToolList", func(t *testing.T) {
+ tools := toolBridge.ListAvailableTools()
+ if len(tools) == 0 {
+ t.Error("Expected at least one tool")
+ }
+ if tools[0] != "MockTool" {
+ t.Errorf("Expected MockTool, got %s", tools[0])
+ }
+ })
+
+ t.Run("ToolSchema", func(t *testing.T) {
+ schema, err := toolBridge.GetToolSchema("MockTool")
+ if err != nil {
+ t.Fatalf("Failed to get schema: %v", err)
+ }
+ if schema["name"] != "MockTool" {
+ t.Error("Schema name mismatch")
+ }
+ })
+
+ t.Run("ToolCall", func(t *testing.T) {
+ ctx := context.Background()
+ result, err := toolBridge.CallTool(ctx, "MockTool", map[string]any{
+ "message": "Test message",
+ }, nil)
+
+ if err != nil {
+ t.Fatalf("Failed to call tool: %v", err)
+ }
+
+ if !result.Success {
+ t.Errorf("Tool call failed: %s", result.Error)
+ }
+
+ resultMap, ok := result.Result.(map[string]any)
+ if !ok {
+ t.Fatal("Expected map result")
+ }
+
+ if resultMap["echo"] != "Test message" {
+ t.Errorf("Expected echo 'Test message', got %v", resultMap["echo"])
+ }
+ })
+}
+
+// TestPythonRuntimeToolInjection 测试 Python 运行时工具注入
+func TestPythonRuntimeToolInjection(t *testing.T) {
+ runtime := NewPythonRuntime(nil)
+
+ // 测试无工具的简单模式
+ t.Run("NoToolsMode", func(t *testing.T) {
+ code := "print('Hello World')"
+ result, err := runtime.Execute(context.Background(), code, map[string]any{})
+ if err != nil {
+ t.Fatalf("Failed to execute: %v", err)
+ }
+ if !result.Success {
+ t.Error("Expected success")
+ }
+ })
+
+ // 测试带工具的 PTC 模式
+ t.Run("PTCMode", func(t *testing.T) {
+ runtime.SetTools([]string{"Read", "Write"})
+ runtime.SetBridgeURL("http://localhost:8080")
+
+ // 验证生成的代码包含工具注入
+ wrappedCode := runtime.wrapCode("print('test')", map[string]any{})
+
+ // 检查是否包含关键组件
+ if !contains(wrappedCode, "_AsterBridge") {
+ t.Error("Expected _AsterBridge class in wrapped code")
+ }
+ if !contains(wrappedCode, "async def _user_main()") {
+ t.Error("Expected async main wrapper")
+ }
+ if !contains(wrappedCode, "Read") {
+ t.Error("Expected Read tool in injected tools")
+ }
+ })
+}
+
+// contains 检查字符串是否包含子串
+func contains(s, substr string) bool {
+ return len(s) >= len(substr) && (s == substr || len(s) > len(substr) && (s[:len(substr)] == substr || contains(s[1:], substr)))
+}
+
+// checkPythonAiohttp 检查 Python 和 aiohttp 是否可用
+func checkPythonAiohttp() bool {
+ runtime := NewPythonRuntime(nil)
+ if !runtime.IsAvailable() {
+ return false
+ }
+
+ // 检查 aiohttp 是否安装
+ code := "import aiohttp"
+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
+ defer cancel()
+
+ result, err := runtime.Execute(ctx, code, map[string]any{})
+ return err == nil && result.Success
+}
diff --git a/pkg/tools/bridge/runtime.go b/pkg/tools/bridge/runtime.go
index e5f7bac..0ffe400 100644
--- a/pkg/tools/bridge/runtime.go
+++ b/pkg/tools/bridge/runtime.go
@@ -33,13 +33,13 @@ type CodeRuntime interface {
// ExecutionResult 代码执行结果
type ExecutionResult struct {
- Success bool `json:"success"`
- Output any `json:"output,omitempty"`
- Stdout string `json:"stdout,omitempty"`
- Stderr string `json:"stderr,omitempty"`
- Error string `json:"error,omitempty"`
- ExitCode int `json:"exit_code"`
- Duration int64 `json:"duration_ms"`
+ Success bool `json:"success"`
+ Output any `json:"output,omitempty"`
+ Stdout string `json:"stdout,omitempty"`
+ Stderr string `json:"stderr,omitempty"`
+ Error string `json:"error,omitempty"`
+ ExitCode int `json:"exit_code"`
+ Duration int64 `json:"duration_ms"`
}
// RuntimeConfig 运行时配置
@@ -62,8 +62,10 @@ func DefaultRuntimeConfig() *RuntimeConfig {
// PythonRuntime Python 运行时
type PythonRuntime struct {
- config *RuntimeConfig
- pythonPath string
+ config *RuntimeConfig
+ pythonPath string
+ availableTools []string // PTC: 可用工具列表
+ bridgeURL string // PTC: HTTP 桥接服务器地址
}
// NewPythonRuntime 创建 Python 运行时
@@ -95,6 +97,16 @@ func (r *PythonRuntime) IsAvailable() bool {
return err == nil
}
+// SetTools 设置可用工具列表 (PTC 支持)
+func (r *PythonRuntime) SetTools(tools []string) {
+ r.availableTools = tools
+}
+
+// SetBridgeURL 设置 HTTP 桥接服务器地址 (PTC 支持)
+func (r *PythonRuntime) SetBridgeURL(url string) {
+ r.bridgeURL = url
+}
+
func (r *PythonRuntime) Execute(ctx context.Context, code string, input map[string]any) (*ExecutionResult, error) {
start := time.Now()
@@ -174,7 +186,10 @@ func (r *PythonRuntime) Execute(ctx context.Context, code string, input map[stri
func (r *PythonRuntime) wrapCode(code string, input map[string]any) string {
inputJSON, _ := json.Marshal(input)
- return fmt.Sprintf(`import json
+
+ // 如果没有配置工具,使用简单包装
+ if len(r.availableTools) == 0 {
+ return fmt.Sprintf(`import json
import sys
# Input data
@@ -183,6 +198,148 @@ _input = json.loads('%s')
# User code
%s
`, string(inputJSON), code)
+ }
+
+ // PTC 模式: 注入工具桥接代码
+ bridgeURL := r.bridgeURL
+ if bridgeURL == "" {
+ bridgeURL = "http://localhost:8080"
+ }
+
+ // 生成工具列表 JSON
+ toolsJSON, _ := json.Marshal(r.availableTools)
+
+ return fmt.Sprintf(`import json
+import asyncio
+import sys
+import os
+
+# ========== Aster Bridge SDK (内联) ==========
+try:
+ import aiohttp
+except ImportError:
+ print("Error: aiohttp is required. Install it with: pip install aiohttp", file=sys.stderr)
+ sys.exit(1)
+
+class _ToolExecutionError(Exception):
+ """工具执行错误"""
+ pass
+
+class _NetworkError(Exception):
+ """网络错误"""
+ pass
+
+class _AsterBridge:
+ def __init__(self, base_url, max_retries=3, retry_delay=0.5):
+ self.base_url = base_url
+ self.max_retries = max_retries
+ self.retry_delay = retry_delay
+ self._session = None
+
+ async def _get_session(self):
+ if self._session is None or self._session.closed:
+ self._session = aiohttp.ClientSession()
+ return self._session
+
+ async def call_tool(self, name, **kwargs):
+ last_error = None
+ for attempt in range(self.max_retries):
+ try:
+ session = await self._get_session()
+ async with session.post(
+ f"{self.base_url}/tools/call",
+ json={"tool": name, "input": kwargs},
+ timeout=aiohttp.ClientTimeout(total=60),
+ ) as resp:
+ if resp.status >= 500:
+ error_text = await resp.text()
+ last_error = _NetworkError(f"Server error (HTTP {resp.status}): {error_text}")
+ if attempt < self.max_retries - 1:
+ await asyncio.sleep(self.retry_delay * (2 ** attempt))
+ continue
+ raise last_error
+ if resp.status >= 400:
+ error_text = await resp.text()
+ raise _NetworkError(f"Client error (HTTP {resp.status}): {error_text}")
+ result = await resp.json()
+ if not result.get("success"):
+ error_msg = result.get("error", "Unknown error")
+ raise _ToolExecutionError(f"Tool {name} failed: {error_msg}")
+ return result.get("result")
+ except aiohttp.ClientConnectorError as e:
+ last_error = _NetworkError(f"Connection error: {str(e)}. Is bridge server running?")
+ if attempt < self.max_retries - 1:
+ await asyncio.sleep(self.retry_delay * (2 ** attempt))
+ continue
+ raise last_error
+ except aiohttp.ClientError as e:
+ last_error = _NetworkError(f"Network error: {str(e)}")
+ if attempt < self.max_retries - 1:
+ await asyncio.sleep(self.retry_delay * (2 ** attempt))
+ continue
+ raise last_error
+ except _ToolExecutionError:
+ raise
+ except asyncio.TimeoutError:
+ last_error = _NetworkError(f"Tool {name} timed out after 60 seconds")
+ if attempt < self.max_retries - 1:
+ await asyncio.sleep(self.retry_delay * (2 ** attempt))
+ continue
+ raise last_error
+ if last_error:
+ raise last_error
+ raise _NetworkError(f"Failed to call tool {name} after {self.max_retries} attempts")
+
+ async def close(self):
+ if self._session and not self._session.closed:
+ await self._session.close()
+
+# 初始化桥接
+_bridge = _AsterBridge("%s")
+
+# 动态生成工具函数
+def _create_tool_function(bridge, tool_name):
+ async def tool_func(**kwargs):
+ return await bridge.call_tool(tool_name, **kwargs)
+ tool_func.__name__ = tool_name
+ return tool_func
+
+# 注入工具到全局命名空间
+_available_tools = %s
+for _tool_name in _available_tools:
+ globals()[_tool_name] = _create_tool_function(_bridge, _tool_name)
+
+# ========== 用户代码开始 ==========
+
+# Input data
+_input = json.loads('%s')
+
+# 包装用户代码在 async main 中
+async def _user_main():
+%s
+
+# 运行用户代码
+if __name__ == "__main__":
+ try:
+ asyncio.run(_user_main())
+ finally:
+ # 确保关闭会话
+ asyncio.run(_bridge.close())
+`, bridgeURL, string(toolsJSON), string(inputJSON), indentCode(code, " "))
+}
+
+// indentCode 缩进代码
+func indentCode(code string, indent string) string {
+ lines := strings.Split(code, "\n")
+ var indented []string
+ for _, line := range lines {
+ if strings.TrimSpace(line) == "" {
+ indented = append(indented, "")
+ } else {
+ indented = append(indented, indent+line)
+ }
+ }
+ return strings.Join(indented, "\n")
}
// NodeJSRuntime Node.js 运行时
@@ -476,6 +633,20 @@ func (m *RuntimeManager) AvailableLanguages() []Language {
return langs
}
+// SetPythonTools 设置 Python 运行时的可用工具列表 (PTC 支持)
+func (m *RuntimeManager) SetPythonTools(tools []string) {
+ if runtime, ok := m.runtimes[LangPython].(*PythonRuntime); ok {
+ runtime.SetTools(tools)
+ }
+}
+
+// SetPythonBridgeURL 设置 Python 运行时的 HTTP 桥接服务器地址 (PTC 支持)
+func (m *RuntimeManager) SetPythonBridgeURL(url string) {
+ if runtime, ok := m.runtimes[LangPython].(*PythonRuntime); ok {
+ runtime.SetBridgeURL(url)
+ }
+}
+
// DetectLanguage 根据文件扩展名检测语言
func DetectLanguage(filename string) Language {
ext := strings.ToLower(filepath.Ext(filename))
diff --git a/pkg/tools/builtin/codeexecute.go b/pkg/tools/builtin/codeexecute.go
index cb52a8f..33443f8 100644
--- a/pkg/tools/builtin/codeexecute.go
+++ b/pkg/tools/builtin/codeexecute.go
@@ -3,6 +3,7 @@ package builtin
import (
"context"
"fmt"
+ "sync"
"time"
"github.com/astercloud/aster/pkg/tools"
@@ -14,6 +15,12 @@ import (
type CodeExecuteTool struct {
runtimeManager *bridge.RuntimeManager
toolBridge *bridge.ToolBridge
+
+ // PTC 支持
+ httpServer *bridge.HTTPBridgeServer
+ bridgeURL string
+ serverStarted bool
+ mu sync.Mutex
}
// NewCodeExecuteTool 创建代码执行工具
@@ -38,9 +45,17 @@ func NewCodeExecuteToolWithBridge(toolBridge *bridge.ToolBridge) *CodeExecuteToo
return &CodeExecuteTool{
runtimeManager: bridge.NewRuntimeManager(nil),
toolBridge: toolBridge,
+ bridgeURL: "http://localhost:8080", // 默认桥接 URL
}
}
+// SetBridgeURL 设置 HTTP 桥接服务器地址
+func (t *CodeExecuteTool) SetBridgeURL(url string) {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+ t.bridgeURL = url
+}
+
func (t *CodeExecuteTool) Name() string {
return "CodeExecute"
}
@@ -71,7 +86,54 @@ func (t *CodeExecuteTool) InputSchema() map[string]any {
}
}
+// ensureBridgeServer 确保 HTTP 桥接服务器已启动 (PTC 支持)
+func (t *CodeExecuteTool) ensureBridgeServer(tc *tools.ToolContext) error {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+
+ // 如果已经启动,直接返回
+ if t.serverStarted {
+ return nil
+ }
+
+ // 如果没有 toolBridge,不启动服务器(非 PTC 模式)
+ if t.toolBridge == nil {
+ return nil
+ }
+
+ // 创建并启动 HTTP 桥接服务器
+ t.httpServer = bridge.NewHTTPBridgeServer(t.toolBridge, t.bridgeURL)
+
+ // 设置工具上下文工厂
+ t.httpServer.SetContextFactory(func() *tools.ToolContext {
+ return tc
+ })
+
+ // 异步启动服务器
+ if err := t.httpServer.StartAsync(); err != nil {
+ return fmt.Errorf("failed to start HTTP bridge server: %w", err)
+ }
+
+ // 获取可用工具列表
+ availableTools := t.toolBridge.ListAvailableTools()
+
+ // 设置 RuntimeManager 的工具列表和桥接 URL
+ t.runtimeManager.SetPythonTools(availableTools)
+ t.runtimeManager.SetPythonBridgeURL(t.bridgeURL)
+
+ t.serverStarted = true
+ return nil
+}
+
func (t *CodeExecuteTool) Execute(ctx context.Context, input map[string]any, tc *tools.ToolContext) (any, error) {
+ // 确保 HTTP 桥接服务器已启动 (PTC 支持)
+ if err := t.ensureBridgeServer(tc); err != nil {
+ return map[string]any{
+ "success": false,
+ "error": fmt.Sprintf("failed to start bridge server: %v", err),
+ }, nil
+ }
+
// 解析参数
langStr, ok := input["language"].(string)
if !ok {
diff --git a/pkg/types/message.go b/pkg/types/message.go
index 5343e7b..ac7e42d 100644
--- a/pkg/types/message.go
+++ b/pkg/types/message.go
@@ -39,9 +39,19 @@ func (t *TextBlock) IsContentBlock() {}
// ToolUseBlock 工具使用块
type ToolUseBlock struct {
- ID string `json:"id"`
- Name string `json:"name"`
- Input map[string]any `json:"input"`
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Input map[string]any `json:"input"`
+ Caller *ToolCaller `json:"caller,omitempty"` // PTC: 调用者信息
+}
+
+// ToolCaller 工具调用者信息 (PTC 支持)
+type ToolCaller struct {
+ // Type 调用者类型: "direct" (LLM直接调用) 或 "code_execution_20250825" (代码执行中调用)
+ Type string `json:"type"`
+
+ // ToolID 代码执行工具的 ID (当 Type="code_execution_20250825" 时)
+ ToolID string `json:"tool_id,omitempty"`
}
func (t *ToolUseBlock) IsContentBlock() {}