myagent-ai 1.15.34 → 1.15.35

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.
@@ -637,10 +637,49 @@ class MainAgent(BaseAgent):
637
637
  "response": truncate_str(parsed.response, 500),
638
638
  "parse_success": parsed.parse_success,
639
639
  "needs_correction": parsed.needs_correction,
640
+ "output_block_complete": parsed.output_block_complete,
640
641
  }},
641
642
  stream_callback,
642
643
  )
643
644
 
645
+ # Step 4.2: <output> 块完整性检查 — 不完整的块不输出,触发修正
646
+ if not parsed.output_block_complete:
647
+ logger.warning(
648
+ f"[{task_id}] <output> 块不完整(缺少 </output> 闭合标签),"
649
+ f"跳过本轮输出和工具执行"
650
+ )
651
+ if _xml_correction_retries < 1:
652
+ _xml_correction_retries += 1
653
+ correction_prompt = (
654
+ "你的输出缺少 </output> 闭合标签,XML块不完整,"
655
+ "解析器不会处理不完整的块。\n"
656
+ "请严格按照 <output>...</output> 格式重新输出,"
657
+ "确保所有标签正确闭合。\n\n"
658
+ f"你上一次的原始输出如下:\n{llm_raw}"
659
+ )
660
+ conversation_history.append(
661
+ Message(role="assistant", content=llm_raw)
662
+ )
663
+ conversation_history.append(
664
+ Message(role="user", content=correction_prompt)
665
+ )
666
+ await self._emit_v2_event(
667
+ "v2_reasoning",
668
+ {"content": "⚠️ 模型输出XML块不完整,正在自动修正..."},
669
+ stream_callback,
670
+ )
671
+ continue # 重新进入循环,让 LLM 重新生成
672
+ else:
673
+ # 已重试过,强制终止并提示用户
674
+ logger.warning(f"[{task_id}] XML块仍不完整且已重试,终止循环")
675
+ context.working_memory["final_response"] = "模型输出格式异常,请重新尝试。"
676
+ await self._emit_v2_event(
677
+ "v2_reasoning",
678
+ {"content": "模型输出格式异常,已自动终止。"},
679
+ stream_callback,
680
+ )
681
+ break
682
+
644
683
  # Step 4.5: 解析失败处理 — 回退给 LLM 修正或提取周边文本
645
684
  if not parsed.parse_success:
646
685
  # 即使解析失败,如果 regex fallback 提取到了工具调用,仍然继续执行
@@ -12,7 +12,7 @@ core/deps_checker.py - 自动依赖检测与安装
12
12
 
13
13
  依赖映射:
14
14
  核心功能: openai, aiohttp, requests
15
- 搜索技能: bs4
15
+ 搜索技能: duckduckgo_search, bs4, lxml
16
16
  系统技能: psutil
17
17
  托盘功能: pystray, PIL
18
18
  语音合成: edge_tts
