myagent-ai 1.10.4 → 1.10.6

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.
@@ -53,6 +53,8 @@ class MainAgent(BaseAgent):
53
53
  <get_knowledge>下一轮执行时需要从知识库搜索获得的知识,填写检索关键词或描述。如context中已包含充足的<knowledge>内容,则为空。如需更多专业知识支撑,则填写相关搜索词。</get_knowledge>
54
54
  <askuser>需要询问用户的内容,如无,则为空</askuser>
55
55
  <finish>true/false,是否结束循环调用llm。如"askuser"为非空,则"finish"输出true。否则,根据"context"判断任务是否已完成,是否结束llm回调</finish>
56
+ <finish_reason>当 finish=true 时必填,详细说明为什么现在结束任务(如:任务已完成/需要用户补充信息/信息不足无法继续等)。finish=false 时为空。</finish_reason>
57
+ <next_step>当 finish=false 时必填,描述下一步计划做什么(简洁明了,1-2句话)。finish=true 时为空。</next_step>
56
58
 
57
59
  </output>
58
60
 
@@ -70,7 +72,9 @@ class MainAgent(BaseAgent):
70
72
  11. <get_knowledge>: 如果当前 <knowledge> 内容不足以完成任务,填写需要从知识库搜索的关键词;否则为空
71
73
  12. <askuser>: 当信息不足需要用户补充时,在此填写要问的问题
72
74
  13. <finish>: 当任务已完成或需要等待用户回应时为 true;否则为 false 继续执行
73
- 14. 使用中文输出所有内容
75
+ 14. <finish_reason>: **finish=true 时必须填写**,详细说明结束原因(任务完成/等待用户/信息不足/无法处理等)
76
+ 15. <next_step>: **finish=false 时必须填写**,描述下一步计划做什么,要求简洁明确(1-2句话)
77
+ 16. 使用中文输出所有内容
74
78
 
75
79
  ## 工具选择指南
76
80
  - **搜索信息**: 用 `web_search`(返回标题+URL+摘要),不要用 browser_open
@@ -420,7 +424,7 @@ class MainAgent(BaseAgent):
420
424
  if all_tool_outputs:
421
425
  messages.append(Message(
422
426
  role="user",
423
- content=f"[上一轮工具执行结果汇总]\n{truncate_str(all_tool_outputs, 15000)}"
427
+ content=f"[上一轮工具执行结果汇总]\n{truncate_str(all_tool_outputs, 30000)}"
424
428
  ))
425
429
  all_tool_outputs = ""
426
430
 
@@ -464,6 +468,8 @@ class MainAgent(BaseAgent):
464
468
  "remember": truncate_str(parsed.remember, 200),
465
469
  "ask_user": truncate_str(parsed.ask_user, 200),
466
470
  "finish": parsed.finish,
471
+ "finish_reason": truncate_str(parsed.finish_reason, 200),
472
+ "next_step": truncate_str(parsed.next_step, 200),
467
473
  "parse_success": parsed.parse_success,
468
474
  }},
469
475
  stream_callback,
@@ -700,20 +706,59 @@ class MainAgent(BaseAgent):
700
706
 
701
707
  # 发送工具结果事件
702
708
  # 提取实际输出:SkillResult 有 output/message/data,ExecResult 有 stdout/stderr
