myagent-ai 1.11.0 → 1.11.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.
@@ -293,7 +293,9 @@ class MainAgent(BaseAgent):
293
293
  auto_kb_dir.mkdir(parents=True, exist_ok=True)
294
294
 
295
295
  # 使用 session_id 作为文件名(取前8位避免过长)
296
- safe_session = session_id.replace("-", "")[:8] if session_id else "default"
296
+ # 注意: session_id 可能包含 '/' (来自 agent_path "coder/python-expert"),
297
+ # 必须替换为安全字符,避免创建意外的子目录
298
+ safe_session = session_id.replace("-", "").replace("/", "_")[:12] if session_id else "default"
297
299
  kb_file = auto_kb_dir / f"{safe_session}.md"
298
300
 
299
301
  now_str = datetime.now().strftime("%Y-%m-%d %H:%M")
@@ -69,6 +69,8 @@ class ContextBuilder:
69
69
  self.skill_registry = skill_registry
70
70
  self.knowledge_base_dir = knowledge_base_dir
71
71
  self.max_dialog_chars = max_dialog_chars
72
+ # Agent 专属知识库目录(由调用方动态设置,优先于组织知识库)
73
+ self.agent_knowledge_dir: Optional[str] = None
72
74
 
73
75
  # =========================================================================
74
76
  # 公共接口
@@ -248,8 +250,9 @@ class ContextBuilder:
248
250
  """
249
251
  构建 <knowledge> 段落 —— 知识库 RAG 检索结果。
250
252
 
251
- 使用 KnowledgeRAG (TF-IDF + 余弦相似度) 检索与查询最相关的
252
- 知识片段。当 knowledge_base_dir 未设置或目录不存在时,输出占位提示。
253
+ 搜索优先级:
254
+ 1. Agent 专属知识库 (agent_knowledge_dir) —— 如果有且非空
255
+ 2. 组织知识库 (knowledge_base_dir) —— 兜底
253
256
 
254
257
  Args:
255
258
  query: 搜索查询文本(通常为 <get_knowledge> 内容或用户消息)
@@ -257,31 +260,46 @@ class ContextBuilder:
257
260
  Returns:
258
261
  <knowledge> XML 段落字符串
259
262
  """
260
- if not self.knowledge_base_dir:
263
+ # 优先搜索 Agent 专属知识库
264
+ if self.agent_knowledge_dir:
265
+ agent_result = self._search_knowledge_dir(self.agent_knowledge_dir, query, top_k=5)
266
+ if agent_result:
267
+ return agent_result
268
+
269
+ # 回退到组织知识库
270
+ if self.knowledge_base_dir:
271
+ org_result = self._search_knowledge_dir(self.knowledge_base_dir, query, top_k=5)
272
+ if org_result:
273
+ return org_result
274
+
275
+ # 都没有配置或都为空
276
+ if not self.knowledge_base_dir and not self.agent_knowledge_dir:
261
277
  return "<knowledge>\n(知识库未配置)\n</knowledge>"
278
+ return "<knowledge>\n(未找到相关知识)\n</knowledge>"
279
+
280
+ def _search_knowledge_dir(self, kb_dir: str, query: str, top_k: int = 5) -> str:
281
+ """在指定知识库目录中执行 RAG 搜索并格式化结果"""
282
+ import os as _os
262
283
 
263
284
  if not query.strip():
264
- return "<knowledge>\n(无检索关键词)\n</knowledge>"
285
+ return ""
286
+
287
+ if not kb_dir or not _os.path.isdir(kb_dir):
288
+ return ""
265
289
 
266
290
  try:
267
291
  from knowledge.rag import KnowledgeRAG
268
- import os as _os
269
-
270
- kb_path = self.knowledge_base_dir
271
- if not _os.path.isdir(kb_path):
272
- logger.debug(f"知识库目录不存在: {kb_path}")
273
- return "<knowledge>\n(知识库目录不存在)\n</knowledge>"
274
292
 
275
- rag = KnowledgeRAG(kb_dir=kb_path)
293
+ rag = KnowledgeRAG(kb_dir=kb_dir)
276
294
  rag.build_index()
