初始化项目:PyOpenClaw 核心框架

- 项目结构搭建
- Agent 核心类实现
- LLM 客户端(OpenAI)
- 工具系统框架
- 配置管理
- 简单示例

版本: v0.1.0
This commit is contained in:
yunxiafei
2026-03-16 21:07:31 +08:00
commit 97fb0a47ed
19 changed files with 1365 additions and 0 deletions

18
.env.example Normal file
View File

@@ -0,0 +1,18 @@
# OpenAI API Key
OPENAI_API_KEY=your-openai-api-key-here
# OpenAI Base URL可选用于自定义端点
# OPENAI_BASE_URL=https://api.openai.com/v1
# Anthropic API Key可选
# ANTHROPIC_API_KEY=your-anthropic-api-key-here
# Gateway 配置
GATEWAY_HOST=0.0.0.0
GATEWAY_PORT=8000
# 日志级别
LOG_LEVEL=INFO
# Debug 模式
DEBUG=false

64
.gitignore vendored Normal file
View File

@@ -0,0 +1,64 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual Environment
venv/
ENV/
env/
.venv
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Environment
.env
.env.local
# Logs
*.log
logs/
# Testing
.pytest_cache/
.coverage
htmlcov/
# MyPy
.mypy_cache/
.dmypy.json
dmypy.json
# Ruff
.ruff_cache/
# OS
.DS_Store
Thumbs.db
# Project specific
sessions/
workspace/
*.session.json

119
README.md Normal file
View File

