myagent-ai 1.6.0 → 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 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,8 +110,17 @@ 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
- """调用LLM并流式输出token到SSE response"""
113
+ async def _call_llm_stream(self, messages, tools=None, stream_response=None, text_delta_callback=None, **kwargs):
114
+ """调用LLM并流式输出token到SSE response
115
+
116
+ 当 stream_response 提供时,逐 token 将内容写入 SSE 流。
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 等)。
123
+ """
114
124
  if not self.llm:
115
125
  return LLMResponse(success=False, error="LLM 未初始化")
116
126
 
@@ -118,6 +128,8 @@ class BaseAgent(ABC):
118
128
  if not stream_response:
119
129
  return await self._call_llm(messages, tools=tools, **kwargs)
120
130
 
131
+ import asyncio as _asyncio
132
+
121
133
  self.llm._ensure_client()
122
134
  msg_dicts = [m.to_dict() if hasattr(m, 'to_dict') else m for m in messages]
123
135
  request_kwargs = {
@@ -125,27 +137,201 @@ class BaseAgent(ABC):
125
137
  "messages": msg_dicts,
126
138
  "temperature": self.llm.temperature,
127
139
  "max_tokens": self.llm.max_tokens,
128
- "stream": False, # We'll handle streaming ourselves
140
+ "stream": True,
129
141
  }
130
142
  if tools:
131
143
  request_kwargs["tools"] = tools
132
144
  request_kwargs["tool_choice"] = "auto"
133
145
  request_kwargs.update(kwargs)
134
146
 
147
+ full_text = ""
148
+ tool_calls_acc: Dict[int, Dict] = {} # index -> {id, name, arguments_str}
149
+ finish_reason = ""
150
+
151
+ async def _write_sse(data: dict):
152
+ """将一个事件写入 SSE 流,忽略客户端断开错误"""
153
+ try:
154
+ await stream_response.write(
155
+ ("data: " + json.dumps(data, ensure_ascii=False) + "\n\n").encode()
156
+ )
157
+ except Exception:
158
+ pass # Client disconnected
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
+
135
169
  try:
136
170
  if self.llm.provider in self.llm._OPENAI_COMPATIBLE_PROVIDERS or self.llm.provider == "zhipu":
137
- response = await self.llm._run_with_retry(self.llm._chat_openai, request_kwargs)
171
+ # 使用异步客户端流式
172
+ stream = await self.llm._client.chat.completions.create(**request_kwargs)
173
+
174
+ async for chunk in stream:
175
+ if not chunk.choices:
176
+ if hasattr(chunk, 'usage') and chunk.usage:
177
+ self.llm._record_usage(
178
+ {"prompt_tokens": chunk.usage.prompt_tokens,
179
+ "completion_tokens": chunk.usage.completion_tokens,
180
+ "total_tokens": chunk.usage.total_tokens},
181
+ request_kwargs["model"],
182
+ )
183
+ continue
184
+
185
+ delta = chunk.choices[0].delta
186
+ if chunk.choices[0].finish_reason:
187
+ finish_reason = chunk.choices[0].finish_reason
188
+
189
+ # Handle content delta (stream to client)
190
+ if delta.content:
191
+ await _emit_text_delta(delta.content)
192
+
193
+ # Handle tool_call deltas (accumulate)
194
+ if hasattr(delta, 'tool_calls') and delta.tool_calls:
195
+ for tc_delta in delta.tool_calls:
196
+ idx = tc_delta.index if hasattr(tc_delta, 'index') else 0
197
+ if idx not in tool_calls_acc:
198
+ tool_calls_acc[idx] = {"id": "", "name": "", "arguments": ""}
199
+ if tc_delta.id:
200
+ tool_calls_acc[idx]["id"] = tc_delta.id
201
+ if hasattr(tc_delta, 'function') and tc_delta.function:
202
+ if tc_delta.function.name:
203
+ tool_calls_acc[idx]["name"] = tc_delta.function.name
204
+ if tc_delta.function.arguments:
205
+ tool_calls_acc[idx]["arguments"] += tc_delta.function.arguments
206
+
207
+ # Handle usage in final chunk
208
+ if hasattr(chunk, 'usage') and chunk.usage:
209
+ self.llm._record_usage(
210
+ {"prompt_tokens": chunk.usage.prompt_tokens,
211
+ "completion_tokens": chunk.usage.completion_tokens,
212
+ "total_tokens": chunk.usage.total_tokens},
213
+ request_kwargs["model"],
214
+ )
215
+
138
216
  elif self.llm.provider == "anthropic":
