feat: 新增 core/agents 模块和 nanobot

- 新增 agents 模块,包含 agent、api、skills 等子模块
- 新增 nanobot 项目,支持多渠道集成
- 添加启动脚本 start-all.bat 和 start-all.sh

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 21:29:12 +08:00
parent ecb6be6463
commit 249e7e577a
167 changed files with 31315 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
"""Tools module for X-Agents.
This module provides tool infrastructure for the agent system.
It wraps and extends the nanobot tool implementation.
"""
from nanobot.agent.tools.base import Tool
from nanobot.agent.tools.registry import ToolRegistry
from agents.tools.builtin import (
get_builtin_tools,
ReadFileTool,
WriteFileTool,
ListDirectoryTool,
SearchTool,
WebSearchTool,
CalculatorTool,
GetTimeTool,
BashTool,
)
from agents.tools.manager import ToolManager
def create_default_registry() -> ToolRegistry:
"""Create a tool registry with default tools.
Returns:
Tool registry with built-in tools
"""
registry = ToolRegistry()
# Register built-in tools
for tool in get_builtin_tools():
registry.register(tool)
return registry
__all__ = [
"Tool",
"ToolRegistry",
"ToolManager",
"create_default_registry",
"get_builtin_tools",
"ReadFileTool",
"WriteFileTool",
"ListDirectoryTool",
"SearchTool",
"WebSearchTool",
"CalculatorTool",
"GetTimeTool",
"BashTool",
]

View File