@@ -0,0 +1,119 @@
# PyOpenClaw
**Python 实现 OpenClaw 核心功能**
## 项目简介
本项目是用 Python 重新实现 OpenClaw 核心功能的学习项目。
OpenClaw 是一个多渠道 AI Gateway支持多种消息渠道Discord、Telegram、Slack、飞书等和多种 LLM 提供商OpenAI、Anthropic、Google 等)。
## 核心目标
1. **深入理解 OpenClaw 架构**
2. **掌握 Agent 运行原理**
3. **实践 Python 异步编程**
4. **构建自己的智能体团队**
## 技术栈
- Python 3.11+
- FastAPI - HTTP 框架
- asyncio - 异步运行时
- Pydantic - 数据验证
- OpenAI SDK - LLM 调用
- Docker SDK - 沙箱执行
## 项目结构
```
pyopenclaw/
├── pyopenclaw/
│ ├── __init__.py
│ ├── core/ # 核心层
│ │ ├── __init__.py
│ │ ├── agent.py # Agent 核心类
│ │ ├── llm_client.py # LLM 客户端
│ │ ├── tool_registry.py # 工具注册表
│ │ ├── session.py # 会话管理
│ │ └── memory.py # 记忆系统
│ │
│ ├── tools/ # 工具实现
│ │ ├── __init__.py
│ │ ├── base.py # 工具基类
│ │ ├── file_tools.py # 文件工具
│ │ ├── exec_tools.py # 执行工具
│ │ └── web_tools.py # 网络工具
│ │
│ ├── gateway/ # Gateway 服务
│ │ ├── __init__.py
│ │ ├── server.py # HTTP 服务
│ │ ├── auth.py # 认证
│ │ └── middleware.py # 中间件
│ │
│ └── config/ # 配置管理
│ ├── __init__.py
│ └── settings.py # 配置类
├── tests/ # 测试
├── docs/ # 文档
├── pyproject.toml # 项目配置
├── requirements.txt # 依赖
└── README.md # 本文件
```
## 开发进度
### 阶段 1核心 Agent第 1-3 周)
- [x] 项目初始化
- [ ] Agent 核心类
- [ ] LLM 客户端
- [ ] 基础工具read/write/exec
- [ ] 简单会话管理
### 阶段 2Gateway 服务(第 4-5 周)
- [ ] FastAPI 服务
- [ ] WebSocket 支持
- [ ] 认证系统
- [ ] 会话持久化
### 阶段 3工具系统扩展第 6-7 周)
- [ ] 工具注册表
- [ ] 工具策略
- [ ] 更多工具实现
### 阶段 4渠道插件第 8 周起)
- [ ] 渠道基类
- [ ] Discord 插件
- [ ] Telegram 插件
- [ ] 其他渠道
## 快速开始
```bash
# 安装依赖
pip install -r requirements.txt
# 运行示例
python examples/simple_agent.py
```
## 学习资源
- [OpenClaw 官网](https://openclaw.ai)
- [OpenClaw GitHub](https://github.com/openclaw/openclaw)
- [TypeScript 基础教程](./docs/typescript-tutorial.md)
## 许可证
MIT License
---
**创建时间**: 2026-03-16
**作者**: 云下飞
**助手**: 小白 🐶

103
examples/simple_agent.py Normal file
View File

@@ -0,0 +1,103 @@
"""
简单 Agent 示例。
演示如何使用 PyOpenClaw 创建一个基本的 Agent。
"""
import asyncio
import os
from dotenv import load_dotenv
from pyopenclaw.config.settings import AgentConfig
from pyopenclaw.core.agent import Agent
from pyopenclaw.core.llm_client import create_llm_client
from pyopenclaw.core.tool_registry import ToolRegistry
from pyopenclaw.tools.file_tools import ReadTool, WriteTool
async def main():
"""运行简单 Agent 示例"""
# 加载环境变量
load_dotenv()
# 检查 API Key
api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
print("错误: 请设置 OPENAI_API_KEY 环境变量")
print("示例: export OPENAI_API_KEY='your-api-key'")
return
# 1. 创建 Agent 配置
config = AgentConfig(
name="小白",
model="openai/gpt-4o",
system_prompt="你是一个有用的 AI 助手,名叫小白。你是一只忠诚的电子宠物狗。",
tools=["read", "write"],
max_iterations=10,
)
# 2. 创建 LLM 客户端
llm_client = create_llm_client(
provider="openai",
model="gpt-4o",
api_key=api_key,
)
# 3. 创建工具注册表并注册工具
tool_registry = ToolRegistry()
tool_registry.register(ReadTool())
tool_registry.register(WriteTool())
# 4. 创建 Agent
agent = Agent(
config=config,
llm_client=llm_client,
tool_registry=tool_registry,
)
print("=" * 50)
print("PyOpenClaw 简单示例")
print("=" * 50)
print(f"Agent 名称: {config.name}")
print(f"模型: {config.model}")
print(f"可用工具: {config.tools}")
print("=" * 50)
# 5. 运行 Agent
user_input = "你好,请介绍一下你自己"
print(f"\n用户: {user_input}\n")
print("Agent 回复:")
print("-" * 50)
async for event in agent.run(user_input):
event_type = event.get("type")
if event_type == "content":
# 内容输出
print(event["content"])
if event.get("done"):
break
elif event_type == "tool_call":
# 工具调用
print(f"\n[调用工具: {event['name']}]")
print(f"参数: {event['arguments']}")
elif event_type == "tool_result":
# 工具结果
result = event["result"]
if len(result) > 100:
result = result[:100] + "..."
print(f"结果: {result}\n")
elif event_type == "error":
# 错误
print(f"\n错误: {event['content']}")
break
print("-" * 50)
print("\n运行完成!")
if __name__ == "__main__":
asyncio.run(main())

19
pyopenclaw/__init__.py Normal file
View File

@@ -0,0 +1,19 @@
"""
PyOpenClaw - Python implementation of OpenClaw core functionality.
这是一个学习项目,用于深入理解 OpenClaw 和 Agent 的原理。
"""
__version__ = "0.1.0"
__author__ = "云下飞"
from pyopenclaw.core.agent import Agent, AgentConfig
from pyopenclaw.core.llm_client import LLMClient
from pyopenclaw.core.tool_registry import ToolRegistry
__all__ = [
"Agent",
"AgentConfig",
"LLMClient",
"ToolRegistry",
]

View File

View File

@@ -0,0 +1,65 @@
"""
配置管理模块。
对应 OpenClaw 的 src/agents/defaults.ts 和 src/config/config.ts
"""
from typing import Optional, List
from pydantic import BaseModel, Field
from pydantic_settings import BaseSettings
class ModelConfig(BaseModel):
"""模型配置"""
provider: str = Field(default="openai", description="LLM 提供商")
model_id: str = Field(default="gpt-4o", description="模型 ID")
context_window: int = Field(default=128000, description="上下文窗口大小")
max_tokens: int = Field(default=4096, description="最大输出 token")
class AgentConfig(BaseModel):
"""Agent 配置
对应 OpenClaw 的 AgentConfig 接口
"""
name: str = Field(default="Agent", description="Agent 名称")
model: str = Field(default="openai/gpt-4o", description="模型provider/model_id 格式)")
system_prompt: str = Field(default="", description="系统提示词")
max_iterations: int = Field(default=100, description="最大迭代次数")
timeout: int = Field(default=300, description="超时时间(秒)")
tools: List[str] = Field(default_factory=list, description="可用工具列表")
class Settings(BaseSettings):
"""全局配置
对应 OpenClaw 的 openclaw.json 配置文件
"""
# 基础配置
app_name: str = "PyOpenClaw"
debug: bool = False
# LLM 配置
openai_api_key: Optional[str] = None
openai_base_url: Optional[str] = None
anthropic_api_key: Optional[str] = None
# Agent 默认配置
default_model: str = "openai/gpt-4o"
default_timeout: int = 300
default_max_iterations: int = 100
# Gateway 配置
gateway_host: str = "0.0.0.0"
gateway_port: int = 8000
# 日志配置
log_level: str = "INFO"
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
# 全局配置实例
settings = Settings()

View File

226
pyopenclaw/core/agent.py Normal file
View File

@@ -0,0 +1,226 @@
"""
Agent 核心模块。
对应 OpenClaw 的 src/agents/pi-embedded-runner/run.ts
"""
from typing import List, Dict, Any, Optional, AsyncGenerator
from enum import Enum
from dataclasses import dataclass, field
import asyncio
from loguru import logger
from pyopenclaw.config.settings import AgentConfig, settings
from pyopenclaw.core.llm_client import (
BaseLLMClient,
LLMClient,
Message,
LLMResponse,
ToolCall,
)
from pyopenclaw.core.tool_registry import ToolRegistry
from pyopenclaw.core.memory import Memory
class AgentStatus(Enum):
"""Agent 状态"""
IDLE = "idle"
RUNNING = "running"
WAITING_TOOL = "waiting_tool"
ENDED = "ended"
@dataclass
class AgentMeta:
"""Agent 元数据"""
session_id: str
provider: str
model: str
usage: Optional[Dict[str, int]] = None
prompt_tokens: Optional[int] = None
compaction_count: int = 0
@dataclass
class AgentResult:
"""Agent 运行结果"""
content: str
is_error: bool = False
meta: Optional[AgentMeta] = None
class Agent:
"""Agent 核心类
这是 OpenClaw 的 PI Agent 运行时的 Python 实现。
核心功能:
1. 接收用户输入
2. 调用 LLM 进行思考
3. 决定是否调用工具
4. 执行工具并获取结果
5. 循环直到任务完成
对应 TypeScript 代码:
- src/agents/pi-embedded-runner/run.ts: runEmbeddedPiAgent
"""
def __init__(
self,
config: AgentConfig,
llm_client: BaseLLMClient,
tool_registry: ToolRegistry,
memory: Optional[Memory] = None,
):
self.config = config
self.llm = llm_client
self.tools = tool_registry
self.memory = memory or Memory()
self.status = AgentStatus.IDLE
self._iteration = 0
self._session_id = f"session-{id(self)}"
logger.info(
f"Agent 初始化: name={config.name}, model={config.model}, "
f"tools={config.tools}"
)
async def run(
self,
user_input: str,
stream: bool = False,
) -> AsyncGenerator[Dict[str, Any], None]:
"""
Agent 主运行循环。
对应 OpenClaw 的 runEmbeddedPiAgent 函数。
Args:
user_input: 用户输入
stream: 是否流式输出
Yields:
运行过程中的事件
"""
self.status = AgentStatus.RUNNING
self.memory.add_message("user", user_input)
logger.info(f"Agent 开始运行: input={user_input[:50]}...")
try:
while self._iteration < self.config.max_iterations:
self._iteration += 1
logger.debug(f"迭代 {self._iteration}/{self.config.max_iterations}")
# 1. 构建 LLM 请求
messages = self._build_messages()
tool_schemas = self._get_tool_schemas()
# 2. 调用 LLM
logger.debug("调用 LLM...")
response = await self.llm.chat(
messages=messages,
tools=tool_schemas,
system_prompt=self.config.system_prompt,
)
# 3. 处理响应
if response.stop_reason == "error":
yield {
"type": "error",
"content": "LLM 调用失败",
}
break
# 4. 检查工具调用
if response.tool_calls:
self.status = AgentStatus.WAITING_TOOL
# 执行所有工具调用
for tool_call in response.tool_calls:
yield {
"type": "tool_call",
"name": tool_call.name,
"arguments": tool_call.arguments,
}
# 执行工具
result = await self._execute_tool(tool_call)
# 添加工具结果到记忆
self.memory.add_tool_result(tool_call.name, result)
yield {
"type": "tool_result",
"name": tool_call.name,
"result": result,
}
else:
# 5. 生成最终回复
self.memory.add_message("assistant", response.content)
yield {
"type": "content",
"content": response.content,
"done": True,
}
logger.info(f"Agent 运行完成: iterations={self._iteration}")
break
except asyncio.TimeoutError:
logger.error("Agent 运行超时")
yield {
"type": "error",
"content": "运行超时,请重试或使用 /new 开始新会话",
}
except Exception as e:
logger.error(f"Agent 运行失败: {e}")
yield {
"type": "error",
"content": f"运行失败: {e}",
}
finally:
self.status = AgentStatus.ENDED
async def _execute_tool(self, tool_call: ToolCall) -> str:
"""
执行工具。
对应 OpenClaw 的工具执行逻辑。
"""
logger.info(f"执行工具: {tool_call.name}, 参数: {tool_call.arguments}")
# 1. 检查工具是否存在
if not self.tools.has(tool_call.name):
return f"Error: 工具不存在: {tool_call.name}"
# 2. 检查工具策略
if not self.tools.is_allowed(tool_call.name, self.config.name):
return f"Error: 工具不允许: {tool_call.name}"
# 3. 执行工具
try:
result = await self.tools.execute(
tool_call.name,
tool_call.arguments,
)
return result
except Exception as e:
return f"Error: {e}"
def _build_messages(self) -> List[Message]:
"""构建消息列表"""
return self.memory.get_messages()
def _get_tool_schemas(self) -> List[Dict]:
"""获取工具模式定义"""
return self.tools.get_schemas(self.config.tools)
def reset(self) -> None:
"""重置 Agent 状态"""
self.memory.clear()
self._iteration = 0
self.status = AgentStatus.IDLE
logger.info(f"Agent 已重置: {self.config.name}")

View File

@@ -0,0 +1,222 @@
"""
LLM 客户端模块。
对应 OpenClaw 的 src/agents/pi-embedded-runner/run.ts 中的 LLM 调用部分
"""
from typing import List, Dict, Any, Optional, AsyncGenerator
from abc import ABC, abstractmethod
from dataclasses import dataclass
import json
from loguru import logger
@dataclass
class Message:
"""消息"""
role: str # user, assistant, system, tool
content: str
name: Optional[str] = None # 工具名称role=tool 时)
tool_calls: Optional[List[Dict]] = None
@dataclass
class ToolCall:
"""工具调用"""
id: str
name: str
arguments: Dict[str, Any]
@dataclass
class LLMResponse:
"""LLM 响应"""
content: Optional[str] = None
tool_calls: List[ToolCall] = None
stop_reason: str = "end_turn" # end_turn, tool_calls, error
usage: Optional[Dict[str, int]] = None
def __post_init__(self):
if self.tool_calls is None:
self.tool_calls = []
class BaseLLMClient(ABC):
"""LLM 客户端基类"""
@abstractmethod
async def chat(
self,
messages: List[Message],
tools: Optional[List[Dict]] = None,
system_prompt: Optional[str] = None,
) -> LLMResponse:
"""调用 LLM"""
pass
@abstractmethod
async def chat_stream(
self,
messages: List[Message],
tools: Optional[List[Dict]] = None,
system_prompt: Optional[str] = None,
) -> AsyncGenerator[Dict, None]:
"""流式调用 LLM"""
pass
class OpenAIClient(BaseLLMClient):
"""OpenAI 客户端
对应 OpenClaw 中的 OpenAI 提供商支持
"""
def __init__(
self,
api_key: str,
model: str = "gpt-4o",
base_url: Optional[str] = None,
):
from openai import AsyncOpenAI
self.client = AsyncOpenAI(
api_key=api_key,
base_url=base_url,
)
self.model = model
logger.info(f"OpenAI 客户端初始化: model={model}")
async def chat(
self,
messages: List[Message],
tools: Optional[List[Dict]] = None,
system_prompt: Optional[str] = None,
) -> LLMResponse:
"""调用 OpenAI API"""
# 构建消息列表
formatted_messages = []
if system_prompt:
formatted_messages.append({
"role": "system",
"content": system_prompt
})
for msg in messages:
formatted_messages.append({
"role": msg.role,
"content": msg.content,
**({"name": msg.name} if msg.name else {}),
})
# 构建请求参数
kwargs = {
"model": self.model,
"messages": formatted_messages,
}
if tools:
kwargs["tools"] = tools
kwargs["tool_choice"] = "auto"
try:
response = await self.client.chat.completions.create(**kwargs)
return self._parse_response(response)
except Exception as e:
logger.error(f"OpenAI API 调用失败: {e}")
return LLMResponse(
content=None,
stop_reason="error",
)
async def chat_stream(
self,
messages: List[Message],
tools: Optional[List[Dict]] = None,
system_prompt: Optional[str] = None,
) -> AsyncGenerator[Dict, None]:
"""流式调用 OpenAI API"""
# 构建消息列表
formatted_messages = []
if system_prompt:
formatted_messages.append({
"role": "system",
"content": system_prompt
})
for msg in messages:
formatted_messages.append({
"role": msg.role,
"content": msg.content,
})
# 构建请求参数
kwargs = {
"model": self.model,
"messages": formatted_messages,
"stream": True,
}
if tools:
kwargs["tools"] = tools
kwargs["tool_choice"] = "auto"
try:
stream = await self.client.chat.completions.create(**kwargs)
async for chunk in stream:
if chunk.choices[0].delta.content:
yield {
"type": "content",
"content": chunk.choices[0].delta.content,
}
if chunk.choices[0].delta.tool_calls:
yield {
"type": "tool_call",
"tool_calls": chunk.choices[0].delta.tool_calls,
}
except Exception as e:
logger.error(f"OpenAI 流式调用失败: {e}")
yield {
"type": "error",
"error": str(e),
}
def _parse_response(self, response) -> LLMResponse:
"""解析响应"""
message = response.choices[0].message
# 解析工具调用
tool_calls = []
if message.tool_calls:
for tc in message.tool_calls:
tool_calls.append(ToolCall(
id=tc.id,
name=tc.function.name,
arguments=json.loads(tc.function.arguments),
))
return LLMResponse(
content=message.content,
tool_calls=tool_calls,
stop_reason="tool_calls" if tool_calls else "end_turn",
usage={
"input": response.usage.prompt_tokens,
"output": response.usage.completion_tokens,
"total": response.usage.total_tokens,
} if response.usage else None,
)
def create_llm_client(
provider: str,
model: str,
api_key: str,
base_url: Optional[str] = None,
) -> BaseLLMClient:
"""创建 LLM 客户端工厂函数"""
if provider == "openai":
return OpenAIClient(api_key=api_key, model=model, base_url=base_url)
else:
raise ValueError(f"不支持的提供商: {provider}")

94
pyopenclaw/core/memory.py Normal file
View File

@@ -0,0 +1,94 @@
"""
记忆系统模块。
对应 OpenClaw 的会话消息管理
"""
from typing import List, Dict, Optional
from dataclasses import dataclass, field
from datetime import datetime
from loguru import logger
from pyopenclaw.core.llm_client import Message
@dataclass
class SessionMessage:
"""会话消息"""
role: str
content: str
timestamp: datetime = field(default_factory=datetime.now)
name: Optional[str] = None # 工具名称
class Memory:
"""记忆系统
管理对话历史和上下文。
对应 OpenClaw 的会话消息管理。
"""
def __init__(self, max_messages: int = 100):
self._messages: List[SessionMessage] = []
self.max_messages = max_messages
logger.debug(f"记忆系统初始化: max_messages={max_messages}")
def add_message(self, role: str, content: str) -> None:
"""添加消息"""
self._messages.append(SessionMessage(
role=role,
content=content,
))
logger.debug(f"添加消息: role={role}, content_len={len(content)}")
# 检查是否需要压缩
if len(self._messages) > self.max_messages:
self._compact()
def add_tool_result(self, tool_name: str, result: str) -> None:
"""添加工具结果"""
self._messages.append(SessionMessage(
role="tool",
content=result,
name=tool_name,
))
logger.debug(f"添加工具结果: tool={tool_name}, result_len={len(result)}")
def get_messages(self) -> List[Message]:
"""获取消息列表(转换为 LLM 格式)"""
return [
Message(
role=msg.role,
content=msg.content,
name=msg.name,
)
for msg in self._messages
]
def get_last_n_messages(self, n: int) -> List[Message]:
"""获取最近 n 条消息"""
return self.get_messages()[-n:]
def clear(self) -> None:
"""清空记忆"""
self._messages.clear()
logger.debug("记忆已清空")
def _compact(self) -> None:
"""压缩记忆(保留最近的消息)"""
# 保留最近 80% 的消息
keep_count = int(self.max_messages * 0.8)
self._messages = self._messages[-keep_count:]
logger.info(f"记忆压缩: 保留 {keep_count} 条消息")
def get_message_count(self) -> int:
"""获取消息数量"""
return len(self._messages)
def get_context_size_estimate(self) -> int:
"""估算上下文大小(简单估算)"""
# 假设平均每字符 0.5 token
total_chars = sum(len(msg.content) for msg in self._messages)
return total_chars // 2

View File

@@ -0,0 +1,102 @@
"""
工具注册表模块。
对应 OpenClaw 的 src/agents/tool-policy.ts 和工具管理逻辑
"""
from typing import Dict, List, Optional, Type
from loguru import logger
from pyopenclaw.tools.base import BaseTool, ToolSchema
class ToolRegistry:
"""工具注册表
管理所有可用的工具,提供注册、查询、执行功能。
对应 OpenClaw 的工具系统核心
"""
def __init__(self):
self._tools: Dict[str, BaseTool] = {}
self._allowed_tools: Optional[set] = None # None 表示允许所有
logger.debug("工具注册表初始化")
def register(self, tool: BaseTool) -> None:
"""注册工具"""
self._tools[tool.name] = tool
logger.info(f"注册工具: {tool.name}")
def unregister(self, name: str) -> bool:
"""注销工具"""
if name in self._tools:
del self._tools[name]
logger.info(f"注销工具: {name}")
return True
return False
def get(self, name: str) -> Optional[BaseTool]:
"""获取工具"""
return self._tools.get(name)
def has(self, name: str) -> bool:
"""检查工具是否存在"""
return name in self._tools
def is_allowed(self, name: str, agent_name: str = None) -> bool:
"""检查工具是否被允许"""
if self._allowed_tools is None:
return True
return name in self._allowed_tools
def set_allowed_tools(self, tools: Optional[List[str]]) -> None:
"""设置允许的工具列表"""
self._allowed_tools = set(tools) if tools else None
logger.debug(f"设置允许工具: {tools}")
def list_tools(self) -> List[str]:
"""列出所有工具"""
return list(self._tools.keys())
def get_schemas(self, tool_names: Optional[List[str]] = None) -> List[Dict]:
"""获取工具模式列表
返回 OpenAI 格式的工具定义
"""
if tool_names is None:
tools = list(self._tools.values())
else:
tools = [self._tools[name] for name in tool_names if name in self._tools]
return [tool.get_schema().to_openai_format() for tool in tools]
async def execute(self, name: str, arguments: Dict) -> str:
"""执行工具"""
tool = self._tools.get(name)
if not tool:
error_msg = f"工具不存在: {name}"
logger.error(error_msg)
return f"Error: {error_msg}"
try:
logger.debug(f"执行工具: {name}, 参数: {arguments}")
result = await tool.execute(**arguments)
logger.debug(f"工具执行成功: {name}, 结果长度: {len(result)}")
return result
except Exception as e:
error_msg = f"工具执行失败: {name}, 错误: {e}"
logger.error(error_msg)
return f"Error: {error_msg}"
# 全局工具注册表
_global_registry: Optional[ToolRegistry] = None
def get_global_registry() -> ToolRegistry:
"""获取全局工具注册表"""
global _global_registry
if _global_registry is None:
_global_registry = ToolRegistry()
return _global_registry

View File

View File

66
pyopenclaw/tools/base.py Normal file
View File

@@ -0,0 +1,66 @@
"""
工具基类模块。
对应 OpenClaw 的 src/agents/pi-tools.ts
"""
from typing import Dict, Any, Callable, Optional
from abc import ABC, abstractmethod
from dataclasses import dataclass
@dataclass
class ToolSchema:
"""工具模式定义"""
name: str
description: str
parameters: Dict[str, Any]
def to_openai_format(self) -> Dict:
"""转换为 OpenAI 工具格式"""
return {
"type": "function",
"function": {
"name": self.name,
"description": self.description,
"parameters": self.parameters,
}
}
class BaseTool(ABC):
"""工具基类
对应 OpenClaw 的工具定义
"""
@property
@abstractmethod
def name(self) -> str:
"""工具名称"""
pass
@property
@abstractmethod
def description(self) -> str:
"""工具描述"""
pass
@property
@abstractmethod
def parameters(self) -> Dict[str, Any]:
"""工具参数模式"""
pass
@abstractmethod
async def execute(self, **kwargs) -> str:
"""执行工具"""
pass
def get_schema(self) -> ToolSchema:
"""获取工具模式"""
return ToolSchema(
name=self.name,
description=self.description,
parameters=self.parameters,
)

View File

@@ -0,0 +1,184 @@
"""
文件工具模块。
对应 OpenClaw 的 read/write/edit 工具
"""
import os
from typing import Optional
import aiofiles
from pyopenclaw.tools.base import BaseTool
class ReadTool(BaseTool):
"""读取文件工具"""
@property
def name(self) -> str:
return "read"
@property
def description(self) -> str:
return "读取文件内容,支持指定行号范围"
@property
def parameters(self) -> dict:
return {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "文件路径(绝对路径或相对路径)",
},
"offset": {
"type": "integer",
"description": "起始行号(从 0 开始,默认 0",
},
"limit": {
"type": "integer",
"description": "读取行数(可选,默认读取全部)",
},
},
"required": ["file_path"],
}
async def execute(
self,
file_path: str,
offset: int = 0,
limit: Optional[int] = None,
) -> str:
"""执行文件读取"""
try:
# 检查文件是否存在
if not os.path.exists(file_path):
return f"Error: 文件不存在: {file_path}"
# 异步读取文件
async with aiofiles.open(file_path, "r", encoding="utf-8") as f:
lines = await f.readlines()
# 应用偏移和限制
if limit:
lines = lines[offset:offset + limit]
else:
lines = lines[offset:]
# 返回内容
content = "".join(lines)
return content if content else "(文件为空)"
except Exception as e:
return f"Error: {e}"
class WriteTool(BaseTool):
"""写入文件工具"""
@property
def name(self) -> str:
return "write"
@property
def description(self) -> str:
return "写入文件内容,会覆盖已存在的文件"
@property
def parameters(self) -> dict:
return {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "文件路径",
},
"content": {
"type": "string",
"description": "文件内容",
},
},
"required": ["file_path", "content"],
}
async def execute(self, file_path: str, content: str) -> str:
"""执行文件写入"""
try:
# 创建目录
dir_path = os.path.dirname(file_path)
if dir_path:
os.makedirs(dir_path, exist_ok=True)
# 异步写入文件
async with aiofiles.open(file_path, "w", encoding="utf-8") as f:
await f.write(content)
return f"成功写入文件: {file_path}"
except Exception as e:
return f"Error: {e}"
class EditTool(BaseTool):
"""编辑文件工具"""
@property
def name(self) -> str:
return "edit"
@property
def description(self) -> str:
return "编辑文件,替换指定内容(精确匹配)"
@property
def parameters(self) -> dict:
return {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "文件路径",
},
"old_content": {
"type": "string",
"description": "要替换的内容(必须精确匹配)",
},
"new_content": {
"type": "string",
"description": "替换后的内容",
},
},
"required": ["file_path", "old_content", "new_content"],
}
async def execute(
self,
file_path: str,
old_content: str,
new_content: str,
) -> str:
"""执行文件编辑"""
try:
# 检查文件是否存在
if not os.path.exists(file_path):
return f"Error: 文件不存在: {file_path}"
# 读取文件
async with aiofiles.open(file_path, "r", encoding="utf-8") as f:
content = await f.read()
# 检查内容是否存在
if old_content not in content:
return f"Error: 未找到要替换的内容"
# 替换内容
new_file_content = content.replace(old_content, new_content)
# 写入文件
async with aiofiles.open(file_path, "w", encoding="utf-8") as f:
await f.write(new_file_content)
return f"成功编辑文件: {file_path}"
except Exception as e:
return f"Error: {e}"

