myagent-ai 1.9.6 → 1.9.8

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.
Files changed (2) hide show
  1. package/agents/main_agent.py +24 -985
  2. package/package.json +1 -1
@@ -5,15 +5,13 @@ agents/main_agent.py - 主 Agent
5
5
  """
6
6
  from __future__ import annotations
7
7
 
8
- import json
9
8
  import asyncio
10
9
  from typing import Any, Callable, Dict, List, Optional
11
10
 
12
11
  from core.logger import get_logger
13
12
  from core.llm import LLMClient, LLMResponse, Message
14
13
  from agents.base import BaseAgent, AgentContext
15
- from core.utils import generate_id, timestamp, safe_json_parse, truncate_str
16
- from core.context_manager import ContextManager, ContextConfig, estimate_tokens
14
+ from core.utils import generate_id, timestamp, truncate_str
17
15
  from core.context_builder import ContextBuilder
18
16
  from core.output_parser import ParsedOutput, parse_output, validate_output, extract_surrounding_text
19
17
 
@@ -36,147 +34,10 @@ class MainAgent(BaseAgent):
36
34
  name = "main_agent"
37
35
  description = "AI助手主控Agent,负责理解用户意图、规划任务、调度执行"
38
36
 
39
- SYSTEM_PROMPT = """你是 MyAgent - 一个强大的本地桌面AI助手。你拥有以下能力:
40
-
41
- ## 核心能力
42
- 1. **代码执行**: 可以运行 Python、Shell/Bash、PowerShell 代码
43
- 2. **文件操作**: 读取、写入、搜索、管理文件
44
- 3. **网络搜索**: 搜索互联网、读取网页
45
- 4. **系统操作**: 查询系统信息、管理进程
46
- 5. **浏览器操作**: 自动化浏览器(如已安装 Playwright)
47
- 6. **记忆系统**: 记住用户偏好、历史任务、避免重复犯错
48
-
49
- ## 工作方式(遵循智能体循环规范)
50
- 你必须严格按照 **思考→执行→观察→思考** 的循环模式工作:
51
- 1. **思考**: 分析当前状态,确定下一步要做什么,用自然语言说明你的思路
52
- 2. **执行**: 执行操作(一个命令或一个技能调用)
53
- 3. **观察**: 查看执行结果,分析是否成功
54
- 4. **继续思考**: 基于结果决定下一步,重复以上循环
55
-
56
- **执行模式**:
57
- - **step 模式**(默认):每次只执行一个 action,等待结果后再决定下一步。适用于:
58
- - 后续操作依赖前一步结果(如先读文件再修改)
59
- - 操作可能有副作用需要确认(如删除、写入)
60
- - 不确定操作是否会成功
61
- - **batch 模式**:一次执行多个互不依赖的 action。仅适用于:
62
- - 所有操作之间完全独立,不需要前一步的结果
63
- - 都是简单的只读操作(如读取多个文件、查看多个系统信息)
64
- - 你有很高信心所有操作都会成功
65
-
66
- **关键原则**:
67
- - 默认使用 step 模式(安全优先)
68
- - 只有当你确信多个操作互不依赖时才用 batch 模式
69
- - 每个操作前,用 thought 字段说明你为什么要执行这一步
70
- - 如果前一步失败了,先分析原因,再尝试修复或换一种方法
71
- - 不要猜测结果,始终基于实际执行结果来判断
72
-
73
- ## ⏰ 超时控制规则(强制要求)
74
- 对于每个需要执行的命令(action type="code"),你**必须**在 action 中包含 "timeout_seconds" 字段,
75
- 指定该命令的预估最大执行超时时间(秒)。这是一项强制要求,不能省略。
76
-
77
- 超时时间设定指南:
78
- - 简单打印/计算: 10-30秒
79
- - 文件读写/搜索: 30-60秒
80
- - 网络请求/API调用: 60-120秒
81
- - 数据处理/批量操作: 120-300秒
82
- - 大规模编译/安装: 300-600秒
83
- - 如果不确定,默认设为 120秒
84
-
85
- **如果命令执行超时**: 系统会自动将超时信息反馈给你,包含命令已产生的部分输出。
86
- 你需要分析超时原因并给出改进方案(如优化算法、添加超时参数、分批处理等)。
87
-
88
- ## 输出格式(重要:流式友好)
89
-
90
- **核心原则:先用纯文本说明你的思路,只在执行操作时才输出 JSON。**
91
-
92
- ### 情况 1:需要执行操作
93
- 先用纯文本/markdown简要说明你的分析和计划(用户会实时看到),然后用 JSON 输出操作指令:
94
-
95
- 让我先检查一下文件内容...
96
-
97
- ```action
98
- {"thought": "需要查看文件内容", "mode": "step", "actions": [{"type": "skill", "name": "file_read", "params": {"path": "/a.txt"}}]}
99
- ```
100
-
101
- ### 情况 2:不需要执行操作
102
- 直接用纯文本/markdown回复即可,不需要任何 JSON。
103
-
104
- ### 情况 3:执行完操作后的总结
105
- 操作完成后,直接用纯文本/markdown总结结果,不要再用 JSON。
106
-
107
- ### JSON 操作指令格式
108
- 将 JSON 放在 ```action``` 代码块中,系统会自动识别并执行:
109
-
110
- **step 模式**(默认,逐步执行):
111
- ```action
112
- {"thought": "说明你当前的分析和下一步计划", "mode": "step", "actions": [{"type": "code", "language": "python", "code": "代码", "timeout_seconds": 60}]}
113
- ```
114
-
115
- **batch 模式**(多个独立操作):
116
- ```action
117
- {"thought": "说明为什么可以批量执行", "mode": "batch", "actions": [{"type": "skill", "name": "file_read", "params": {"path": "/a.txt"}}, {"type": "skill", "name": "file_read", "params": {"path": "/b.txt"}}]}
118
- ```
119
-
120
- action type="code" 必须包含 "timeout_seconds" 字段。action type="skill" 或 "memory" 不需要此字段。
121
- 省略 mode 字段时默认为 "step"。
122
-
123
- **注意**:你也可以直接输出裸 JSON(不用 ```action``` 包裹),系统同样能识别。但推荐使用 ```action``` 格式,这样你的分析文本能实时流式展示给用户。
124
-
125
- ## 执行模式任务追踪(重要:强制 JSON 格式)
126
-
127
- 当你处于**执行模式**(Execution Mode)时,你**必须**在每次回复中包含一个 JSON 格式的任务进度列表。
128
- 这个 JSON 必须放在 ```tasklist``` 代码块中,系统会自动解析并实时展示给用户。
129
-
130
- 格式要求:
131
- ```tasklist
132
- [
133
- {"text": "分析需求", "status": "done"},
134
- {"text": "检查文件结构", "status": "done"},
135
- {"text": "修改代码", "status": "running"},
136
- {"text": "测试验证", "status": "pending"}
137
- ]
138
- ```
139
-
140
- status 取值:
141
- - `"pending"` — 待执行
142
- - `"running"` — 正在执行(当前步骤,每次只能有一个)
143
- - `"done"` — 已完成
144
- - `"blocked"` — 被阻塞(等待依赖或其他原因)
145
-
146
- 规则:
147
- 1. 每次回复开头先用纯文本简要分析当前状态(这段文本会实时流式展示给用户)
148
- 2. 然后输出 ```tasklist``` 代码块更新任务进度
149
- 3. 如果需要执行操作,在 tasklist 之后输出 ```action``` 代码块
150
- 4. 如果不需要执行操作(纯文本回复),在 tasklist 之后直接写总结
151
- 5. 如果是简单任务(一步完成),也必须输出任务列表(只有一个条目)
152
- 6. 每次只将一个任务标记为 `running`,其余为 `pending` 或 `done`
153
- 7. 不要手动输出 markdown 格式的任务列表,只用 JSON 格式
154
-
155
- 回复结构示例:
156
- ```
157
- 让我先检查一下文件内容...
158
-
159
- ```tasklist
160
- [{"text": "检查文件内容", "status": "running"}, {"text": "根据结果修改", "status": "pending"}]
161
- ```
162
-
163
- ```action
164
- {"thought": "需要查看文件", "mode": "step", "actions": [{"type": "skill", "name": "file_read", "params": {"path": "/a.txt"}}]}
165
- ```
166
- ```
167
-
168
- ## 重要规则
169
- - 优先使用技能系统完成操作,而不是直接写代码
170
- - 执行危险操作前先警告用户
171
- - 保持回复简洁明了
172
- - 用中文回复
173
- - 绝对不要在回复开头进行自我介绍
174
- - 不要重复问候"""
175
-
176
37
  # =========================================================================
177
- # V2 系统提示词 — 结构化输出格式
38
+ # 系统提示词 — 结构化输出格式
178
39
  # =========================================================================
179
- SYSTEM_PROMPT_V2 = """你是一个强内容分析格式转化引擎,要深入分析以下上下文内容:
40
+ SYSTEM_PROMPT = """你是一个强内容分析格式转化引擎,要深入分析以下上下文内容:
180
41
 
181
42
  严格格式化输出以下内容:
182
43
  <output>
@@ -219,12 +80,7 @@ status 取值:
219
80
  self._iteration_count = 0
220
81
  self._current_task_id: str = ""
221
82
  self._registered_task: bool = False
222
- # 组织上下文缓存(避免重复加载)
223
- self._org_context_cache: str = ""
224
- self._org_context_mtime: float = 0 # 文件修改时间,用于检测变更
225
- # Token 上下文管理器 (滚动摘要 + 预算控制)
226
- self.context_manager = ContextManager(ContextConfig())
227
- # V2 Context Builder (结构化上下文构建)
83
+ # Context Builder (结构化上下文构建)
228
84
  self.context_builder: Optional[ContextBuilder] = None
229
85
  # 执行事件追踪(用于前端展示命令执行过程)
230
86
  self._execution_events: List[Dict] = []
@@ -233,13 +89,13 @@ status 取值:
233
89
  self.active_contexts: Dict[str, AgentContext] = {}
234
90
 
235
91
  def init_context_builder(self, memory_manager=None, skill_registry=None, knowledge_base_dir=None):
236
- """初始化 V2 Context Builder(在系统启动后调用,注入依赖)"""
92
+ """初始化 Context Builder(在系统启动后调用,注入依赖)"""
237
93
  self.context_builder = ContextBuilder(
238
94
  memory_manager=memory_manager,
239
95
  skill_registry=skill_registry,
240
96
  knowledge_base_dir=knowledge_base_dir,
241
97
  )
242
- logger.info("V2 Context Builder 已初始化" + (f" (知识库: {knowledge_base_dir})" if knowledge_base_dir else ""))
98
+ logger.info("Context Builder 已初始化" + (f" (知识库: {knowledge_base_dir})" if knowledge_base_dir else ""))
243
99
 
244
100
  def _add_exec_event(self, event_type: str, data: Dict):
245
101
  """记录一个执行事件(供前端展示)"""
@@ -293,11 +149,7 @@ status 取值:
293
149
  logger.info(f"[{task_id}] 开始处理用户请求: {context.user_message[:100]}")
294
150
 
295
151
  try:
296
- # V2 路由: 如果 context_builder 已初始化,使用 V2 结构化输出循环
297
- if self.context_builder is not None:
298
- logger.info(f"[{task_id}] 检测到 V2 Context Builder,使用 V2 执行循环")
299
- return await self.process_v2(context)
300
- return await self._process_inner(context, task_id)
152
+ return await self.process_v2(context)
301
153
  finally:
302
154
  # 移除活跃上下文
303
155
  self.active_contexts.pop(context.session_id, None)
@@ -306,823 +158,8 @@ status 取值:
306
158
  self.config_broadcaster.unregister(task_id)
307
159
  self._registered_task = False
308
160
 
309
- async def _process_inner(self, context: AgentContext, task_id: str) -> AgentContext:
310
- """内部处理逻辑(被 process 包裹,用于确保注册/注销广播器)"""
311
-
312
- # Step 1: 加载相关记忆
313
- if self.memory_agent:
314
- mem_context = AgentContext(
315
- task_id=task_id,
316
- session_id=context.session_id,
317
- user_message=context.user_message,
318
- metadata={"memory_action": "get_relevant"},
319
- )
320
- await self.memory_agent.process(mem_context)
321
- if "memory_context_prompt" in mem_context.working_memory:
322
- context.working_memory["memory_context_prompt"] = \
323
- mem_context.working_memory["memory_context_prompt"]
324
-
325
- # Step 2: 保存用户消息到短期记忆
326
- if self.memory:
327
- self.memory.add_short_term(
328
- session_id=context.session_id,
329
- role="user",
330
- content=context.user_message,
331
- )
332
-
333
- # Step 3: 主循环(计划-执行-反思)
334
- final_response = ""
335
- max_iter = self.config.agent.max_iterations
336
-
337
- while self._iteration_count < max_iter:
338
- self._iteration_count += 1
339
- logger.debug(f"[{task_id}] 迭代 {self._iteration_count}/{max_iter}")
340
-
341
- # 检查配置热加载广播(每次迭代前检查)
342
- if self.config_broadcaster:
343
- reloaded, reload_type = await self.config_broadcaster.check_and_wait(task_id)
344
- if reloaded:
345
- logger.info(f"[{task_id}] 🔄 {reload_type}已热更新,继续执行 (iteration={self._iteration_count})")
346
- # 更新迭代检查点
347
- if self.config_broadcaster._active_tasks.get(task_id):
348
- self.config_broadcaster._active_tasks[task_id].iteration = self._iteration_count
349
- # 代码热更新后,LLM 客户端可能已被刷新,下次调用会自动重建
350
- if reload_type == "code":
351
- logger.info(f"[{task_id}] 代码模块已热更新,LLM 客户端将在下次调用时自动重建")
352
-
353
- # ── 检查并处理注入的消息 ──
354
- if context.pending_injected_messages:
355
- injected = context.pending_injected_messages.copy()
356
- context.pending_injected_messages.clear()
357
- for msg_text in injected:
358
- logger.info(f"[{task_id}] 注入消息到对话历史: {msg_text[:50]}...")
359
- # 如果不是第一轮,且历史最后一条不是 user 消息,则注入
360
- context.conversation_history.append(
361
- Message(role="user", content=f"[用户中断/补充]: {msg_text}")
362
- )
363
- # 同时也保存到短期记忆
364
- if self.memory:
365
- self.memory.add_short_term(
366
- session_id=context.session_id,
367
- role="user",
368
- content=f"[中断补充]: {msg_text}",
369
- )
370
-
371
- # 构建消息列表
372
- messages = self._build_messages(context)
373
-
374
- # 添加可用工具
375
- tools = self._get_tools()
376
-
377
- # 调用 LLM
378
- response = await self._call_llm(messages, tools=tools)
379
-
380
- if not response.success:
381
- final_response = f"⚠️ LLM 调用失败: {response.error}"
382
- break
383
-
384
- # 解析响应
385
- content = response.content
386
-
387
- # 检查是否有工具调用 (OpenAI function calling)
388
- if response.tool_calls:
389
- # 先将 assistant 的 tool_calls 消息加入历史(OpenAI 格式要求)
390
- context.conversation_history.append(
391
- Message(role="assistant",
392
- content=response.content or "",
393
- tool_calls=response.tool_calls)
394
- )
395
- # 记录工具调用事件
396
- for tc in response.tool_calls:
397
- self._add_exec_event("tool_call", {
398
- "title": f"调用工具: {tc['name']}",
399
- "tool_name": tc["name"],
400
- "arguments": tc.get("arguments", {}),
401
- })
402
- # 执行工具调用
403
- tool_results = await self._handle_tool_calls(
404
- response.tool_calls, context, task_id
405
- )
406
- # 记录工具结果事件
407
- for tc, result in tool_results:
408
- self._add_exec_event("tool_result", {
409
- "title": f"工具结果: {tc['name']}",
410
- "tool_name": tc["name"],
411
- "success": result.get("success", False),
412
- "summary": truncate_str(result.get("output", result.get("error", "")), 500),
413
- "result": result,
414
- })
415
- # 再将工具结果加入消息历史
416
- for tc, result in tool_results:
417
- context.conversation_history.append(
418
- Message(role="tool", content=json.dumps(result, ensure_ascii=False),
419
- tool_call_id=tc["id"], name=tc["name"])
420
- )
421
- # 继续循环,让 LLM 处理工具结果
422
- continue
423
-
424
- # 尝试解析 JSON 操作指令
425
- action_data = safe_json_parse(content)
426
-
427
- if action_data and isinstance(action_data, dict):
428
- # 有结构化的操作指令
429
- if "actions" in action_data:
430
- action_mode = action_data.get("mode", "step")
431
-
432
- # step 模式安全保护:只执行第一个 action
433
- if action_mode == "step" and len(action_data.get("actions", [])) > 1:
434
- action_data = {
435
- "thought": action_data.get("thought", ""),
436
- "mode": "step",
437
- "actions": [action_data["actions"][0]],
438
- }
439
-
440
- # 执行操作列表
441
- results = await self._execute_actions(
442
- action_data, context, task_id
443
- )
444
- # 将结果反馈给 LLM
445
- context.conversation_history.append(
446
- Message(role="assistant", content=content)
447
- )
448
- result_summary = self._summarize_action_results(results)
449
-
450
- # 如果有超时诊断,追加详细信息供 LLM 分析
451
- has_timeout = any(r.get("timed_out") for r in results)
452
- timeout_detail = ""
453
- if has_timeout:
454
- timeout_details = []
455
- for i, r in enumerate(results, 1):
456
- if r.get("timed_out"):
457
- diag = r.get("timeout_diagnosis", {})
458
- timeout_details.append(
459
- f"### 命令 {i} 超时诊断\n"
460
- f"- 原因: {diag.get('diagnosis', '未知')}\n"
461
- f"- 进展: {diag.get('progress', '未知')}\n"
462
- f"- 是否建议重试: {'是' if diag.get('should_retry') else '否'}\n"
463
- f"- 重试策略: {diag.get('retry_strategy', '无')}\n"
464
- f"- 改进建议:\n"
465
- )
466
- for s in diag.get("suggestions", []):
467
- timeout_details.append(f" - {s}")
468
- timeout_detail = "\n\n## ⏰ 超时诊断详情\n" + "\n".join(timeout_details)
469
-
470
- feedback_msg = f"[执行结果]\n{result_summary}\n\n请基于以上结果继续。"
471
- if timeout_detail:
472
- feedback_msg += timeout_detail + "\n\n请根据以上诊断信息决定下一步操作。"
473
-
474
- context.conversation_history.append(
475
- Message(role="user", content=feedback_msg)
476
- )
477
-
478
- # 退出循环判断:step 模式始终继续让 LLM 决定,batch 模式全部成功则退出
479
- all_success = all(r.get("success", False) for r in results)
480
- if action_mode == "batch" and all_success and results:
481
- final_response = action_data.get("thought", "")
482
- if "plan" in action_data and action_data["plan"]:
483
- final_response += "\n\n已完成: " + " → ".join(action_data["plan"])
484
- break
485
-
486
- # 如果有超时且诊断建议不重试,强制中断循环避免无效重试
487
- if has_timeout:
488
- should_abort = False
489
- abort_reasons = []
490
- for i, r in enumerate(results, 1):
491
- if r.get("timed_out"):
492
- diag = r.get("timeout_diagnosis", {})
493
- if diag.get("should_retry") is False:
494
- should_abort = True
495
- abort_reasons.append(
496
- f"命令{i}: {diag.get('diagnosis', '不可恢复的超时')}"
497
- )
498
- if should_abort:
499
- logger.warning(
500
- f"[{task_id}] ⛔ 超时诊断建议不重试,终止当前执行循环 "
501
- f"(iteration={self._iteration_count})"
502
- )
503
- # 构建诊断反馈,强制LLM输出final_answer
504
- abort_msg = (
505
- "[系统通知] 以下命令因超时被终止,且超时诊断结果表明不应重试:\n"
506
- )
507
- for reason in abort_reasons:
508
- abort_msg += f"- {reason}\n"
509
- abort_msg += (
510
- "\n请直接以纯文本或 {\"type\": \"final_answer\", \"content\": \"...\"} "
511
- "格式回复,告知用户任务无法完成的原因和建议的替代方案。"
512
- "不要再尝试执行相同的命令。"
513
- )
514
- context.conversation_history.append(
515
- Message(role="user", content=abort_msg)
516
- )
517
- continue # 让LLM生成final_answer,而不是重试命令
518
-
519
- continue
520
-
521
- # 单个 action
522
- if action_data.get("type") == "final_answer":
523
- final_response = action_data.get("content", content)
524
- break
525
-
526
- # 纯文本回复(没有工具调用)
527
- final_response = content
528
- break
529
-
530
- # 保存助手回复到短期记忆
531
- if self.memory and final_response:
532
- self.memory.add_short_term(
533
- session_id=context.session_id,
534
- role="assistant",
535
- content=final_response,
536
- )
537
-
538
- # ── 保存工具调用过程到短期记忆(供历史记录查看)──
539
- if self.memory and self._execution_events:
540
- try:
541
- tool_summary_parts = []
542
- for evt in self._execution_events:
543
- etype = evt.get("type", "")
544
- if etype in ("tool_call", "skill_call"):
545
- tool_name = evt.get("tool_name") or evt.get("skill_name", "")
546
- args = evt.get("arguments") or evt.get("params", {})
547
- tool_summary_parts.append(f"🔧 调用工具: {tool_name}({json.dumps(args, ensure_ascii=False)[:200]})")
548
- elif etype in ("tool_result", "skill_result"):
549
- tool_name = evt.get("tool_name") or evt.get("skill_name", "")
550
- success = evt.get("success", False)
551
- summary = evt.get("summary", "")
552
- status = "✅" if success else "❌"
553
- tool_summary_parts.append(f" {status} 结果: {summary[:300]}")
554
- elif etype == "code_exec":
555
- lang = evt.get("language", "")
556
- code_preview = evt.get("code_preview", "")
557
- stdout = evt.get("stdout", "")
558
- success = evt.get("success", False)
559
- status = "✅" if success else "❌"
560
- tool_summary_parts.append(f"💻 执行 {lang}: {code_preview[:150]}")
561
- if stdout:
562
- tool_summary_parts.append(f" {status} 输出: {stdout[:300]}")
563
- elif etype == "code_result":
564
- pass # code_result is duplicate of code_exec with final status
565
- if tool_summary_parts:
566
- exec_log = "\n".join(tool_summary_parts)
567
- self.memory.add_short_term(
568
- session_id=context.session_id,
569
- role="tool",
570
- content=exec_log,
571
- importance=0.3,
572
- )
573
- except Exception as e:
574
- logger.warning(f"保存工具调用过程失败: {e}")
575
-
576
- # 清理工作记忆
577
- context.working_memory["final_response"] = final_response
578
- context.working_memory["iterations"] = self._iteration_count
579
-
580
- logger.info(f"[{task_id}] 处理完成 (迭代 {self._iteration_count} 次)")
581
-
582
- # 检查是否需要增量总结对话 (使用滚动摘要,不再清空短期记忆)
583
- if self.memory_agent and self._iteration_count > 5:
584
- try:
585
- summary_context = AgentContext(
586
- session_id=context.session_id,
587
- metadata={"memory_action": "summarize"},
588
- )
589
- await self.memory_agent.process(summary_context)
590
- # 将生成的摘要更新到 ContextManager 的滚动摘要缓冲
591
- summary_result = summary_context.working_memory.get("summary_result", "")
592
- if summary_result:
593
- self.context_manager.summary_buffer.update_summary(
594
- session_id=context.session_id,
595
- new_summary=summary_result,
596
- summarized_count=summary_context.working_memory.get("summarized_count", 0),
597
- )
598
- logger.info(
599
- f"[{task_id}] 滚动摘要已更新 (session={context.session_id})"
600
- )
601
- except Exception as e:
602
- logger.warning(f"对话总结失败: {e}")
603
-
604
- return context
605
-
606
- def _build_messages(self, context: AgentContext) -> List[Message]:
607
- """构建完整的消息列表 (使用 ContextManager 进行 Token 预算优化)"""
608
- # 合并系统提示 + 组织上下文
609
- system_prompt = self.SYSTEM_PROMPT
610
- org_context = self._build_org_context(context)
611
- if org_context:
612
- system_prompt += "\n\n" + org_context
613
-
614
- # 注入 Agent 专属 system_prompt(如果有覆盖)
615
- agent_prompt = context.metadata.get("agent_override_prompt") if context.metadata else None
616
- agent_path = context.metadata.get("agent_override_path") if context.metadata else None
617
- if agent_prompt:
618
- prefix = f"## 当前角色\n你当前正在扮演「{agent_path}」Agent。请严格遵循以下角色设定:\n\n"
619
- system_prompt += "\n\n" + prefix + agent_prompt
620
-
621
- # 通用规则:禁止重复自我介绍(避免每次对话都问好)
622
- system_prompt += "\n\n## 对话规则\n- 绝对不要在回复开头进行自我介绍(如'你好,我是XXX'),直接回答用户的问题或执行任务\n- 不要重复问候,除非用户主动打招呼"
623
-
624
- # 执行模式:强调主动执行能力
625
- chat_mode = (context.metadata.get("chat_mode") or '') if context.metadata else ''
626
- if chat_mode == 'exec':
627
- system_prompt += "\n\n## 执行模式 (当前激活)\n你当前处于执行模式,请务必主动使用可用工具执行操作,而不是只提供建议或反问用户。\n- 默认逐步执行(step 模式),每次一个操作,等待结果再决定下一步\n- 仅当多个操作完全独立时使用 batch 模式\n- 优先使用技能系统(skill)完成任务\n- 需要执行代码时,直接使用 code action 执行\n- 遇到不确定的操作,先尝试执行,失败后再调整\n- 不要反复询问用户是否要执行,直接执行并报告结果\n- 每步操作前先用 thought 说明你的计划"
628
-
629
- # 记忆上下文
630
- memory_ctx = context.working_memory.get("memory_context_prompt", "")
631
-
632
- # 使用 ContextManager 构建优化后的消息列表
633
- raw_messages = self.context_manager.build_context(
634
- system_prompt=system_prompt,
635
- memory_context=memory_ctx,
636
- conversation_history=context.conversation_history,
637
- user_message=context.user_message,
638
- session_id=context.session_id,
639
- )
640
-
641
- # 转换回 Message 对象 (ContextManager 返回 dict)
642
- messages = []
643
- for m in raw_messages:
644
- if isinstance(m, dict):
645
- messages.append(Message(**m))
646
- elif isinstance(m, Message):
647
- messages.append(m)
648
- else:
649
- messages.append(m)
650
-
651
- return messages
652
-
653
- def _get_tools(self) -> Optional[List[Dict]]:
654
- """获取可用工具列表(OpenAI function calling 格式)"""
655
- if not self.skills:
656
- return None
657
- try:
658
- return self.skills.get_all_schemas()
659
- except Exception as e:
660
- logger.warning(f"获取工具列表失败: {e}")
661
- return None
662
-
663
- @staticmethod
664
- def _get_skill_permission(skill_name: str) -> Optional[str]:
665
- """
666
- 根据技能名称映射到对应的权限项。
667
-
668
- Returns:
669
- 权限项名称,或 None(不需要特殊权限的技能)
670
- """
671
- # 文件技能
672
- if skill_name in ("file_write", "file_delete", "file_move"):
673
- return "file_write"
674
- if skill_name in ("file_read", "file_list", "file_search"):
675
- return "file_read"
676
- # 网络/搜索技能
677
- if skill_name in ("web_search", "web_read", "url_read"):
678
- return "network"
679
- # 系统技能涉及执行
680
- if skill_name in ("command_run",):
681
- return "execution"
682
- # 浏览器技能需要网络
683
- if skill_name in ("browser_open", "browser_click", "browser_fill"):
684
- return "network"
685
- # 浏览器扩展技能 - 截图/JS执行/导航/关闭
686
- if skill_name in ("browser_screenshot", "browser_eval", "browser_navigate", "browser_close"):
687
- return "network"
688
- # 桌面 GUI 自动化技能 - 涉及系统级执行权限
689
- if skill_name in ("screenshot", "mouse_click", "mouse_drag", "type_text",
690
- "hotkey", "window_list", "window_focus", "screen_element"):
691
- return "execution"
692
- # 其他技能不需要特殊权限检查
693
- return None
694
-
695
- async def _handle_tool_calls(
696
- self,
697
- tool_calls: List[Dict],
698
- context: AgentContext,
699
- task_id: str,
700
- ) -> List[tuple]:
701
- """处理 OpenAI function calling 工具调用"""
702
- results = []
703
- for tc in tool_calls:
704
- name = tc["name"]
705
- args = tc.get("arguments", {})
706
- logger.info(f"[{task_id}] 调用工具: {name}({args})")
707
-
708
- try:
709
- if self.skills:
710
- result = await self.skills.execute(name, **args)
711
- result_dict = result.to_dict()
712
- else:
713
- result_dict = {"success": False, "error": "技能系统未初始化"}
714
- except Exception as e:
715
- result_dict = {"success": False, "error": str(e)}
716
-
717
- results.append((tc, result_dict))
718
-
719
- # 记录错误模式
720
- if not result_dict.get("success") and self.memory:
721
- self.memory.record_error_pattern(
722
- error=f"工具 {name} 失败: {result_dict.get('error', '')}",
723
- )
724
-
725
- return results
726
-
727
- async def _execute_actions(
728
- self,
729
- action_data: Dict,
730
- context: AgentContext,
731
- task_id: str,
732
- ) -> List[Dict]:
733
- """执行操作列表"""
734
- results = []
735
- actions = action_data.get("actions", [])
736
-
737
- for action in actions:
738
- action_type = action.get("type", "")
739
-
740
- if action_type == "skill" and self.skills:
741
- # 权限检查: 根据技能类别检查权限
742
- skill_name = action.get("name", "")
743
- skill_perm = self._get_skill_permission(skill_name)
744
- if skill_perm and not self.check_permission(skill_perm):
745
- from core.permissions import PermissionManager
746
- label = PermissionManager.PERMISSION_LABELS.get(skill_perm, skill_perm)
747
- results.append({
748
- "success": False,
749
- "error": f"[权限] 当前 Agent 没有'{label}'权限,操作被拒绝",
750
- "metadata": {"permission_denied": skill_perm},
751
- })
752
- self._add_exec_event("skill_call", {
753
- "title": f"技能调用被拒: {skill_name}",
754
- "skill_name": skill_name,
755
- "success": False,
756
- "error": f"权限不足: {label}",
757
- })
758
- continue
759
-
760
- # 记录技能调用事件
761
- self._add_exec_event("skill_call", {
762
- "title": f"调用技能: {skill_name}",
763
- "skill_name": skill_name,
764
- "params": action.get("params", {}),
765
- })
766
- result = await self.skills.execute(
767
- action.get("name", ""),
768
- **action.get("params", {}),
769
- )
770
- result_dict = result.to_dict()
771
- results.append(result_dict)
772
- # 记录技能结果事件
773
- self._add_exec_event("skill_result", {
774
- "title": f"技能结果: {skill_name}",
775
- "skill_name": skill_name,
776
- "success": result_dict.get("success", False),
777
- "summary": truncate_str(str(result_dict.get("output", result_dict.get("error", ""))), 500),
778
- })
779
-
780
- elif action_type == "code" and self.executor:
781
- # 权限检查: execution
782
- if not self.check_permission("execution"):
783
- results.append({
784
- "success": False,
785
- "error": "[权限] 当前 Agent 没有代码执行权限,操作被拒绝",
786
- "metadata": {"permission_denied": "execution"},
787
- })
788
- self._add_exec_event("code_exec", {
789
- "title": f"代码执行被拒",
790
- "language": action.get("language", "unknown"),
791
- "code_preview": truncate_str(action.get("code", ""), 200),
792
- "success": False,
793
- "error": "代码执行权限不足",
794
- })
795
- continue
796
-
797
- code_lang = action.get("language", "python")
798
- code_text = action.get("code", "")
799
-
800
- # 提取 LLM 预估的超时时间
801
- timeout_seconds = action.get("timeout_seconds")
802
- if timeout_seconds is None:
803
- # 如果 LLM 未提供 timeout_seconds,使用默认值并记录警告
804
- timeout_seconds = self.config.executor.timeout if hasattr(self.config, 'executor') else 120
805
- logger.warning(
806
- f"[{task_id}] LLM 未指定 timeout_seconds,使用默认值 {timeout_seconds}s"
807
- )
808
- else:
809
- try:
810
- timeout_seconds = int(timeout_seconds)
811
- # 限制超时范围: 最小 5s,最大 3600s
812
- timeout_seconds = max(5, min(timeout_seconds, 3600))
813
- except (ValueError, TypeError):
814
- timeout_seconds = 120
815
- logger.warning(
816
- f"[{task_id}] timeout_seconds 值无效,使用默认值 120s"
817
- )
818
-
819
- # 记录代码执行开始事件
820
- self._add_exec_event("code_exec", {
821
- "title": f"执行 {code_lang} 代码",
822
- "language": code_lang,
823
- "code": code_text,
824
- "code_preview": truncate_str(code_text, 200),
825
- "status": "running",
826
- "timeout": timeout_seconds,
827
- })
828
-
829
- # 注入权限检查器到 executor(用于更细粒度的检查)
830
- self.executor.set_permission_checker(
831
- self.check_permission, self.name
832
- )
833
-
834
- exec_result = await self.executor.execute(
835
- language=action.get("language", "python"),
836
- code=action.get("code", ""),
837
- timeout=timeout_seconds,
838
- )
839
-
840
- # 设置超时追踪字段(executor 已直接设置 timed_out,这里确保 timeout_limit)
841
- if not exec_result.timeout_limit:
842
- exec_result.timeout_limit = timeout_seconds
843
-
844
- result_dict = exec_result.to_dict()
845
-
846
- # 记录代码执行结果事件
847
- self._add_exec_event("code_result", {
848
- "title": f"{'超时' if exec_result.timed_out else '成功' if exec_result.success else '失败'}: {code_lang}",
849
- "language": code_lang,
850
- "code_preview": truncate_str(action.get("code", ""), 200),
851
- "success": exec_result.success,
852
- "timed_out": exec_result.timed_out,
853
- "exit_code": exec_result.exit_code,
854
- "execution_time": round(exec_result.execution_time, 3),
855
- "timeout": timeout_seconds,
856
- "stdout": truncate_str(exec_result.stdout, 5000),
857
- "stderr": truncate_str(exec_result.stderr, 3000),
858
- "error": truncate_str(exec_result.error, 2000),
859
- "result": result_dict,
860
- })
861
-
862
- # 超时后自动触发 LLM 诊断分析
863
- if exec_result.timed_out:
864
- logger.info(
865
- f"[{task_id}] ⏰ 命令执行超时 ({timeout_seconds}s),"
866
- f"自动触发 LLM 诊断分析..."
867
- )
868
- diagnosis = await self._diagnose_timeout(
869
- action, exec_result, context, task_id
870
- )
871
- result_dict["timeout_diagnosis"] = diagnosis
872
-
873
- results.append(result_dict)
874
-
875
- elif action_type == "memory" and self.memory_agent:
876
- mem_ctx = AgentContext(
877
- task_id=task_id,
878
- session_id=context.session_id,
879
- metadata={
880
- "memory_action": action.get("action", ""),
881
- **action.get("data", {}),
882
- },
883
- )
884
- await self.memory_agent.process(mem_ctx)
885
- results.append({"success": True, "action": "memory"})
886
-
887
- elif action_type == "final":
888
- break
889
-
890
- else:
891
- results.append({
892
- "success": False,
893
- "error": f"未知操作类型: {action_type}",
894
- })
895
-
896
- return results
897
-
898
- @staticmethod
899
- def _build_timeout_diagnosis_message(action: Dict, exec_result: 'ExecResult') -> str:
900
- """构建超时诊断的用户消息(避免 f-string 内换行问题)"""
901
- parts = []
902
- parts.append("## 超时命令信息")
903
- parts.append(f"- 语言: {action.get('language', 'unknown')}")
904
- parts.append(f"- 超时上限: {exec_result.timeout_limit}s")
905
- parts.append(f"- 实际执行时间: {exec_result.execution_time:.1f}s")
906
- parts.append(f"- 代码:")
907
- parts.append(f"```{action.get('language', '')}")
908
- parts.append(f"{action.get('code', '')}")
909
- parts.append("```")
910
- parts.append("")
911
- parts.append("## 执行输出(超时前)")
912
-
913
- if exec_result.stdout and exec_result.stdout.strip():
914
- parts.append("### 标准输出")
915
- parts.append("```")
916
- parts.append(exec_result.stdout[:5000])
917
- parts.append("```")
918
- parts.append("")
919
- else:
920
- parts.append("(无输出)")
921
- parts.append("")
922
-
923
- if exec_result.stderr and exec_result.stderr.strip():
924
- parts.append("### 标准错误")
925
- parts.append("```")
926
- parts.append(exec_result.stderr[:3000])
927
- parts.append("```")
928
- else:
929
- parts.append("(无错误输出)")
930
-
931
- return "\n".join(parts)
932
-
933
- async def _diagnose_timeout(
934
- self,
935
- action: Dict,
936
- exec_result: 'ExecResult',
937
- context: AgentContext,
938
- task_id: str,
939
- ) -> Dict[str, Any]:
940
- """
941
- 超时后自动调用 LLM 诊断分析执行问题。
942
-
943
- 分析维度:
944
- 1. 超时原因诊断(死循环/网络阻塞/数据量过大/缺少输入等)
945
- 2. 基于已有输出的进展评估
946
- 3. 具体改进建议(优化代码/拆分任务/添加超时参数等)
947
- 4. 是否值得重试
948
-
949
- Returns:
950
- 诊断结果字典
951
- """
952
- diagnosis_messages = [
953
- Message(
954
- role="system",
955
- content=(
956
- "你是一个执行超时诊断专家。一个命令刚刚因超时被强制终止,"
957
- "请仔细分析以下信息,给出诊断结论和改进建议。\n\n"
958
- "请用 JSON 格式回复:\n"
959
- "{\n"
960
- ' "diagnosis": "超时原因诊断(简明扼要)",\n'
961
- ' "progress": "已完成的进度评估",\n'
962
- ' "suggestions": ["改进建议1", "改进建议2", ...],\n'
963
- ' "should_retry": true/false,\n'
964
- ' "retry_strategy": "如果建议重试,说明重试策略",\n'
965
- ' "estimated_fix_time": "预计修复后需要的时间(秒)"\n'
966
- "}"
967
- ),
968
- ),
969
- Message(
970
- role="user",
971
- content=self._build_timeout_diagnosis_message(action, exec_result),
972
- ),
973
- ]
974
-
975
- try:
976
- diagnosis = await self._call_llm_json(diagnosis_messages)
977
- if "error" in diagnosis:
978
- logger.warning(f"[{task_id}] 超时诊断 LLM 调用失败: {diagnosis['error']}")
979
- return {
980
- "diagnosis": "LLM 诊断调用失败",
981
- "suggestions": ["检查命令是否有死循环", "尝试减少处理数据量"],
982
- "should_retry": False,
983
- }
984
-
985
- # 记录诊断结果
986
- logger.info(
987
- f"[{task_id}] ⏰ 超时诊断完成: {diagnosis.get('diagnosis', 'N/A')}"
988
- )
989
-
990
- # 如果诊断建议重试且提供了重试策略,记录到记忆中
991
- if diagnosis.get("should_retry") and self.memory:
992
- self.memory.record_error_pattern(
993
- error=(
994
- f"命令超时 ({exec_result.timeout_limit}s): "
995
- f"{action.get('code', '')[:200]}\n"
996
- f"诊断: {diagnosis.get('diagnosis', '')}\n"
997
- f"建议: {'; '.join(diagnosis.get('suggestions', []))}"
998
- )
999
- )
1000
-
1001
- return diagnosis
1002
-
1003
- except Exception as e:
1004
- logger.error(f"[{task_id}] 超时诊断异常: {e}")
1005
- return {
1006
- "diagnosis": f"诊断过程异常: {str(e)}",
1007
- "suggestions": ["检查命令逻辑", "手动测试命令"],
1008
- "should_retry": False,
1009
- }
1010
-
1011
- def _summarize_action_results(self, results: List[Dict]) -> str:
1012
- """汇总操作结果"""
1013
- if not results:
1014
- return "无操作结果"
1015
-
1016
- parts = []
1017
- for i, r in enumerate(results, 1):
1018
- status = "✅" if r.get("success") else "❌"
1019
- msg = r.get("message", r.get("error", ""))[:200]
1020
-
1021
- # 超时特殊标记
1022
- if r.get("timed_out"):
1023
- status = "⏰"
1024
- limit = r.get("timeout_limit", "?")
1025
- actual = r.get("execution_time", "?")
1026
- msg = f"执行超时 (上限{limit}s, 实际{actual}s)"
1027
-
1028
- # 追加诊断结果
1029
- diagnosis = r.get("timeout_diagnosis")
1030
- if diagnosis:
1031
- diag_text = diagnosis.get("diagnosis", "")
1032
- suggestions = diagnosis.get("suggestions", [])
1033
- msg += f"\n 诊断: {diag_text}"
1034
- if suggestions:
1035
- msg += f"\n 建议: {'; '.join(suggestions[:3])}"
1036
-
1037
- parts.append(f"{i}. {status} {msg}")
1038
-
1039
- return "\n".join(parts)
1040
-
1041
- def _build_org_context(self, context: AgentContext) -> str:
1042
- """
1043
- 构建组织上下文信息。
1044
-
1045
- 如果组织功能已启用:
1046
- 1. 读取 organization.md 内容
1047
- 2. 对用户查询进行 RAG 搜索,获取相关知识片段
1048
- 3. 返回格式化的上下文字符串
1049
-
1050
- Returns:
1051
- 格式化的组织上下文,未启用时返回空字符串
1052
- """
1053
- try:
1054
- # 检查配置中是否启用了组织功能
1055
- org_enabled = False
1056
- if hasattr(self.config, 'organization'):
1057
- org_enabled = self.config.organization.enabled
1058
- if not org_enabled:
1059
- return ""
1060
-
1061
- parts = []
1062
-
1063
- # 1. 读取 organization.md(使用 mtime 缓存检测变更)
1064
- try:
1065
- from config import get_config
1066
- config_mgr = get_config()
1067
- org_dir = config_mgr.data_dir / "organization"
1068
- org_info_path = org_dir / "organization.md"
1069
- if org_info_path.exists():
1070
- current_mtime = org_info_path.stat().st_mtime
1071
- if current_mtime != self._org_context_mtime:
1072
- content = org_info_path.read_text(encoding="utf-8")
1073
- # 检查是否仍是模板(包含占位注释)
1074
- is_template = "<!-- 在此填写" in content
1075
- if not is_template:
1076
- self._org_context_cache = content
1077
- else:
1078
- self._org_context_cache = ""
1079
- self._org_context_mtime = current_mtime
1080
- except Exception as e:
1081
- logger.debug(f"读取组织信息失败: {e}")
1082
-
1083
- if self._org_context_cache:
1084
- parts.append("## 组织信息\n")
1085
- # 截取合理的长度,避免占用过多 token
1086
- org_text = self._org_context_cache[:3000]
1087
- parts.append(org_text)
1088
- parts.append("")
1089
-
1090
- # 2. RAG 搜索组织知识库
1091
- try:
1092
- from config import get_config
1093
- from knowledge.rag import KnowledgeRAG
1094
-
1095
- config_mgr = get_config()
1096
- kb_dir = config_mgr.data_dir / "organization" / "knowledge"
1097
- if kb_dir.exists() and any(kb_dir.iterdir()):
1098
- rag = KnowledgeRAG(kb_dir=kb_dir)
1099
- rag.build_index()
1100
-
1101
- if rag.total_chunks > 0:
1102
- # 使用用户消息作为查询
1103
- query = context.user_message
1104
- results = rag.search(query, top_k=3)
1105
- if results:
1106
- parts.append("## 相关知识库内容\n")
1107
- for r in results:
1108
- parts.append(
1109
- f"### 📄 {r.file_name} (片段 {r.chunk_index + 1})\n"
1110
- f"{r.content[:1000]}\n"
1111
- )
1112
- except Exception as e:
1113
- logger.debug(f"组织知识库搜索失败: {e}")
1114
-
1115
- if not parts:
1116
- return ""
1117
-
1118
- return "[组织上下文]\n请参考以下组织信息回答用户问题。如果组织信息与用户问题无关,可以忽略。\n\n" + "\n".join(parts)
1119
-
1120
- except Exception as e:
1121
- logger.debug(f"构建组织上下文失败: {e}")
1122
- return ""
1123
-
1124
161
  # =========================================================================
1125
- # V2 执行循环 — 结构化输出 + Context Builder + Output Parser
162
+ # 执行循环 — 结构化输出 + Context Builder + Output Parser
1126
163
  # =========================================================================
1127
164
 
1128
165
  async def _emit_v2_event(self, event_type: str, data: Dict, stream_callback: Optional[Callable] = None):
@@ -1229,7 +266,7 @@ status 取值:
1229
266
 
1230
267
  核心流程:
1231
268
  1. 使用 ContextBuilder 构建 <context> XML
1232
- 2. 将 context 注入 SYSTEM_PROMPT_V2,调用 LLM
269
+ 2. 将 context 注入 SYSTEM_PROMPT,调用 LLM
1233
270
  3. 使用 OutputParser 解析 <output> XML
1234
271
  4. 根据 parsed.tools_to_call 依次执行工具
1235
272
  5. 任一工具超时 → 强制回调 LLM
@@ -1251,11 +288,7 @@ status 取值:
1251
288
  self._current_task_id = task_id
1252
289
  self.clear_execution_events()
1253
290
 
1254
- if not self.context_builder:
1255
- logger.warning("Context Builder 未初始化,回退到 V1 执行循环")
1256
- return await self._process_inner(context, task_id)
1257
-
1258
- logger.info(f"[{task_id}] V2 执行循环启动: {context.user_message[:100]}")
291
+ logger.info(f"[{task_id}] 执行循环启动: {context.user_message[:100]}")
1259
292
 
1260
293
  try:
1261
294
  return await self._process_v2_inner(
@@ -1350,8 +383,14 @@ status 取值:
1350
383
  stream_callback,
1351
384
  )
1352
385
 
1353
- # Step 2: 构建系统消息 (SYSTEM_PROMPT_V2 + context XML)
1354
- system_content = self.SYSTEM_PROMPT_V2 + "\n\n" + context_xml
386
+ # Step 2: 构建系统消息 context XML 插入 SYSTEM_PROMPT 的 "上下文" 占位处
387
+ _CONTEXT_PLACEHOLDER = "__CONTEXT_PLACEHOLDER__"
388
+ _prompt_with_placeholder = (
389
+ "你是一个强内容分析格式转化引擎,要深入分析以下上下文内容:\n\n"
390
+ + _CONTEXT_PLACEHOLDER + "\n\n"
391
+ + self.SYSTEM_PROMPT.split("\n", 1)[1]
392
+ )
393
+ system_content = _prompt_with_placeholder.replace(_CONTEXT_PLACEHOLDER, context_xml)
1355
394
 
1356
395
  # Step 3: 调用 LLM
1357
396
  messages = [Message(role="system", content=system_content)]
@@ -1512,20 +551,20 @@ status 取值:
1512
551
  )
1513
552
 
1514
553
  # Step 9: 处理 askuser(askuser 非空时,finish 必为 true)
1515
- if parsed.askuser:
1516
- logger.info(f"[{task_id}] 需要询问用户: {parsed.askuser[:100]}")
1517
- context.working_memory["final_response"] = parsed.askuser
1518
- context.working_memory["ask_user"] = parsed.askuser
554
+ if parsed.ask_user:
555
+ logger.info(f"[{task_id}] 需要询问用户: {parsed.ask_user[:100]}")
556
+ context.working_memory["final_response"] = parsed.ask_user
557
+ context.working_memory["ask_user"] = parsed.ask_user
1519
558
  await self._emit_v2_event(
1520
559
  "v2_ask_user",
1521
- {"question": parsed.askuser},
560
+ {"question": parsed.ask_user},
1522
561
  stream_callback,
1523
562
  )
1524
563
  if self.memory:
1525
564
  self.memory.add_short_term(
1526
565
  session_id=context.session_id,
1527
566
  role="assistant",
1528
- content=parsed.askuser,
567
+ content=parsed.ask_user,
1529
568
  )
1530
569
  break
1531
570
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.9.6",
3
+ "version": "1.9.8",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {