初始化项目:PyOpenClaw 核心框架
- 项目结构搭建 - Agent 核心类实现 - LLM 客户端(OpenAI) - 工具系统框架 - 配置管理 - 简单示例 版本: v0.1.0
This commit is contained in:
18
.env.example
Normal file
18
.env.example
Normal 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
64
.gitignore
vendored
Normal 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
119
README.md
Normal 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)
|
||||||
|
- [ ] 简单会话管理
|
||||||
|
|
||||||
|
### 阶段 2:Gateway 服务(第 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
103
examples/simple_agent.py
Normal 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
19
pyopenclaw/__init__.py
Normal 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",
|
||||||
|
]
|
||||||
0
pyopenclaw/config/__init__.py
Normal file
0
pyopenclaw/config/__init__.py
Normal file
65
pyopenclaw/config/settings.py
Normal file
65
pyopenclaw/config/settings.py
Normal 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()
|
||||||
0
pyopenclaw/core/__init__.py
Normal file
0
pyopenclaw/core/__init__.py
Normal file
226
pyopenclaw/core/agent.py
Normal file
226
pyopenclaw/core/agent.py
Normal 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}")
|
||||||
222
pyopenclaw/core/llm_client.py
Normal file
222
pyopenclaw/core/llm_client.py
Normal 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
94
pyopenclaw/core/memory.py
Normal 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
|
||||||
102
pyopenclaw/core/tool_registry.py
Normal file
102
pyopenclaw/core/tool_registry.py
Normal 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
|
||||||
0
pyopenclaw/gateway/__init__.py
Normal file
0
pyopenclaw/gateway/__init__.py
Normal file
0
pyopenclaw/tools/__init__.py
Normal file
0
pyopenclaw/tools/__init__.py
Normal file
66
pyopenclaw/tools/base.py
Normal file
66
pyopenclaw/tools/base.py
Normal 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,
|
||||||
|
)
|
||||||
184
pyopenclaw/tools/file_tools.py
Normal file
184
pyopenclaw/tools/file_tools.py
Normal 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
62
pyproject.toml
Normal 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
21
requirements.txt
Normal 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
0
tests/__init__.py
Normal file
Reference in New Issue
Block a user