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.
- package/agents/main_agent.py +3 -1
- package/core/context_builder.py +34 -16
- package/package.json +1 -1
- package/web/api_server.py +19 -0
- package/web/ui/chat/chat_main.js +99 -64
package/agents/main_agent.py
CHANGED
|
@@ -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
|
-
|
|
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")
|
package/core/context_builder.py
CHANGED
|
@@ -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
|
-
|
|
252
|
-
|
|
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
|
-
|
|
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 "
|
|
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=
|
|
293
|
+
rag = KnowledgeRAG(kb_dir=kb_dir)
|
|
276
294
|
rag.build_index()
|
|
277
295
|
|
|
278
296
|
if rag.total_chunks == 0:
|
|
279
|
-
return "
|
|
297
|
+
return ""
|
|
280
298
|
|
|
281
|
-
results = rag.search(query, top_k=
|
|
299
|
+
results = rag.search(query, top_k=top_k)
|
|
282
300
|
|
|
283
301
|
if not results:
|
|
284
|
-
return "
|
|
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
|
|
301
|
-
return "
|
|
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
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:
|
package/web/ui/chat/chat_main.js
CHANGED
|
@@ -2002,108 +2002,112 @@ async function clearCurrentChat() {
|
|
|
2002
2002
|
}
|
|
2003
2003
|
|
|
2004
2004
|
// ── Group History Messages ──
|
|
2005
|
-
// Groups consecutive non-user messages (assistant +
|
|
2006
|
-
// with parts[] for timeline rendering, matching the streaming display
|
|
2007
|
-
//
|
|
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
|
|
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
|
-
//
|
|
2027
|
-
if (msg.
|
|
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++;
|
|
2050
|
+
i++;
|
|
2032
2051
|
|
|
2033
|
-
// Collect following tool
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
const
|
|
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 (
|
|
2040
|
-
|
|
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:
|
|
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:
|
|
2065
|
+
title: next.content.substring(0, 80) || '工具执行结果',
|
|
2061
2066
|
success: isOk,
|
|
2062
|
-
summary:
|
|
2067
|
+
summary: next.content.substring(0, 500),
|
|
2068
|
+
result: { output: next.content.substring(0, 2000) },
|
|
2063
2069
|
}
|
|
2064
2070
|
});
|
|
2065
|
-
|
|
2066
|
-
|
|
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:
|
|
2077
|
+
id: _evtId++,
|
|
2071
2078
|
type: 'tool_call',
|
|
2072
|
-
title:
|
|
2079
|
+
title: toolName ? ('调用工具: ' + toolName) : next.content.substring(0, 100),
|
|
2080
|
+
tool_name: toolName,
|
|
2073
2081
|
status: 'done',
|
|
2074
2082
|
}
|
|
2075
2083
|
});
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
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.
|
|
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
|
|
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:
|
|
2120
|
+
id: _evtId++,
|
|
2117
2121
|
type: 'tool_call',
|
|
2118
|
-
title:
|
|
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:
|
|
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
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
parts.push({
|
|
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:
|
|
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
|
}
|