matrix-for-agents 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentmatrix/__init__.py +20 -0
- agentmatrix/agents/__init__.py +1 -0
- agentmatrix/agents/base.py +572 -0
- agentmatrix/agents/claude_coder.py +10 -0
- agentmatrix/agents/data_crawler.py +14 -0
- agentmatrix/agents/post_office.py +212 -0
- agentmatrix/agents/report_writer.py +14 -0
- agentmatrix/agents/secretary.py +10 -0
- agentmatrix/agents/stateful.py +10 -0
- agentmatrix/agents/user_proxy.py +82 -0
- agentmatrix/agents/worker.py +30 -0
- agentmatrix/backends/__init__.py +1 -0
- agentmatrix/backends/llm_client.py +414 -0
- agentmatrix/backends/mock_llm.py +35 -0
- agentmatrix/cli_runner.py +94 -0
- agentmatrix/core/__init__.py +0 -0
- agentmatrix/core/action.py +50 -0
- agentmatrix/core/browser/bing.py +208 -0
- agentmatrix/core/browser/browser_adapter.py +298 -0
- agentmatrix/core/browser/browser_common.py +85 -0
- agentmatrix/core/browser/drission_page_adapter.py +1296 -0
- agentmatrix/core/browser/google.py +230 -0
- agentmatrix/core/cerebellum.py +121 -0
- agentmatrix/core/events.py +22 -0
- agentmatrix/core/loader.py +185 -0
- agentmatrix/core/loader_v1.py +146 -0
- agentmatrix/core/log_util.py +158 -0
- agentmatrix/core/message.py +32 -0
- agentmatrix/core/prompt_engine.py +30 -0
- agentmatrix/core/runtime.py +211 -0
- agentmatrix/core/session.py +20 -0
- agentmatrix/db/__init__.py +1 -0
- agentmatrix/db/database.py +79 -0
- agentmatrix/db/vector_db.py +213 -0
- agentmatrix/docs/Design.md +109 -0
- agentmatrix/docs/Framework Capbilities.md +105 -0
- agentmatrix/docs/Planner Design.md +148 -0
- agentmatrix/docs/crawler_flow.md +110 -0
- agentmatrix/docs/report_writer.md +83 -0
- agentmatrix/docs/review.md +99 -0
- agentmatrix/docs/skill_design.md +23 -0
- agentmatrix/profiles/claude_coder.yml +40 -0
- agentmatrix/profiles/mark.yml +26 -0
- agentmatrix/profiles/planner.yml +21 -0
- agentmatrix/profiles/prompts/base.txt +88 -0
- agentmatrix/profiles/prompts/base_v1.txt +101 -0
- agentmatrix/profiles/prompts/base_v2.txt +94 -0
- agentmatrix/profiles/tom_the_data_crawler.yml +38 -0
- agentmatrix/profiles/user_proxy.yml +17 -0
- agentmatrix/skills/__init__.py +1 -0
- agentmatrix/skills/crawler_helpers.py +315 -0
- agentmatrix/skills/data_crawler.py +777 -0
- agentmatrix/skills/filesystem.py +204 -0
- agentmatrix/skills/notebook.py +158 -0
- agentmatrix/skills/project_management.py +114 -0
- agentmatrix/skills/report_writer.py +194 -0
- agentmatrix/skills/report_writer_utils.py +379 -0
- agentmatrix/skills/search_tool.py +383 -0
- agentmatrix/skills/terminal_ctrl.py +122 -0
- agentmatrix/skills/utils.py +33 -0
- agentmatrix/skills/web_searcher.py +1107 -0
- matrix_for_agents-0.1.2.dist-info/METADATA +44 -0
- matrix_for_agents-0.1.2.dist-info/RECORD +66 -0
- matrix_for_agents-0.1.2.dist-info/WHEEL +5 -0
- matrix_for_agents-0.1.2.dist-info/licenses/LICENSE +190 -0
- matrix_for_agents-0.1.2.dist-info/top_level.txt +1 -0
agentmatrix/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent-Matrix: An intelligent agent framework.
|
|
3
|
+
|
|
4
|
+
Agent-Matrix is a flexible framework for building and managing AI agents
|
|
5
|
+
with pluggable skills, backend LLM integrations, and message-based communication.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
# Core imports
|
|
11
|
+
from .core.runtime import AgentMatrix
|
|
12
|
+
from .core.message import Email
|
|
13
|
+
from .agents.post_office import PostOffice
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"AgentMatrix",
|
|
17
|
+
"Email",
|
|
18
|
+
"PostOffice",
|
|
19
|
+
"__version__",
|
|
20
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Agents module for Agent-Matrix framework."""
|
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Dict, Optional, Callable, List
|
|
3
|
+
from ..core.message import Email
|
|
4
|
+
from ..core.session import TaskSession
|
|
5
|
+
from ..core.events import AgentEvent
|
|
6
|
+
from ..core.action import register_action
|
|
7
|
+
import traceback
|
|
8
|
+
from dataclasses import asdict
|
|
9
|
+
import inspect
|
|
10
|
+
import json
|
|
11
|
+
import textwrap
|
|
12
|
+
from ..skills.filesystem import FileSkillMixin
|
|
13
|
+
from ..core.log_util import AutoLoggerMixin
|
|
14
|
+
import logging
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
class BaseAgent(FileSkillMixin,AutoLoggerMixin):
|
|
18
|
+
_log_from_attr = "name" # 日志名字来自 self.name 属性
|
|
19
|
+
|
|
20
|
+
_custom_log_level = logging.DEBUG
|
|
21
|
+
def __init__(self, profile):
|
|
22
|
+
self.name = profile["name"]
|
|
23
|
+
self.description = profile["description"]
|
|
24
|
+
self.prompt_template = profile.get("prompt_template", "base")
|
|
25
|
+
self.full_prompt = profile["full_prompt"]
|
|
26
|
+
# full_prompt是 prompte loaded from prompate_template,
|
|
27
|
+
# 后面会和system_prompt合并,然后再替换其他变量生成最终的system prompt
|
|
28
|
+
self.system_prompt = profile["system_prompt"] #system_prompt is actually persona prompt
|
|
29
|
+
self.profile = profile
|
|
30
|
+
self.instruction_to_caller = profile["instruction_to_caller"]
|
|
31
|
+
self.backend_model = profile.get("backend_model", "default_llm")
|
|
32
|
+
self.brain = None
|
|
33
|
+
self.status = "IDLE"
|
|
34
|
+
self.last_received_email = None #最后收到的信
|
|
35
|
+
self.cerebellum = None
|
|
36
|
+
self.workspace_root = None
|
|
37
|
+
self.post_office = None
|
|
38
|
+
self.last_email_processed = True
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# 标准组件
|
|
45
|
+
self.inbox = asyncio.Queue()
|
|
46
|
+
#self.sessions = {}
|
|
47
|
+
self.sessions: Dict[str, TaskSession] = {} # Key: Original Msg ID
|
|
48
|
+
self.reply_mapping: Dict[str, str] = {} # Key: Outgoing Msg ID -> Value: Session ID
|
|
49
|
+
|
|
50
|
+
# 事件回调 (Server 注入)
|
|
51
|
+
self.async_event_callback: Optional[Callable] = None
|
|
52
|
+
|
|
53
|
+
self.actions_map = {} # name -> method
|
|
54
|
+
self.actions_meta = {} # name -> metadata (给小脑看)
|
|
55
|
+
self.current_session = None
|
|
56
|
+
self.current_user_session_id = None
|
|
57
|
+
|
|
58
|
+
self._scan_methods()
|
|
59
|
+
|
|
60
|
+
self.logger.info(f"Agent {self.name} 初始化完成")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _scan_methods(self):
|
|
66
|
+
"""扫描并生成元数据"""
|
|
67
|
+
for name, method in inspect.getmembers(self, predicate=inspect.ismethod):
|
|
68
|
+
if getattr(method, "_is_action", False):
|
|
69
|
+
|
|
70
|
+
# 1. 提取基础信息
|
|
71
|
+
desc = method._action_desc
|
|
72
|
+
param_infos = method._action_param_infos
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
self.actions_map[name] = method
|
|
77
|
+
self.actions_meta[name] = {
|
|
78
|
+
"action": name,
|
|
79
|
+
"description": desc,
|
|
80
|
+
"params": param_infos
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def current_private_workspace(self) -> Path:
|
|
85
|
+
"""
|
|
86
|
+
获取当前 session 的个人工作目录(如果不存在则自动创建)
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Path: 个人工作目录路径,格式为 workspace_root / user_session_id / agents / agent_name
|
|
90
|
+
"""
|
|
91
|
+
if not self.workspace_root:
|
|
92
|
+
#raise ValueError("workspace_root is not set")
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
user_session_id = self.current_user_session_id or "default"
|
|
96
|
+
workspace = Path(self.workspace_root) / user_session_id / "agents" / self.name
|
|
97
|
+
|
|
98
|
+
# 如果目录不存在,创建目录
|
|
99
|
+
workspace.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
|
|
101
|
+
return workspace
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def current_workspace(self) -> Path:
|
|
105
|
+
"""
|
|
106
|
+
获取当前 session 的共享工作目录(如果不存在则自动创建)
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Path: 共享工作目录路径,格式为 workspace_root / user_session_id / shared
|
|
110
|
+
"""
|
|
111
|
+
if not self.workspace_root:
|
|
112
|
+
#raise ValueError("workspace_root is not set")
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
user_session_id = self.current_user_session_id or "default"
|
|
116
|
+
workspace = Path(self.workspace_root) / user_session_id / "shared"
|
|
117
|
+
|
|
118
|
+
# 如果目录不存在,创建目录
|
|
119
|
+
workspace.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
|
|
121
|
+
return workspace
|
|
122
|
+
|
|
123
|
+
def get_prompt(self):
|
|
124
|
+
"""生成给 LLM 的完整 Prompt"""
|
|
125
|
+
prompt = self.full_prompt
|
|
126
|
+
prompt =prompt.replace("{{ name }}", self.name)
|
|
127
|
+
prompt =prompt.replace("{{ description }}", self.description)
|
|
128
|
+
prompt =prompt.replace("{{ system_prompt }}", self.system_prompt)
|
|
129
|
+
yellow_page = self.post_office.yellow_page_exclude_me(self.name)
|
|
130
|
+
prompt =prompt.replace("{{ yellow_page }}", yellow_page)
|
|
131
|
+
capabilities_menu = self.get_capabilities_summary()
|
|
132
|
+
prompt = prompt.replace("{{ capabilities }}", capabilities_menu)
|
|
133
|
+
return prompt
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def get_capabilities_summary(self) -> str:
|
|
138
|
+
"""
|
|
139
|
+
生成给 Brain 看的能力清单 (Menu)。
|
|
140
|
+
只包含 Action Name 和 Description,不包含 JSON 参数细节。
|
|
141
|
+
"""
|
|
142
|
+
lines = []
|
|
143
|
+
if not self.actions_meta:
|
|
144
|
+
return "(No capabilities registered)"
|
|
145
|
+
|
|
146
|
+
for name, meta in self.actions_meta.items():
|
|
147
|
+
# 格式示例: - read_file: 读取文件内容。只读取文本文件。
|
|
148
|
+
lines.append(f"- {name}: {meta['description']}")
|
|
149
|
+
|
|
150
|
+
return "\n".join(lines)
|
|
151
|
+
|
|
152
|
+
def _generate_tools_prompt(self):
|
|
153
|
+
"""生成给 SLM 看的 Prompt"""
|
|
154
|
+
prompt = ""
|
|
155
|
+
for name, meta in self.actions_meta.items():
|
|
156
|
+
# 这里直接把 Schema dump 成 json 字符串
|
|
157
|
+
# 这种格式是目前开源模型微调 Function Calling 最常用的格式
|
|
158
|
+
schema_str = json.dumps(meta["params"], ensure_ascii=False)
|
|
159
|
+
prompt += textwrap.dedent(f"""
|
|
160
|
+
### Action name: {name} ###
|
|
161
|
+
Description:
|
|
162
|
+
{meta['description']}
|
|
163
|
+
|
|
164
|
+
ACTION JSON DEFINITION:
|
|
165
|
+
{schema_str}
|
|
166
|
+
|
|
167
|
+
""")
|
|
168
|
+
|
|
169
|
+
return prompt
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def get_introduction(self):
|
|
174
|
+
"""
|
|
175
|
+
生成给其他 Agent 看的说明书 (Protocol Description)
|
|
176
|
+
这是之前 AgentManifest.to_prompt() 的逻辑
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
f"--- Agent: {self.name} ---\n"
|
|
181
|
+
f"Description: {self.description}\n"
|
|
182
|
+
f"Instruction: {self.instruction_to_caller}\n"
|
|
183
|
+
f"--------------------------\n"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
async def emit(self, event_type, content, payload={}):
|
|
189
|
+
"""
|
|
190
|
+
发送事件到注册的事件回调函数
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
event_type (str): 事件的类型,用于标识不同种类的事件,
|
|
194
|
+
|
|
195
|
+
content (str): 事件的主要内容描述
|
|
196
|
+
payload (dict, optional): 事件的附加数据,默认为None,当为None时会使用空字典
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
None
|
|
200
|
+
|
|
201
|
+
Raises:
|
|
202
|
+
无显式抛出异常
|
|
203
|
+
"""
|
|
204
|
+
# 检查是否存在事件回调函数
|
|
205
|
+
if self.async_event_callback:
|
|
206
|
+
# 创建新的事件对象,包含事件类型、发送者名称、内容和附加数据
|
|
207
|
+
event = AgentEvent(event_type, self.name,self.status, content, payload)
|
|
208
|
+
# 异步调用事件回调函数,将事件对象传递过去
|
|
209
|
+
await self.async_event_callback(event)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
async def run(self):
|
|
214
|
+
await self.emit("SYSTEM", f"{self.name} Started")
|
|
215
|
+
while True:
|
|
216
|
+
try:
|
|
217
|
+
email = await asyncio.wait_for(self.inbox.get(), timeout=3)
|
|
218
|
+
try:
|
|
219
|
+
self.last_received_email = email
|
|
220
|
+
self.last_email_processed = False
|
|
221
|
+
await self.process_email(email)
|
|
222
|
+
except Exception as e:
|
|
223
|
+
self.logger.exception(f"Failed to process email in {self.name}")
|
|
224
|
+
finally:
|
|
225
|
+
self.inbox.task_done()
|
|
226
|
+
self.last_email_processed = True
|
|
227
|
+
except asyncio.TimeoutError:
|
|
228
|
+
# 可选:定期任务、健康检查等
|
|
229
|
+
continue
|
|
230
|
+
except Exception as e:
|
|
231
|
+
self.logger.exception(f"Unexpected error in {self.name} main loop")
|
|
232
|
+
await asyncio.sleep(1) # 防止异常风暴
|
|
233
|
+
|
|
234
|
+
async def process_email(self, email: Email):
|
|
235
|
+
# 1. Session Management (Routing)
|
|
236
|
+
self.logger.debug(f"New Email")
|
|
237
|
+
self.logger.debug(str(email))
|
|
238
|
+
session = self._resolve_session(email)
|
|
239
|
+
self.current_session = session
|
|
240
|
+
self.current_user_session_id = session.user_session_id
|
|
241
|
+
#print(f"Prcessing email from {email.sender} , {email.subject}")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# 2. Add incoming message to history (Input)
|
|
245
|
+
self._add_message_to_history(email)
|
|
246
|
+
|
|
247
|
+
# 3. The "Think-Act" Loop
|
|
248
|
+
# 设置一个最大步数,防止 LLM 死循环自言自语
|
|
249
|
+
MAX_STEPS = 100
|
|
250
|
+
step_count = 0
|
|
251
|
+
|
|
252
|
+
async def ask_brain_clarification(question: str) -> str:
|
|
253
|
+
# 1. 构造一个临时的 Context
|
|
254
|
+
# 我们不希望把 Cerebellum 的琐碎问题污染到主 History 里
|
|
255
|
+
# 所以我们 copy 一份当前的 history,附加上问题
|
|
256
|
+
temp_messages = session.history.copy()
|
|
257
|
+
|
|
258
|
+
# 2. 注入 System Prompt,告诉 Brain 现在是在回答小脑的质询
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
temp_messages.append({"role": "user", "content": f"[INTERNAL QUERY] {question} "})
|
|
262
|
+
|
|
263
|
+
# 3. Brain 思考
|
|
264
|
+
response = await self.brain.think(temp_messages)
|
|
265
|
+
return response['reply']
|
|
266
|
+
|
|
267
|
+
while step_count < MAX_STEPS:
|
|
268
|
+
step_count += 1
|
|
269
|
+
|
|
270
|
+
# A. Brain: Think (Context -> Intention)
|
|
271
|
+
context = self._get_llm_context(session)
|
|
272
|
+
|
|
273
|
+
intention = await self.brain.think(context)
|
|
274
|
+
self.logger.debug(intention)
|
|
275
|
+
|
|
276
|
+
intention = intention['reply']
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
action_signal = None
|
|
281
|
+
#check if there's [ACTION SIGNAL] format in intention
|
|
282
|
+
if "[ACTION SIGNAL]:" in intention:
|
|
283
|
+
action_signal = intention.split("[ACTION SIGNAL]:",1)[1].strip()
|
|
284
|
+
else:
|
|
285
|
+
# 没有遵守格式,强制要求重写
|
|
286
|
+
self._add_brain_intention_to_history(intention)
|
|
287
|
+
self._add_question_to_brain("Action signal should begin with `[ACTION SIGNAL]:`")
|
|
288
|
+
continue
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# 2. Cerebellum 翻译
|
|
293
|
+
manifest_str = self._generate_tools_prompt()
|
|
294
|
+
contacts = self.post_office.get_contact_list(exclude = self.name)
|
|
295
|
+
contacts = "\n".join(contacts)
|
|
296
|
+
|
|
297
|
+
action_json = await self.cerebellum.negotiate(
|
|
298
|
+
initial_intent=action_signal,
|
|
299
|
+
tools_manifest=manifest_str,
|
|
300
|
+
contacts = contacts,
|
|
301
|
+
brain_callback=ask_brain_clarification
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
action_name = action_json.get("action")
|
|
308
|
+
self.logger.debug(f"{self.name} will do action: \n{action_name}")
|
|
309
|
+
params = action_json.get("params", {})
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# 4. 查找方法
|
|
314
|
+
method = self.actions_map.get(action_name)
|
|
315
|
+
if not method:
|
|
316
|
+
# 幻觉处理:Brain 编造了一个不存在的动作
|
|
317
|
+
self._add_intention_feedback_to_history(intention, "Body is tired, need to take a break") #希望Brain会选择“Take a Break"!
|
|
318
|
+
self.logger.error(f"Tried to do an unknown action: {action_name}")
|
|
319
|
+
continue
|
|
320
|
+
|
|
321
|
+
# 5. 执行方法 (Execution)
|
|
322
|
+
try:
|
|
323
|
+
# 获取动作类型
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
if action_name != "rest_n_wait":
|
|
327
|
+
result = await method(**params)
|
|
328
|
+
self.logger.debug(f"{self.name} did action: \n{result}")
|
|
329
|
+
# 同步动作:把结果喂回给 Brain,继续循环
|
|
330
|
+
self._add_intention_feedback_to_history(intention, action_name, result)
|
|
331
|
+
#self.logger.debug(f"{self.current_session.history[:-2]}")
|
|
332
|
+
continue
|
|
333
|
+
|
|
334
|
+
else:
|
|
335
|
+
#如果大脑输出了 rest_n_wait 的意图,我们就结束本次循环了,不用回答了,只要记录下它最后说的
|
|
336
|
+
self._add_brain_intention_to_history(intention)
|
|
337
|
+
self.logger.debug(f"{self.name} will rest and wait")
|
|
338
|
+
self.status = "WAITING" #wait for email reply
|
|
339
|
+
break
|
|
340
|
+
|
|
341
|
+
except Exception as e:
|
|
342
|
+
# 执行报错:把 Python 异常喂回给 Brain
|
|
343
|
+
# Brain 看到报错后,可能会决定 "google search error" 或者 "ask coder"
|
|
344
|
+
self.logger.exception("Body执行错误")
|
|
345
|
+
self._add_intention_feedback_to_history(intention, action_name, f"Something wrong happened : {e}")
|
|
346
|
+
continue
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _resolve_session(self, email: Email) -> TaskSession:
|
|
351
|
+
# Case A: Reply
|
|
352
|
+
if email.in_reply_to and email.in_reply_to in self.reply_mapping:
|
|
353
|
+
session_id = self.reply_mapping.pop(email.in_reply_to)
|
|
354
|
+
return self.sessions[session_id]
|
|
355
|
+
|
|
356
|
+
# Case B: New Task
|
|
357
|
+
session = TaskSession(
|
|
358
|
+
session_id=email.id,
|
|
359
|
+
original_sender=email.sender,
|
|
360
|
+
history=[],
|
|
361
|
+
status="RUNNING",
|
|
362
|
+
user_session_id=email.user_session_id
|
|
363
|
+
)
|
|
364
|
+
self.sessions[email.id] = session
|
|
365
|
+
return session
|
|
366
|
+
|
|
367
|
+
def _add_message_to_history(self, email: Email):
|
|
368
|
+
# 如果是新 Session,注入 System Prompt
|
|
369
|
+
session = self.current_session
|
|
370
|
+
if len(session.history) == 0:
|
|
371
|
+
session.history.append({"role": "system", "content": self.get_prompt()})
|
|
372
|
+
|
|
373
|
+
# 注入用户/同事的邮件
|
|
374
|
+
|
|
375
|
+
content = "[INCOMING MAIL]\n"
|
|
376
|
+
content+= f"{email}"
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
session.history.append({"role": "user", "content": content})
|
|
380
|
+
|
|
381
|
+
def _add_intention_feedback_to_history(self, intention, action_name, result=None):
|
|
382
|
+
session = self.current_session
|
|
383
|
+
# 把动作执行结果反馈给 LLM
|
|
384
|
+
msg_body = "[BODY FEEDBACK]\n"
|
|
385
|
+
if result:
|
|
386
|
+
|
|
387
|
+
msg_body +=f"Action: '{action_name}'\n"
|
|
388
|
+
msg_body +=textwrap.dedent(f"""Result:
|
|
389
|
+
{result}
|
|
390
|
+
""")
|
|
391
|
+
else:
|
|
392
|
+
msg_body +=f" '{action_name}'\n"
|
|
393
|
+
session.history.append({"role": "assistant", "content": intention})
|
|
394
|
+
session.history.append({"role": "user", "content": msg_body})
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _add_brain_intention_to_history(self, intention):
|
|
400
|
+
session = self.current_session
|
|
401
|
+
session.history.append({"role": "assistant", "content": intention})
|
|
402
|
+
|
|
403
|
+
def _add_question_to_brain(self, question):
|
|
404
|
+
session = self.current_session
|
|
405
|
+
|
|
406
|
+
session.history.append({"role": "user", "content": f"[INTERNAL QUERY]: {question}\n"})
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _get_llm_context(self, session: TaskSession) -> List[Dict]:
|
|
411
|
+
"""
|
|
412
|
+
[多态的关键]
|
|
413
|
+
Worker: 返回完整的 history。
|
|
414
|
+
Planner: 将重写此方法,返回 State + Latest Message。
|
|
415
|
+
"""
|
|
416
|
+
return session.history
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@register_action(
|
|
420
|
+
"休息一下,工作做完了,或者需要等待回信才能继续",
|
|
421
|
+
param_infos={
|
|
422
|
+
|
|
423
|
+
}
|
|
424
|
+
)
|
|
425
|
+
async def rest_n_wait(self):
|
|
426
|
+
# 什么都不做,直接返回
|
|
427
|
+
pass
|
|
428
|
+
|
|
429
|
+
@register_action(
|
|
430
|
+
"Take a break,让身体恢复一下",
|
|
431
|
+
param_infos={
|
|
432
|
+
|
|
433
|
+
}
|
|
434
|
+
)
|
|
435
|
+
async def take_a_break(self):
|
|
436
|
+
# 什么都不做,直接返回
|
|
437
|
+
await asyncio.sleep(60)
|
|
438
|
+
return "Return from Break"
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
@register_action(
|
|
444
|
+
"发邮件给同事,这是和其他人沟通的唯一方式",
|
|
445
|
+
param_infos={
|
|
446
|
+
"to": "收件人 (e.g. 'User', 'Planner', 'Coder')",
|
|
447
|
+
"body": "邮件内容",
|
|
448
|
+
"subject": "邮件主题 (可选,如果不填,系统会自动截取 body 的前20个字)"
|
|
449
|
+
}
|
|
450
|
+
)
|
|
451
|
+
async def send_email(self, to, body, subject=None):
|
|
452
|
+
# 构造邮件
|
|
453
|
+
# 如果 发给 session 的 original_sender,则 in_reply_to = session.session_id
|
|
454
|
+
# 如果 发给 其他同事,则检查 是不是 to 是不是 等于 self.last_email.sender
|
|
455
|
+
# 如果是,则 in_reply_to = self.last_email.id
|
|
456
|
+
# 否则,in_reply_to = session.session_id
|
|
457
|
+
session = self.current_session
|
|
458
|
+
last_email = self.last_received_email
|
|
459
|
+
in_reply_to = session.session_id
|
|
460
|
+
if to == last_email.sender:
|
|
461
|
+
in_reply_to = last_email.id
|
|
462
|
+
if not subject:
|
|
463
|
+
# 如果 body 很短,直接用 body 做 subject
|
|
464
|
+
# 如果 body 很长,截取前 20 个字 + ...
|
|
465
|
+
clean_body = body.strip().replace('\n', ' ')
|
|
466
|
+
subject = clean_body[:20] + "..." if len(clean_body) > 20 else clean_body
|
|
467
|
+
msg = Email(
|
|
468
|
+
sender=self.name,
|
|
469
|
+
recipient=to,
|
|
470
|
+
subject=subject,
|
|
471
|
+
body=body,
|
|
472
|
+
in_reply_to=in_reply_to,
|
|
473
|
+
user_session_id=session.user_session_id
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
await self.post_office.dispatch(msg)
|
|
479
|
+
self.reply_mapping[msg.id] = self.current_session.session_id
|
|
480
|
+
return f"Email sent to {to}"
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def get_snapshot(self):
|
|
488
|
+
"""
|
|
489
|
+
核心可观察性方法:返回 Agent 当前的完整状态快照
|
|
490
|
+
"""
|
|
491
|
+
# 1. 统计当前正在进行的会话
|
|
492
|
+
active_sessions_data = []
|
|
493
|
+
for sess_id, session in self.sessions.items():
|
|
494
|
+
active_sessions_data.append({
|
|
495
|
+
"session_id": sess_id,
|
|
496
|
+
"original_sender": session.original_sender,
|
|
497
|
+
"status": session.status,
|
|
498
|
+
"history_length": len(session.history),
|
|
499
|
+
# 这里甚至可以把最后一条对话内容截取出来展示
|
|
500
|
+
"last_message": session.history[-1]['content'][:50] + "..." if session.history else ""
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
# 2. 统计正在等待的外部请求
|
|
504
|
+
waiting_for = []
|
|
505
|
+
for msg_id, sess_id in self.reply_mapping.items():
|
|
506
|
+
waiting_for.append({
|
|
507
|
+
"waiting_msg_id": msg_id,
|
|
508
|
+
"belongs_to_session": sess_id
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
return {
|
|
512
|
+
"name": self.name,
|
|
513
|
+
"is_alive": True, # 这里可以加心跳检查
|
|
514
|
+
"inbox_depth": self.inbox.qsize(), # 还有多少信没读
|
|
515
|
+
"sessions_count": len(self.sessions),
|
|
516
|
+
"sessions": active_sessions_data, # 详细上下文
|
|
517
|
+
"waiting_map": waiting_for # 依赖关系
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
def dump_state(self) -> Dict:
|
|
521
|
+
"""生成当前 Agent 的完整快照"""
|
|
522
|
+
|
|
523
|
+
# 1. 提取收件箱里所有未读邮件
|
|
524
|
+
# Queue 没法直接序列化,得把东西取出来变成 List
|
|
525
|
+
inbox_content = []
|
|
526
|
+
while not self.inbox.empty():
|
|
527
|
+
email = self.inbox.get_nowait()
|
|
528
|
+
inbox_content.append(asdict(email)) # Email 也需要 to_dict
|
|
529
|
+
self.inbox.task_done()
|
|
530
|
+
|
|
531
|
+
# 2. 提取 Session
|
|
532
|
+
sessions_dump = {k: v.to_dict() for k, v in self.sessions.items()}
|
|
533
|
+
|
|
534
|
+
# 额外检查:如果保存时正在处理某封信,把它塞回 Inbox 的头部!
|
|
535
|
+
# 这样下次启动时,Agent 会重新处理这封信,相当于“断点重试”
|
|
536
|
+
if self.last_received_email and not self.last_email_processed:
|
|
537
|
+
inbox_content.insert(0, asdict(self.last_received_email))
|
|
538
|
+
|
|
539
|
+
return {
|
|
540
|
+
"name": self.name,
|
|
541
|
+
"inbox": inbox_content,
|
|
542
|
+
"sessions": sessions_dump,
|
|
543
|
+
"reply_mapping": self.reply_mapping,
|
|
544
|
+
# 如果是 Planner,它会有额外的 project_state,
|
|
545
|
+
# 可以通过 hasattr 检查或者子类覆盖 dump_state
|
|
546
|
+
"extra_state": getattr(self, "project_state", None)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
def load_state(self, snapshot: Dict):
|
|
550
|
+
"""从快照恢复现场"""
|
|
551
|
+
# 1. 恢复收件箱
|
|
552
|
+
for email_dict in snapshot["inbox"]:
|
|
553
|
+
# 假设 Email 类有 from_dict
|
|
554
|
+
email = Email(**email_dict)
|
|
555
|
+
self.inbox.put_nowait(email)
|
|
556
|
+
|
|
557
|
+
# 2. 恢复 Sessions
|
|
558
|
+
self.sessions = {
|
|
559
|
+
k: TaskSession.from_dict(v)
|
|
560
|
+
for k, v in snapshot["sessions"].items()
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
# 3. 恢复路由表
|
|
564
|
+
self.reply_mapping = snapshot["reply_mapping"]
|
|
565
|
+
|
|
566
|
+
# 4. 恢复额外状态 (Planner)
|
|
567
|
+
if snapshot.get("extra_state"):
|
|
568
|
+
self.project_state = snapshot["extra_state"]
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# agents/claude_coder.py
|
|
2
|
+
from ..agents.stateful import StatefulAgent
|
|
3
|
+
from ..agents.base import BaseAgent
|
|
4
|
+
from ..skills.terminal_ctrl import TerminalSkillMixin
|
|
5
|
+
from ..skills.project_management import ProjectManagementMixin
|
|
6
|
+
|
|
7
|
+
class CoderAgent(BaseAgent, TerminalSkillMixin,ProjectManagementMixin ):
|
|
8
|
+
def __init__(self, profile):
|
|
9
|
+
super().__init__(profile)
|
|
10
|
+
# Mixin 的方法会自动被 _scan_methods 扫描到
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from ..agents.base import BaseAgent
|
|
2
|
+
from ..skills.data_crawler import DigitalInternCrawlerMixin
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
class DataCrawler(BaseAgent, DigitalInternCrawlerMixin):
|
|
7
|
+
_custom_log_level = logging.DEBUG
|
|
8
|
+
def __init__(self, profile ):
|
|
9
|
+
super().__init__(profile)
|
|
10
|
+
self.browser_adapter = None
|
|
11
|
+
#self.sem = asyncio.Semaphore(5)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|