@@ -62,7 +62,9 @@ DEPENDENCIES: List[DepInfo] = [
62
62
  DepInfo("requests", "requests", "2.31.0", "core", "all"),
63
63
 
64
64
  # ── 搜索技能 ──
65
+ DepInfo("duckduckgo_search", "duckduckgo-search", "6.0.0", "search", "all"),
65
66
  DepInfo("bs4", "beautifulsoup4", "4.12.0", "search", "all"),
67
+ DepInfo("lxml", "lxml", "5.0.0", "search", "all"),
66
68
 
67
69
  # ── 系统技能 ──
68
70
  DepInfo("psutil", "psutil", "5.9.0", "system", "all"),
@@ -152,6 +152,7 @@ class ParsedOutput:
152
152
  raw_text: str = ""
153
153
  parse_success: bool = False
154
154
  needs_correction: bool = False
155
+ output_block_complete: bool = False # </output> 闭合标签是否存在
155
156
 
156
157
 
157
158
  # ---------------------------------------------------------------------------
@@ -191,7 +192,7 @@ def _canonical_tag(tag_name: str) -> str:
191
192
  return _ALIAS_TO_CANONICAL.get(lower, lower)
192
193
 
193
194
 
194
- def _extract_tag_content(text: str, tag_name: str, stop_tags: List[str] | None = None) -> str:
195
+ def _extract_tag_content(text: str, tag_name: str, stop_tags: List[str] | None = None, *, conservative: bool = False) -> str:
195
196
  """Extract the text content of ``<tag_name>…</tag_name>`` from *text*.
196
197
 
197
198
  Fault-tolerant strategies tried in order:
@@ -226,6 +227,10 @@ def _extract_tag_content(text: str, tag_name: str, stop_tags: List[str] | None =
226
227
  if m:
227
228
  return m.group(1)
228
229
 
230
+ # Conservative mode: only extract properly closed tags, skip all fallbacks
231
+ if conservative:
232
+ return ""
233
+
229
234
  # Strategy 2: Unclosed — content runs until the next opening/closing
230
235
  # sibling tag or </output>.
231
236
  sibling_names = [t for t in stop_tags if t.lower() != tag_name.lower()]
@@ -270,6 +275,8 @@ def _extract_all_tag_blocks(
270
275
  text: str,
271
276
  tag_name: str,
272
277
  parent_close_tag: str | None = None,
278
+ *,
279
+ conservative: bool = False,
273
280
  ) -> List[str]:
274
281
  """Extract all ``<tag_name>…`` blocks from *text*.
275
282
 
@@ -293,6 +300,10 @@ def _extract_all_tag_blocks(
293
300
  if properly_closed:
294
301
  return properly_closed
295
302
 
303
+ # Conservative mode: only extract properly closed blocks
304
+ if conservative:
305
+ return []
306
+
296
307
  # Strategy 2: Split by <tag> openings — each segment is a block
297
308
  positions = [
298
309
  m.end() for m in re.finditer(rf"<{tag_esc}[^>]*>", text, re.IGNORECASE)
@@ -377,6 +388,22 @@ def _strip_outer_noise(text: str) -> str:
377
388
  return text
378
389
 
379
390
 
391
+ def is_output_block_complete(raw_text: str) -> bool:
392
+ """Check if *raw_text* contains a properly closed ``<output>...</output>`` block.
393
+
394
+ Returns:
395
+ True if both ``<output>`` and ``</output>`` tags are present.
396
+ False if neither tag, or only the opening tag, is found.
397
+ """
398
+ if not raw_text:
399
+ return False
400
+ open_m = re.search(r"<output[^>]*>", raw_text, re.IGNORECASE)
401
+ if open_m is None:
402
+ return False
403
+ close_m = re.search(r"</output\s*>", raw_text[open_m.end():], re.IGNORECASE)
404
+ return close_m is not None
405
+
406
+
380
407
  # ---------------------------------------------------------------------------
381
408
  # Core custom parser — NO xml.etree.ElementTree
382
409
  # ---------------------------------------------------------------------------
@@ -399,63 +426,73 @@ def _custom_parse(raw_text: str) -> ParsedOutput:
399
426
  parsed.needs_correction = True
400
427
  return parsed
401
428
 
429
+ # ── Step 0: Check <output> block completeness ──
430
+ parsed.output_block_complete = is_output_block_complete(raw_text)
431
+ conservative = not parsed.output_block_complete
432
+
433
+ if conservative:
434
+ logger.info(
435
+ "XML <output> 块不完整(缺少 </output> 闭合标签),"
436
+ "启用保守解析模式(仅提取完整闭合的标签)"
437
+ )
438
+
402
439
  # ── Step 1: Strip non-XML noise (text before/after <output>) ──
403
440
  body = _strip_outer_noise(raw_text)
404
441
 
405
442
  # ── Step 2: Extract each known top-level tag ──
406
443
 
407
444
  # usersays_correct
408
- raw_val = _extract_tag_content(body, "usersays_correct")
445
+ raw_val = _extract_tag_content(body, "usersays_correct", conservative=conservative)
409
446
  parsed.usersays_correct = _safe_strip(raw_val)
410
447
 
411
448
  # task_plan
412
- raw_val = _extract_tag_content(body, "task_plan")
449
+ raw_val = _extract_tag_content(body, "task_plan", conservative=conservative)
413
450
  parsed.task_plan = _safe_strip(raw_val)
414
451
 
415
452
  # response
416
- raw_val = _extract_tag_content(body, "response")
453
+ raw_val = _extract_tag_content(body, "response", conservative=conservative)
417
454
  parsed.response = _safe_strip(raw_val)
418
455
 
419
456
  # recall
420
- raw_val = _extract_tag_content(body, "recall")
457
+ raw_val = _extract_tag_content(body, "recall", conservative=conservative)
421
458
  parsed.recall = _safe_strip(raw_val)
422
459
 
423
460
  # knowledge
424
- raw_val = _extract_tag_content(body, "knowledge")
461
+ raw_val = _extract_tag_content(body, "knowledge", conservative=conservative)
425
462
  parsed.knowledge = _safe_strip(raw_val)
426
463
 
427
464
  # askuser (also try alias ask_user)
428
- raw_val = _extract_tag_content(body, "askuser")
465
+ raw_val = _extract_tag_content(body, "askuser", conservative=conservative)
429
466
  if not raw_val.strip():
430
- raw_val = _extract_tag_content(body, "ask_user")
467
+ raw_val = _extract_tag_content(body, "ask_user", conservative=conservative)
431
468
  parsed.ask_user = _safe_strip(raw_val)
432
469
 
433
470
  # get_knowledge
434
- raw_val = _extract_tag_content(body, "get_knowledge")
471
+ raw_val = _extract_tag_content(body, "get_knowledge", conservative=conservative)
435
472
  parsed.get_knowledge = _safe_strip(raw_val)
436
473
 
437
474
  # finish
438
- raw_val = _extract_tag_content(body, "finish")
475
+ raw_val = _extract_tag_content(body, "finish", conservative=conservative)
439
476
  parsed.finish = _parse_bool(raw_val, False)
440
477
 
441
478
  # finish_reason
442
- raw_val = _extract_tag_content(body, "finish_reason")
479
+ raw_val = _extract_tag_content(body, "finish_reason", conservative=conservative)
443
480
  parsed.finish_reason = _safe_strip(raw_val)
444
481
 
445
482
  # next_step
446
- raw_val = _extract_tag_content(body, "next_step")
483
+ raw_val = _extract_tag_content(body, "next_step", conservative=conservative)
447
484
  parsed.next_step = _safe_strip(raw_val)
448
485
 
449
486
  # mainsubject [v1.15.8] 会话标题自动命名
450
- raw_val = _extract_tag_content(body, "mainsubject")
487
+ raw_val = _extract_tag_content(body, "mainsubject", conservative=conservative)
451
488
  parsed.mainsubject = _safe_strip(raw_val)
452
489
 
453
490
  # ── Step 3: Parse <remember> (may contain <type> and <content>) ──
454
- remember_raw = _extract_tag_content(body, "remember")
491
+ remember_raw = _extract_tag_content(body, "remember", conservative=conservative)
455
492
  if remember_raw.strip():
456
493
  # Try structured format: <type>global</type><content>...</content>
457
- type_val = _extract_tag_content(remember_raw, "type", REMEMBER_INNER_TAGS)
458
- content_val = _extract_tag_content(remember_raw, "content", REMEMBER_INNER_TAGS)
494
+ type_val = _extract_tag_content(remember_raw, "type", REMEMBER_INNER_TAGS, conservative=conservative)
495
+ content_val = _extract_tag_content(remember_raw, "content", REMEMBER_INNER_TAGS, conservative=conservative)
459
496
 
460
497
  if content_val.strip():
461
498
  mem_type = _safe_strip(type_val) or "session"
@@ -469,9 +506,9 @@ def _custom_parse(raw_text: str) -> ParsedOutput:
469
506
  parsed.remember_type = "session"
470
507
 
471
508
  # ── Step 4: Parse <toolstocal> → list of tool dicts ──
472
- toolstocal_raw = _extract_tag_content(body, "toolstocal")
509
+ toolstocal_raw = _extract_tag_content(body, "toolstocal", conservative=conservative)
473
510
  if toolstocal_raw.strip():
474
- parsed.tools_to_call = _parse_toolstocal(toolstocal_raw)
511
+ parsed.tools_to_call = _parse_toolstocal(toolstocal_raw, conservative=conservative)
475
512
 
476
513
  # ── Step 5: Determine parse success ──
477
514
  has_content = bool(
@@ -516,31 +553,32 @@ def _custom_parse(raw_text: str) -> ParsedOutput:
516
553
  return parsed
517
554
 
518
555
 
519
- def _parse_toolstocal(toolstocal_content: str) -> List[Dict[str, Any]]:
556
+ def _parse_toolstocal(toolstocal_content: str, *, conservative: bool = False) -> List[Dict[str, Any]]:
520
557
  """Parse ``<toolstocal>`` body into a list of tool descriptors."""
521
558
  tools: List[Dict[str, Any]] = []
522
559
 
523
560
  tool_blocks = _extract_all_tag_blocks(
524
- toolstocal_content, "tool", parent_close_tag="</toolstocal>"
561
+ toolstocal_content, "tool", parent_close_tag="</toolstocal>",
562
+ conservative=conservative,
525
563
  )
526
564
 
527
565
  for block in tool_blocks:
528
566
  tool: Dict[str, Any] = {
529
567
  "beforecalltext": _safe_strip(
530
- _extract_tag_content(block, "beforecalltext", TOOL_INNER_TAGS)
568
+ _extract_tag_content(block, "beforecalltext", TOOL_INNER_TAGS, conservative=conservative)
531
569
  ),
532
570
  "toolname": _safe_strip(
533
- _extract_tag_content(block, "toolname", TOOL_INNER_TAGS)
571
+ _extract_tag_content(block, "toolname", TOOL_INNER_TAGS, conservative=conservative)
534
572
  ),
535
573
  "parms": _safe_strip(
536
- _extract_tag_content(block, "parms", TOOL_INNER_TAGS)
574
+ _extract_tag_content(block, "parms", TOOL_INNER_TAGS, conservative=conservative)
537
575
  ),
538
576
  "timeout": _parse_int(
539
- _extract_tag_content(block, "timeout", TOOL_INNER_TAGS),
577
+ _extract_tag_content(block, "timeout", TOOL_INNER_TAGS, conservative=conservative),
540
578
  _DEFAULT_TIMEOUT,
541
579
  ),
542
580
  "callback": _parse_bool(
543
- _extract_tag_content(block, "callback", TOOL_INNER_TAGS),
581
+ _extract_tag_content(block, "callback", TOOL_INNER_TAGS, conservative=conservative),
544
582
  _DEFAULT_CALLBACK,
545
583
  ),
546
584
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.15.34",
3
+ "version": "1.15.35",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
package/requirements.txt CHANGED
@@ -11,8 +11,9 @@ requests>=2.31.0
11
11
  # ============================================================
12
12
  # 技能系统 - 搜索
13
13
  # ============================================================
14
- # 搜索使用 requests + BeautifulSoup (html.parser),无需额外库
14
+ duckduckgo-search>=6.0.0
15
15
  beautifulsoup4>=4.12.0
16
+ lxml>=5.0.0
16
17
  psutil>=5.9.0
17
18
 
18
19
  # ============================================================
@@ -54,11 +55,6 @@ edge-tts>=6.1.0
54
55
  # ============================================================
55
56
  faster-whisper>=1.0.0
56
57
 
57
- # ============================================================
58
- # 音频处理 (TTS 服务端处理)
59
- # ============================================================
60
- pydub>=0.25.1
61
-
62
58
  # ============================================================
63
59
  # Anthropic Claude (可选)
64
60
  # ============================================================
package/setup.py CHANGED
@@ -27,19 +27,22 @@ setup(
27
27
  "openai>=1.12.0",
28
28
  "aiohttp>=3.9.0",
29
29
  "requests>=2.31.0",
30
- # 搜索 (BeautifulSoup + html.parser)
30
+ # 搜索
31
+ "duckduckgo-search>=6.0.0",
31
32
  "beautifulsoup4>=4.12.0",
33
+ "lxml>=5.0.0",
32
34
  "psutil>=5.9.0",
33
- # 图像处理
35
+ # 系统托盘
36
+ "pystray>=0.19.5",
34
37
  "Pillow>=10.0.0",
35
38
  # 语音合成
36
39
  "edge-tts>=6.1.0",
37
40
  # 语音识别 (本地 STT)
38
41
  "faster-whisper>=1.0.0",
39
- # 音频处理
40
- "pydub>=0.25.1",
42
+ # 浏览器自动化 (ChromeDev MCP, 无需 Playwright)
41
43
  # 桌面 GUI 自动化 (内置技能)
42
44
  "pynput>=1.7.6",
45
+ "pygetwindow>=0.0.9",
43
46
  "mss>=9.0.0",
44
47
  ],
45
48
  extras_require={
@@ -47,8 +50,6 @@ setup(
47
50
  "discord": ["discord.py>=2.3.0"],
48
51
  "anthropic": ["anthropic>=0.18.0"],
49
52
  "communication": ["cryptography>=41.0.0", "websockets>=12.0"],
50
- "tray": ["pystray>=0.19.5"],
51
- "gui": ["pygetwindow>=0.0.9"],
52
53
  "voice": ["faster-whisper>=1.0.0"],
53
54
  "all": [
54
55
  "python-telegram-bot>=21.0",
@@ -56,8 +57,6 @@ setup(
56
57
  "anthropic>=0.18.0",
57
58
  "cryptography>=41.0.0",
58
59
  "websockets>=12.0",
59
- "pystray>=0.19.5",
60
- "pygetwindow>=0.0.9",
61
60
  "faster-whisper>=1.0.0",
62
61
  ],
63
62
  },
package/start.js CHANGED
@@ -264,11 +264,11 @@ function parseRequirements(reqFile) {
264
264
  // 特殊映射: pip 包名和 import 名不一致的
265
265
  const importMap = {
266
266
  "beautifulsoup4": "bs4",
267
+ "py_getwindow": "pygetwindow",
267
268
  "python_telegram_bot": "telegram",
268
269
  "pillow": "PIL",
269
270
  "psutil": "psutil",
270
271
  "edge_tts": "edge_tts",
271
- "pydub": "pydub",
272
272
  };
273
273
  modules.push(importMap[pkg] || pkg);
274
274
  }
@@ -286,12 +286,14 @@ const CORE_PACKAGES = [
286
286
  "openai>=1.12.0",
287
287
  "aiohttp>=3.9.0",
288
288
  "requests>=2.31.0",
289
+ "duckduckgo-search>=6.0.0",
289
290
  "beautifulsoup4>=4.12.0",
291
+ "lxml>=5.0.0",
290
292
  "psutil>=5.9.0",
293
+ "pystray>=0.19.5",
291
294
  "Pillow>=10.0.0",
292
295
  "edge-tts>=6.1.0",
293
296
  "faster-whisper>=1.0.0",
294
- "pydub>=0.25.1",
295
297
  "anthropic>=0.18.0",
296
298
  ];
297
299
 
package/start.sh CHANGED
@@ -137,8 +137,8 @@ install_deps() {
137
137
  # fallback: 直接安装核心包
138
138
  "$VENV_PY" -m pip install --quiet \
139
139
  "openai>=1.12.0" "aiohttp>=3.9.0" "requests>=2.31.0" \
140
- "beautifulsoup4>=4.12.0" \
141
- "psutil>=5.9.0" --disable-pip-version-check 2>/dev/null || true
140
+ "duckduckgo-search>=6.0.0" "beautifulsoup4>=4.12.0" \
141
+ "lxml>=5.0.0" "psutil>=5.9.0" --disable-pip-version-check 2>/dev/null || true
142
142
  success "核心依赖安装完成"
143
143
  fi
144
144
  }