709
+ def _format_data_for_llm(data):
710
+ """将结构化 data 格式化为 LLM 可读的文本"""
711
+ if data is None:
712
+ return ""
713
+ if isinstance(data, str):
714
+ return data
715
+ if isinstance(data, dict):
716
+ # 搜索结果列表格式 (web_search)
717
+ results = data.get("results")
718
+ if isinstance(results, list):
719
+ lines = []
720
+ for i, r in enumerate(results, 1):
721
+ title = r.get("title", "")
722
+ url = r.get("url", "")
723
+ snippet = r.get("snippet", "")
724
+ lines.append(f"{i}. {title}\n URL: {url}\n {snippet}")
725
+ return "\n".join(lines)
726
+ # 网页内容格式 (web_read)
727
+ if "url" in data and "content" in data:
728
+ title = data.get("title", "")
729
+ content = data.get("content", "")
730
+ lines = [f"标题: {title}", f"URL: {data['url']}", f"内容:\n{content}"]
731
+ return "\n".join(lines)
732
+ # 通用 dict: key-value 格式
733
+ parts = []
734
+ for k, v in data.items():
735
+ if k == "results":
736
+ continue # 已在上面处理
737
+ parts.append(f"{k}: {v}")
738
+ return "\n".join(parts) if parts else str(data)
739
+ if isinstance(data, list):
740
+ return "\n".join(str(item) for item in data)
741
+ return str(data)
742
+
703
743
  def _extract_tool_output(tr):
704
744
  """从工具结果中提取实际输出文本"""
745
+ # 优先使用 output 字段 (技能明确设置的输出)
705
746
  out = tr.get("output", "")
706
747
  if out:
707
748
  return out
749
+ # 如果 output 为空,尝试智能格式化 data 字段
750
+ data = tr.get("data")
751
+ if data is not None:
752
+ formatted = _format_data_for_llm(data)
753
+ if formatted:
754
+ return formatted
755
+ # 降级到 message / stdout / error
708
756
  out = tr.get("message", "")
709
757
  if out:
710
758
  return out
711
759
  out = tr.get("stdout", "")
712
760
  if out:
713
761
  return out
714
- data = tr.get("data")
715
- if data is not None:
716
- return str(data) if not isinstance(data, str) else data
717
762
  return tr.get("error", "")
718
763
 
719
764
  tool_output_text = _extract_tool_output(tool_result)
@@ -745,16 +790,18 @@ class MainAgent(BaseAgent):
745
790
  need_callback = True
746
791
 
747
792
  output_str = tool_output_text
793
+ # 搜索和网页读取类工具允许更长的输出
794
+ _max_output = 6000 if tool_name in ("web_search", "web_read", "url_read") else 3000
748
795
  tool_outputs_parts.append(
749
796
  f"### {before_call}\n"
750
797
  f"**工具**: {tool_name}\n"
751
798
  f"**结果**: {'成功' if tool_result.get('success') else '失败'}\n"
752
- f"{truncate_str(output_str, 2000)}\n"
799
+ f"{truncate_str(output_str, _max_output)}\n"
753
800
  )
754
801
 
755
802
  conversation_history.append(Message(
756
803
  role="assistant",
757
- content=f"执行工具 {tool_name}:\n{truncate_str(output_str, 3000)}",
804
+ content=f"执行工具 {tool_name}:\n{truncate_str(output_str, _max_output)}",
758
805
  ))