@@ -0,0 +1,431 @@
"""Built-in tools for X-Agents."""
import asyncio
import json
import re
from pathlib import Path
from typing import Any
from nanobot.agent.tools.base import Tool
class ReadFileTool(Tool):
"""Read file contents."""
def __init__(self, workspace: Path | None = None):
self._workspace = workspace
@property
def name(self) -> str:
return "read_file"
@property
def description(self) -> str:
return "Read the contents of a file from the local filesystem."
@property
def parameters(self) -> dict[str, Any]:
return {
"type": "object",
"properties": {
"path": {"type": "string", "description": "The file path to read"},
"offset": {
"type": "integer",
"description": "Line number to start reading from (1-indexed)",
"default": 1,
},
"limit": {
"type": "integer",
"description": "Maximum number of lines to read",
"default": 100,
},
},
"required": ["path"],
}
async def execute(self, path: str, offset: int = 1, limit: int = 100, **kwargs: Any) -> str:
try:
file_path = Path(path)
if not file_path.is_absolute() and self._workspace:
file_path = self._workspace / file_path
if not file_path.exists():
return f"Error: File not found: {path}"
if not file_path.is_file():
return f"Error: Not a file: {path}"
lines = file_path.read_text(encoding="utf-8").split("\n")
start = max(0, offset - 1)
end = min(len(lines), start + limit)
result_lines = [f"{i+1:4d}| {line}" for i, line in enumerate(lines[start:end], start=start+1)]
return f"File: {file_path}\nLines {start+1}-{end}/{len(lines)}\n\n" + "\n".join(result_lines)
except Exception as e:
return f"Error reading file: {str(e)}"
class WriteFileTool(Tool):
"""Write content to a file."""
def __init__(self, workspace: Path | None = None):
self._workspace = workspace
@property
def name(self) -> str:
return "write_file"
@property
def description(self) -> str:
return "Write content to a file. Creates the file if it doesn't exist."
@property
def parameters(self) -> dict[str, Any]:
return {
"type": "object",
"properties": {
"path": {"type": "string", "description": "The file path to write to"},
"content": {"type": "string", "description": "Content to write to the file"},
"append": {
"type": "boolean",
"description": "Append to existing file instead of overwriting",
"default": False,
},
},
"required": ["path", "content"],
}
async def execute(self, path: str, content: str, append: bool = False, **kwargs: Any) -> str:
try:
file_path = Path(path)
if not file_path.is_absolute() and self._workspace:
file_path = self._workspace / file_path
file_path.parent.mkdir(parents=True, exist_ok=True)
mode = "a" if append else "w"
with open(file_path, mode, encoding="utf-8") as f:
f.write(content)
return f"Successfully wrote to {file_path}"
except Exception as e:
return f"Error writing file: {str(e)}"
class ListDirectoryTool(Tool):
"""List directory contents."""
def __init__(self, workspace: Path | None = None):
self._workspace = workspace
@property
def name(self) -> str:
return "list_directory"
@property
def description(self) -> str:
return "List files and directories in a given path."
@property
def parameters(self) -> dict[str, Any]:
return {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Directory path to list",
"default": ".",
},
"recursive": {
"type": "boolean",
"description": "List recursively",
"default": False,
},
},
}
async def execute(self, path: str = ".", recursive: bool = False, **kwargs: Any) -> str:
try:
dir_path = Path(path)
if not dir_path.is_absolute() and self._workspace:
dir_path = self._workspace / dir_path
if not dir_path.exists():
return f"Error: Path not found: {path}"
if not dir_path.is_dir():
return f"Error: Not a directory: {path}"
if recursive:
items = []
for item in dir_path.rglob("*"):
rel = item.relative_to(dir_path)
prefix = "[D]" if item.is_dir() else "[F]"
items.append(f"{prefix} {rel}")
return "\n".join(sorted(items)) or "(empty)"
else:
items = []
for item in dir_path.iterdir():
prefix = "[D]" if item.is_dir() else "[F]"
items.append(f"{prefix} {item.name}")
return "\n".join(sorted(items)) or "(empty)"
except Exception as e:
return f"Error listing directory: {str(e)}"
class SearchTool(Tool):
"""Search for text in files."""
def __init__(self, workspace: Path | None = None):
self._workspace = workspace
@property
def name(self) -> str:
return "search"
@property
def description(self) -> str:
return "Search for text patterns in files using regex."
@property
def parameters(self) -> dict[str, Any]:
return {
"type": "object",
"properties": {
"pattern": {"type": "string", "description": "Regex pattern to search for"},
"path": {
"type": "string",
"description": "Directory path to search in",
"default": ".",
},
"file_pattern": {
"type": "string",
"description": "File glob pattern (e.g., *.py)",
"default": "*",
},
"case_sensitive": {
"type": "boolean",
"description": "Case sensitive search",
"default": True,
},
},
"required": ["pattern"],
}
async def execute(
self,
pattern: str,
path: str = ".",
file_pattern: str = "*",
case_sensitive: bool = True,
**kwargs: Any,
) -> str:
try:
search_path = Path(path)
if not search_path.is_absolute() and self._workspace:
search_path = self._workspace / search_path
if not search_path.exists():
return f"Error: Path not found: {path}"
flags = 0 if case_sensitive else re.IGNORECASE
regex = re.compile(pattern, flags)
results = []
for file_path in search_path.rglob(file_pattern):
if not file_path.is_file():
continue
try:
content = file_path.read_text(encoding="utf-8")
for i, line in enumerate(content.split("\n"), 1):
if regex.search(line):
results.append(f"{file_path}:{i}: {line.strip()[:100]}")
except Exception:
continue
if not results:
return f"No matches found for: {pattern}"
return f"Found {len(results)} matches:\n" + "\n".join(results[:50])
except Exception as e:
return f"Error searching: {str(e)}"
class WebSearchTool(Tool):
"""Search the web for information."""
def __init__(self):
pass
@property
def name(self) -> str:
return "web_search"
@property
def description(self) -> str:
return "Search the web for information using a search engine."
@property
def parameters(self) -> dict[str, Any]:
return {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query"},
"max_results": {
"type": "integer",
"description": "Maximum number of results",
"default": 5,
},
},
"required": ["query"],
}
async def execute(self, query: str, max_results: int = 5, **kwargs: Any) -> str:
# Placeholder for web search implementation
# In production, this would use a search API (e.g., Google, Bing, SerpAPI)
return f"Web search not implemented yet. Query: {query}"
class CalculatorTool(Tool):
"""Simple calculator tool."""
@property
def name(self) -> str:
return "calculator"
@property
def description(self) -> str:
return "Evaluate a mathematical expression."
@property
def parameters(self) -> dict[str, Any]:
return {
"type": "object",
"properties": {
"expression": {"type": "string", "description": "Mathematical expression to evaluate"},
},
"required": ["expression"],
}
async def execute(self, expression: str, **kwargs: Any) -> str:
try:
# Safe evaluation - only allow basic math operators
allowed_chars = set("0123456789+-*/.() ")
if not all(c in allowed_chars for c in expression):
return "Error: Invalid characters in expression"
result = eval(expression) # Note: In production, use a safer parser
return f"{expression} = {result}"
except Exception as e:
return f"Error evaluating expression: {str(e)}"
class GetTimeTool(Tool):
"""Get current time."""
@property
def name(self) -> str:
return "get_time"
@property
def description(self) -> str:
return "Get the current date and time."
@property
def parameters(self) -> dict[str, Any]:
return {
"type": "object",
"properties": {
"timezone": {
"type": "string",
"description": "Timezone (e.g., UTC, Asia/Shanghai)",
"default": "UTC",
},
},
}
async def execute(self, timezone: str = "UTC", **kwargs: Any) -> str:
from datetime import datetime, timezone
try:
if timezone.upper() != "UTC":
# For non-UTC timezones, return simple result
return f"Timezone '{timezone}' not supported. Current UTC time: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}"
except Exception:
pass
now = datetime.now(timezone.utc)
return now.strftime("%Y-%m-%d %H:%M:%S UTC")
class BashTool(Tool):
"""Execute bash commands."""
def __init__(self, workspace: Path | None = None):
self._workspace = workspace
@property
def name(self) -> str:
return "bash"
@property
def description(self) -> str:
return "Execute a bash command and return its output."
@property
def parameters(self) -> dict[str, Any]:
return {
"type": "object",
"properties": {
"command": {"type": "string", "description": "Command to execute"},
"timeout": {
"type": "integer",
"description": "Timeout in seconds",
"default": 30,
},
},
"required": ["command"],
}
async def execute(self, command: str, timeout: int = 30, **kwargs: Any) -> str:
try:
process = await asyncio.create_subprocess_shell(
command,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
try:
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout)
result = []
if stdout:
result.append(stdout.decode("utf-8"))
if stderr:
result.append(f"STDERR: {stderr.decode('utf-8')}")
return "\n".join(result) or "Command completed with no output"
except asyncio.TimeoutError:
process.kill()
return f"Error: Command timed out after {timeout} seconds"
except Exception as e:
return f"Error executing command: {str(e)}"
def get_builtin_tools(workspace: Path | None = None) -> list[Tool]:
"""Get list of all built-in tools.
Args:
workspace: Optional workspace path for file operations
Returns:
List of Tool instances
"""
return [
ReadFileTool(workspace),
WriteFileTool(workspace),
ListDirectoryTool(workspace),
SearchTool(workspace),
WebSearchTool(),
CalculatorTool(),
GetTimeTool(),
BashTool(workspace),
]