139
- response = await self.llm._run_with_retry(self.llm._chat_anthropic, messages, request_kwargs)
217
+ loop = _asyncio.get_running_loop()
218
+
219
+ system_msg = ""
220
+ anth_messages = []
221
+ for m in messages:
222
+ role = m.role if hasattr(m, 'role') else m.get("role", "user")
223
+ content = m.content if hasattr(m, 'content') else m.get("content", "")
224
+ if role == "system":
225
+ system_msg = content
226
+ continue
227
+ anth_messages.append({"role": role, "content": content})
228
+
229
+ create_kwargs = {
230
+ "model": self.llm.model,
231
+ "messages": anth_messages,
232
+ "max_tokens": self.llm.max_tokens,
233
+ "stream": True,
234
+ }
235
+ if system_msg:
236
+ create_kwargs["system"] = system_msg
237
+
238
+ def _create_stream():
239
+ return self.llm._client.messages.create(**create_kwargs)
240
+
241
+ stream = await loop.run_in_executor(None, _create_stream)
242
+
243
+ def _next_event(it):
244
+ try:
245
+ return next(it)
246
+ except StopIteration:
247
+ return None
248
+
249
+ iterator = iter(stream)
250
+ while True:
251
+ event = await loop.run_in_executor(None, _next_event, iterator)
252
+ if event is None:
253
+ break
254
+ if event.type == "content_block_delta":
255
+ if hasattr(event.delta, "text"):
256
+ await _emit_text_delta(event.delta.text)
257
+ elif event.type == "message_stop":
258
+ finish_reason = "stop"
259
+
140
260
  elif self.llm.provider == "ollama":
141
- response = await self.llm._run_with_retry(self.llm._chat_ollama, request_kwargs)
261
+ loop = _asyncio.get_running_loop()
262
+ import requests as req_lib
263
+
264
+ url = f"{self.llm.base_url}/api/chat"
265
+ payload = {
266
+ "model": self.llm.model,
267
+ "messages": msg_dicts,
268
+ "stream": True,
269
+ "options": {
270
+ "temperature": self.llm.temperature,
271
+ "num_predict": self.llm.max_tokens,
272
+ },
273
+ }
274
+
275
+ def _request():
276
+ r = req_lib.post(url, json=payload, stream=True, timeout=self.llm.timeout)
277
+ r.raise_for_status()
278
+ return r.iter_lines()
279
+
280
+ lines_iter = await loop.run_in_executor(None, _request)
281
+
282
+ def _next_line(it):
283
+ try:
284
+ return next(it)
285
+ except StopIteration:
286
+ return None
287
+
288
+ iterator = iter(lines_iter)
289
+ while True:
290
+ line = await loop.run_in_executor(None, _next_line, iterator)
291
+ if line is None:
292
+ break
293
+ try:
294
+ data = json.loads(line.decode('utf-8') if isinstance(line, bytes) else line)
295
+ content = data.get("message", {}).get("content", "")
296
+ if content:
297
+ await _emit_text_delta(content)
298
+ if data.get("done"):
299
+ finish_reason = "stop"
300
+ # Record usage from Ollama
301
+ usage = data.get("prompt_eval_count") or data.get("eval_count")
302
+ if data.get("prompt_eval_count"):
303
+ self.llm._record_usage(
304
+ {"prompt_tokens": data.get("prompt_eval_count", 0),
305
+ "completion_tokens": data.get("eval_count", 0),
306
+ "total_tokens": data.get("prompt_eval_count", 0) + data.get("eval_count", 0)},
307
+ self.llm.model,
308
+ )
309
+ except Exception:
310
+ continue
142
311
  else:
143
- return LLMResponse(success=False, error="未知提供商")
144
-
145
- if response.usage:
146
- self.llm._record_usage(response.usage, response.model)
147
-
148
- return response
312
+ return LLMResponse(success=False, error="未知提供商,不支持流式")
313
+
314
+ # Build tool_calls list from accumulated deltas
315
+ final_tool_calls = []
316
+ for idx in sorted(tool_calls_acc.keys()):
317
+ tc = tool_calls_acc[idx]
318
+ try:
319
+ args = json.loads(tc["arguments"]) if tc["arguments"] else {}
320
+ except json.JSONDecodeError:
321
+ args = {}
322
+ final_tool_calls.append({
323
+ "id": tc["id"],
324
+ "name": tc["name"],
325
+ "arguments": args,
326
+ })
327
+
328
+ return LLMResponse(
329
+ success=True,
330
+ content=full_text,
331
+ tool_calls=final_tool_calls,
332
+ finish_reason=finish_reason,
333
+ model=request_kwargs.get("model", self.llm.model),
334
+ )
149
335
  except Exception as e:
150
336
  logger.error(f"LLM 流式调用失败: {e}")
151
337
  return LLMResponse(success=False, error=str(e))
@@ -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
- "plan": ["步骤1", "步骤2"],
92
+ "thought": "说明你当前的分析和下一步计划",
93
+ "mode": "step",
81
94
  "actions": [
82
- {"type": "skill", "name": "技能名", "params": {}},
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
- **注意**: action type="code" 必须包含 "timeout_seconds" 字段。action type="skill" 或 "memory" 不需要此字段。
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
- 你可以用 markdown 格式回复普通对话。
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 OpenAI
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 OpenAI
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._client = OpenAI(**kwargs)
260
- logger.info(f"OpenAI 客户端已初始化 (model={self.model})")
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 / 兼容接口调用 (含 Zhipu)"""
440
- loop = asyncio.get_running_loop()
441
+ """OpenAI / 兼容接口调用 (异步)"""
441
442
  try:
442
- response = await loop.run_in_executor(
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