277
295
 
278
296
  if rag.total_chunks == 0:
279
- return "<knowledge>\n(知识库为空,无索引内容)\n</knowledge>"
297
+ return ""
280
298
 
281
- results = rag.search(query, top_k=5)
299
+ results = rag.search(query, top_k=top_k)
282
300
 
283
301
  if not results:
284
- return "<knowledge>\n(未找到相关知识)\n</knowledge>"
302
+ return ""
285
303
 
286
304
  lines: List[str] = ["<knowledge>"]
287
305
  for i, chunk in enumerate(results, 1):
@@ -297,8 +315,8 @@ class ContextBuilder:
297
315
  return "\n".join(lines)
298
316
 
299
317
  except Exception as e:
300
- logger.warning(f"知识库 RAG 检索失败: {e}")
301
- return "<knowledge>\n(知识库检索不可用)\n</knowledge>"
318
+ logger.warning(f"知识库 RAG 检索失败 ({kb_dir}): {e}")
319
+ return ""
302
320
 
303
321
  def _build_recent_dialog(
304
322
  self,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.11.0",
3
+ "version": "1.11.2",
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
@@ -3153,6 +3153,11 @@ class ApiServer:
3153
3153
  # 标记执行模式(传递给 MainAgent 用于增强 system prompt)
3154
3154
  if self.core.main_agent:
3155
3155
  self.core.main_agent._chat_mode = chat_mode
3156
+ # 设置 Agent 专属知识库目录
3157
+ if agent_path and self.core.main_agent and self.core.main_agent.context_builder:
3158
+ agent_kb_dir = self._get_agent_knowledge_dir(agent_path)
3159
+ if agent_kb_dir.exists() and any(agent_kb_dir.iterdir()):
3160
+ self.core.main_agent.context_builder.agent_knowledge_dir = str(agent_kb_dir)
3156
3161
 
3157
3162
  try:
3158
3163
  response = await self.core.process_message(message, session_id)
@@ -3161,6 +3166,8 @@ class ApiServer:
3161
3166
  self.core.main_agent._agent_override_prompt = None
3162
3167
  self.core.main_agent._agent_override_path = None
3163
3168
  self.core.main_agent._chat_mode = ""
3169
+ if self.core.main_agent.context_builder:
3170
+ self.core.main_agent.context_builder.agent_knowledge_dir = None
3164
3171
 
3165
3172
  # 检查是否成功(如果回复包含错误标记)
3166
3173
  if response and not response.startswith("⚠️ LLM 调用失败") and not response.startswith("❌"):
@@ -3289,6 +3296,15 @@ class ApiServer:
3289
3296
  _original_exec_mode = agent.executor.execution_mode
3290
3297
  agent.executor.set_execution_mode(_exec_mode)
3291
3298
 
3299
+ # ── 设置 Agent 专属知识库目录(优先于组织知识库)──
3300
+ if agent_path and agent.context_builder:
3301
+ agent_kb_dir = self._get_agent_knowledge_dir(agent_path)
3302
+ if agent_kb_dir.exists() and any(agent_kb_dir.iterdir()):
3303
+ agent.context_builder.agent_knowledge_dir = str(agent_kb_dir)
3304
+ logger.debug(f"[{session_id}] 使用 Agent 专属知识库: {agent_kb_dir}")
3305
+ else:
3306
+ agent.context_builder.agent_knowledge_dir = None
3307
+
3292
3308
  # Clear execution events from previous runs
3293
3309
  agent.clear_execution_events()
3294
3310
 
@@ -3370,6 +3386,9 @@ class ApiServer:
3370
3386
  # 恢复执行引擎原始模式(防止影响后续 Agent 请求)
3371
3387
  if _original_exec_mode is not None and agent.executor:
3372
3388
  agent.executor.set_execution_mode(_original_exec_mode)
3389
+ # 清理 Agent 专属知识库目录设置(防止影响其他 Agent 请求)
3390
+ if agent.context_builder:
3391
+ agent.context_builder.agent_knowledge_dir = None
3373
3392
 
3374
3393
  # V2 结束后:如果 task_list_store 中有任务,确保最终推送一次
3375
3394
  if chat_mode == "exec" and session_id in self._task_list_store:
@@ -2002,108 +2002,112 @@ async function clearCurrentChat() {
2002
2002
  }
2003
2003
 
2004
2004
  // ── Group History Messages ──
2005
- // Groups consecutive non-user messages (assistant + tool) into single assistant messages
2006
- // with parts[] for timeline rendering, matching the streaming display format.
2007
- // This creates: user → [assistant (speak → tool → speak → tool)] → user → ...
2005
+ // Groups consecutive non-user messages (assistant + tool_call + tool_result) into single
2006
+ // assistant messages with parts[] for timeline rendering, matching the streaming display.
2007
+ //
2008
+ // DB storage pattern:
2009
+ // user_input → role="user", key="user_input"
2010
+ // text → role="assistant", key=""
2011
+ // tool_call → role="assistant", key="tool_call" (NOT role="tool"!)
2012
+ // tool_result→ role="tool", key="tool_result"
2013
+ //
2014
+ // Result: user → [assistant { text → tool_call → tool_result → text → ... }] → user
2008
2015
  function groupHistoryMessages(messages) {
2009
2016
  if (!Array.isArray(messages) || messages.length === 0) return messages;
2010
2017
 
2011
2018
  const grouped = [];
2012
2019
  let i = 0;
2020
+ let _evtId = 0; // Event ID counter
2013
2021
 
2014
2022
  while (i < messages.length) {
2015
2023
  const msg = messages[i];
2016
2024
 
2017
2025
  if (msg.role === 'user') {
2018
- // User message: pass through as-is
2019
2026
  grouped.push({ role: 'user', content: msg.content, time: msg.time || '' });
2020
2027
  i++;
2021
2028
  } else if (msg.role === 'assistant') {
2022
- // Start of a new agent group: collect all consecutive non-user messages
2029
+ // Start of a new agent group: collect ALL consecutive non-user messages
2023
2030
  const parts = [];
2024
2031
  let lastAssistantTime = msg.time || '';
2025
2032
 
2026
- // If assistant has content, add as text part
2027
- if (msg.content && msg.content.trim() && msg.content !== '(无回复)') {
2033
+ // Process the first assistant message
2034
+ if (msg.key === 'tool_call') {
2035
+ const toolName = (msg.content.match(/^调用工具:\s*(\S+)/) || [])[1] || '';
2036
+ parts.push({
2037
+ type: 'exec',
2038
+ data: {
2039
+ id: _evtId++,
2040
+ type: 'tool_call',
2041
+ title: toolName ? ('调用工具: ' + toolName) : msg.content.substring(0, 100),
2042
+ tool_name: toolName,
2043
+ status: 'done',
2044
+ }
2045
+ });
2046
+ } else if (msg.content && msg.content.trim() && msg.content !== '(无回复)') {
2028
2047
  parts.push({ type: 'text', content: msg.content });
2029
2048
  }
2030
2049
 
2031
- i++; // Move to next message
2050
+ i++;
2032
2051
 
2033
- // Collect following tool messages
2034
- while (i < messages.length && messages[i].role === 'tool') {
2035
- const toolMsg = messages[i];
2036
- const isResult = toolMsg.key === 'tool_result';
2037
- const isCall = toolMsg.key === 'tool_call';
2052
+ // Collect all following tool_call (role=assistant), tool_result (role=tool),
2053
+ // and assistant text messages into the same group.
2054
+ // This handles: text → tool_call → tool_result → text → tool_call → tool_result → text
2055
+ while (i < messages.length) {
2056
+ const next = messages[i];
2038
2057
 
2039
- if (isCall) {
2040
- // Extract tool name from content
2041
- const toolName = (toolMsg.content.match(/^调用工具:\s*(\S+)/) || [])[1] || '';
2058
+ if (next.role === 'tool') {
2059
+ const isOk = !next.content.includes('失败');
2042
2060
  parts.push({
2043
2061
  type: 'exec',
2044
2062
  data: {
2045
- id: 'hist_tool_' + i,
2046
- type: 'tool_call',
2047
- title: toolMsg.content.substring(0, 100) || ('调用工具: ' + toolName),
2048
- tool_name: toolName,
2049
- status: 'done',
2050
- }
2051
- });
2052
- } else if (isResult) {
2053
- // Determine success/failure from content
2054
- const isOk = !toolMsg.content.includes('失败');
2055
- parts.push({
2056
- type: 'exec',
2057
- data: {
2058
- id: 'hist_tool_' + i,
2063
+ id: _evtId++,
2059
2064
  type: 'tool_result',
2060
- title: (toolMsg.content.substring(0, 80) || '工具执行结果'),
2065
+ title: next.content.substring(0, 80) || '工具执行结果',
2061
2066
  success: isOk,
2062
- summary: toolMsg.content.substring(0, 500),
2067
+ summary: next.content.substring(0, 500),
2068
+ result: { output: next.content.substring(0, 2000) },
2063
2069
  }
2064
2070
  });
2065
- } else {
2066
- // Generic tool message
2071
+ i++;
2072
+ } else if (next.role === 'assistant' && next.key === 'tool_call') {
2073
+ const toolName = (next.content.match(/^调用工具:\s*(\S+)/) || [])[1] || '';
2067
2074
  parts.push({
2068
2075
  type: 'exec',
2069
2076
  data: {
2070
- id: 'hist_tool_' + i,
2077
+ id: _evtId++,
2071
2078
  type: 'tool_call',
2072
- title: toolMsg.content.substring(0, 100) || '工具调用',
2079
+ title: toolName ? ('调用工具: ' + toolName) : next.content.substring(0, 100),
2080
+ tool_name: toolName,
2073
2081
  status: 'done',
2074
2082
  }
2075
2083
  });
2076
- }
2077
-
2078
- i++;
2079
-
2080
- // If next message is an assistant message, add its content as a text part and continue
2081
- // This handles the pattern: text → tool → text → tool
2082
- if (i < messages.length && messages[i].role === 'assistant') {
2083
- const nextAssistant = messages[i];
2084
- if (nextAssistant.content && nextAssistant.content.trim() && nextAssistant.content !== '(无回复)') {
2085
- parts.push({ type: 'text', content: nextAssistant.content });
2086
- lastAssistantTime = nextAssistant.time || lastAssistantTime;
2084
+ lastAssistantTime = next.time || lastAssistantTime;
2085
+ i++;
2086
+ } else if (next.role === 'assistant') {
2087
+ if (next.content && next.content.trim() && next.content !== '(无回复)') {
2088
+ parts.push({ type: 'text', content: next.content });
2087
2089
  }
2090
+ lastAssistantTime = next.time || lastAssistantTime;
2088
2091
  i++;
2092
+ } else {
2093
+ break;
2089
2094
  }
2090
2095
  }
2091
2096
 
2092
- // Create grouped assistant message with parts
2093
- // Assemble content from text parts for backward compat
2094
2097
  const textParts = parts.filter(p => p.type === 'text');
2095
- const assembledContent = textParts.map(p => p.content).join('\n\n');
2098
+ const assembledContent = textParts.length > 0
2099
+ ? textParts[textParts.length - 1].content
2100
+ : '';
2096
2101
 
2097
2102
  grouped.push({
2098
2103
  role: 'assistant',
2099
- content: assembledContent || '',
2104
+ content: assembledContent,
2100
2105
  time: lastAssistantTime,
2101
2106
  parts: parts.length > 0 ? parts : undefined,
2102
- // Also collect exec_events for backward compat display
2103
2107
  exec_events: parts.filter(p => p.type === 'exec').map(p => p.data),
2104
2108
  });
2105
2109
  } else if (msg.role === 'tool') {
2106
- // Orphan tool message (no preceding assistant) — wrap in an assistant group
2110
+ // Orphan tool message — wrap in an assistant group
2107
2111
  const parts = [];
2108
2112
  const isResult = msg.key === 'tool_result';
2109
2113
  const isCall = msg.key === 'tool_call';
@@ -2113,9 +2117,9 @@ function groupHistoryMessages(messages) {
2113
2117
  parts.push({
2114
2118
  type: 'exec',
2115
2119
  data: {
2116
- id: 'hist_tool_' + i,
2120
+ id: _evtId++,
2117
2121
  type: 'tool_call',
2118
- title: msg.content.substring(0, 100) || ('调用工具: ' + toolName),
2122
+ title: toolName ? ('调用工具: ' + toolName) : msg.content.substring(0, 100),
2119
2123
  tool_name: toolName,
2120
2124
  status: 'done',
2121
2125
  }
@@ -2125,34 +2129,65 @@ function groupHistoryMessages(messages) {
2125
2129
  parts.push({
2126
2130
  type: 'exec',
2127
2131
  data: {
2128
- id: 'hist_tool_' + i,
2132
+ id: _evtId++,
2129
2133
  type: 'tool_result',
2130
2134
  title: msg.content.substring(0, 80) || '工具执行结果',
2131
2135
  success: isOk,
2132
2136
  summary: msg.content.substring(0, 500),
2137
+ result: { output: msg.content.substring(0, 2000) },
2133
2138
  }
2134
2139
  });
2135
2140
  }
2136
2141
 
2137
2142
  i++;
2138
- // Check if next is assistant (to include its content)
2139
- if (i < messages.length && messages[i].role === 'assistant') {
2140
- const nextAssistant = messages[i];
2141
- if (nextAssistant.content && nextAssistant.content.trim() && nextAssistant.content !== '(无回复)') {
2142
- parts.push({ type: 'text', content: nextAssistant.content });
2143
+ while (i < messages.length) {
2144
+ const next = messages[i];
2145
+ if (next.role === 'assistant' && next.key === 'tool_call') {
2146
+ const toolName = (next.content.match(/^调用工具:\s*(\S+)/) || [])[1] || '';
2147
+ parts.push({
2148
+ type: 'exec',
2149
+ data: {
2150
+ id: _evtId++,
2151
+ type: 'tool_call',
2152
+ title: toolName ? ('调用工具: ' + toolName) : next.content.substring(0, 100),
2153
+ tool_name: toolName,
2154
+ status: 'done',
2155
+ }
2156
+ });
2157
+ i++;
2158
+ } else if (next.role === 'assistant') {
2159
+ if (next.content && next.content.trim() && next.content !== '(无回复)') {
2160
+ parts.push({ type: 'text', content: next.content });
2161
+ }
2162
+ i++;
2163
+ } else if (next.role === 'tool') {
2164
+ const isOk = !next.content.includes('失败');
2165
+ parts.push({
2166
+ type: 'exec',
2167
+ data: {
2168
+ id: _evtId++,
2169
+ type: 'tool_result',
2170
+ title: next.content.substring(0, 80) || '工具执行结果',
2171
+ success: isOk,
2172
+ summary: next.content.substring(0, 500),
2173
+ result: { output: next.content.substring(0, 2000) },
2174
+ }
2175
+ });
2176
+ i++;
2177
+ } else {
2178
+ break;
2143
2179
  }
2144
- i++;
2145
2180
  }
2146
2181
 
2182
+ const textParts = parts.filter(p => p.type === 'text');
2147
2183
  grouped.push({
2148
2184
  role: 'assistant',
2149
- content: parts.filter(p => p.type === 'text').map(p => p.content).join('\n\n'),
2185
+ content: textParts.length > 0 ? textParts[textParts.length - 1].content : '',
2150
2186
  time: msg.time || '',
2151
2187
  parts: parts.length > 0 ? parts : undefined,
2152
2188
  exec_events: parts.filter(p => p.type === 'exec').map(p => p.data),
2153
2189
  });
2154
2190
  } else {
2155
- // Skip unknown roles
2156
2191
  i++;
2157
2192
  }
2158
2193
  }