mirror of
https://github.com/czl9707/build-your-own-openclaw.git
synced 2026-08-14 00:47:59 +00:00
add Chinese Translation
This commit is contained in:
+6
-1
@@ -57,4 +57,9 @@ generate_diff.sh
|
|||||||
.worktrees/
|
.worktrees/
|
||||||
|
|
||||||
# build time populate
|
# build time populate
|
||||||
web/public/steps
|
web/public/steps
|
||||||
|
|
||||||
|
# agents
|
||||||
|
.agents/
|
||||||
|
skills-lock.json
|
||||||
|
.claude/
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
# 步骤 00:只是一个聊天循环
|
||||||
|
|
||||||
|
> 所有智能体都从一个简单的聊天循环开始。
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
复制配置文件并添加你的 API 密钥:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp default_workspace/config.example.yaml default_workspace/config.user.yaml
|
||||||
|
# 编辑 config.user.yaml 添加你的 API 密钥
|
||||||
|
```
|
||||||
|
|
||||||
|
## 这节做什么
|
||||||
|
|
||||||
|
AI 智能体的基础:一个简单的聊天循环,用户输入,LLM 响应。
|
||||||
|
|
||||||
|
<img src="00-chat-loop.svg" align="center" width="100%" />
|
||||||
|
|
||||||
|
## 关键组件
|
||||||
|
|
||||||
|
- **ChatLoop**:处理用户输入并显示响应
|
||||||
|
- **LLM Call**:将消息历史发送给 LLM 提供商并获取响应
|
||||||
|
- **Session**:管理对话状态和消息历史,LLM 始终看到完整历史
|
||||||
|
|
||||||
|
[src/mybot/cli/chat.py](src/mybot/cli/chat.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ChatLoop:
|
||||||
|
async def run(self) -> None:
|
||||||
|
self.console.print(
|
||||||
|
Panel(
|
||||||
|
Text("Welcome to my-bot!", style="bold cyan"),
|
||||||
|
title="Chat",
|
||||||
|
border_style="cyan",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.console.print("Type 'quit' or 'exit' to end the session.\n")
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
user_input = await asyncio.to_thread(self.get_user_input)
|
||||||
|
|
||||||
|
if user_input.lower() in ("quit", "exit", "q"):
|
||||||
|
self.console.print("\n[bold yellow]Goodbye![/bold yellow]")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not user_input:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = await self.session.chat(user_input)
|
||||||
|
self.display_agent_response(response)
|
||||||
|
except Exception as e:
|
||||||
|
self.console.print(f"\n[bold red]Error:[/bold red] {e}\n")
|
||||||
|
|
||||||
|
except (KeyboardInterrupt, EOFError):
|
||||||
|
self.console.print("\n[bold yellow]Goodbye![/bold yellow]")
|
||||||
|
```
|
||||||
|
|
||||||
|
[src/mybot/core/agent.py](src/mybot/core/agent.py)
|
||||||
|
|
||||||
|
``` python
|
||||||
|
class AgentSession:
|
||||||
|
async def chat(self, message: str) -> str:
|
||||||
|
user_msg: Message = {"role": "user", "content": message}
|
||||||
|
self.state.add_message(user_msg)
|
||||||
|
|
||||||
|
messages = self.state.build_messages()
|
||||||
|
response = await self.agent.llm.chat(messages)
|
||||||
|
|
||||||
|
assistant_msg: Message = {"role": "assistant", "content": response}
|
||||||
|
self.state.add_message(assistant_msg)
|
||||||
|
|
||||||
|
return response
|
||||||
|
```
|
||||||
|
|
||||||
|
[src/mybot/provider/llm/base.py](src/mybot/provider/llm/base.py)
|
||||||
|
|
||||||
|
``` python
|
||||||
|
class LLMProvider:
|
||||||
|
async def chat(
|
||||||
|
self,
|
||||||
|
messages: list[Message],
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> str:
|
||||||
|
request_kwargs: dict[str, Any] = {
|
||||||
|
"model": self.model,
|
||||||
|
"messages": messages,
|
||||||
|
"api_key": self.api_key,
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.api_base:
|
||||||
|
request_kwargs["api_base"] = self.api_base
|
||||||
|
request_kwargs.update(kwargs)
|
||||||
|
|
||||||
|
response = await acompletion(**request_kwargs)
|
||||||
|
message = cast(Choices, response.choices[0]).message
|
||||||
|
|
||||||
|
return message.content or ""
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 试一试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd 00-chat-loop
|
||||||
|
uv run my-bot chat
|
||||||
|
|
||||||
|
# Type 'quit' or 'exit' to end the session.
|
||||||
|
|
||||||
|
# You: Hello, who is this?
|
||||||
|
# pickle: Meow! Hello there! I'm Pickle, your friendly cat assistant. 🐾
|
||||||
|
# You: I am Zane, Nice to meet you.
|
||||||
|
# pickle: Nice to meet you, Zane! *purrs happily* 🐱
|
||||||
|
```
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
[步骤 01:工具](../01-tools/) - 让智能体能真正做事
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
# 步骤 01:给你的智能体一个工具
|
||||||
|
|
||||||
|
> 简单的工具比你想象的更强大。Read、Write、Bash 就足够了。
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
与步骤 00 相同 - 复制配置文件并添加你的 API 密钥:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp default_workspace/config.example.yaml default_workspace/config.user.yaml
|
||||||
|
# 编辑 config.user.yaml 添加你的 API 密钥
|
||||||
|
```
|
||||||
|
|
||||||
|
## 这节做什么
|
||||||
|
|
||||||
|
让智能体能真正*做事*——不只是聊天。
|
||||||
|
|
||||||
|
<img src="01-tools.svg" align="center" width="100%" />
|
||||||
|
|
||||||
|
## 关键组件
|
||||||
|
|
||||||
|
- **Stop Reason**:聊天循环可能因为 "end_turn" 或 "tool_use" 而停止
|
||||||
|
- **Tools**:管理可用工具并执行工具调用
|
||||||
|
- **Tool Calling Loop**:智能体调用工具,将结果添加到历史,继续对话
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[src/mybot/tools/base.py](src/mybot/tools/base.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class BaseTool(ABC):
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
parameters: dict[str, Any]
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def execute(self, session: "AgentSession", **kwargs: Any) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_tool_schema(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": self.name,
|
||||||
|
"description": self.description,
|
||||||
|
"parameters": self.parameters,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
[src/mybot/core/agent.py](src/mybot/core/agent.py)
|
||||||
|
|
||||||
|
工具集成到聊天循环:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentSession:
|
||||||
|
async def chat(self, message: str) -> str:
|
||||||
|
user_msg: Message = {"role": "user", "content": message}
|
||||||
|
self.state.add_message(user_msg)
|
||||||
|
|
||||||
|
tool_schemas = self.tools.get_tool_schemas()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
messages = self.state.build_messages()
|
||||||
|
content, tool_calls = await self.agent.llm.chat(messages, tool_schemas)
|
||||||
|
|
||||||
|
assistant_msg: Message = {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": content,
|
||||||
|
"tool_calls": [...],
|
||||||
|
}
|
||||||
|
self.state.add_message(assistant_msg)
|
||||||
|
|
||||||
|
if not tool_calls:
|
||||||
|
break
|
||||||
|
|
||||||
|
await self._handle_tool_calls(tool_calls)
|
||||||
|
|
||||||
|
return content
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 试一试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd 01-tools
|
||||||
|
uv run my-bot chat
|
||||||
|
|
||||||
|
# You: Hey Can you read your README.md please?
|
||||||
|
# pickle: I found and read the README.md file! 🐱
|
||||||
|
|
||||||
|
# # Step 01: Tools - Read, Write, Bash is Powerful Enough
|
||||||
|
|
||||||
|
# Give the agent the ability to execute tools (read, write, edit, bash) and interact with the filesystem.
|
||||||
|
# [More lines]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
[步骤 02:技能](../02-skills/) - 用 SKILL.md 动态加载能力。
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# 步骤 02:技能
|
||||||
|
|
||||||
|
> 用 `SKILL.md` 扩展你的智能体。
|
||||||
|
|
||||||
|
技能是在运行时延迟加载的能力,参考 [官方文档](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview) 了解更多详情。
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
与步骤 00 相同 - 复制配置文件并添加你的 API 密钥:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp default_workspace/config.example.yaml default_workspace/config.user.yaml
|
||||||
|
# 编辑 config.user.yaml 添加你的 API 密钥
|
||||||
|
```
|
||||||
|
|
||||||
|
## 这节做什么
|
||||||
|
|
||||||
|
技能是运行时按需加载的能力。这不是 Openclaw 发明的,是个开放标准,详见 [官方文档](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview)。
|
||||||
|
|
||||||
|
<img src="02-skills.svg" align="center" width="100%" />
|
||||||
|
|
||||||
|
## 关键组件
|
||||||
|
|
||||||
|
- **SkillDef**:技能定义(id、name、description、content)
|
||||||
|
- **SKILL.md**:YAML 前言 + markdown 正文格式
|
||||||
|
- **skill tool**:列出可用技能并按需加载内容的动态工具
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[src/mybot/tools/skill_tool.py](src/mybot/tools/skill_tool.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
def create_skill_tool(skill_loader: "SkillLoader"):
|
||||||
|
"""Factory function to create skill tool with dynamic schema."""
|
||||||
|
skill_metadata = skill_loader.discover_skills()
|
||||||
|
|
||||||
|
# Build XML description of available skills
|
||||||
|
skills_xml = "<skills>\n"
|
||||||
|
for meta in skill_metadata:
|
||||||
|
skills_xml += f' <skill name="{meta.name}">{meta.description}</skill>\n'
|
||||||
|
skills_xml += "</skills>"
|
||||||
|
|
||||||
|
@tool(name="skill", description=f"Load skill. {skills_xml}", ...)
|
||||||
|
async def skill_tool(skill_name: str, session: "AgentSession") -> str:
|
||||||
|
skill_def = skill_loader.load_skill(skill_name)
|
||||||
|
return skill_def.content
|
||||||
|
|
||||||
|
return skill_tool
|
||||||
|
```
|
||||||
|
|
||||||
|
## 两种实现方式
|
||||||
|
|
||||||
|
Openclaw 不用单独的工具,而是**系统提示注入 + 文件读取**。
|
||||||
|
|
||||||
|
**工具方式(本教程):**
|
||||||
|
- `skill` 工具列出可用技能,按需加载内容
|
||||||
|
- 工具描述里带上技能元数据
|
||||||
|
- 智能体调用工具获取技能
|
||||||
|
|
||||||
|
**系统提示方式(OpenClaw):**
|
||||||
|
- 技能元数据注入系统提示
|
||||||
|
- 智能体用 `read` 工具读 SKILL.md
|
||||||
|
- 工具注册表更干净
|
||||||
|
|
||||||
|
> 想把技能做成系统提示的一部分,看 [步骤 13:多层提示](../13-multi-layer-prompts/)。
|
||||||
|
|
||||||
|
## 试一试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd 02-skills
|
||||||
|
uv run my-bot chat
|
||||||
|
|
||||||
|
# You: What skills do you have available?
|
||||||
|
# pickle: Hi there! 🐱 I have access to two specialized skills:
|
||||||
|
#
|
||||||
|
# - **cron-ops**: Create, list, and delete scheduled cron jobs
|
||||||
|
# - **skill-creator**: Guide for creating effective skills
|
||||||
|
#
|
||||||
|
# Is there something specific you'd like to do with either of these, or do you have another task I can help you with?
|
||||||
|
#
|
||||||
|
# You: Create a skill to access Weather Information
|
||||||
|
# pickle: [Loads and create a weather-info skill]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
[步骤 03:持久化](../03-persistence/) - 跨会话记住对话
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# 步骤 03:持久化
|
||||||
|
|
||||||
|
> 保存你的对话。
|
||||||
|
保存和恢复对话历史,让智能体记住过去的交互。
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
与步骤 00 相同 - 复制配置文件并添加你的 API 密钥:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp default_workspace/config.example.yaml default_workspace/config.user.yaml
|
||||||
|
# 编辑 config.user.yaml 添加你的 API 密钥
|
||||||
|
```
|
||||||
|
|
||||||
|
## 这节做什么
|
||||||
|
|
||||||
|
<img src="03-persistence.svg" align="center" width="100%" />
|
||||||
|
|
||||||
|
文件系统结构:
|
||||||
|
|
||||||
|
```
|
||||||
|
.history/
|
||||||
|
├── index.jsonl # 会话元数据
|
||||||
|
└── sessions/
|
||||||
|
└── {session_id}.jsonl # 消息(每个会话一个文件)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 关键组件
|
||||||
|
|
||||||
|
- **.history/index.jsonl**:基于 JSONL 文件的会话索引,包含元数据
|
||||||
|
- **.history/sessions/{id}.jsonl**:基于 JSONL 文件的消息存储
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[src/mybot/core/history.py](src/mybot/core/history.py) - 新文件
|
||||||
|
|
||||||
|
```python
|
||||||
|
class HistoryStore:
|
||||||
|
def create_session(self, agent_id: str, session_id: str) -> dict:
|
||||||
|
"""Create a new conversation session."""
|
||||||
|
|
||||||
|
def save_message(self, session_id: str, message: HistoryMessage) -> None:
|
||||||
|
"""Save a message to history."""
|
||||||
|
|
||||||
|
def get_messages(self, session_id: str) -> list[HistoryMessage]:
|
||||||
|
"""Get all messages for a session."""
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 试一试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd 03-persistence
|
||||||
|
uv run my-bot chat
|
||||||
|
|
||||||
|
# 每次运行都会启动一个新会话
|
||||||
|
# 消息保存到 .history/ 目录
|
||||||
|
```
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
[步骤 04:斜杠命令](../04-slash-commands/) - 直接命令调用
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# 步骤 04:斜杠命令
|
||||||
|
|
||||||
|
> 直接控制会话。
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
与步骤 00 相同 - 复制配置文件并添加你的 API 密钥:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp default_workspace/config.example.yaml default_workspace/config.user.yaml
|
||||||
|
# 编辑 config.user.yaml 添加你的 API 密钥
|
||||||
|
```
|
||||||
|
|
||||||
|
## 这节做什么
|
||||||
|
|
||||||
|
在聊天里输入 `/help`、`/skills`、`/session` 这类命令,直接执行确定性的功能。
|
||||||
|
|
||||||
|
### 架构
|
||||||
|
|
||||||
|
<img src="04-slash-commands.svg" align="center" width="100%" />
|
||||||
|
|
||||||
|
## 关键组件
|
||||||
|
|
||||||
|
- **Command**:斜杠命令的基类(异步 execute 方法)
|
||||||
|
- **CommandRegistry**:注册和派发命令
|
||||||
|
- **Commands**:`/help`、`/skills`、`/session`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[src/mybot/core/commands/base.py](src/mybot/core/commands/base.py) - 新文件
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Command(ABC):
|
||||||
|
"""Base class for slash commands."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
aliases: list[str] = []
|
||||||
|
description: str = ""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def execute(self, args: str, session: "AgentSession") -> str:
|
||||||
|
"""Execute the command and return response string."""
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
[src/mybot/core/commands/registry.py](src/mybot/core/commands/registry.py) - 新文件
|
||||||
|
|
||||||
|
```python
|
||||||
|
class CommandRegistry:
|
||||||
|
def register(self, cmd: Command) -> None:
|
||||||
|
"""Register a command and its aliases."""
|
||||||
|
|
||||||
|
async def dispatch(self, input: str, session: "AgentSession") -> str | None:
|
||||||
|
"""Parse and execute a slash command. Returns None if not a command."""
|
||||||
|
```
|
||||||
|
|
||||||
|
[src/mybot/cli/chat.py](src/mybot/cli/chat.py) - 添加命令分发
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def run(self) -> None:
|
||||||
|
# ... Say Hello
|
||||||
|
while True:
|
||||||
|
# ... Get user input
|
||||||
|
|
||||||
|
# Check for slash commands
|
||||||
|
cmd_response = await self.session.command_registry.dispatch(
|
||||||
|
user_input, self.session
|
||||||
|
)
|
||||||
|
if cmd_response is not None:
|
||||||
|
self.console.print(cmd_response)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Normal chat
|
||||||
|
response = await self.session.chat(user_input)
|
||||||
|
self.display_agent_response(response)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## 设计选择
|
||||||
|
|
||||||
|
斜杠命令要不要写进会话历史?两种都行:
|
||||||
|
- 不写:命令是控制,不是对话
|
||||||
|
- 写:方便回溯做了什么操作
|
||||||
|
|
||||||
|
看你的场景选。
|
||||||
|
|
||||||
|
## 试一试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd 04-slash-commands
|
||||||
|
uv run my-bot chat
|
||||||
|
|
||||||
|
# Try the commands:
|
||||||
|
# You: /help
|
||||||
|
# **Available Commands:**
|
||||||
|
# /help, /? - Show available commands
|
||||||
|
# /skills - List all skills or show skill details
|
||||||
|
# /session - Show current session details
|
||||||
|
|
||||||
|
# You: /session
|
||||||
|
# **Session ID:** `abc123...`
|
||||||
|
# **Agent:** Pickle (pickle)
|
||||||
|
# **Created:** 2026-03-08T12:00:00
|
||||||
|
# **Messages:** 0
|
||||||
|
```
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
[步骤 05:压缩](../05-compaction/) - 继续聊天...
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# 步骤 05:压缩
|
||||||
|
|
||||||
|
> 打包历史,继续前进...
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
与步骤 00 相同 - 复制配置文件并添加你的 API 密钥:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp default_workspace/config.example.yaml default_workspace/config.user.yaml
|
||||||
|
# 编辑 config.user.yaml 添加你的 API 密钥
|
||||||
|
```
|
||||||
|
|
||||||
|
## 这节做什么
|
||||||
|
|
||||||
|
聊久了上下文会爆。压缩就是把旧消息总结一下,滚动到新会话继续聊。
|
||||||
|
|
||||||
|
<img src="05-compaction.svg" align="center" width="100%" />
|
||||||
|
|
||||||
|
- 上下文超过阈值?
|
||||||
|
- 截断过大的工具结果。
|
||||||
|
- 仍然过大?
|
||||||
|
- 总结旧消息。
|
||||||
|
- 滚动到新会话。
|
||||||
|
|
||||||
|
|
||||||
|
## 关键组件
|
||||||
|
|
||||||
|
- **Token 估算**:用 litellm 的 token_counter
|
||||||
|
- **截断策略**:先截大工具结果,再总结旧消息
|
||||||
|
- **上下文压缩**:总结旧消息,作为新会话的前几个提示
|
||||||
|
- **命令**:`/compact` 手动压缩,`/context` 看使用量
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[src/mybot/core/context_guard.py](src/mybot/core/context_guard.py) - 新文件
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass
|
||||||
|
class ContextGuard:
|
||||||
|
token_threshold: int = 160000 # 80% of 200k context
|
||||||
|
|
||||||
|
def estimate_tokens(self, state: SessionState) -> int:
|
||||||
|
return token_counter(model=state.agent.agent_def.llm.model, messages=state.build_messages())
|
||||||
|
|
||||||
|
async def check_and_compact(self, state: SessionState) -> SessionState:
|
||||||
|
token_count =
|
||||||
|
|
||||||
|
if self.estimate_tokens(state) < self.token_threshold:
|
||||||
|
return state
|
||||||
|
|
||||||
|
state.messages = self._truncate_large_tool_results(state.messages)
|
||||||
|
|
||||||
|
if self.estimate_tokens(state) < self.token_threshold:
|
||||||
|
return state
|
||||||
|
|
||||||
|
return await self._compact_messages(state)
|
||||||
|
```
|
||||||
|
|
||||||
|
[src/mybot/core/agent.py](src/mybot/core/agent.py) - 集成
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def chat(self, message: str) -> str:
|
||||||
|
# ... add user message ...
|
||||||
|
|
||||||
|
while True:
|
||||||
|
messages = self.state.build_messages()
|
||||||
|
# Check and compact before LLM call
|
||||||
|
self.state = await self.context_guard.check_and_compact(self.state)
|
||||||
|
content, tool_calls = await self.agent.llm.chat(messages, tool_schemas)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 试一试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd 05-compaction
|
||||||
|
uv run my-bot chat
|
||||||
|
|
||||||
|
# Check context usage anytime:
|
||||||
|
# You: /context
|
||||||
|
# **Messages:** 12
|
||||||
|
# **Tokens:** 15,420 (9.6% of 160,000 threshold)
|
||||||
|
|
||||||
|
# You: /compact
|
||||||
|
# ✓ Context compacted. 8 messages retained.
|
||||||
|
```
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
[步骤 06:Web 工具](../06-web-tools/) - 添加网络搜索和 URL 阅读功能
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# 步骤 06:Web 工具
|
||||||
|
|
||||||
|
> 你的智能体想看看更大的世界。
|
||||||
|
> 归根结底,它们只是两个新工具。
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp default_workspace/config.example.yaml default_workspace/config.user.yaml
|
||||||
|
# 编辑 config.user.yaml 添加你的 API 密钥
|
||||||
|
# 取消注释 websearch 和 webread 部分
|
||||||
|
# 添加你的 websearch api 密钥
|
||||||
|
```
|
||||||
|
|
||||||
|
## 这节做什么
|
||||||
|
|
||||||
|
LLM 懂 Python,但不知道昨天 PyPI 上发了什么新包。加两个工具让它能搜网页、读 URL。
|
||||||
|
|
||||||
|
<img src="06-web-tools.svg" align="center" width="100%" />
|
||||||
|
|
||||||
|
## 关键组件
|
||||||
|
|
||||||
|
- **WebSearchProvider**:网络搜索提供商。
|
||||||
|
- **WebReadProvider**:网页阅读提供商。
|
||||||
|
- **Tools**:`websearch` 和 `webread` 工具。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[src/mybot/provider/web_search/](src/mybot/provider/web_search/)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class WebSearchProvider(ABC):
|
||||||
|
async def search(self, query: str) -> list[SearchResult]: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
[src/mybot/provider/web_read/](src/mybot/provider/web_read/)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class WebReadProvider(ABC):
|
||||||
|
async def read(self, url: str) -> ReadResult: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
[src/mybot/tools/websearch_tool.py](src/mybot/tools/websearch_tool.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
@tool(...)
|
||||||
|
async def websearch(query: str, session: "AgentSession") -> str:
|
||||||
|
results = await provider.search(query)
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
return "No results found."
|
||||||
|
output = []
|
||||||
|
for i, r in enumerate(results, 1):
|
||||||
|
output.append(f"{i}. **{r.title}**\n {r.url}\n {r.snippet}")
|
||||||
|
return "\n\n".join(output)
|
||||||
|
```
|
||||||
|
|
||||||
|
[src/mybot/tools/webread_tool.py](src/mybot/tools/webread_tool.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
@tool(...)
|
||||||
|
async def webread(url: str, session: "AgentSession") -> str:
|
||||||
|
result = await provider.read(url)
|
||||||
|
if result.error:
|
||||||
|
return f"Error reading {url}: {result.error}"
|
||||||
|
|
||||||
|
return f"**{result.title}**\n\n{result.content}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 试一试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd 06-web-tools
|
||||||
|
uv run my-bot chat
|
||||||
|
|
||||||
|
# You: What is pickle bot? search online please.
|
||||||
|
# pickle: Based on my search, there are actually a few different things called "Pickle Bot":
|
||||||
|
|
||||||
|
# ### 1. **Pickle Robot Company** 🤖
|
||||||
|
# ### 2. **Pickle Bot (Discord Bot)** 💬
|
||||||
|
# ### 3. **pickle-bot (GitHub)** 🐱
|
||||||
|
# An open-source project described as:
|
||||||
|
# - "Your own AI assistant, speak like a cat"
|
||||||
|
# - "Pickle is a standard little cat"
|
||||||
|
# - A customizable AI assistant that you can name, talk to, and teach
|
||||||
|
|
||||||
|
# The GitHub version sounds like it could be related to me - a cat-speaking AI assistant! 😺
|
||||||
|
|
||||||
|
# Which one were you curious about?
|
||||||
|
```
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
[步骤 07:事件驱动](../07-event-driven/) - 重构为基于事件的架构
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# 步骤 07:事件驱动架构
|
||||||
|
|
||||||
|
> 让你的智能体超越 CLI。
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
与步骤 06 相同 - 复制配置文件并添加你的 API 密钥:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp default_workspace/config.example.yaml default_workspace/config.user.yaml
|
||||||
|
# 编辑 config.user.yaml 添加你的 API 密钥
|
||||||
|
```
|
||||||
|
|
||||||
|
## 这节做什么
|
||||||
|
|
||||||
|
这步改动较大。用事件总线把消息源和智能体执行解耦,后面几步都依赖这个架构。
|
||||||
|
|
||||||
|
<img src="07-event-driven.svg" align="center" width="100%" />
|
||||||
|
|
||||||
|
## 关键组件
|
||||||
|
|
||||||
|
- **EventBus** - 用于事件分发的中心发布/订阅
|
||||||
|
- **Events** - InboundEvent、OutboundEvent
|
||||||
|
- **Workers** - 处理事件的后台任务
|
||||||
|
- **AgentWorker** - 处理 InboundEvent → 执行智能体会话 → 发出 OutboundEvent
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[src/mybot/core/events.py](src/mybot/core/events.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass
|
||||||
|
class InboundEvent(Event):
|
||||||
|
session_id: str
|
||||||
|
content: str
|
||||||
|
retry_count: int = 0
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class OutboundEvent(Event):
|
||||||
|
session_id: str
|
||||||
|
content: str
|
||||||
|
error: str | None = None
|
||||||
|
```
|
||||||
|
|
||||||
|
[src/mybot/core/eventbus.py](src/mybot/core/eventbus.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class EventBus(Worker):
|
||||||
|
def subscribe(
|
||||||
|
self, event_class: type[E], handler: Callable[[E], Awaitable[None]]
|
||||||
|
) -> None:
|
||||||
|
"""Subscribe a handler to an event class."""
|
||||||
|
self._queue: asyncio.Queue[Event] = asyncio.Queue()
|
||||||
|
|
||||||
|
def unsubscribe(self, handler: Handler) -> None:
|
||||||
|
"""Remove a handler from all subscriptions."""
|
||||||
|
|
||||||
|
async def publish(self, event: Event) -> None:
|
||||||
|
"""Publish an event to the internal queue (non-blocking)."""
|
||||||
|
await self._queue.put(event)
|
||||||
|
|
||||||
|
async def run(self) -> None:
|
||||||
|
"""Process events from queue, starting with recovery."""
|
||||||
|
logger.info("EventBus started")
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
event = await self._queue.get()
|
||||||
|
try:
|
||||||
|
await self._dispatch(event)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error dispatching event: {e}")
|
||||||
|
finally:
|
||||||
|
self._queue.task_done()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logger.info("EventBus stopping...")
|
||||||
|
raise
|
||||||
|
```
|
||||||
|
|
||||||
|
[src/mybot/server/agent_worker.py](src/mybot/server/agent_worker.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentWorker(SubscriberWorker):
|
||||||
|
def __init__(self, context):
|
||||||
|
self.context.eventbus.subscribe(InboundEvent, self.dispatch_event)
|
||||||
|
|
||||||
|
async def dispatch_event(self, event: InboundEvent):
|
||||||
|
agent = Agent(agent_def, self.context)
|
||||||
|
session = agent.resume_session(event.session_id)
|
||||||
|
response = await session.chat(event.content)
|
||||||
|
|
||||||
|
result = OutboundEvent(
|
||||||
|
session_id=event.session_id,
|
||||||
|
content=response,
|
||||||
|
)
|
||||||
|
await self.context.eventbus.publish(result)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 试一试
|
||||||
|
|
||||||
|
跑起来和上一步一样,看不出区别。
|
||||||
|
|
||||||
|
急的读者直接跳 [步骤 09:频道](../09-channels/)。
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
[步骤 08:配置热重载](../08-config-hot-reload/) - 配置合并和配置热重载
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# 步骤 08:配置热重载
|
||||||
|
|
||||||
|
> 无需重启即可编辑。
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
与步骤 06 相同 - 复制配置文件并添加你的 API 密钥:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp default_workspace/config.example.yaml default_workspace/config.user.yaml
|
||||||
|
# 编辑 config.user.yaml 添加你的 API 密钥
|
||||||
|
```
|
||||||
|
|
||||||
|
## 这节做什么
|
||||||
|
|
||||||
|
改配置不用重启服务。用 watchdog 监听文件变化,自动热加载。
|
||||||
|
|
||||||
|
<img src="08-config-hot-reload.svg" align="center" width="100%" />
|
||||||
|
|
||||||
|
|
||||||
|
## 关键组件
|
||||||
|
|
||||||
|
- **ConfigReloader** - 使用 watchdog 监视工作区中的配置文件更改
|
||||||
|
- **Config Merging** - 通过深度合并,运行时配置覆盖用户配置
|
||||||
|
|
||||||
|
|
||||||
|
[src/mybot/utils/config.py](src/mybot/utils/config.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Config(BaseModel):
|
||||||
|
"""Configuration with hot reload support."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _load_merged_configs(cls, workspace_dir: Path) -> dict[str, Any]:
|
||||||
|
config_data: dict[str, Any] = {}
|
||||||
|
|
||||||
|
user_config = workspace_dir / "config.user.yaml"
|
||||||
|
runtime_config = workspace_dir / "config.runtime.yaml"
|
||||||
|
|
||||||
|
with open(user_config) as f:
|
||||||
|
config_data = cls._deep_merge(config_data, yaml.safe_load(f) or {})
|
||||||
|
|
||||||
|
with open(runtime_config) as f:
|
||||||
|
config_data = cls._deep_merge(config_data, yaml.safe_load(f) or {})
|
||||||
|
|
||||||
|
return config_data
|
||||||
|
|
||||||
|
def reload(self) -> bool:
|
||||||
|
config_data = self._load_merged_configs(self.workspace)
|
||||||
|
new_config = Config.model_validate(config_data)
|
||||||
|
|
||||||
|
for field_name in Config.model_fields:
|
||||||
|
setattr(self, field_name, getattr(new_config, field_name))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigHandler(FileSystemEventHandler):
|
||||||
|
"""Handles config file modification events."""
|
||||||
|
|
||||||
|
def __init__(self, config: Config):
|
||||||
|
self._config = config
|
||||||
|
|
||||||
|
def on_modified(self, event):
|
||||||
|
"""Reload config when config.user.yaml changes."""
|
||||||
|
if not event.is_directory and event.src_path.endswith("config.user.yaml"):
|
||||||
|
self._config.reload()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 试一试
|
||||||
|
|
||||||
|
和上一步一样跑,改 `config.user.yaml` 会自动生效。
|
||||||
|
|
||||||
|
急的读者跳 [步骤 09:频道](../09-channels/)。
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
[步骤 09:频道](../09-channels/) - 支持 CLI、Telegram 和其他接口的多平台支持。
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
# 步骤 09:频道
|
||||||
|
|
||||||
|
> 在手机上与你的智能体对话。
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp default_workspace/config.example.yaml default_workspace/config.user.yaml
|
||||||
|
# 编辑 config.user.yaml 添加你的 API 密钥
|
||||||
|
# 配置 Telegram Bot Token
|
||||||
|
```
|
||||||
|
## 这节做什么
|
||||||
|
|
||||||
|
让智能体接入 Telegram、Discord 等平台。
|
||||||
|
|
||||||
|
<img src="09-channels.svg" align="center" width="100%" />
|
||||||
|
|
||||||
|
- 用户通过平台发送消息(Telegram、Discord)
|
||||||
|
- 频道接收消息并创建 EventSource
|
||||||
|
- ChannelWorker 将 InboundEvent 发布到 EventBus
|
||||||
|
- AgentWorker 处理事件并生成响应
|
||||||
|
- AgentWorker 将 OutboundEvent 发布到 EventBus
|
||||||
|
- DeliveryWorker 接收 OutboundEvent
|
||||||
|
- DeliveryWorker 查找会话的源并通过适当的频道发送
|
||||||
|
|
||||||
|
## 关键组件
|
||||||
|
|
||||||
|
- **EventSource** - 平台特定事件源的抽象基类(CLI、Telegram、Discord)
|
||||||
|
- **Channel** - 具有 run/reply/stop 接口的消息平台抽象基类
|
||||||
|
- **ChannelWorker** - 管理多个频道并发布 InboundEvents
|
||||||
|
- **DeliveryWorker** - 订阅 OutboundEvents 并通过适当的频道投递
|
||||||
|
- **Event Persistence** - 出站事件持久化和故障恢复,防止消息丢失
|
||||||
|
|
||||||
|
|
||||||
|
[src/mybot/channel/base.py](src/mybot/channel/base.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Channel(ABC, Generic[T]):
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def platform_name(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def run(self, on_message: Callable[[str, T], Awaitable[None]]) -> None:
|
||||||
|
"""Run the channel. Blocks until stop() is called."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def reply(self, content: str, source: T) -> None:
|
||||||
|
"""Reply to incoming message."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def stop(self) -> None:
|
||||||
|
"""Stop listening and cleanup resources."""
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
[src/mybot/server/channel_worker.py](src/mybot/server/channel_worker.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ChannelWorker(Worker):
|
||||||
|
async def run(self) -> None:
|
||||||
|
channel_tasks = [
|
||||||
|
channel.run(self._create_callback(channel.platform_name))
|
||||||
|
for channel in self.channels
|
||||||
|
]
|
||||||
|
await asyncio.gather(*channel_tasks)
|
||||||
|
|
||||||
|
def _create_callback(self, platform: str):
|
||||||
|
async def callback(message: str, source: EventSource) -> None:
|
||||||
|
session_id = self._get_or_create_session_id(source)
|
||||||
|
|
||||||
|
event = InboundEvent(
|
||||||
|
session_id=session_id,
|
||||||
|
source=source,
|
||||||
|
content=message,
|
||||||
|
)
|
||||||
|
await self.context.eventbus.publish(event)
|
||||||
|
|
||||||
|
return callback
|
||||||
|
|
||||||
|
def _get_or_create_session_id(self, source: EventSource) -> str:
|
||||||
|
source_session = self.context.config.sources.get(str(source))
|
||||||
|
if source_session:
|
||||||
|
return source_session.session_id
|
||||||
|
|
||||||
|
agent_def = self.context.agent_loader.load(self.context.config.default_agent)
|
||||||
|
agent = Agent(agent_def, self.context)
|
||||||
|
session = agent.new_session(source)
|
||||||
|
|
||||||
|
# Cache the session
|
||||||
|
self.context.config.set_runtime(
|
||||||
|
f"sources.{source}", SourceSessionConfig(session_id=session.session_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
return session.session_id
|
||||||
|
```
|
||||||
|
|
||||||
|
- 每个 EventSource(例如 "platform-telegram:123:456")映射到一个会话
|
||||||
|
- 第一条消息创建会话,后续消息复用它
|
||||||
|
- 会话 ID 缓存在 config.runtime.yaml 中
|
||||||
|
|
||||||
|
[src/mybot/server/delivery_worker.py](src/mybot/server/delivery_worker.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class DeliveryWorker(SubscriberWorker):
|
||||||
|
"""Delivers outbound messages to platforms."""
|
||||||
|
|
||||||
|
async def handle_event(self, event: OutboundEvent) -> None:
|
||||||
|
"""Handle an outbound message event."""
|
||||||
|
session_info = self._get_session_source(event.session_id)
|
||||||
|
source = self._get_delivery_source(session_info)
|
||||||
|
|
||||||
|
if source and source.platform_name:
|
||||||
|
channel = self._get_channel(source.platform_name)
|
||||||
|
if channel:
|
||||||
|
await channel.reply(event.content, source)
|
||||||
|
|
||||||
|
self.context.eventbus.ack(event)
|
||||||
|
```
|
||||||
|
|
||||||
|
[src/mybot/core/eventbus.py](src/mybot/core/eventbus.py.py)
|
||||||
|
|
||||||
|
``` python
|
||||||
|
class EventBus(Worker):
|
||||||
|
async def run(self) -> None:
|
||||||
|
await self._recover()
|
||||||
|
while True:
|
||||||
|
# ... Dispatching Events
|
||||||
|
|
||||||
|
async def _dispatch(self, event: Event) -> None:
|
||||||
|
await self._persist_outbound(event)
|
||||||
|
await self._notify_subscribers(event)
|
||||||
|
|
||||||
|
async def _recover(self) -> int:
|
||||||
|
pending_files = list(self.pending_dir.glob("*.json"))
|
||||||
|
|
||||||
|
for file_path in pending_files:
|
||||||
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
event = deserialize_event(data)
|
||||||
|
await self._notify_subscribers(event)
|
||||||
|
|
||||||
|
return len(pending_files)
|
||||||
|
|
||||||
|
def ack(self, event: Event) -> None:
|
||||||
|
filename = f"{event.timestamp}_{event.session_id}.json"
|
||||||
|
final_path = self.pending_dir / filename
|
||||||
|
if final_path.exists():
|
||||||
|
final_path.unlink()
|
||||||
|
```
|
||||||
|
|
||||||
|
- **出站事件持久化流程**:
|
||||||
|
- `EventBus.publish()` 将事件排队到内部 asyncio 队列
|
||||||
|
- 对每个事件调用 `EventBus._dispatch()`
|
||||||
|
- `_persist_outbound()` 原子地将 OutboundEvent 写入磁盘(tmp 文件 + fsync + 重命名)
|
||||||
|
- `_notify_subscribers()` 将事件分发给所有订阅者(例如 DeliveryWorker)
|
||||||
|
|
||||||
|
- **故障恢复流程**:
|
||||||
|
- EventBus 启动时,`_recover()` 扫描 pending 目录中的 `.json` 文件
|
||||||
|
- 每个待处理事件被反序列化并重新分发给订阅者
|
||||||
|
- 只有在成功投递后,DeliveryWorker 才调用 `eventbus.ack(event)`
|
||||||
|
- `ack()` 删除持久化文件,确认投递完成
|
||||||
|
|
||||||
|
## 试一试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd 09-channels
|
||||||
|
uv run my-bot server
|
||||||
|
# Send message from the channel of your choice.
|
||||||
|
```
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
[步骤 10:WebSocket](../10-websocket/) - 用于与智能体交互的实时 Web 接口。
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# 步骤 10:WebSocket
|
||||||
|
|
||||||
|
> 想要以编程方式与智能体交互?
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp default_workspace/config.example.yaml default_workspace/config.user.yaml
|
||||||
|
# 编辑 config.user.yaml 添加你的 API 密钥
|
||||||
|
# 取消注释 api 部分
|
||||||
|
```
|
||||||
|
|
||||||
|
## 这节做什么
|
||||||
|
|
||||||
|
开个 WebSocket 接口,方便程序调用。
|
||||||
|
|
||||||
|
<img src="10-websocket.svg" align="center" width="100%" />
|
||||||
|
|
||||||
|
## 关键组件
|
||||||
|
|
||||||
|
- **WebSocketWorker** - 管理 WebSocket 连接并广播事件
|
||||||
|
- **WebSocket Handle** - 具有 WebSocket 端点的 Web 服务器
|
||||||
|
|
||||||
|
[src/mybot/server/websocket_worker.py](src/mybot/server/websocket_worker.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class WebSocketWorker(SubscriberWorker):
|
||||||
|
"""Manages WebSocket connections and event broadcasting."""
|
||||||
|
|
||||||
|
def __init__(self, context: "SharedContext"):
|
||||||
|
self.clients: Set[WebSocket] = set()
|
||||||
|
|
||||||
|
# Auto-subscribe to event classes
|
||||||
|
for event_class in [InboundEvent, OutboundEvent]:
|
||||||
|
self.context.eventbus.subscribe(event_class, self.handle_event)
|
||||||
|
|
||||||
|
async def handle_connection(self, ws: WebSocket) -> None:
|
||||||
|
self.clients.add(ws)
|
||||||
|
try:
|
||||||
|
await self._run_client_loop(ws)
|
||||||
|
finally:
|
||||||
|
self.clients.discard(ws)
|
||||||
|
|
||||||
|
async def handle_event(self, event: Event) -> None:
|
||||||
|
event_dict = {"type": event.__class__.__name__}
|
||||||
|
event_dict.update(dataclasses.asdict(event))
|
||||||
|
|
||||||
|
for client in list(self.clients):
|
||||||
|
try:
|
||||||
|
await client.send_json(event_dict)
|
||||||
|
except Exception:
|
||||||
|
self.clients.discard(client)
|
||||||
|
```
|
||||||
|
|
||||||
|
[src/mybot/server/app.py](src/mybot/server/app.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
def create_app(context: SharedContext) -> FastAPI:
|
||||||
|
app = FastAPI(title="MyBot WebSocket Server")
|
||||||
|
# ... wiring
|
||||||
|
|
||||||
|
@app.websocket("/ws")
|
||||||
|
async def websocket_endpoint(websocket: WebSocket):
|
||||||
|
await websocket.accept()
|
||||||
|
if context.websocket_worker is None:
|
||||||
|
await websocket.close(code=1013, reason="WebSocket not available")
|
||||||
|
return
|
||||||
|
await context.websocket_worker.handle_connection(websocket)
|
||||||
|
|
||||||
|
return app
|
||||||
|
```
|
||||||
|
|
||||||
|
## 试一试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd 10-websocket
|
||||||
|
uv run my-bot server
|
||||||
|
|
||||||
|
# INFO: Application startup complete.
|
||||||
|
# INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
||||||
|
```
|
||||||
|
|
||||||
|
在另一个终端中
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
wscat -c ws://localhost:8000/ws
|
||||||
|
> {"source": "test", "content": "Hello, Pickle!"}
|
||||||
|
< {"type":"InboundEvent","session_id":"c8419b2b-fc20-49a6-8fd7-79a00eeb71c5","source":"platform-ws:test","content":"Hello, Pickle!","timestamp":1773369408.214437,"retry_count":0}
|
||||||
|
< {"type":"OutboundEvent","session_id":"c8419b2b-fc20-49a6-8fd7-79a00eeb71c5","source":"agent:pickle","content":"*waves paws excitedly* Hello there! 🐱\n\nI'm Pickle, your friendly cat assistant!","timestamp":1773369422.7538216,"error":null}
|
||||||
|
>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
[步骤 11:多智能体路由](../11-multi-agent-routing/) - 将消息路由到专门的智能体。
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
# 步骤 11:多智能体路由
|
||||||
|
|
||||||
|
> 将正确的任务路由到正确的智能体。
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
与步骤 10 相同 - 复制配置文件并添加你的 API 密钥:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp default_workspace/config.example.yaml default_workspace/config.user.yaml
|
||||||
|
# 编辑 config.user.yaml 添加你的 API 密钥
|
||||||
|
```
|
||||||
|
|
||||||
|
## 这节做什么
|
||||||
|
|
||||||
|
根据消息来源路由到不同的智能体。
|
||||||
|
|
||||||
|
<img src="11-multi-agent-routing.svg" align="center" width="100%" />
|
||||||
|
|
||||||
|
## 关键组件
|
||||||
|
|
||||||
|
- **AgentLoader** - 发现并加载多个智能体定义
|
||||||
|
- **RoutingTable** - 正则匹配 + 分层优先级,把消息源路由到智能体
|
||||||
|
- **Binding** - 源模式 + 智能体映射,自动计算优先级
|
||||||
|
- **Commands** - `/route`、`/bindings`、`/agents` 管理路由
|
||||||
|
|
||||||
|
[src/mybot/core/agent_loader.py](src/mybot/core/agent_loader.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentLoader:
|
||||||
|
def discover_agents(self) -> list[AgentDef]:
|
||||||
|
"""Scan agents directory and return list of valid AgentDef."""
|
||||||
|
return discover_definitions(
|
||||||
|
self.config.agents_path, "AGENT.md", self._parse_agent_def
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
在 `<workspace>/agents/<agent_id>/AGENT.md` 定义智能体
|
||||||
|
|
||||||
|
[src/mybot/core/routing.py](src/mybot/core/routing.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass
|
||||||
|
class Binding:
|
||||||
|
agent: str
|
||||||
|
value: str
|
||||||
|
tier: int
|
||||||
|
pattern: Pattern # Compiled regex
|
||||||
|
|
||||||
|
def _compute_tier(self) -> int:
|
||||||
|
"""Compute specificity tier."""
|
||||||
|
if not any(c in self.value for c in r".*+?[]()|^$"):
|
||||||
|
return 0 # Exact match
|
||||||
|
if ".*" in self.value:
|
||||||
|
return 2 # Wildcard
|
||||||
|
return 1 # Specific regex
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RoutingTable:
|
||||||
|
def _load_bindings(self) -> list[Binding]:
|
||||||
|
bindings_data = self.context.config.routing.get("bindings", [])
|
||||||
|
|
||||||
|
bindings_with_order = [
|
||||||
|
(Binding(agent=b["agent"], value=b["value"]), i)
|
||||||
|
for i, b in enumerate(bindings_data)
|
||||||
|
]
|
||||||
|
bindings_with_order.sort(key=lambda x: (x[0].tier, x[1]))
|
||||||
|
self.bindings = [b for b, _ in bindings_with_order]
|
||||||
|
|
||||||
|
return self.bindings
|
||||||
|
|
||||||
|
def resolve(self, source: str) -> str:
|
||||||
|
for binding in self._load_bindings():
|
||||||
|
if binding.pattern.match(source):
|
||||||
|
return binding.agent
|
||||||
|
return self.context.config.default_agent
|
||||||
|
|
||||||
|
def get_or_create_session_id(self, source: EventSource) -> str:
|
||||||
|
source_session = self.context.config.sources.get(str(source))
|
||||||
|
if source_session:
|
||||||
|
return source_session.session_id
|
||||||
|
|
||||||
|
# Resolve agent and create new session
|
||||||
|
agent_id = self.resolve(str(source))
|
||||||
|
agent_def = self.context.agent_loader.load(agent_id)
|
||||||
|
agent = Agent(agent_def, self.context)
|
||||||
|
session = agent.new_session(source)
|
||||||
|
|
||||||
|
self.context.config.set_runtime(
|
||||||
|
f"sources.{str(source)}", SourceSessionConfig(session_id=session.session_id)
|
||||||
|
)
|
||||||
|
return session.session_id
|
||||||
|
```
|
||||||
|
|
||||||
|
- **分层路由**:从最具体的规则开始匹配。
|
||||||
|
- **兜底**:没匹配上就用默认智能体。
|
||||||
|
|
||||||
|
|
||||||
|
[src/mybot/server/channel_worker.py](src/mybot/server/channel_worker.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def callback(message: str, source: EventSource) -> None:
|
||||||
|
# ... validation ...
|
||||||
|
|
||||||
|
# Use routing_table to resolve agent from bindings
|
||||||
|
session_id = self.context.routing_table.get_or_create_session_id(source)
|
||||||
|
|
||||||
|
# Publish event
|
||||||
|
event = InboundEvent(session_id=session_id, source=source, content=message)
|
||||||
|
await self.context.eventbus.publish(event)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 试一试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd 11-multi-agent-routing
|
||||||
|
uv run my-bot agents chat
|
||||||
|
|
||||||
|
# You: /agent
|
||||||
|
# pickle: **Agents:**
|
||||||
|
# - `cookie`: Memory manager for storing, organizing, and retrieving memories
|
||||||
|
# - `pickle`: A friendly cat assistant talk to user directly, managing daily tasks. (current)
|
||||||
|
|
||||||
|
# You: /bindings
|
||||||
|
# pickle: No routing bindings configured.
|
||||||
|
|
||||||
|
# You: /route platform-ws:* cookie
|
||||||
|
# pickle: ✓ Route bound: `platform-ws:*` → `cookie`
|
||||||
|
```
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
[步骤 12:Cron + Heartbeat](../12-cron-heartbeat/) - 定时任务和健康监控。
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# 步骤 12:Cron + Heartbeat
|
||||||
|
|
||||||
|
> 智能体在你睡觉时工作。
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
与步骤 09 相同 - 复制配置文件并添加你的 API 密钥:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp default_workspace/config.example.yaml default_workspace/config.user.yaml
|
||||||
|
# 编辑 config.user.yaml 添加你的 API 密钥
|
||||||
|
```
|
||||||
|
|
||||||
|
## 这节做什么
|
||||||
|
|
||||||
|
定时任务——智能体按 cron 表达式自动跑。
|
||||||
|
|
||||||
|
<img src="12-cron-heartbeat.svg" align="center" width="100%" />
|
||||||
|
|
||||||
|
|
||||||
|
## 关键组件
|
||||||
|
|
||||||
|
- **CRON.md & CronDef** - Cron 任务定义
|
||||||
|
- **CronWorker** - 每分钟检查待执行任务的后台工作器
|
||||||
|
- **DispatchEvent** - 内部任务调度的事件类型
|
||||||
|
- **DispatchResultEvent** - 调度任务返回的结果事件
|
||||||
|
- **Cron-Ops Skill** - 用于创建、列出和删除定时 cron 任务的技能(实现为技能以避免额外的工具注册)
|
||||||
|
|
||||||
|
[src/mybot/core/cron_loader.py](src/mybot/core/cron_loader.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class CronDef(BaseModel):
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
agent: str
|
||||||
|
schedule: str
|
||||||
|
prompt: str
|
||||||
|
one_off: bool = False
|
||||||
|
```
|
||||||
|
|
||||||
|
[src/mybot/server/cron_worker.py](src/mybot/server/cron_worker.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class CronWorker(Worker):
|
||||||
|
async def run(self) -> None:
|
||||||
|
while True:
|
||||||
|
await self._tick()
|
||||||
|
await asyncio.sleep(60)
|
||||||
|
|
||||||
|
async def _tick(self) -> None:
|
||||||
|
jobs = self.context.cron_loader.discover_crons()
|
||||||
|
due_jobs = find_due_jobs(jobs)
|
||||||
|
|
||||||
|
for cron_def in due_jobs:
|
||||||
|
event = DispatchEvent(
|
||||||
|
session_id=session.session_id,
|
||||||
|
source=CronEventSource(cron_id=cron_def.id),
|
||||||
|
content=cron_def.prompt,
|
||||||
|
)
|
||||||
|
await self.context.eventbus.publish(event)
|
||||||
|
```
|
||||||
|
|
||||||
|
[default_workspace/crons/hello-world/CRON.md](../default_workspace/skills/cron-ops/SKILL.md)
|
||||||
|
|
||||||
|
Cron 操作功能使用 **SKILL 系统**实现,而不是注册专用工具,这避免了工具注册表的膨胀。
|
||||||
|
|
||||||
|
## 试一试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd 12-cron-heartbeat
|
||||||
|
uv run my-bot server
|
||||||
|
|
||||||
|
# From Channel of your choice:
|
||||||
|
|
||||||
|
# You: Send me some Cat Meme every morning.
|
||||||
|
# pickle: I've scheduled a "Cat Meme" cron job you every morning 9 AM. You'll find those meme shortly! *purrs* 🐱
|
||||||
|
```
|
||||||
|
|
||||||
|
## CRON vs HEARTBEAT
|
||||||
|
|
||||||
|
- **HEARTBEAT**:只有一个,固定间隔跑,不管几点
|
||||||
|
- **CRON**:可以有多个,按 cron 表达式跑,精确到分钟
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
[步骤 13:多层提示](../13-multi-layer-prompts/) - 响应式系统提示。
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# 步骤 13:多层提示
|
||||||
|
|
||||||
|
> 更多上下文,更多上下文,更多上下文。
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
与步骤 09 相同 - 复制配置文件并添加你的 API 密钥:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp default_workspace/config.example.yaml default_workspace/config.user.yaml
|
||||||
|
# 编辑 config.user.yaml 添加你的 API 密钥
|
||||||
|
```
|
||||||
|
|
||||||
|
## 这节做什么
|
||||||
|
|
||||||
|
系统提示分多层组装:身份、性格、工作区上下文、运行时信息。
|
||||||
|
|
||||||
|
<img src="13-multi-layer-prompts.svg" align="center" width="100%" />
|
||||||
|
|
||||||
|
## 关键组件
|
||||||
|
|
||||||
|
- **AgentDef** - `soul_md` 扩展
|
||||||
|
- **PromptBuilder** - 将所有提示层组装成最终系统提示
|
||||||
|
|
||||||
|
[src/mybot/core/agent_loader.py](src/mybot/core/agent_loader.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentDef(BaseModel):
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
description: str = ""
|
||||||
|
agent_md: str
|
||||||
|
soul_md: str = "" # NEW: Personality layer (optional)
|
||||||
|
llm: LLMConfig
|
||||||
|
allow_skills: bool = False
|
||||||
|
```
|
||||||
|
|
||||||
|
[src/mybot/core/prompt_builder.py](src/mybot/core/prompt_builder.py)
|
||||||
|
```python
|
||||||
|
class PromptBuilder:
|
||||||
|
def build(self, state: "SessionState") -> str:
|
||||||
|
layers = []
|
||||||
|
|
||||||
|
# Layer 1: Identity
|
||||||
|
layers.append(state.agent.agent_def.agent_md)
|
||||||
|
|
||||||
|
# Layer 2: Soul (optional)
|
||||||
|
if state.agent.agent_def.soul_md:
|
||||||
|
layers.append(f"## Personality\n\n{state.agent.agent_def.soul_md}")
|
||||||
|
|
||||||
|
# Layer 3: Bootstrap context (BOOTSTRAP.md + AGENTS.md + crons)
|
||||||
|
bootstrap = self._load_bootstrap_context()
|
||||||
|
if bootstrap:
|
||||||
|
layers.append(bootstrap)
|
||||||
|
|
||||||
|
# Layer 4: Runtime context
|
||||||
|
layers.append(self._build_runtime_context(agent_id, timestamp))
|
||||||
|
|
||||||
|
# Layer 5: Channel hint
|
||||||
|
layers.append(self._build_channel_hint(source))
|
||||||
|
|
||||||
|
return "\n\n".join(layers)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例工作区设置
|
||||||
|
|
||||||
|
- [default_workspace/agents/pickle/AGENT.md](../default_workspace/agents/pickle/AGENT.md) - 智能体身份、能力和行为准则(带有配置的 YAML 前言)
|
||||||
|
- [default_workspace/agents/pickle/SOUL.md](../default_workspace/agents/pickle/SOUL.md) - 定义智能体角色和语气的个性层
|
||||||
|
- [default_workspace/BOOTSTRAP.md](../default_workspace/BOOTSTRAP.md) - 描述目录结构、文件用途以及智能体、技能、cron 和记忆路径模板的工作区指南
|
||||||
|
- [default_workspace/AGENTS.md](../default_workspace/AGENTS.md) - 列出所有智能体,以及把任务调度给专用智能体(比如记忆操作)的模式
|
||||||
|
|
||||||
|
|
||||||
|
## 试一试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd 13-multi-layer-prompts
|
||||||
|
uv run my-bot server
|
||||||
|
|
||||||
|
# From Channel of your choice:
|
||||||
|
|
||||||
|
# You: When are Where are we talking?
|
||||||
|
# pickle: Meow! Let me check... We're chatting right now via Telegram! *twitches ears*
|
||||||
|
|
||||||
|
# The current time is 2026-03-13 at 23:04:45. So we're here, in this conversation, happening in real-time. 🐱
|
||||||
|
```
|
||||||
|
|
||||||
|
## 扩展
|
||||||
|
|
||||||
|
架构可以按需加层。比如加个**记忆层**,注入历史对话的相关内容。
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
[步骤 14:主动发消息](../14-post-message-back/) - 智能体主动发起通信。
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# 步骤 14:主动发消息
|
||||||
|
|
||||||
|
> 你的智能体想和你说话。
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
与步骤 09 相同 - 复制配置文件并添加你的 API 密钥:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp default_workspace/config.example.yaml default_workspace/config.user.yaml
|
||||||
|
# 编辑 config.user.yaml 添加你的 API 密钥
|
||||||
|
```
|
||||||
|
|
||||||
|
## 这节做什么
|
||||||
|
|
||||||
|
智能体可以主动给你发消息,不只是响应你。cron 任务里特别有用。
|
||||||
|
|
||||||
|
<img src="14-post-message-back.svg" align="center" width="100%" />
|
||||||
|
|
||||||
|
## 关键组件
|
||||||
|
|
||||||
|
- **post_message_tool** - 启用频道时创建工具的工厂
|
||||||
|
- **DeliveryWorker** - 处理 OutboundEvent 到平台的投递
|
||||||
|
|
||||||
|
[src/mybot/tools/post_message_tool.py](src/mybot/tools/post_message_tool.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
@tool(...)
|
||||||
|
async def post_message(content: str, session: "AgentSession") -> str:
|
||||||
|
event = OutboundEvent(
|
||||||
|
session_id=session.session_id,
|
||||||
|
source=AgentEventSource(agent_id=session.agent.agent_def.id),
|
||||||
|
content=content,
|
||||||
|
timestamp=time.time(),
|
||||||
|
)
|
||||||
|
await context.eventbus.publish(event)
|
||||||
|
return "Message queued for delivery"
|
||||||
|
|
||||||
|
return post_message
|
||||||
|
```
|
||||||
|
|
||||||
|
## 试一试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd 14-post-message-back
|
||||||
|
uv run my-bot server
|
||||||
|
|
||||||
|
# From Channel of your choice:
|
||||||
|
|
||||||
|
# You: Say Hi to me after 5 minutes.
|
||||||
|
# pickle: I've scheduled a one-time "Hi" for you in about 2 minutes. You'll hear from me shortly! *purrs* ✅
|
||||||
|
|
||||||
|
# roughly 5 mins later
|
||||||
|
|
||||||
|
# pickle: Hi there! 👋 Just wanted to pop in and say hello! Hope you're having a wonderful day!
|
||||||
|
```
|
||||||
|
|
||||||
|
## 限制
|
||||||
|
|
||||||
|
`post_message` 工具只在 Cron 任务里能用。
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
[步骤 15:智能体调度](../15-agent-dispatch/) - 多智能体协作。
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
# 步骤 15:智能体调度
|
||||||
|
|
||||||
|
> 你的智能体想和朋友一起工作!
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
与步骤 09 相同 - 复制配置文件并添加你的 API 密钥:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp default_workspace/config.example.yaml default_workspace/config.user.yaml
|
||||||
|
# 编辑 config.user.yaml 添加你的 API 密钥
|
||||||
|
```
|
||||||
|
|
||||||
|
## 这节做什么
|
||||||
|
|
||||||
|
智能体可以把活派给其他智能体干。
|
||||||
|
|
||||||
|
<img src="15-agent-dispatch.svg" align="center" width="100%" />
|
||||||
|
|
||||||
|
## 关键组件
|
||||||
|
|
||||||
|
- **subagent_tool** - 创建调度工具的工厂,动态生成 schema
|
||||||
|
|
||||||
|
[src/mybot/tools/subagent_tool.py](src/mybot/tools/subagent_tool.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
def create_subagent_dispatch_tool(
|
||||||
|
current_agent_id: str,
|
||||||
|
context: "SharedContext",
|
||||||
|
) -> BaseTool | None:
|
||||||
|
available_agents = context.agent_loader.discover_agents()
|
||||||
|
dispatchable_agents = [a for a in available_agents if a.id != current_agent_id]
|
||||||
|
|
||||||
|
agents_desc = "<available_agents>\n"
|
||||||
|
for agent_def in dispatchable_agents:
|
||||||
|
agents_desc += f' <agent id="{agent_def.id}">{agent_def.description}</agent>\n'
|
||||||
|
agents_desc += "</available_agents>"
|
||||||
|
|
||||||
|
@tool(
|
||||||
|
name="subagent_dispatch",
|
||||||
|
description=f"Dispatch a task to a specialized subagent.\n{agents_desc}",
|
||||||
|
parameters={...},
|
||||||
|
)
|
||||||
|
async def subagent_dispatch(
|
||||||
|
agent_id: str, task: str, session: "AgentSession", context: str = ""
|
||||||
|
) -> str:
|
||||||
|
agent_def = shared_context.agent_loader.load(agent_id)
|
||||||
|
agent = Agent(agent_def, shared_context)
|
||||||
|
agent_source = AgentEventSource(agent_id=current_agent_id)
|
||||||
|
agent_session = agent.new_session(agent_source)
|
||||||
|
session_id = agent_session.session_id
|
||||||
|
|
||||||
|
user_message = task
|
||||||
|
if context:
|
||||||
|
user_message = f"{task}\n\nContext:\n{context}"
|
||||||
|
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
result_future: asyncio.Future[str] = loop.create_future()
|
||||||
|
|
||||||
|
# Create temp handler that filters by session_id
|
||||||
|
async def handle_result(event: DispatchResultEvent) -> None:
|
||||||
|
if event.session_id == session_id:
|
||||||
|
if not result_future.done():
|
||||||
|
if event.error:
|
||||||
|
result_future.set_exception(Exception(event.error))
|
||||||
|
else:
|
||||||
|
result_future.set_result(event.content)
|
||||||
|
|
||||||
|
# Subscribe to DispatchResultEvent events
|
||||||
|
shared_context.eventbus.subscribe(DispatchResultEvent, handle_result)
|
||||||
|
|
||||||
|
try:
|
||||||
|
event = DispatchEvent(
|
||||||
|
session_id=session_id,
|
||||||
|
source=AgentEventSource(agent_id=current_agent_id),
|
||||||
|
content=user_message,
|
||||||
|
timestamp=time.time(),
|
||||||
|
parent_session_id=session.session_id,
|
||||||
|
)
|
||||||
|
await shared_context.eventbus.publish(event)
|
||||||
|
|
||||||
|
response = await result_future
|
||||||
|
finally:
|
||||||
|
shared_context.eventbus.unsubscribe(handle_result)
|
||||||
|
|
||||||
|
result = {"result": response, "session_id": session_id}
|
||||||
|
return json.dumps(result)
|
||||||
|
|
||||||
|
return subagent_dispatch
|
||||||
|
```
|
||||||
|
|
||||||
|
### 调度机制的工作原理
|
||||||
|
|
||||||
|
调度机制基于 **eventbus** 模式:
|
||||||
|
|
||||||
|
1. **发布**:主智能体调用 `subagent_dispatch`,向 eventbus 发送 `DispatchEvent`
|
||||||
|
2. **订阅**:临时处理器订阅 `DispatchResultEvent`,按 session ID 过滤
|
||||||
|
3. **等待**:主智能体等 future,子智能体完成后发布 `DispatchResultEvent`,future 解析
|
||||||
|
4. **清理**:收到结果后,处理程序从 eventbus 取消订阅。
|
||||||
|
|
||||||
|
|
||||||
|
## 试一试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd 15-agent-dispatch
|
||||||
|
uv run my-bot chat
|
||||||
|
|
||||||
|
# You: Ask Cookie to read our README.
|
||||||
|
# pickle: Cookie has sent our README back! *purrs* 🐱
|
||||||
|
|
||||||
|
# # Step 15: Agent Dispatch
|
||||||
|
|
||||||
|
# > Your Agent want friends to work with!
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意
|
||||||
|
|
||||||
|
### 其他多智能体模式
|
||||||
|
|
||||||
|
直接调度不是唯一方式:
|
||||||
|
|
||||||
|
- **共享任务队列**:智能体从同一个队列领任务,互不直接对话
|
||||||
|
- **Tmux 技能**:给智能体一个 Tmux 技能,让它自己开多窗口干多件事
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
[步骤 16:并发控制](../16-concurrency-control/) - 速率限制和队列管理。
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# 步骤 16:并发控制
|
||||||
|
|
||||||
|
> 太多 Pickle 同时运行?
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
与步骤 09 相同 - 复制配置文件并添加你的 API 密钥:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp default_workspace/config.example.yaml default_workspace/config.user.yaml
|
||||||
|
# 编辑 config.user.yaml 添加你的 API 密钥
|
||||||
|
```
|
||||||
|
|
||||||
|
## 这节做什么
|
||||||
|
|
||||||
|
限制同一智能体同时跑几个实例,防止资源爆掉。
|
||||||
|
|
||||||
|
<img src="16-concurrency-control.svg" align="center" width="100%" />
|
||||||
|
|
||||||
|
## 关键组件
|
||||||
|
|
||||||
|
- **AgentDef.max_concurrency** - 每个智能体可配置的限制
|
||||||
|
- **基于信号量的并发控制** - 达到并发限制时阻塞
|
||||||
|
|
||||||
|
|
||||||
|
[src/mybot/server/agent_worker.py](src/mybot/server/agent_worker.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentWorker(SubscriberWorker):
|
||||||
|
def __init__(self, context: "SharedContext"):
|
||||||
|
super().__init__(context)
|
||||||
|
self._semaphores: dict[str, asyncio.Semaphore] = {}
|
||||||
|
|
||||||
|
async def exec_session(self, event, agent_def: "AgentDef") -> None:
|
||||||
|
sem = self._get_or_create_semaphore(agent_def)
|
||||||
|
|
||||||
|
async with sem: # Blocks if limit reached
|
||||||
|
# ... execute session ...
|
||||||
|
|
||||||
|
self._maybe_cleanup_semaphores(agent_def)
|
||||||
|
|
||||||
|
def _get_or_create_semaphore(self, agent_def: "AgentDef") -> asyncio.Semaphore:
|
||||||
|
if agent_def.id not in self._semaphores:
|
||||||
|
self._semaphores[agent_def.id] = asyncio.Semaphore(
|
||||||
|
agent_def.max_concurrency
|
||||||
|
)
|
||||||
|
return self._semaphores[agent_def.id]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 试一试
|
||||||
|
|
||||||
|
`Cookie` 的 `max_concurrency` 设成 1,从两个不同源触发它。
|
||||||
|
|
||||||
|
## 并发控制粒度
|
||||||
|
|
||||||
|
- **按智能体**(本实现):限制每种智能体同时跑几个
|
||||||
|
- **按源**:限制同一用户同时发几个请求
|
||||||
|
- **按优先级**:高优先级任务预留容量
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
[步骤 17:记忆](../17-memory/) - 长期知识系统。
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# 步骤 17:记忆
|
||||||
|
|
||||||
|
> 记住我!
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
与步骤 09 相同 - 复制配置文件并添加你的 API 密钥:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp default_workspace/config.example.yaml default_workspace/config.user.yaml
|
||||||
|
# 编辑 config.user.yaml 添加你的 API 密钥
|
||||||
|
```
|
||||||
|
|
||||||
|
## 这节做什么
|
||||||
|
|
||||||
|
长期记忆,跨会话记住用户信息。
|
||||||
|
|
||||||
|
```
|
||||||
|
pickle: @cookie Do you know <topic> about user?
|
||||||
|
cookie: Yes, <content>.
|
||||||
|
```
|
||||||
|
|
||||||
|
## 关键组件
|
||||||
|
|
||||||
|
- **Memory agent** - 专门用于记忆管理的智能体
|
||||||
|
- [default_workspace/agents/cookie/AGENT.md](../default_workspace/agents/cookie/AGENT.md)
|
||||||
|
|
||||||
|
## 试一试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd 17-memory
|
||||||
|
uv run my-bot chat
|
||||||
|
|
||||||
|
# You: Remember that I my name is Zane
|
||||||
|
# Pickle: Got it! I've saved that preference.
|
||||||
|
|
||||||
|
uv run my-bot chat
|
||||||
|
|
||||||
|
# User: What's my name?
|
||||||
|
# Pickle: Based on your memory, you name is Zane! Hi Zane! 😸
|
||||||
|
```
|
||||||
|
|
||||||
|
## 实现方式对比
|
||||||
|
|
||||||
|
| 方法 | 描述 |
|
||||||
|
|------|------|
|
||||||
|
| **专用智能体**(本实现)| 通过调度访问的记忆智能体 |
|
||||||
|
| **内置工具**| 主智能体直接带记忆工具 |
|
||||||
|
| **基于技能**| 用 grep 等 CLI 工具 |
|
||||||
|
| **向量数据库**| embedding + 语义搜索 |
|
||||||
|
|
||||||
|
### 记忆目录结构(Pickle Bot)
|
||||||
|
|
||||||
|
```
|
||||||
|
memories/
|
||||||
|
├── topics/
|
||||||
|
│ ├── preferences.md # 用户偏好
|
||||||
|
│ └── identity.md # 用户信息
|
||||||
|
├── projects/
|
||||||
|
│ └── my-project.md # 项目特定笔记
|
||||||
|
└── daily-notes/
|
||||||
|
└── 2024-01-15.md # 每日日志
|
||||||
|
```
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
部署、扩展和定制!
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
# Influencer Campaign Plan
|
|
||||||
|
|
||||||
Goal: Leverage influencers with existing audiences to drive awareness and stars for the tutorial.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Strategy Overview
|
|
||||||
|
|
||||||
**Problem:** Twitter has 0 followers, Dev.to posts get <100 reads, Reddit is anti-AI hype.
|
|
||||||
|
|
||||||
**Solution:** Get people with audiences to discover and share the tutorial.
|
|
||||||
|
|
||||||
**Approach:** Personalized outreach to AI/ML content creators offering value (content ideas, tutorial resource).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Target Influencer Categories
|
|
||||||
|
|
||||||
### Priority 1: YouTube AI/ML Creators
|
|
||||||
- Make tutorial videos about agents, LangChain, etc.
|
|
||||||
- Always looking for content ideas
|
|
||||||
- High production value = high trust
|
|
||||||
|
|
||||||
**Search terms:**
|
|
||||||
- "LangChain tutorial" on YouTube
|
|
||||||
- "AI agent python" on YouTube
|
|
||||||
- "Build your own AI agent" on YouTube
|
|
||||||
|
|
||||||
### Priority 2: Twitter/X AI Devs
|
|
||||||
- Active in AI Twitter community
|
|
||||||
- Post about agents, LLMs, tools
|
|
||||||
- Often share interesting projects
|
|
||||||
|
|
||||||
**Search terms:**
|
|
||||||
- #AIagents #LLM #Python on Twitter
|
|
||||||
- "building AI agents" on Twitter
|
|
||||||
- Follow conversations in AI agent threads
|
|
||||||
|
|
||||||
### Priority 3: Newsletter Writers
|
|
||||||
- Smaller than TLDR but still valuable
|
|
||||||
- Often accept free submissions or features
|
|
||||||
- Looking for interesting projects to share
|
|
||||||
|
|
||||||
**Search terms:**
|
|
||||||
- "AI newsletter" + "submit"
|
|
||||||
- Python newsletters
|
|
||||||
- Developer newsletters
|
|
||||||
|
|
||||||
### Priority 4: LinkedIn AI Creators
|
|
||||||
- Growing audience for dev content
|
|
||||||
- Less competition than Twitter
|
|
||||||
- Algorithm favors educational posts
|
|
||||||
|
|
||||||
### Priority 5: Dev.to / Medium Writers
|
|
||||||
- Already posting about AI agents
|
|
||||||
- Might collaborate or cross-reference
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Outreach Process
|
|
||||||
|
|
||||||
### Step 1: Research (30 min)
|
|
||||||
- Find 10-15 potential influencers per category
|
|
||||||
- Check their recent content - is it relevant?
|
|
||||||
- Note their style (tutorials, news, opinions)
|
|
||||||
- Find their contact (email, DM, contact form)
|
|
||||||
|
|
||||||
### Step 2: Personalize (5 min per influencer)
|
|
||||||
- Watch/read 1-2 pieces of their content
|
|
||||||
- Find something specific to reference
|
|
||||||
- Customize email template with their name + content reference
|
|
||||||
|
|
||||||
### Step 3: Send
|
|
||||||
- Use Template A (preferred) or Template B (shorter)
|
|
||||||
- Keep it brief, value-focused
|
|
||||||
- No ask for share - just "thought you might find interesting"
|
|
||||||
|
|
||||||
### Step 4: Follow Up
|
|
||||||
- If no response in 5-7 days, send one gentle follow-up
|
|
||||||
- If they respond, engage genuinely
|
|
||||||
- If they share, thank them publicly
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Email Templates
|
|
||||||
|
|
||||||
### Template A: Tutorial Collaboration (Primary)
|
|
||||||
|
|
||||||
```
|
|
||||||
Subject: 18-step AI agent tutorial - thought you might find interesting
|
|
||||||
|
|
||||||
Hi [Name],
|
|
||||||
|
|
||||||
I've been following your content on [specific topic they cover] -
|
|
||||||
great stuff, especially [mention specific video/post].
|
|
||||||
|
|
||||||
I created something that might interest you or your audience:
|
|
||||||
|
|
||||||
A step-by-step tutorial teaching developers to build AI agents
|
|
||||||
from scratch - no frameworks, just understanding.
|
|
||||||
|
|
||||||
18 progressive steps:
|
|
||||||
- Chat loop -> Tools -> Skills -> Memory
|
|
||||||
- Multi-agent orchestration
|
|
||||||
- Production features (WebSocket, cron, concurrency)
|
|
||||||
|
|
||||||
Each step has runnable Python code + explanation.
|
|
||||||
|
|
||||||
[GitHub Link: https://github.com/czl9707/build-your-own-openclaw]
|
|
||||||
|
|
||||||
Would you be interested in this for a video/thread topic?
|
|
||||||
Happy to answer any questions or provide more context.
|
|
||||||
|
|
||||||
[Your name]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Template B: Short & Direct (Alternative)
|
|
||||||
|
|
||||||
```
|
|
||||||
Subject: AI agent tutorial you might like
|
|
||||||
|
|
||||||
Hi [Name],
|
|
||||||
|
|
||||||
Built a tutorial on constructing AI agents from first principles.
|
|
||||||
|
|
||||||
18 steps, runnable code, works with any LLM provider.
|
|
||||||
|
|
||||||
[GitHub Link: https://github.com/czl9707/build-your-own-openclaw]
|
|
||||||
|
|
||||||
Thought it might be up your alley.
|
|
||||||
|
|
||||||
[Your name]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Template C: DM Version (Twitter/LinkedIn)
|
|
||||||
|
|
||||||
```
|
|
||||||
Love your content on [topic].
|
|
||||||
|
|
||||||
I made an 18-step tutorial on building AI agents from scratch.
|
|
||||||
No frameworks - teaches how agents actually work.
|
|
||||||
|
|
||||||
[GitHub Link]
|
|
||||||
|
|
||||||
Thought you might find it interesting for content ideas!
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Influencer Tracking
|
|
||||||
|
|
||||||
| Name | Platform | Followers | Contact | Status | Notes |
|
|
||||||
|------|----------|-----------|---------|--------|-------|
|
|
||||||
| | | | | | |
|
|
||||||
| | | | | | |
|
|
||||||
| | | | | | |
|
|
||||||
|
|
||||||
**Status options:**
|
|
||||||
- `researching` - Found but not contacted
|
|
||||||
- `contacted` - Sent email/DM
|
|
||||||
- `responded` - They replied
|
|
||||||
- `shared` - They shared the tutorial
|
|
||||||
- `declined` - Not interested
|
|
||||||
- `no_response` - No reply after follow-up
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Success Metrics
|
|
||||||
|
|
||||||
| Metric | Target | Tracking |
|
|
||||||
|--------|--------|----------|
|
|
||||||
| Influencers contacted | 30+ | Manual |
|
|
||||||
| Response rate | 10%+ | Manual |
|
|
||||||
| Shares/features | 3+ | Manual |
|
|
||||||
| Stars from campaign | 100+ | GitHub insights |
|
|
||||||
| Traffic from campaign | Track via UTM | Google Analytics |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Timeline
|
|
||||||
|
|
||||||
| Week | Activity |
|
|
||||||
|------|----------|
|
|
||||||
| 1 | Research influencers, compile list |
|
|
||||||
| 2 | Send first batch (10-15 emails) |
|
|
||||||
| 3 | Follow up on no-responses, send second batch |
|
|
||||||
| 4 | Engage with responders, track results |
|
|
||||||
| 5+ | Ongoing: add new influencers, maintain relationships |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Do's and Don'ts
|
|
||||||
|
|
||||||
### Do:
|
|
||||||
- Personalize every message
|
|
||||||
- Reference their actual content
|
|
||||||
- Offer value (content ideas, not just "share my thing")
|
|
||||||
- Be patient (responses take time)
|
|
||||||
- Thank anyone who shares
|
|
||||||
|
|
||||||
### Don't:
|
|
||||||
- Send generic copy-paste messages
|
|
||||||
- Ask directly for shares
|
|
||||||
- Spam multiple contacts at same org
|
|
||||||
- Get discouraged by no-responses
|
|
||||||
- Follow up more than once
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
1. [ ] Research 15 YouTube AI/ML creators
|
|
||||||
2. [ ] Research 15 Twitter/X AI devs
|
|
||||||
3. [ ] Compile contact info in tracking table
|
|
||||||
4. [ ] Send first batch of emails using Template A
|
|
||||||
5. [ ] Set up UTM tracking for campaign traffic
|
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# 构建你自己的 OpenClaw
|
||||||
|
|
||||||
|
从简单的聊天循环开始,一步步搭建 [OpenClaw](https://github.com/openclaw/openclaw) 的精简版。
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
18 个步骤,每步包含:
|
||||||
|
|
||||||
|
- 讲解关键组件和设计决策的 `README.md`
|
||||||
|
- 可直接运行的代码
|
||||||
|
|
||||||
|
**参考项目:** [pickle-bot](https://github.com/czl9707/pickle-bot)
|
||||||
|
|
||||||
|
## 教程结构
|
||||||
|
|
||||||
|
### 第一阶段:能干的单智能体(步骤 0-6)
|
||||||
|
|
||||||
|
让智能体学会聊天、用工具、加载技能、保存对话、上网搜索。
|
||||||
|
|
||||||
|
- [**00-chat-loop**](./00-chat-loop/) - 只是一个聊天循环
|
||||||
|
- [**01-tools**](./01-tools/) - 给你的智能体一个工具
|
||||||
|
- [**02-skills**](./02-skills/) - 用 `SKILL.md` 扩展你的智能体
|
||||||
|
- [**03-persistence**](./03-persistence/) - 保存你的对话
|
||||||
|
- [**04-slash-commands**](./04-slash-commands/) - 直接控制会话
|
||||||
|
- [**05-compaction**](./05-compaction/) - 打包历史,继续前进...
|
||||||
|
- [**06-web-tools**](./06-web-tools/) - 你的智能体想看看更大的世界
|
||||||
|
|
||||||
|
### 第二阶段:事件驱动(步骤 7-10)
|
||||||
|
|
||||||
|
换成事件驱动架构,支持多平台接入。
|
||||||
|
|
||||||
|
- [**07-event-driven**](./07-event-driven/) - 让你的智能体超越 CLI
|
||||||
|
- [**08-config-hot-reload**](./08-config-hot-reload/) - 无需重启即可编辑
|
||||||
|
- [**09-channels**](./09-channels/) - 在手机上与你的智能体对话
|
||||||
|
- [**10-websocket**](./10-websocket/) - 想要以编程方式与智能体交互?
|
||||||
|
|
||||||
|
### 第三阶段:自主与多智能体(步骤 11-15)
|
||||||
|
|
||||||
|
定时任务、智能路由、多智能体协作。
|
||||||
|
|
||||||
|
- [**11-multi-agent-routing**](./11-multi-agent-routing/) - 将正确的任务路由到正确的智能体
|
||||||
|
- [**12-cron-heartbeat**](./12-cron-heartbeat/) - 智能体在你睡觉时工作
|
||||||
|
- [**13-multi-layer-prompts**](./13-multi-layer-prompts/) - 更多上下文,更多上下文,更多上下文
|
||||||
|
- [**14-post-message-back**](./14-post-message-back/) - 你的智能体想和你说话
|
||||||
|
- [**15-agent-dispatch**](./15-agent-dispatch/) - 智能体调度,把活派给别的智能体
|
||||||
|
|
||||||
|
### 第四阶段:生产就绪(步骤 16-17)
|
||||||
|
|
||||||
|
并发控制和长期记忆。
|
||||||
|
|
||||||
|
- [**16-concurrency-control**](./16-concurrency-control/) - 太多 Pickle 同时运行?
|
||||||
|
- [**17-memory**](./17-memory/) - 记住我!
|
||||||
|
|
||||||
|
## 如何使用本教程
|
||||||
|
|
||||||
|
### 配置 API 密钥
|
||||||
|
|
||||||
|
1. 复制配置模板:
|
||||||
|
```bash
|
||||||
|
cp default_workspace/config.example.yaml default_workspace/config.user.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 编辑 `config.user.yaml` 填入 API 密钥:
|
||||||
|
- [LiteLLM providers](https://docs.litellm.ai/docs/providers) 列出所有支持的模型提供商
|
||||||
|
- [Provider Examples](PROVIDER_EXAMPLES.md) 有配置示例
|
||||||
|
|
||||||
|
## 贡献
|
||||||
|
|
||||||
|
每个步骤独立实现,欢迎提 PR。
|
||||||
Reference in New Issue
Block a user