View File

@@ -0,0 +1,108 @@
"""Tool manager for loading and managing tools."""
import logging
from pathlib import Path
from typing import Any
from nanobot.agent.tools.registry import ToolRegistry
from agents.tools.builtin import get_builtin_tools
logger = logging.getLogger(__name__)
class ToolManager:
"""Manages tools for the agent."""
def __init__(self, workspace: Path | None = None):
"""Initialize tool manager.
Args:
workspace: Optional workspace path
"""
self.workspace = workspace
self.registry = ToolRegistry()
self._load_builtin_tools()
def _load_builtin_tools(self) -> None:
"""Load all built-in tools."""
tools = get_builtin_tools(self.workspace)
for tool in tools:
self.registry.register(tool)
logger.info(f"Loaded {len(tools)} built-in tools")
def register_tool(self, tool: Any) -> None:
"""Register a custom tool.
Args:
tool: Tool instance to register
"""
self.registry.register(tool)
logger.info(f"Registered tool: {tool.name}")
def unregister_tool(self, name: str) -> None:
"""Unregister a tool.
Args:
name: Tool name to unregister
"""
self.registry.unregister(name)
logger.info(f"Unregistered tool: {name}")
def get_tool(self, name: str) -> Any:
"""Get a tool by name.
Args:
name: Tool name
Returns:
Tool instance or None
"""
return self.registry.get(name)
def has_tool(self, name: str) -> bool:
"""Check if a tool is registered.
Args:
name: Tool name
Returns:
True if tool exists
"""
return self.registry.has(name)
def list_tools(self) -> list[str]:
"""List all registered tool names.
Returns:
List of tool names
"""
return self.registry.tool_names
def get_tool_definitions(self) -> list[dict[str, Any]]:
"""Get all tool definitions in OpenAI format.
Returns:
List of tool schemas
"""
return self.registry.get_definitions()
async def execute_tool(self, name: str, params: dict[str, Any]) -> str:
"""Execute a tool by name.
Args:
name: Tool name
params: Tool parameters
Returns:
Tool execution result
"""
return await self.registry.execute(name, params)
def __len__(self) -> int:
"""Get number of registered tools."""
return len(self.registry)
def __contains__(self, name: str) -> bool:
"""Check if tool is registered."""
return name in self.registry