62
pyproject.toml Normal file
View File

@@ -0,0 +1,62 @@
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "pyopenclaw"
version = "0.1.0"
description = "Python implementation of OpenClaw core functionality"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.11"
authors = [
{name = "云下飞"}
]
keywords = ["ai", "agent", "llm", "openclaw"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"fastapi>=0.109.0",
"uvicorn[standard]>=0.27.0",
"pydantic>=2.5.0",
"pydantic-settings>=2.1.0",
"openai>=1.10.0",
"httpx>=0.26.0",
"python-dotenv>=1.0.0",
"loguru>=0.7.2",
"aiofiles>=23.2.1",
]
[project.optional-dependencies]
dev = [
"pytest>=7.4.0",
"pytest-asyncio>=0.23.0",
"black>=24.1.0",
"ruff>=0.1.0",
"mypy>=1.8.0",
]
[project.scripts]
pyopenclaw = "pyopenclaw.cli:main"
[tool.setuptools.packages.find]
where = ["."]
[tool.black]
line-length = 100
target-version = ["py311"]
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.mypy]
python_version = "3.11"
strict = true

21
requirements.txt Normal file
View File

@@ -0,0 +1,21 @@
# Core
fastapi>=0.109.0
uvicorn[standard]>=0.27.0
pydantic>=2.5.0
pydantic-settings>=2.1.0
# LLM
openai>=1.10.0
httpx>=0.26.0
# Utils
python-dotenv>=1.0.0
loguru>=0.7.2
aiofiles>=23.2.1
# Development
pytest>=7.4.0
pytest-asyncio>=0.23.0
black>=24.1.0
ruff>=0.1.0
mypy>=1.8.0

0
tests/__init__.py Normal file
View File