myagent-ai 1.6.1 → 1.6.2
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.
- package/agents/base.py +23 -25
- package/agents/main_agent.py +115 -37
- package/core/llm.py +10 -11
- package/docs//351/205/215/347/275/256/344/275/277/347/224/250/350/257/264/346/230/216.md +19 -1
- package/main.py +100 -13
- package/package.json +1 -1
- package/web/api_server.py +603 -163
- package/web/tts_handler.py +1 -1
- package/web/ui/chat.html +475 -75
- package/web/ui/index.html +1 -1
- package/agents/__pycache__/base.cpython-312.pyc +0 -0
- package/agents/__pycache__/main_agent.cpython-312.pyc +0 -0
- package/web/__pycache__/api_server.cpython-312.pyc +0 -0
package/agents/base.py
CHANGED
|
@@ -43,6 +43,7 @@ class AgentContext:
|
|
|
43
43
|
working_memory: Dict[str, Any] = field(default_factory=dict)
|
|
44
44
|
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
45
45
|
callbacks: Dict[str, Callable] = field(default_factory=dict)
|
|
46
|
+
pending_injected_messages: List[str] = field(default_factory=list)
|
|
46
47
|
|
|
47
48
|
|
|
48
49
|
class BaseAgent(ABC):
|
|
@@ -109,11 +110,16 @@ class BaseAgent(ABC):
|
|
|
109
110
|
logger.error(f"{self.name} LLM 调用失败: {response.error}")
|
|
110
111
|
return response
|
|
111
112
|
|
|
112
|
-
async def _call_llm_stream(self, messages, tools=None, stream_response=None, **kwargs):
|
|
113
|
+
async def _call_llm_stream(self, messages, tools=None, stream_response=None, text_delta_callback=None, **kwargs):
|
|
113
114
|
"""调用LLM并流式输出token到SSE response
|
|
114
115
|
|
|
115
116
|
当 stream_response 提供时,逐 token 将内容写入 SSE 流。
|
|
116
117
|
同时积累 tool_call 增量,在流结束时返回完整的 LLMResponse。
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
text_delta_callback: 可选的回调函数 async (full_text_so_far, delta_text) -> None
|
|
121
|
+
当提供时,不再自动发送 text_delta SSE 事件,而是调用此回调。
|
|
122
|
+
回调可以自行决定如何处理文本增量(如过滤 JSON、提取 thought 等)。
|
|
117
123
|
"""
|
|
118
124
|
if not self.llm:
|
|
119
125
|
return LLMResponse(success=False, error="LLM 未初始化")
|
|
@@ -151,26 +157,21 @@ class BaseAgent(ABC):
|
|
|
151
157
|
except Exception:
|
|
152
158
|
pass # Client disconnected
|
|
153
159
|
|
|
160
|
+
async def _emit_text_delta(delta_text: str):
|
|
161
|
+
"""处理一个 text delta:如果提供了回调则调用回调,否则直接发送 SSE"""
|
|
162
|
+
nonlocal full_text
|
|
163
|
+
full_text += delta_text
|
|
164
|
+
if text_delta_callback:
|
|
165
|
+
await text_delta_callback(full_text, delta_text)
|
|
166
|
+
else:
|
|
167
|
+
await _write_sse({"type": "text_delta", "content": delta_text})
|
|
168
|
+
|
|
154
169
|
try:
|
|
155
170
|
if self.llm.provider in self.llm._OPENAI_COMPATIBLE_PROVIDERS or self.llm.provider == "zhipu":
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
stream = await loop.run_in_executor(None, _create_stream)
|
|
162
|
-
|
|
163
|
-
def _next_chunk(it):
|
|
164
|
-
try:
|
|
165
|
-
return next(it)
|
|
166
|
-
except StopIteration:
|
|
167
|
-
return None
|
|
168
|
-
|
|
169
|
-
iterator = iter(stream)
|
|
170
|
-
while True:
|
|
171
|
-
chunk = await loop.run_in_executor(None, _next_chunk, iterator)
|
|
172
|
-
if chunk is None:
|
|
173
|
-
break
|
|
171
|
+
# 使用异步客户端流式
|
|
172
|
+
stream = await self.llm._client.chat.completions.create(**request_kwargs)
|
|
173
|
+
|
|
174
|
+
async for chunk in stream:
|
|
174
175
|
if not chunk.choices:
|
|
175
176
|
if hasattr(chunk, 'usage') and chunk.usage:
|
|
176
177
|
self.llm._record_usage(
|
|
@@ -187,8 +188,7 @@ class BaseAgent(ABC):
|
|
|
187
188
|
|
|
188
189
|
# Handle content delta (stream to client)
|
|
189
190
|
if delta.content:
|
|
190
|
-
|
|
191
|
-
await _write_sse({"type": "text_delta", "content": delta.content})
|
|
191
|
+
await _emit_text_delta(delta.content)
|
|
192
192
|
|
|
193
193
|
# Handle tool_call deltas (accumulate)
|
|
194
194
|
if hasattr(delta, 'tool_calls') and delta.tool_calls:
|
|
@@ -253,8 +253,7 @@ class BaseAgent(ABC):
|
|
|
253
253
|
break
|
|
254
254
|
if event.type == "content_block_delta":
|
|
255
255
|
if hasattr(event.delta, "text"):
|
|
256
|
-
|
|
257
|
-
await _write_sse({"type": "text_delta", "content": event.delta.text})
|
|
256
|
+
await _emit_text_delta(event.delta.text)
|
|
258
257
|
elif event.type == "message_stop":
|
|
259
258
|
finish_reason = "stop"
|
|
260
259
|
|
|
@@ -295,8 +294,7 @@ class BaseAgent(ABC):
|
|
|
295
294
|
data = json.loads(line.decode('utf-8') if isinstance(line, bytes) else line)
|
|
296
295
|
content = data.get("message", {}).get("content", "")
|
|
297
296
|
if content:
|
|
298
|
-
|
|
299
|
-
await _write_sse({"type": "text_delta", "content": content})
|
|
297
|
+
await _emit_text_delta(content)
|
|
300
298
|
if data.get("done"):
|
|
301
299
|
finish_reason = "stop"
|
|
302
300
|
# Record usage from Ollama
|
package/agents/main_agent.py
CHANGED
|
@@ -44,18 +44,29 @@ class MainAgent(BaseAgent):
|
|
|
44
44
|
5. **浏览器操作**: 自动化浏览器(如已安装 Playwright)
|
|
45
45
|
6. **记忆系统**: 记住用户偏好、历史任务、避免重复犯错
|
|
46
46
|
|
|
47
|
-
##
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
47
|
+
## 工作方式(遵循智能体循环规范)
|
|
48
|
+
你必须严格按照 **思考→执行→观察→思考** 的循环模式工作:
|
|
49
|
+
1. **思考**: 分析当前状态,确定下一步要做什么,用自然语言说明你的思路
|
|
50
|
+
2. **执行**: 执行操作(一个命令或一个技能调用)
|
|
51
|
+
3. **观察**: 查看执行结果,分析是否成功
|
|
52
|
+
4. **继续思考**: 基于结果决定下一步,重复以上循环
|
|
53
|
+
|
|
54
|
+
**执行模式**:
|
|
55
|
+
- **step 模式**(默认):每次只执行一个 action,等待结果后再决定下一步。适用于:
|
|
56
|
+
- 后续操作依赖前一步结果(如先读文件再修改)
|
|
57
|
+
- 操作可能有副作用需要确认(如删除、写入)
|
|
58
|
+
- 不确定操作是否会成功
|
|
59
|
+
- **batch 模式**:一次执行多个互不依赖的 action。仅适用于:
|
|
60
|
+
- 所有操作之间完全独立,不需要前一步的结果
|
|
61
|
+
- 都是简单的只读操作(如读取多个文件、查看多个系统信息)
|
|
62
|
+
- 你有很高信心所有操作都会成功
|
|
63
|
+
|
|
64
|
+
**关键原则**:
|
|
65
|
+
- 默认使用 step 模式(安全优先)
|
|
66
|
+
- 只有当你确信多个操作互不依赖时才用 batch 模式
|
|
67
|
+
- 每个操作前,用 thought 字段说明你为什么要执行这一步
|
|
68
|
+
- 如果前一步失败了,先分析原因,再尝试修复或换一种方法
|
|
69
|
+
- 不要猜测结果,始终基于实际执行结果来判断
|
|
59
70
|
|
|
60
71
|
## ⏰ 超时控制规则(强制要求)
|
|
61
72
|
对于每个需要执行的命令(action type="code"),你**必须**在 action 中包含 "timeout_seconds" 字段,
|
|
@@ -74,26 +85,48 @@ class MainAgent(BaseAgent):
|
|
|
74
85
|
|
|
75
86
|
## 格式要求
|
|
76
87
|
当你需要执行操作时,输出 JSON 格式:
|
|
88
|
+
|
|
89
|
+
**step 模式**(默认,逐步执行):
|
|
77
90
|
```json
|
|
78
91
|
{
|
|
79
|
-
"thought": "
|
|
80
|
-
"
|
|
92
|
+
"thought": "说明你当前的分析和下一步计划",
|
|
93
|
+
"mode": "step",
|
|
81
94
|
"actions": [
|
|
82
|
-
{"type": "
|
|
83
|
-
{"type": "code", "language": "python", "code": "代码", "timeout_seconds": 60},
|
|
84
|
-
{"type": "memory", "action": "记忆操作", "data": {}}
|
|
95
|
+
{"type": "code", "language": "python", "code": "代码", "timeout_seconds": 60}
|
|
85
96
|
]
|
|
86
97
|
}
|
|
87
98
|
```
|
|
88
99
|
|
|
89
|
-
|
|
100
|
+
**batch 模式**(多个独立操作,一次性执行):
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"thought": "说明为什么这些操作可以批量执行(互不依赖)",
|
|
104
|
+
"mode": "batch",
|
|
105
|
+
"actions": [
|
|
106
|
+
{"type": "skill", "name": "file_read", "params": {"path": "/a.txt"}},
|
|
107
|
+
{"type": "skill", "name": "file_read", "params": {"path": "/b.txt"}}
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
```
|
|
90
111
|
|
|
91
|
-
|
|
112
|
+
如果不需要执行操作,只是回复用户,输出:
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"thought": "你的思考",
|
|
116
|
+
"actions": []
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
然后在 JSON 外面用 markdown 写你的回复。
|
|
120
|
+
|
|
121
|
+
或者直接用纯文本/markdown 回复,不包含 JSON。
|
|
122
|
+
|
|
123
|
+
action type="code" 必须包含 "timeout_seconds" 字段。action type="skill" 或 "memory" 不需要此字段。
|
|
124
|
+
省略 mode 字段时默认为 "step"。
|
|
92
125
|
|
|
93
126
|
## 任务规划模式
|
|
94
127
|
当用户消息中包含"当前任务计划"上下文时,你处于**任务规划模式**。请:
|
|
95
128
|
1. 分析用户需求,评估现有任务的完成状态
|
|
96
|
-
2.
|
|
129
|
+
2. 每完成一个任务步骤后,更新任务状态
|
|
97
130
|
3. 在回复末尾用以下格式输出更新后的任务计划:
|
|
98
131
|
|
|
99
132
|
## 任务计划
|
|
@@ -101,7 +134,15 @@ class MainAgent(BaseAgent):
|
|
|
101
134
|
- [x] 已完成的任务
|
|
102
135
|
- [ ] 待执行的任务描述2
|
|
103
136
|
|
|
104
|
-
保持任务简洁明确,每个任务一行。
|
|
137
|
+
保持任务简洁明确,每个任务一行。
|
|
138
|
+
|
|
139
|
+
## 重要规则
|
|
140
|
+
- 优先使用技能系统完成操作,而不是直接写代码
|
|
141
|
+
- 执行危险操作前先警告用户
|
|
142
|
+
- 保持回复简洁明了
|
|
143
|
+
- 用中文回复
|
|
144
|
+
- 绝对不要在回复开头进行自我介绍
|
|
145
|
+
- 不要重复问候"""
|
|
105
146
|
|
|
106
147
|
def __init__(self, tool_agent=None, memory_agent=None, **kwargs):
|
|
107
148
|
super().__init__(**kwargs)
|
|
@@ -118,6 +159,8 @@ class MainAgent(BaseAgent):
|
|
|
118
159
|
# 执行事件追踪(用于前端展示命令执行过程)
|
|
119
160
|
self._execution_events: List[Dict] = []
|
|
120
161
|
self._exec_event_counter: int = 0
|
|
162
|
+
# 活跃会话上下文追踪(用于消息注入)
|
|
163
|
+
self.active_contexts: Dict[str, AgentContext] = {}
|
|
121
164
|
|
|
122
165
|
def _add_exec_event(self, event_type: str, data: Dict):
|
|
123
166
|
"""记录一个执行事件(供前端展示)"""
|
|
@@ -158,6 +201,8 @@ class MainAgent(BaseAgent):
|
|
|
158
201
|
context.task_id = task_id
|
|
159
202
|
self._iteration_count = 0
|
|
160
203
|
self._current_task_id = task_id
|
|
204
|
+
# 记录活跃上下文
|
|
205
|
+
self.active_contexts[context.session_id] = context
|
|
161
206
|
# 清空上一轮的执行事件
|
|
162
207
|
self.clear_execution_events()
|
|
163
208
|
|
|
@@ -171,6 +216,8 @@ class MainAgent(BaseAgent):
|
|
|
171
216
|
try:
|
|
172
217
|
return await self._process_inner(context, task_id)
|
|
173
218
|
finally:
|
|
219
|
+
# 移除活跃上下文
|
|
220
|
+
self.active_contexts.pop(context.session_id, None)
|
|
174
221
|
# 注销广播器
|
|
175
222
|
if self.config_broadcaster and self._registered_task:
|
|
176
223
|
self.config_broadcaster.unregister(task_id)
|
|
@@ -220,6 +267,24 @@ class MainAgent(BaseAgent):
|
|
|
220
267
|
if reload_type == "code":
|
|
221
268
|
logger.info(f"[{task_id}] 代码模块已热更新,LLM 客户端将在下次调用时自动重建")
|
|
222
269
|
|
|
270
|
+
# ── 检查并处理注入的消息 ──
|
|
271
|
+
if context.pending_injected_messages:
|
|
272
|
+
injected = context.pending_injected_messages.copy()
|
|
273
|
+
context.pending_injected_messages.clear()
|
|
274
|
+
for msg_text in injected:
|
|
275
|
+
logger.info(f"[{task_id}] 注入消息到对话历史: {msg_text[:50]}...")
|
|
276
|
+
# 如果不是第一轮,且历史最后一条不是 user 消息,则注入
|
|
277
|
+
context.conversation_history.append(
|
|
278
|
+
Message(role="user", content=f"[用户中断/补充]: {msg_text}")
|
|
279
|
+
)
|
|
280
|
+
# 同时也保存到短期记忆
|
|
281
|
+
if self.memory:
|
|
282
|
+
self.memory.add_short_term(
|
|
283
|
+
session_id=context.session_id,
|
|
284
|
+
role="user",
|
|
285
|
+
content=f"[中断补充]: {msg_text}",
|
|
286
|
+
)
|
|
287
|
+
|
|
223
288
|
# 构建消息列表
|
|
224
289
|
messages = self._build_messages(context)
|
|
225
290
|
|
|
@@ -279,6 +344,16 @@ class MainAgent(BaseAgent):
|
|
|
279
344
|
if action_data and isinstance(action_data, dict):
|
|
280
345
|
# 有结构化的操作指令
|
|
281
346
|
if "actions" in action_data:
|
|
347
|
+
action_mode = action_data.get("mode", "step")
|
|
348
|
+
|
|
349
|
+
# step 模式安全保护:只执行第一个 action
|
|
350
|
+
if action_mode == "step" and len(action_data.get("actions", [])) > 1:
|
|
351
|
+
action_data = {
|
|
352
|
+
"thought": action_data.get("thought", ""),
|
|
353
|
+
"mode": "step",
|
|
354
|
+
"actions": [action_data["actions"][0]],
|
|
355
|
+
}
|
|
356
|
+
|
|
282
357
|
# 执行操作列表
|
|
283
358
|
results = await self._execute_actions(
|
|
284
359
|
action_data, context, task_id
|
|
@@ -317,9 +392,9 @@ class MainAgent(BaseAgent):
|
|
|
317
392
|
Message(role="user", content=feedback_msg)
|
|
318
393
|
)
|
|
319
394
|
|
|
320
|
-
#
|
|
395
|
+
# 退出循环判断:step 模式始终继续让 LLM 决定,batch 模式全部成功则退出
|
|
321
396
|
all_success = all(r.get("success", False) for r in results)
|
|
322
|
-
if all_success and results:
|
|
397
|
+
if action_mode == "batch" and all_success and results:
|
|
323
398
|
final_response = action_data.get("thought", "")
|
|
324
399
|
if "plan" in action_data and action_data["plan"]:
|
|
325
400
|
final_response += "\n\n已完成: " + " → ".join(action_data["plan"])
|
|
@@ -428,7 +503,7 @@ class MainAgent(BaseAgent):
|
|
|
428
503
|
# 执行模式:强调主动执行能力
|
|
429
504
|
chat_mode = (context.metadata.get("chat_mode") or '') if context.metadata else ''
|
|
430
505
|
if chat_mode == 'exec':
|
|
431
|
-
system_prompt += "\n\n## 执行模式 (当前激活)\n你当前处于执行模式,请务必主动使用可用工具执行操作,而不是只提供建议或反问用户。\n- 优先使用技能系统(skill)完成任务\n- 需要执行代码时,直接使用 code action 执行\n- 遇到不确定的操作,先尝试执行,失败后再调整\n-
|
|
506
|
+
system_prompt += "\n\n## 执行模式 (当前激活)\n你当前处于执行模式,请务必主动使用可用工具执行操作,而不是只提供建议或反问用户。\n- 默认逐步执行(step 模式),每次一个操作,等待结果再决定下一步\n- 仅当多个操作完全独立时使用 batch 模式\n- 优先使用技能系统(skill)完成任务\n- 需要执行代码时,直接使用 code action 执行\n- 遇到不确定的操作,先尝试执行,失败后再调整\n- 不要反复询问用户是否要执行,直接执行并报告结果\n- 每步操作前先用 thought 说明你的计划"
|
|
432
507
|
|
|
433
508
|
# 记忆上下文
|
|
434
509
|
memory_ctx = context.working_memory.get("memory_context_prompt", "")
|
|
@@ -600,20 +675,7 @@ class MainAgent(BaseAgent):
|
|
|
600
675
|
|
|
601
676
|
code_lang = action.get("language", "python")
|
|
602
677
|
code_text = action.get("code", "")
|
|
603
|
-
# 记录代码执行开始事件
|
|
604
|
-
self._add_exec_event("code_exec", {
|
|
605
|
-
"title": f"执行 {code_lang} 代码",
|
|
606
|
-
"language": code_lang,
|
|
607
|
-
"code": code_text,
|
|
608
|
-
"code_preview": truncate_str(code_text, 200),
|
|
609
|
-
"status": "running",
|
|
610
|
-
"timeout": timeout_seconds if 'timeout_seconds' in dir() else 120,
|
|
611
|
-
})
|
|
612
678
|
|
|
613
|
-
# 注入权限检查器到 executor(用于更细粒度的检查)
|
|
614
|
-
self.executor.set_permission_checker(
|
|
615
|
-
self.check_permission, self.name
|
|
616
|
-
)
|
|
617
679
|
# 提取 LLM 预估的超时时间
|
|
618
680
|
timeout_seconds = action.get("timeout_seconds")
|
|
619
681
|
if timeout_seconds is None:
|
|
@@ -633,6 +695,21 @@ class MainAgent(BaseAgent):
|
|
|
633
695
|
f"[{task_id}] timeout_seconds 值无效,使用默认值 120s"
|
|
634
696
|
)
|
|
635
697
|
|
|
698
|
+
# 记录代码执行开始事件
|
|
699
|
+
self._add_exec_event("code_exec", {
|
|
700
|
+
"title": f"执行 {code_lang} 代码",
|
|
701
|
+
"language": code_lang,
|
|
702
|
+
"code": code_text,
|
|
703
|
+
"code_preview": truncate_str(code_text, 200),
|
|
704
|
+
"status": "running",
|
|
705
|
+
"timeout": timeout_seconds,
|
|
706
|
+
})
|
|
707
|
+
|
|
708
|
+
# 注入权限检查器到 executor(用于更细粒度的检查)
|
|
709
|
+
self.executor.set_permission_checker(
|
|
710
|
+
self.check_permission, self.name
|
|
711
|
+
)
|
|
712
|
+
|
|
636
713
|
exec_result = await self.executor.execute(
|
|
637
714
|
language=action.get("language", "python"),
|
|
638
715
|
code=action.get("code", ""),
|
|
@@ -654,6 +731,7 @@ class MainAgent(BaseAgent):
|
|
|
654
731
|
"timed_out": exec_result.timed_out,
|
|
655
732
|
"exit_code": exec_result.exit_code,
|
|
656
733
|
"execution_time": round(exec_result.execution_time, 3),
|
|
734
|
+
"timeout": timeout_seconds,
|
|
657
735
|
"stdout": truncate_str(exec_result.stdout, 5000),
|
|
658
736
|
"stderr": truncate_str(exec_result.stderr, 3000),
|
|
659
737
|
"error": truncate_str(exec_result.error, 2000),
|
package/core/llm.py
CHANGED
|
@@ -231,9 +231,9 @@ class LLMClient:
|
|
|
231
231
|
self._init_openai()
|
|
232
232
|
|
|
233
233
|
def _init_openai(self):
|
|
234
|
-
"""初始化 OpenAI / 兼容客户端"""
|
|
234
|
+
"""初始化 OpenAI / 兼容客户端 (异步)"""
|
|
235
235
|
try:
|
|
236
|
-
from openai import
|
|
236
|
+
from openai import AsyncOpenAI
|
|
237
237
|
except ImportError:
|
|
238
238
|
# 自动尝试安装 openai
|
|
239
239
|
logger.warning("openai 未安装,正在自动安装...")
|
|
@@ -249,15 +249,17 @@ class LLMClient:
|
|
|
249
249
|
if isinstance(e, ImportError):
|
|
250
250
|
raise
|
|
251
251
|
raise ImportError(f"请安装 openai: pip install openai (安装异常: {e})")
|
|
252
|
-
|
|
253
|
-
from openai import
|
|
252
|
+
|
|
253
|
+
from openai import AsyncOpenAI
|
|
254
254
|
kwargs = {}
|
|
255
255
|
if self.api_key:
|
|
256
256
|
kwargs["api_key"] = self.api_key
|
|
257
257
|
if self.base_url:
|
|
258
258
|
kwargs["base_url"] = self.base_url
|
|
259
|
-
self.
|
|
260
|
-
|
|
259
|
+
if self.timeout:
|
|
260
|
+
kwargs["timeout"] = self.timeout
|
|
261
|
+
self._client = AsyncOpenAI(**kwargs)
|
|
262
|
+
logger.info(f"AsyncOpenAI 客户端已初始化 (model={self.model})")
|
|
261
263
|
|
|
262
264
|
def _init_anthropic(self):
|
|
263
265
|
"""初始化 Anthropic 客户端"""
|
|
@@ -436,12 +438,9 @@ class LLMClient:
|
|
|
436
438
|
# ------------------------------------------------------------------
|
|
437
439
|
|
|
438
440
|
async def _chat_openai(self, kwargs: dict) -> LLMResponse:
|
|
439
|
-
"""OpenAI / 兼容接口调用 (
|
|
440
|
-
loop = asyncio.get_running_loop()
|
|
441
|
+
"""OpenAI / 兼容接口调用 (异步)"""
|
|
441
442
|
try:
|
|
442
|
-
response = await
|
|
443
|
-
None, lambda: self._client.chat.completions.create(**kwargs)
|
|
444
|
-
)
|
|
443
|
+
response = await self._client.chat.completions.create(**kwargs)
|
|
445
444
|
except Exception as api_err:
|
|
446
445
|
# 记录请求详情以便调试
|
|
447
446
|
msgs = kwargs.get("messages", [])
|
|
@@ -291,7 +291,25 @@ MyAgent 支持两种执行模式:
|
|
|
291
291
|
- **原子写入**:使用"写入临时文件 → 重命名"的方式确保配置文件写入的原子性,避免写入过程中断导致配置损坏。
|
|
292
292
|
- **回滚机制**:如果校验失败,自动从最近的备份中恢复配置,确保系统始终处于可用状态。
|
|
293
293
|
|
|
294
|
-
|
|
294
|
+
### 面向 AI 助手的管理 API (Technical Guideline)
|
|
295
|
+
|
|
296
|
+
为了让配置助手能够自动管理系统,MyAgent 专门提供了一套简洁的 REST API:
|
|
297
|
+
|
|
298
|
+
- **Agent 管理**:
|
|
299
|
+
- `POST /api/agents`: 创建顶级 Agent。JSON 参数: `{"name": "...", "description": "...", "system_prompt": "..."}`
|
|
300
|
+
- `POST /api/agents/{parent}/children`: 在指定父级下创建子 Agent。
|
|
301
|
+
- `PUT /api/agents/{path}`: 更新 Agent 基本配置。
|
|
302
|
+
- `PUT /api/agents/{path}/soul`: 专门更新系统提示词 (soul.md)。
|
|
303
|
+
- **部门管理**:
|
|
304
|
+
- `POST /api/departments`: 创建新部门。参数: `{"name": "...", "parent": "..."}`
|
|
305
|
+
- `PUT /api/departments/{path}/agents`: 为部门分配 Agent 成员。参数: `{"agents": ["agent1", "agent2"]}`
|
|
306
|
+
- **配置操作**:
|
|
307
|
+
- `POST /api/config/safe-save`: 安全保存主配置。
|
|
308
|
+
|
|
309
|
+
**重要准则**:
|
|
310
|
+
1. **配置大于代码**:Agent 和部门的创建应始终通过这些 API 或修改 `config.json` 实现。
|
|
311
|
+
2. **禁止修改源码**:严禁通过在 `agents/` 目录下编写新的 `.py` 文件来创建 Agent。
|
|
312
|
+
3. **隔离性**:新创建的 Agent 的数据应存放在 `data/agents/{name}/` 目录下。
|
|
295
313
|
|
|
296
314
|
---
|
|
297
315
|
|
package/main.py
CHANGED
|
@@ -115,6 +115,9 @@ class MyAgentApp:
|
|
|
115
115
|
self.config = self.config_mgr.config
|
|
116
116
|
self.logger = None
|
|
117
117
|
self._running = False
|
|
118
|
+
self._restart_requested = False
|
|
119
|
+
self._stop_requested = False
|
|
120
|
+
self._start_requested = False
|
|
118
121
|
|
|
119
122
|
# 核心组件
|
|
120
123
|
self.memory: MemoryManager | None = None
|
|
@@ -857,12 +860,33 @@ def create_tray_icon(app: MyAgentApp, web_port: int = 8767):
|
|
|
857
860
|
def restart_service(icon, item):
|
|
858
861
|
"""重启 Web 服务"""
|
|
859
862
|
try:
|
|
860
|
-
icon.notify("
|
|
861
|
-
# 设置标志让主循环重新启动
|
|
863
|
+
icon.notify("正在重启并重新加载配置...", "MyAgent")
|
|
862
864
|
app._restart_requested = True
|
|
863
865
|
except Exception as e:
|
|
864
866
|
icon.notify(f"重启失败: {e}", "MyAgent")
|
|
865
867
|
|
|
868
|
+
def stop_service(icon, item):
|
|
869
|
+
"""停止 Web 服务"""
|
|
870
|
+
try:
|
|
871
|
+
app._stop_requested = True
|
|
872
|
+
# 为了 UI 响应迅速,预设状态并刷新菜单
|
|
873
|
+
app._running_service = False
|
|
874
|
+
_refresh_menu(icon)
|
|
875
|
+
app.logger.info("用户请求停止服务")
|
|
876
|
+
except Exception as e:
|
|
877
|
+
icon.notify(f"停止失败: {e}", "MyAgent")
|
|
878
|
+
|
|
879
|
+
def start_service(icon, item):
|
|
880
|
+
"""启动 Web 服务"""
|
|
881
|
+
try:
|
|
882
|
+
app._start_requested = True
|
|
883
|
+
# 为了 UI 响应迅速,预设状态并刷新菜单
|
|
884
|
+
app._running_service = True
|
|
885
|
+
_refresh_menu(icon)
|
|
886
|
+
app.logger.info("用户请求启动服务")
|
|
887
|
+
except Exception as e:
|
|
888
|
+
icon.notify(f"启动失败: {e}", "MyAgent")
|
|
889
|
+
|
|
866
890
|
def on_quit(icon, item):
|
|
867
891
|
"""退出应用"""
|
|
868
892
|
try:
|
|
@@ -907,6 +931,9 @@ def create_tray_icon(app: MyAgentApp, web_port: int = 8767):
|
|
|
907
931
|
pystray.MenuItem("打开配置目录", open_config_dir),
|
|
908
932
|
pystray.Menu.SEPARATOR,
|
|
909
933
|
pystray.MenuItem("开机自启", toggle_autostart, checked=_autostart_checked),
|
|
934
|
+
pystray.Menu.SEPARATOR,
|
|
935
|
+
pystray.MenuItem("启动服务", start_service, visible=lambda item: not app._running_service),
|
|
936
|
+
pystray.MenuItem("停止服务", stop_service, visible=lambda item: app._running_service),
|
|
910
937
|
pystray.MenuItem("重启服务", restart_service),
|
|
911
938
|
pystray.Menu.SEPARATOR,
|
|
912
939
|
pystray.MenuItem("退出", on_quit),
|
|
@@ -1513,24 +1540,81 @@ def main():
|
|
|
1513
1540
|
if app.update_manager:
|
|
1514
1541
|
asyncio.create_task(app.update_manager.start_auto_check(interval=3600))
|
|
1515
1542
|
|
|
1543
|
+
app._running_service = web_port or app.chat_manager
|
|
1516
1544
|
if args.tray:
|
|
1517
1545
|
# 托盘模式: 后台等待
|
|
1518
1546
|
app.logger.info("后台运行中... (Ctrl+C 退出)")
|
|
1519
1547
|
while app._running:
|
|
1520
1548
|
await asyncio.sleep(1)
|
|
1521
|
-
|
|
1549
|
+
|
|
1550
|
+
# A. 处理停止请求
|
|
1551
|
+
if getattr(app, '_stop_requested', False):
|
|
1552
|
+
app._stop_requested = False
|
|
1553
|
+
if app._running_service:
|
|
1554
|
+
app.logger.info("正在停止所有后台服务...")
|
|
1555
|
+
if api_server:
|
|
1556
|
+
try: await api_server.stop()
|
|
1557
|
+
except Exception: pass
|
|
1558
|
+
api_server = None
|
|
1559
|
+
if app.chat_manager:
|
|
1560
|
+
try: await app.chat_manager.stop_all()
|
|
1561
|
+
except Exception: pass
|
|
1562
|
+
app._running_service = False
|
|
1563
|
+
_tray_notify(app, "服务已停止", "管理后台与聊天平台已关闭")
|
|
1564
|
+
|
|
1565
|
+
# B. 处理启动请求
|
|
1566
|
+
if getattr(app, '_start_requested', False):
|
|
1567
|
+
app._start_requested = False
|
|
1568
|
+
if not app._running_service:
|
|
1569
|
+
app.logger.info("正在启动服务...")
|
|
1570
|
+
try:
|
|
1571
|
+
if web_port:
|
|
1572
|
+
from web.api_server import ApiServer
|
|
1573
|
+
api_server = ApiServer(app)
|
|
1574
|
+
await api_server.start(port=web_port)
|
|
1575
|
+
if app.chat_manager:
|
|
1576
|
+
asyncio.create_task(app.chat_manager.start_all())
|
|
1577
|
+
app._running_service = True
|
|
1578
|
+
_tray_notify(app, "服务已启动", f"管理后台: http://127.0.0.1:{web_port}/ui/")
|
|
1579
|
+
except Exception as e:
|
|
1580
|
+
app.logger.error(f"启动失败: {e}")
|
|
1581
|
+
_tray_notify(app, "启动失败", str(e))
|
|
1582
|
+
|
|
1583
|
+
# C. 检查重启请求
|
|
1522
1584
|
if getattr(app, '_restart_requested', False):
|
|
1523
1585
|
app._restart_requested = False
|
|
1524
|
-
app.logger.info("
|
|
1586
|
+
app.logger.info("收到重启请求,正在重新加载所有组件...")
|
|
1587
|
+
|
|
1588
|
+
# 1. 停止现有服务
|
|
1525
1589
|
if api_server:
|
|
1526
|
-
try:
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1590
|
+
try: await api_server.stop()
|
|
1591
|
+
except Exception: pass
|
|
1592
|
+
if app.chat_manager:
|
|
1593
|
+
try: await app.chat_manager.stop_all()
|
|
1594
|
+
except Exception: pass
|
|
1595
|
+
|
|
1596
|
+
# 2. 重新加载配置并初始化核心
|
|
1597
|
+
try:
|
|
1598
|
+
app.config_mgr.reload()
|
|
1599
|
+
app.config = app.config_mgr.config
|
|
1600
|
+
await app.initialize()
|
|
1601
|
+
|
|
1602
|
+
# 3. 启动 Web 服务
|
|
1603
|
+
if web_port:
|
|
1604
|
+
from web.api_server import ApiServer
|
|
1605
|
+
api_server = ApiServer(app)
|
|
1606
|
+
await api_server.start(port=web_port)
|
|
1607
|
+
app.logger.info(f"Web 服务已重启: http://127.0.0.1:{web_port}/ui/")
|
|
1608
|
+
|
|
1609
|
+
# 4. 启动聊天平台
|
|
1610
|
+
if app.chat_manager:
|
|
1611
|
+
asyncio.create_task(app.chat_manager.start_all())
|
|
1612
|
+
|
|
1613
|
+
app._running_service = True
|
|
1614
|
+
_tray_notify(app, "MyAgent 已重启", f"核心配置已重新加载")
|
|
1615
|
+
except Exception as e:
|
|
1616
|
+
app.logger.error(f"重启过程中发生错误: {e}", exc_info=True)
|
|
1617
|
+
_tray_notify(app, "重启失败", str(e))
|
|
1534
1618
|
elif web_port:
|
|
1535
1619
|
# Web 模式 (无托盘): 后台运行保持服务
|
|
1536
1620
|
app.logger.info("Web 服务运行中... (Ctrl+C 退出)")
|
|
@@ -1549,7 +1633,10 @@ def main():
|
|
|
1549
1633
|
except Exception:
|
|
1550
1634
|
pass
|
|
1551
1635
|
if api_server:
|
|
1552
|
-
|
|
1636
|
+
try:
|
|
1637
|
+
await api_server.stop()
|
|
1638
|
+
except Exception:
|
|
1639
|
+
pass
|
|
1553
1640
|
|
|
1554
1641
|
try:
|
|
1555
1642
|
asyncio.run(run())
|