107
core/agents/tools/sync.py Normal file
View File

@@ -0,0 +1,107 @@
"""Tool synchronization between Python Agent and Go backend."""
import asyncio
import logging
from typing import Any
import aiohttp
logger = logging.getLogger(__name__)
class ToolSyncClient:
"""Client for syncing tools to Go backend."""
def __init__(self, base_url: str, agent_id: str = "default"):
"""Initialize tool sync client.
Args:
base_url: Go backend base URL
agent_id: Agent ID
"""
self.base_url = base_url.rstrip("/")
self.agent_id = agent_id
self._session = None
async def _get_session(self) -> aiohttp.ClientSession:
"""Get or create aiohttp session."""
if self._session is None or self._session.closed:
self._session = aiohttp.ClientSession()
return self._session
async def close(self) -> None:
"""Close the session."""
if self._session and not self._session.closed:
await self._session.close()
async def sync_tools(
self,
tools: list[dict[str, Any]],
) -> tuple[int, str]:
"""Sync tools to Go backend.
Args:
tools: List of tool definitions
Returns:
Tuple of (synced_count, message)
"""
url = f"{self.base_url}/tool/sync-from-python"
# Transform tools to match Go backend format
python_tools = []
for tool in tools:
func = tool.get("function", {})
python_tools.append({
"name": func.get("name"),
"description": func.get("description"),
"parameters": func.get("parameters", "{}"),
"category": "python", # Default category for Python tools
})
payload = {"tools": python_tools}
try:
session = await self._get_session()
async with session.post(url, json=payload) as response:
if response.status == 200:
result = await response.json()
count = result.get("synced_count", 0)
return count, f"Synced {count} tools successfully"
else:
text = await response.text()
return 0, f"Failed to sync tools: {response.status} - {text}"
except Exception as e:
logger.error(f"Error syncing tools: {e}")
return 0, f"Error syncing tools: {e}"
async def sync_registry_tools(
registry,
base_url: str,
agent_id: str = "default",
) -> tuple[int, str]:
"""Sync tools from a ToolRegistry to Go backend.
Args:
registry: ToolRegistry instance
base_url: Go backend base URL
agent_id: Agent ID
Returns:
Tuple of (synced_count, message)
"""
client = ToolSyncClient(base_url, agent_id)
try:
# Get all tool definitions
tools = registry.get_definitions()
if not tools:
return 0, "No tools to sync"
# Sync tools
count, message = await client.sync_tools(tools)
return count, message
finally:
await client.close()