759
806
  conversation_history.append(Message(
760
807
  role="user",
@@ -86,6 +86,8 @@ class ParsedOutput:
86
86
  get_knowledge: Knowledge search keywords for the next loop iteration.
87
87
  The ContextBuilder will use this to perform RAG retrieval.
88
88
  finish: When ``True`` the execution loop should terminate.
89
+ finish_reason: When finish=True, explains why the task is ending.
90
+ next_step: When finish=False, describes what to do next.
89
91
  raw_text: The verbatim raw text returned by the LLM.
90
92
  parse_success: Whether the XML was parsed successfully (``True``)
91
93
  or the regex fallback was used (``False``).
@@ -99,6 +101,8 @@ class ParsedOutput:
99
101
  ask_user: str = ""
100
102
  get_knowledge: str = ""
101
103
  finish: bool = False
104
+ finish_reason: str = ""
105
+ next_step: str = ""
102
106
  response: str = "" # 模型对用户的直接回复(友好自然的话语)
103
107
  raw_text: str = ""
104
108
  parse_success: bool = False
@@ -363,6 +367,8 @@ def _parse_xml_content(xml_content: str) -> ParsedOutput:
363
367
  parsed.ask_user = _safe_strip(root.findtext("askuser"))
364
368
  parsed.get_knowledge = _safe_strip(root.findtext("get_knowledge"))
365
369
  parsed.finish = _parse_bool(root.findtext("finish"), False)
370
+ parsed.finish_reason = _safe_strip(root.findtext("finish_reason"))
371
+ parsed.next_step = _safe_strip(root.findtext("next_step"))
366
372
  parsed.response = _safe_strip(root.findtext("response"))
367
373
 
368
374
  return parsed
@@ -393,6 +399,8 @@ def _fallback_regex_parse(raw_text: str) -> ParsedOutput:
393
399
  parsed.ask_user = _safe_strip(tag_map.get("askuser"))
394
400
  parsed.get_knowledge = _safe_strip(tag_map.get("get_knowledge"))
395
401
  parsed.finish = _parse_bool(tag_map.get("finish"), False)
402
+ parsed.finish_reason = _safe_strip(tag_map.get("finish_reason"))
403
+ parsed.next_step = _safe_strip(tag_map.get("next_step"))
396
404
  parsed.response = _safe_strip(tag_map.get("response"))
397
405
 
398
406
  # For toolstocal we attempt to find individual <tool> blocks.
@@ -482,6 +490,8 @@ def parse_output(raw_text: str) -> ParsedOutput:
482
490
  parsed.ask_user = _safe_strip(root.findtext("askuser"))
483
491
  parsed.get_knowledge = _safe_strip(root.findtext("get_knowledge"))
484
492
  parsed.finish = _parse_bool(root.findtext("finish"), False)
493
+ parsed.finish_reason = _safe_strip(root.findtext("finish_reason"))
494
+ parsed.next_step = _safe_strip(root.findtext("next_step"))
485
495
  parsed.response = _safe_strip(root.findtext("response"))
486
496
  return parsed
487
497
  except ET.ParseError:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.10.4",
3
+ "version": "1.10.6",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
package/web/api_server.py CHANGED
@@ -2357,6 +2357,9 @@ class ApiServer:
2357
2357
  pass
2358
2358
  return web.json_response({**agent_info, "sessions": sessions})
2359
2359
 
2360
+ # Internal keys that should not appear in chat history UI
2361
+ _HIDDEN_KEYS = {"llm_output", "tool_call", "tool_result"}
2362
+
2360
2363
  async def handle_get_messages(self, request):
2361
2364
  sid = request.match_info["sid"]
2362
2365
  if not self.core.memory: return web.json_response([])
@@ -2364,6 +2367,8 @@ class ApiServer:
2364
2367
  offset = int(request.query.get("offset", 0))
2365
2368
  entries = self.core.memory.get_conversation(sid, limit=limit + offset)
2366
2369
  entries = entries[offset:]
2370
+ # Filter out internal entries (LLM raw output, tool calls/results)
2371
+ entries = [e for e in entries if (e.key or "") not in self._HIDDEN_KEYS]
2367
2372
  return web.json_response([{"role": e.role, "content": e.content, "time": e.created_at, "key": e.key or ""} for e in entries])
2368
2373
 
2369
2374
  async def handle_get_messages_query(self, request):
@@ -2376,6 +2381,8 @@ class ApiServer:
2376
2381
  offset = int(request.query.get("offset", 0))
2377
2382
  entries = self.core.memory.get_conversation(sid, limit=limit + offset)
2378
2383
  entries = entries[offset:]
2384
+ # Filter out internal entries (LLM raw output, tool calls/results)
2385
+ entries = [e for e in entries if (e.key or "") not in self._HIDDEN_KEYS]
2379
2386
  return web.json_response([{"role": e.role, "content": e.content, "time": e.created_at, "key": e.key or ""} for e in entries])
2380
2387
 
2381
2388
  async def handle_delete_session(self, request):
@@ -1765,9 +1765,14 @@ async function selectSession(id) {
1765
1765
  const loaded = (Array.isArray(data) ? data : []).filter(function(m) {
1766
1766
  return m && (m.role === 'user' || m.role === 'assistant' || m.role === 'tool');
1767
1767
  }).map(function(m) {
1768
+ var content = (m.content != null) ? String(m.content) : '';
1769
+ // Strip XML tags from assistant messages (backend may store raw LLM XML output)
1770
+ if (m.role === 'assistant' && content && content.trim().startsWith('<')) {
1771
+ content = (typeof _stripXmlTags === 'function') ? _stripXmlTags(content) : content;
1772
+ }
1768
1773
  return {
1769
1774
  role: m.role || 'assistant',
1770
- content: (m.content != null) ? String(m.content) : '',
1775
+ content: content,
1771
1776
  time: m.time || m.created_at || '',
1772
1777
  key: m.key || '',
1773
1778
  };
@@ -1824,9 +1829,14 @@ async function loadMoreMessages() {
1824
1829
  const loaded = data.filter(function(m) {
1825
1830
  return m && (m.role === 'user' || m.role === 'assistant' || m.role === 'tool');
1826
1831
  }).map(function(m) {
1832
+ var content = (m.content != null) ? String(m.content) : '';
1833
+ // Strip XML tags from assistant messages (backend may store raw LLM XML output)
1834
+ if (m.role === 'assistant' && content && content.trim().startsWith('<')) {
1835
+ content = (typeof _stripXmlTags === 'function') ? _stripXmlTags(content) : content;
1836
+ }
1827
1837
  return {
1828
1838
  role: m.role || 'assistant',
1829
- content: (m.content != null) ? String(m.content) : '',
1839
+ content: content,
1830
1840
  time: m.time || m.created_at || '',
1831
1841
  };
1832
1842
  });
@@ -305,22 +305,25 @@ function updateStreamingMessage(msgIdx) {
305
305
  const container = document.getElementById('messagesInner');
306
306
  if (!container) return;
307
307
 
308
- // Find or create the streaming message row
309
- const rows = container.querySelectorAll('.message-row.assistant');
308
+ // Find the streaming message row by counting ALL message rows (not just assistant)
309
+ // msgIdx is the global index in state.messages, so we count all rows to match
310
+ const allRows = container.querySelectorAll('.message-row');
310
311
  let targetRow = null;
311
- // Count assistant rows to find the right one
312
- let assistantCount = 0;
313
- for (const row of rows) {
314
- assistantCount++;
315
- if (assistantCount === msgIdx + 1) {
312
+ let rowCount = 0;
313
+ for (const row of allRows) {
314
+ if (rowCount === msgIdx) {
316
315
  targetRow = row;
317
316
  break;
318
317
  }
318
+ rowCount++;
319
319
  }
320
320
 
321
321
  // Fallback: if we can't find by count, use last assistant row
322
- if (!targetRow && rows.length > 0) {
323
- targetRow = rows[rows.length - 1];
322
+ if (!targetRow) {
323
+ const assistantRows = container.querySelectorAll('.message-row.assistant');
324
+ if (assistantRows.length > 0) {
325
+ targetRow = assistantRows[assistantRows.length - 1];
326
+ }
324
327
  }
325
328
 
326
329
  if (!targetRow) {
@@ -343,12 +346,32 @@ function updateStreamingMessage(msgIdx) {
343
346
  }
344
347
  }
345
348
  if (msg.reasoning) {
346
- // Count words/chars during streaming for live counter
347
349
  const reasoningLen = msg.reasoning.length;
348
350
  const reasoningWordCount = msg.streaming
349
351
  ? '<span class="thought-word-count">' + reasoningLen + ' 字</span>'
350
352
  : '';
351
- const reasoningHtml = `<details class="thought-block ${msg.streaming ? 'streaming' : ''}" ${msg.streaming ? 'open' : ''}>
353
+ if (reasoningDetails) {
354
+ // Incremental update: only update word count and badge, append new text to content
355
+ const label = reasoningDetails.querySelector('.thought-label');
356
+ if (label) label.innerHTML = '模型推理过程' + reasoningWordCount;
357
+ const badge = reasoningDetails.querySelector('.thought-badge');
358
+ if (badge) badge.textContent = msg.streaming ? '推理中...' : '已完成';
359
+ // Incremental text append for streaming (avoid full markdown rebuild)
360
+ const thoughtContent = reasoningDetails.querySelector('.thought-content');
361
+ if (thoughtContent && msg.streaming) {
362
+ const prevLen = reasoningDetails._lastReasoningLen || 0;
363
+ if (msg.reasoning.length > prevLen) {
364
+ const newText = msg.reasoning.substring(prevLen);
365
+ thoughtContent.insertAdjacentHTML('beforeend', renderMarkdown(newText));
366
+ reasoningDetails._lastReasoningLen = msg.reasoning.length;
367
+ }
368
+ } else if (thoughtContent && !msg.streaming) {
369
+ // Final render once streaming stops
370
+ thoughtContent.innerHTML = renderMarkdown(msg.reasoning);
371
+ reasoningDetails._lastReasoningLen = msg.reasoning.length;
372
+ }
373
+ } else {
374
+ const reasoningHtml = `<details class="thought-block ${msg.streaming ? 'streaming' : ''}" ${msg.streaming ? 'open' : ''}>
352
375
  <summary>
353
376
  <span class="thought-icon">💡</span>
354
377
  <span class="thought-label">模型推理过程${reasoningWordCount}</span>
@@ -356,10 +379,10 @@ function updateStreamingMessage(msgIdx) {
356
379
  </summary>
357
380
  <div class="thought-content">${renderMarkdown(msg.reasoning)}</div>
358
381
  </details>`;
359
- if (reasoningDetails) {
360
- reasoningDetails.outerHTML = reasoningHtml;
361
- } else {
362
382
  contentArea.insertAdjacentHTML('afterbegin', reasoningHtml);
383
+ // Set initial length tracking
384
+ const newBlock = contentArea.querySelector(':scope > .thought-block');
385
+ if (newBlock) newBlock._lastReasoningLen = msg.reasoning.length;
363
386
  }
364
387
  }
365
388
 
@@ -378,7 +401,26 @@ function updateStreamingMessage(msgIdx) {
378
401
  const thoughtWordCount = msg.streaming
379
402
  ? '<span class="thought-word-count">' + thoughtLen + ' 字</span>'
380
403
  : '';
381
- const thoughtHtml = `<details class="thought-block ${msg.streaming ? 'streaming' : ''}" ${msg.streaming ? 'open' : ''}>
404
+ if (thoughtBlock) {
405
+ // Incremental update for thought block too
406
+ const label = thoughtBlock.querySelector('.thought-label');
407
+ if (label) label.innerHTML = 'Agent 思考过程' + thoughtWordCount;
408
+ const badge = thoughtBlock.querySelector('.thought-badge');
409
+ if (badge) badge.textContent = msg.streaming ? '思考中...' : '已完成';
410
+ const thoughtContent = thoughtBlock.querySelector('.thought-content');
411
+ if (thoughtContent && msg.streaming) {
412
+ const prevLen = thoughtBlock._lastThoughtLen || 0;
413
+ if (msg.thought.length > prevLen) {
414
+ const newText = msg.thought.substring(prevLen);
415
+ thoughtContent.insertAdjacentHTML('beforeend', renderMarkdown(newText));
416
+ thoughtBlock._lastThoughtLen = msg.thought.length;
417
+ }
418
+ } else if (thoughtContent && !msg.streaming) {
419
+ thoughtContent.innerHTML = renderMarkdown(msg.thought);
420
+ thoughtBlock._lastThoughtLen = msg.thought.length;
421
+ }
422
+ } else {
423
+ const thoughtHtml = `<details class="thought-block ${msg.streaming ? 'streaming' : ''}" ${msg.streaming ? 'open' : ''}>
382
424
  <summary>
383
425
  <span class="thought-icon">💭</span>
384
426
  <span class="thought-label">Agent 思考过程${thoughtWordCount}</span>
@@ -386,16 +428,14 @@ function updateStreamingMessage(msgIdx) {
386
428
  </summary>
387
429
  <div class="thought-content">${renderMarkdown(msg.thought)}</div>
388
430
  </details>`;
389
- if (thoughtBlock) {
390
- thoughtBlock.outerHTML = thoughtHtml;
391
- } else {
392
- // Insert after reasoning block if exists, otherwise at beginning
393
431
  const existingReasoning = contentArea.querySelectorAll('.thought-block');
394
432
  if (existingReasoning.length > 0) {
395
433
  existingReasoning[existingReasoning.length - 1].insertAdjacentHTML('afterend', thoughtHtml);
396
434
  } else {
397
435
  contentArea.insertAdjacentHTML('afterbegin', thoughtHtml);
398
436
  }
437
+ const newBlock = contentArea.querySelectorAll('.thought-block');
438
+ if (newBlock.length > 0) newBlock[newBlock.length - 1]._lastThoughtLen = msg.thought.length;
399
439
  }
400
440
  }
401
441
 
@@ -417,7 +457,26 @@ function updateStreamingMessage(msgIdx) {
417
457
  const v2WordCount = msg.streaming
418
458
  ? '<span class="thought-word-count">' + v2Len + ' 字</span>'
419
459
  : '';
420
- const v2Html = `<details class="thought-block ${msg.streaming ? 'streaming' : ''}" ${msg.streaming ? 'open' : ''}>
460
+ if (v2ReasoningBlock) {
461
+ // Incremental update for V2 reasoning block
462
+ const label = v2ReasoningBlock.querySelector('.thought-label');
463
+ if (label) label.innerHTML = 'V2 推理过程' + v2WordCount;
464
+ const badge = v2ReasoningBlock.querySelector('.thought-badge');
465
+ if (badge) badge.textContent = msg.streaming ? '推理中...' : '已完成';
466
+ const thoughtContent = v2ReasoningBlock.querySelector('.thought-content');
467
+ if (thoughtContent && msg.streaming) {
468
+ const prevLen = v2ReasoningBlock._lastV2Len || 0;
469
+ if (msg._v2Reasoning.length > prevLen) {
470
+ const newText = msg._v2Reasoning.substring(prevLen);
471
+ thoughtContent.insertAdjacentHTML('beforeend', renderMarkdown(newText));
472
+ v2ReasoningBlock._lastV2Len = msg._v2Reasoning.length;
473
+ }
474
+ } else if (thoughtContent && !msg.streaming) {
475
+ thoughtContent.innerHTML = renderMarkdown(msg._v2Reasoning);
476
+ v2ReasoningBlock._lastV2Len = msg._v2Reasoning.length;
477
+ }
478
+ } else if (!msg.thought) {
479
+ const v2Html = `<details class="thought-block ${msg.streaming ? 'streaming' : ''}" ${msg.streaming ? 'open' : ''}>
421
480
  <summary>
422
481
  <span class="thought-icon">🧠</span>
423
482
  <span class="thought-label">V2 推理过程${v2WordCount}</span>
@@ -425,10 +484,9 @@ function updateStreamingMessage(msgIdx) {
425
484
  </summary>
426
485
  <div class="thought-content">${renderMarkdown(msg._v2Reasoning)}</div>
427
486
  </details>`;
428
- if (v2ReasoningBlock) {
429
- v2ReasoningBlock.outerHTML = v2Html;
430
- } else if (!msg.thought) {
431
487
  contentArea.insertAdjacentHTML('afterbegin', v2Html);
488
+ const newBlock = contentArea.querySelector(':scope > .thought-block');
489
+ if (newBlock) newBlock._lastV2Len = msg._v2Reasoning.length;
432
490
  }
433
491
  }
434
492
  }