myagent-ai 1.9.9 → 1.10.1
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/base.py +11 -1
- package/agents/main_agent.py +104 -20
- package/config.py +8 -0
- package/core/context_builder.py +131 -16
- package/core/logger.py +16 -1
- package/core/output_parser.py +141 -5
- package/departments/manager.py +60 -5
- package/groups/manager.py +5 -4
- package/main.py +5 -4
- package/package.json +3 -3
- package/web/__pycache__/api_server.cpython-312.pyc +0 -0
- package/web/api_server.py +331 -84
- package/web/ui/chat/chat.css +163 -0
- package/web/ui/chat/chat_container.html +2 -2
- package/web/ui/chat/chat_main.js +378 -32
- package/web/ui/chat/flow_engine.js +231 -75
- package/web/ui/chat/groupchat.js +7 -4
- package/web/ui/chat/middle_chat.html +21 -0
- package/web/ui/index.html +247 -38
package/agents/base.py
CHANGED
|
@@ -142,6 +142,12 @@ class BaseAgent(ABC):
|
|
|
142
142
|
if tools:
|
|
143
143
|
request_kwargs["tools"] = tools
|
|
144
144
|
request_kwargs["tool_choice"] = "auto"
|
|
145
|
+
# ── 推理模式 (Reasoning) ──
|
|
146
|
+
if self.llm.reasoning:
|
|
147
|
+
if self.llm.provider in self.llm._OPENAI_COMPATIBLE_PROVIDERS or self.llm.provider == "zhipu":
|
|
148
|
+
request_kwargs["reasoning_effort"] = self.llm.reasoning_effort
|
|
149
|
+
if self.llm.max_tokens < 8192:
|
|
150
|
+
request_kwargs["max_tokens"] = 8192
|
|
145
151
|
request_kwargs.update(kwargs)
|
|
146
152
|
|
|
147
153
|
full_text = ""
|
|
@@ -351,9 +357,13 @@ class BaseAgent(ABC):
|
|
|
351
357
|
"arguments": args,
|
|
352
358
|
})
|
|
353
359
|
|
|
360
|
+
# 对于推理模型(如 o1/DeepSeek-R1),如果 content 为空但有 reasoning 内容,
|
|
361
|
+
# 使用 reasoning 内容作为最终回复
|
|
362
|
+
final_content = full_text if full_text.strip() else (full_reasoning if full_reasoning.strip() else full_text)
|
|
363
|
+
|
|
354
364
|
return LLMResponse(
|
|
355
365
|
success=True,
|
|
356
|
-
content=
|
|
366
|
+
content=final_content,
|
|
357
367
|
tool_calls=final_tool_calls,
|
|
358
368
|
finish_reason=finish_reason,
|
|
359
369
|
model=request_kwargs.get("model", self.llm.model),
|
package/agents/main_agent.py
CHANGED
|
@@ -39,8 +39,9 @@ class MainAgent(BaseAgent):
|
|
|
39
39
|
# =========================================================================
|
|
40
40
|
SYSTEM_PROMPT = """你是一个强内容分析格式转化引擎,要深入分析以下上下文内容:
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
严格以XML格式化输出以下内容:
|
|
43
43
|
<output>
|
|
44
|
+
<response>直接回复用户的内容。这是一段友好、自然的话语,用于向用户说明你正在做什么,或者回应用户的问题/问候。要求简洁、有礼貌、符合对话场景。如果用户只是问候,简单回应即可;如果用户有具体任务,要说明你的计划。</response>
|
|
44
45
|
<usersays_correct>根据用户输入的"usersays"内容,结合上下文优化为新的用户输入,如果"usersays"为空,这里输出为空。</usersays_correct>
|
|
45
46
|
<task_plan>如"context"包含非空"task_plan",则更新它,变为当前输出。否则,根据"context", 以MD 的格式,制定新任务列表。</task_plan>
|
|
46
47
|
|
|
@@ -59,18 +60,19 @@ class MainAgent(BaseAgent):
|
|
|
59
60
|
|
|
60
61
|
## 核心规则
|
|
61
62
|
1. 你必须且只能输出 <output> XML 结构,不要输出任何其他文本
|
|
62
|
-
2. <
|
|
63
|
-
3. <
|
|
64
|
-
4. <
|
|
65
|
-
5. <
|
|
66
|
-
6. <
|
|
67
|
-
7. <
|
|
68
|
-
8. <
|
|
69
|
-
9. <
|
|
70
|
-
10. <
|
|
71
|
-
11. <
|
|
72
|
-
12.
|
|
73
|
-
13.
|
|
63
|
+
2. <response>: 必须输出一段直接回复用户的话语(这是用户实际看到的回复),要求简洁友好、自然流畅。不要只输出任务计划而不说话!
|
|
64
|
+
3. <usersays_correct>: 如果 context 中 usersays 非空,则根据对话语境优化为更准确的用户意图表达
|
|
65
|
+
4. <task_plan>: 使用 Markdown 列表格式,每项包含任务描述和完成状态标记 [x]/[ ]
|
|
66
|
+
5. <toolstocal>: 列出所有需要执行的工具调用,每个工具包含完整的参数说明
|
|
67
|
+
6. <timeout>: 预估超时秒数(简单操作10-30s,文件操作30-60s,网络请求60-120s,数据处理120-300s)
|
|
68
|
+
7. <callback>: 如果该工具的执行结果对后续决策有影响,设为 true;否则设为 false
|
|
69
|
+
8. <remember>: 仅从最新用户输入(userprint 或 usersays_correct)中提炼值得长期记忆的关键信息,不要重复提炼历史对话中已有的记忆。如果本轮没有新信息需要记忆,则为空
|
|
70
|
+
9. <recall>: 描述下一轮执行时需要从记忆库中检索的内容关键词
|
|
71
|
+
10. <get_knowledge>: 如果当前 <knowledge> 内容不足以完成任务,填写需要从知识库搜索的关键词;否则为空
|
|
72
|
+
11. <askuser>: 当信息不足需要用户补充时,在此填写要问的问题
|
|
73
|
+
12. <finish>: 当任务已完成或需要等待用户回应时为 true;否则为 false 继续执行
|
|
74
|
+
13. 使用中文输出所有内容
|
|
75
|
+
14. 优先使用技能系统(skill)而非直接写代码执行
|
|
74
76
|
"""
|
|
75
77
|
|
|
76
78
|
def __init__(self, tool_agent=None, memory_agent=None, **kwargs):
|
|
@@ -173,8 +175,6 @@ class MainAgent(BaseAgent):
|
|
|
173
175
|
stream_callback(event)
|
|
174
176
|
except Exception as e:
|
|
175
177
|
logger.debug(f"V2 SSE 事件发送失败 ({event_type}): {e}")
|
|
176
|
-
else:
|
|
177
|
-
logger.debug(f"[v2-event] {event_type}: {data}")
|
|
178
178
|
|
|
179
179
|
async def _merge_duplicate_memory(
|
|
180
180
|
self,
|
|
@@ -323,12 +323,29 @@ class MainAgent(BaseAgent):
|
|
|
323
323
|
|
|
324
324
|
conversation_history = list(context.conversation_history or [])
|
|
325
325
|
|
|
326
|
+
# 从 DB 加载历史对话(如果 conversation_history 为空且 memory 可用)
|
|
327
|
+
if self.memory and not conversation_history:
|
|
328
|
+
try:
|
|
329
|
+
db_history = self.memory.get_conversation(
|
|
330
|
+
session_id=context.session_id,
|
|
331
|
+
limit=100,
|
|
332
|
+
)
|
|
333
|
+
if db_history:
|
|
334
|
+
conversation_history = [
|
|
335
|
+
Message(role=entry.role, content=entry.content)
|
|
336
|
+
for entry in db_history
|
|
337
|
+
]
|
|
338
|
+
logger.info(f"[{task_id}] 从 DB 加载了 {len(conversation_history)} 条历史对话")
|
|
339
|
+
except Exception as e:
|
|
340
|
+
logger.warning(f"[{task_id}] 加载历史对话失败: {e}")
|
|
341
|
+
|
|
326
342
|
# 保存用户消息到短期记忆
|
|
327
343
|
if self.memory:
|
|
328
344
|
self.memory.add_short_term(
|
|
329
345
|
session_id=context.session_id,
|
|
330
346
|
role="user",
|
|
331
347
|
content=context.user_message,
|
|
348
|
+
key="user_input",
|
|
332
349
|
)
|
|
333
350
|
|
|
334
351
|
# 加载相关记忆 (recall from previous round or initial load)
|
|
@@ -391,7 +408,6 @@ class MainAgent(BaseAgent):
|
|
|
391
408
|
+ self.SYSTEM_PROMPT.split("\n", 1)[1]
|
|
392
409
|
)
|
|
393
410
|
system_content = _prompt_with_placeholder.replace(_CONTEXT_PLACEHOLDER, context_xml)
|
|
394
|
-
|
|
395
411
|
# Step 3: 调用 LLM
|
|
396
412
|
messages = [Message(role="system", content=system_content)]
|
|
397
413
|
|
|
@@ -420,6 +436,16 @@ class MainAgent(BaseAgent):
|
|
|
420
436
|
llm_raw = response.content
|
|
421
437
|
logger.debug(f"[{task_id}] LLM 输出 (前500字): {llm_raw[:500]}")
|
|
422
438
|
|
|
439
|
+
# 保存 LLM 原始输出到短期记忆(用于回溯和审计)
|
|
440
|
+
if self.memory:
|
|
441
|
+
self.memory.add_short_term(
|
|
442
|
+
session_id=context.session_id,
|
|
443
|
+
role="assistant",
|
|
444
|
+
content=llm_raw,
|
|
445
|
+
key="llm_output",
|
|
446
|
+
importance=0.3,
|
|
447
|
+
)
|
|
448
|
+
|
|
423
449
|
# Step 4: 解析结构化输出
|
|
424
450
|
parsed = parse_output(llm_raw)
|
|
425
451
|
|
|
@@ -452,7 +478,17 @@ class MainAgent(BaseAgent):
|
|
|
452
478
|
)
|
|
453
479
|
break
|
|
454
480
|
else:
|
|
455
|
-
|
|
481
|
+
# XML 解析失败且无法提取文本,发送原始输出作为备选
|
|
482
|
+
logger.warning(f"[{task_id}] 无法提取文本,发送原始 LLM 输出")
|
|
483
|
+
final_text = llm_raw.strip() if llm_raw.strip() else "处理完毕。"
|
|
484
|
+
context.working_memory["final_response"] = final_text
|
|
485
|
+
await self._emit_v2_event("v2_reasoning", {"content": final_text}, stream_callback)
|
|
486
|
+
if self.memory:
|
|
487
|
+
self.memory.add_short_term(
|
|
488
|
+
session_id=context.session_id,
|
|
489
|
+
role="assistant",
|
|
490
|
+
content=final_text,
|
|
491
|
+
)
|
|
456
492
|
break
|
|
457
493
|
|
|
458
494
|
warnings = validate_output(parsed)
|
|
@@ -463,6 +499,18 @@ class MainAgent(BaseAgent):
|
|
|
463
499
|
if parsed.usersays_correct:
|
|
464
500
|
context.working_memory["usersays_correct"] = parsed.usersays_correct
|
|
465
501
|
|
|
502
|
+
# Step 5.5: 处理 response — 直接回复用户的内容
|
|
503
|
+
if parsed.response:
|
|
504
|
+
response_text = parsed.response.strip()
|
|
505
|
+
if response_text:
|
|
506
|
+
logger.debug(f"[{task_id}] 模型回复用户: {response_text[:100]}")
|
|
507
|
+
context.working_memory["model_response"] = response_text
|
|
508
|
+
await self._emit_v2_event(
|
|
509
|
+
"v2_reasoning",
|
|
510
|
+
{"content": response_text},
|
|
511
|
+
stream_callback,
|
|
512
|
+
)
|
|
513
|
+
|
|
466
514
|
# Step 6: 处理 remember — 查重+LLM合并后存入长期记忆
|
|
467
515
|
if parsed.remember:
|
|
468
516
|
try:
|
|
@@ -645,11 +693,30 @@ class MainAgent(BaseAgent):
|
|
|
645
693
|
)
|
|
646
694
|
|
|
647
695
|
# 发送工具结果事件
|
|
696
|
+
# 提取实际输出:SkillResult 有 output/message/data,ExecResult 有 stdout/stderr
|
|
697
|
+
def _extract_tool_output(tr):
|
|
698
|
+
"""从工具结果中提取实际输出文本"""
|
|
699
|
+
out = tr.get("output", "")
|
|
700
|
+
if out:
|
|
701
|
+
return out
|
|
702
|
+
out = tr.get("message", "")
|
|
703
|
+
if out:
|
|
704
|
+
return out
|
|
705
|
+
out = tr.get("stdout", "")
|
|
706
|
+
if out:
|
|
707
|
+
return out
|
|
708
|
+
data = tr.get("data")
|
|
709
|
+
if data is not None:
|
|
710
|
+
return str(data) if not isinstance(data, str) else data
|
|
711
|
+
return tr.get("error", "")
|
|
712
|
+
|
|
713
|
+
tool_output_text = _extract_tool_output(tool_result)
|
|
714
|
+
|
|
648
715
|
await self._emit_v2_event(
|
|
649
716
|
"v2_tool_result",
|
|
650
717
|
{"tool": {"toolname": tool_name}, "result": {
|
|
651
718
|
"success": tool_result.get("success", False),
|
|
652
|
-
"output": truncate_str(
|
|
719
|
+
"output": truncate_str(tool_output_text, 3000),
|
|
653
720
|
"error": truncate_str(tool_result.get("error", ""), 1000),
|
|
654
721
|
"timed_out": tool_result.get("timed_out", False),
|
|
655
722
|
}},
|
|
@@ -660,7 +727,7 @@ class MainAgent(BaseAgent):
|
|
|
660
727
|
"title": f"工具结果: {tool_name}",
|
|
661
728
|
"tool_name": tool_name,
|
|
662
729
|
"success": tool_result.get("success", False),
|
|
663
|
-
"summary": truncate_str(
|
|
730
|
+
"summary": truncate_str(tool_output_text, 500),
|
|
664
731
|
"result": tool_result,
|
|
665
732
|
})
|
|
666
733
|
|
|
@@ -671,7 +738,7 @@ class MainAgent(BaseAgent):
|
|
|
671
738
|
elif should_callback:
|
|
672
739
|
need_callback = True
|
|
673
740
|
|
|
674
|
-
output_str =
|
|
741
|
+
output_str = tool_output_text
|
|
675
742
|
tool_outputs_parts.append(
|
|
676
743
|
f"### {before_call}\n"
|
|
677
744
|
f"**工具**: {tool_name}\n"
|
|
@@ -688,6 +755,23 @@ class MainAgent(BaseAgent):
|
|
|
688
755
|
content=f"[工具 {tool_name} 执行完成] {'成功' if tool_result.get('success') else '失败'}",
|
|
689
756
|
))
|
|
690
757
|
|
|
758
|
+
# 保存工具调用到短期记忆
|
|
759
|
+
if self.memory:
|
|
760
|
+
self.memory.add_short_term(
|
|
761
|
+
session_id=context.session_id,
|
|
762
|
+
role="assistant",
|
|
763
|
+
content=f"调用工具: {tool_name}\n参数: {truncate_str(parms, 1000)}",
|
|
764
|
+
key="tool_call",
|
|
765
|
+
importance=0.4,
|
|
766
|
+
)
|
|
767
|
+
self.memory.add_short_term(
|
|
768
|
+
session_id=context.session_id,
|
|
769
|
+
role="tool",
|
|
770
|
+
content=f"[{tool_name}] {'成功' if tool_result.get('success') else '失败'}\n{truncate_str(output_str, 5000)}",
|
|
771
|
+
key="tool_result",
|
|
772
|
+
importance=0.4,
|
|
773
|
+
)
|
|
774
|
+
|
|
691
775
|
all_tool_outputs = "\n".join(tool_outputs_parts)
|
|
692
776
|
|
|
693
777
|
# Step 12: 工具执行完毕后,根据 finish 标志决定是否回调 LLM
|
package/config.py
CHANGED
|
@@ -23,11 +23,15 @@ from typing import Optional, Dict, Any, List
|
|
|
23
23
|
class LLMConfig:
|
|
24
24
|
"""LLM 大模型配置"""
|
|
25
25
|
provider: str = "openai" # openai | anthropic | ollama | custom
|
|
26
|
+
api_type: str = "openai-completions" # API 类型
|
|
26
27
|
api_key: str = ""
|
|
27
28
|
base_url: str = "https://api.openai.com/v1"
|
|
28
29
|
model: str = "gpt-4"
|
|
29
30
|
temperature: float = 0.1
|
|
30
31
|
max_tokens: int = 4096
|
|
32
|
+
context_window: int = 128000 # 上下文窗口大小
|
|
33
|
+
input_modes: List[str] = field(default_factory=lambda: ["text"]) # 支持的输入模式
|
|
34
|
+
reasoning: bool = False # 是否支持推理
|
|
31
35
|
timeout: int = 120 # 请求超时(秒)
|
|
32
36
|
max_retries: int = 3 # 最大重试次数
|
|
33
37
|
# Anthropic 专用
|
|
@@ -86,11 +90,15 @@ class ModelEntry:
|
|
|
86
90
|
id: str = "" # 唯一标识符,如 "gpt-4o", "claude-3.5-sonnet"
|
|
87
91
|
name: str = "" # 显示名称
|
|
88
92
|
provider: str = "" # openai | anthropic | ollama | zhipu | custom
|
|
93
|
+
api_type: str = "openai-completions" # API 类型:openai-completions | openai-chat | anthropic | ollama | custom
|
|
89
94
|
model: str = "" # API 调用使用的实际模型字符串
|
|
90
95
|
base_url: str = "" # 自定义 Base URL(空=使用 provider 默认值)
|
|
91
96
|
api_key: str = "" # 专用 API Key(空=使用全局默认值)
|
|
92
97
|
max_tokens: int = 4096
|
|
93
98
|
temperature: float = 0.1
|
|
99
|
+
context_window: int = 128000 # 上下文窗口大小(token)
|
|
100
|
+
input_modes: List[str] = field(default_factory=lambda: ["text"]) # 支持的输入模式: text, image, video, audio
|
|
101
|
+
reasoning: bool = True # 是否支持推理(如 o1 系列)
|
|
94
102
|
enabled: bool = True
|
|
95
103
|
|
|
96
104
|
|
package/core/context_builder.py
CHANGED
|
@@ -118,10 +118,11 @@ class ContextBuilder:
|
|
|
118
118
|
kb_query = get_knowledge.strip() if get_knowledge else query
|
|
119
119
|
|
|
120
120
|
sections: List[str] = [
|
|
121
|
+
self._build_datetime(),
|
|
121
122
|
self._build_whomi(agent_name, agent_description, agent_override_prompt),
|
|
122
123
|
self._build_memory(query, session_id),
|
|
123
124
|
self._build_knowledge(kb_query),
|
|
124
|
-
self._build_recent_dialog(conversation_history, self.max_dialog_chars),
|
|
125
|
+
self._build_recent_dialog(conversation_history, self.max_dialog_chars, session_id),
|
|
125
126
|
self._build_user_input(user_typed_text, user_voice_text),
|
|
126
127
|
self._build_task_plan(task_plan),
|
|
127
128
|
self._build_tools(self.skill_registry),
|
|
@@ -140,6 +141,24 @@ class ContextBuilder:
|
|
|
140
141
|
# 各段落构建方法
|
|
141
142
|
# =========================================================================
|
|
142
143
|
|
|
144
|
+
def _build_datetime(self) -> str:
|
|
145
|
+
"""
|
|
146
|
+
构建 <datetime> 段落 —— 当前日期时间(精确到秒)。
|
|
147
|
+
让 LLM 知道当前时间,以便给出与时间相关的回答。
|
|
148
|
+
"""
|
|
149
|
+
from datetime import datetime
|
|
150
|
+
now = datetime.now()
|
|
151
|
+
weekdays = ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"]
|
|
152
|
+
date_str = now.strftime("%Y年%m月%d日")
|
|
153
|
+
time_str = now.strftime("%H:%M:%S")
|
|
154
|
+
weekday = weekdays[now.weekday()]
|
|
155
|
+
return (
|
|
156
|
+
f"<datetime>\n"
|
|
157
|
+
f"当前时间: {date_str} {weekday} {time_str}\n"
|
|
158
|
+
f"时间戳: {now.timestamp()}\n"
|
|
159
|
+
f"</datetime>"
|
|
160
|
+
)
|
|
161
|
+
|
|
143
162
|
def _build_whomi(
|
|
144
163
|
self,
|
|
145
164
|
agent_name: str,
|
|
@@ -275,16 +294,18 @@ class ContextBuilder:
|
|
|
275
294
|
self,
|
|
276
295
|
conversation_history: List["Message"],
|
|
277
296
|
max_chars: int,
|
|
297
|
+
session_id: str = "",
|
|
278
298
|
) -> str:
|
|
279
299
|
"""
|
|
280
300
|
构建 <resentdialog> 段落 —— 近期对话历史。
|
|
281
301
|
|
|
282
|
-
|
|
283
|
-
|
|
302
|
+
将对话格式化为带角色标签的文本。当历史过长时,将较早的消息
|
|
303
|
+
压缩为摘要,保留近期消息完整呈现,总字符数不超过 max_chars。
|
|
284
304
|
|
|
285
305
|
Args:
|
|
286
306
|
conversation_history: 对话历史消息列表
|
|
287
307
|
max_chars: 最大字符数限制
|
|
308
|
+
session_id: 会话 ID(用于 MemoryManager 摘要)
|
|
288
309
|
|
|
289
310
|
Returns:
|
|
290
311
|
<resentdialog> XML 段落字符串
|
|
@@ -300,32 +321,126 @@ class ContextBuilder:
|
|
|
300
321
|
"tool": "工具",
|
|
301
322
|
}
|
|
302
323
|
|
|
303
|
-
#
|
|
304
|
-
|
|
324
|
+
# 过滤空消息并格式化
|
|
325
|
+
filtered_msgs: List[tuple] = []
|
|
305
326
|
for msg in conversation_history:
|
|
306
327
|
role = getattr(msg, "role", "user")
|
|
307
328
|
content = getattr(msg, "content", "")
|
|
308
329
|
if not content.strip():
|
|
309
330
|
continue
|
|
310
|
-
|
|
311
331
|
label = role_labels.get(role, role)
|
|
312
|
-
|
|
332
|
+
filtered_msgs.append((label, content.strip()))
|
|
333
|
+
|
|
334
|
+
if not filtered_msgs:
|
|
335
|
+
return "<resentdialog>\n(无对话历史)\n</resentdialog>"
|
|
336
|
+
|
|
337
|
+
# 当消息超过阈值时,将旧消息压缩为摘要
|
|
338
|
+
SUMMARY_THRESHOLD = 30 # 超过30条时启用摘要
|
|
339
|
+
RECENT_KEEP = 15 # 保留最近15条完整消息
|
|
340
|
+
SUMMARY_BUDGET = 3000 # 摘要最大字符数
|
|
341
|
+
|
|
342
|
+
prefix_text = ""
|
|
343
|
+
recent_msgs = filtered_msgs
|
|
344
|
+
|
|
345
|
+
if len(filtered_msgs) > SUMMARY_THRESHOLD:
|
|
346
|
+
old_msgs = filtered_msgs[:-RECENT_KEEP]
|
|
347
|
+
recent_msgs = filtered_msgs[-RECENT_KEEP:]
|
|
348
|
+
prefix_text = self._build_dialog_summary(old_msgs, SUMMARY_BUDGET)
|
|
349
|
+
|
|
350
|
+
# 格式化近期消息
|
|
351
|
+
formatted_lines: List[str] = []
|
|
352
|
+
if prefix_text:
|
|
353
|
+
formatted_lines.append(prefix_text)
|
|
354
|
+
formatted_lines.append("") # 空行分隔
|
|
355
|
+
|
|
356
|
+
for label, content in recent_msgs:
|
|
357
|
+
formatted_lines.append(f"[{label}] {_xml_escape(content)}")
|
|
313
358
|
|
|
314
359
|
dialog_text = "\n".join(formatted_lines)
|
|
315
360
|
|
|
316
|
-
#
|
|
361
|
+
# Token 预算裁剪:超预算时从最早的消息开始移除
|
|
317
362
|
if len(dialog_text) > max_chars:
|
|
318
|
-
#
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
363
|
+
# 保留摘要前缀,裁剪近期消息部分
|
|
364
|
+
if prefix_text:
|
|
365
|
+
# 先尝试只裁剪近期消息
|
|
366
|
+
recent_budget = max_chars - len(prefix_text) - 100
|
|
367
|
+
if recent_budget < 500:
|
|
368
|
+
# 摘要本身太长,整体裁剪
|
|
369
|
+
dialog_text = dialog_text[-max_chars:]
|
|
370
|
+
else:
|
|
371
|
+
recent_part = "\n".join(formatted_lines[len(formatted_lines) - len(recent_msgs):])
|
|
372
|
+
if len(recent_part) > recent_budget:
|
|
373
|
+
recent_part = self._trim_messages_from_start(recent_part, recent_budget)
|
|
374
|
+
dialog_text = prefix_text + "\n\n" + recent_part
|
|
375
|
+
else:
|
|
376
|
+
dialog_text = self._trim_messages_from_start(dialog_text, max_chars)
|
|
377
|
+
dialog_text = "(... 前面的对话已被裁剪 ...)\n" + dialog_text
|
|
326
378
|
|
|
327
379
|
return f"<resentdialog>\n{dialog_text}\n</resentdialog>"
|
|
328
380
|
|
|
381
|
+
def _build_dialog_summary(self, old_msgs: List[tuple], max_chars: int) -> str:
|
|
382
|
+
"""
|
|
383
|
+
将旧消息列表压缩为摘要文本。
|
|
384
|
+
|
|
385
|
+
策略: 提取每条消息的第一行作为要点,保留关键信息的同时大幅压缩篇幅。
|
|
386
|
+
如果有 MemoryManager,也可以调用 LLM 生成摘要(未来扩展)。
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
old_msgs: (label, content) 元组列表
|
|
390
|
+
max_chars: 摘要最大字符数
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
摘要文本字符串
|
|
394
|
+
"""
|
|
395
|
+
if not old_msgs:
|
|
396
|
+
return ""
|
|
397
|
+
|
|
398
|
+
summary_parts: List[str] = ["[历史对话摘要]"]
|
|
399
|
+
for label, content in old_msgs:
|
|
400
|
+
# 提取第一行或前100字符作为要点
|
|
401
|
+
first_line = content.split("\n")[0].strip()
|
|
402
|
+
if len(first_line) > 100:
|
|
403
|
+
first_line = first_line[:100] + "..."
|
|
404
|
+
summary_parts.append(f"- [{label}] {first_line}")
|
|
405
|
+
|
|
406
|
+
summary_text = "\n".join(summary_parts)
|
|
407
|
+
|
|
408
|
+
# 摘要本身也要限制长度
|
|
409
|
+
if len(summary_text) > max_chars:
|
|
410
|
+
summary_text = summary_text[:max_chars] + "\n... (更多历史已省略)"
|
|
411
|
+
|
|
412
|
+
return summary_text
|
|
413
|
+
|
|
414
|
+
def _trim_messages_from_start(self, text: str, max_chars: int) -> str:
|
|
415
|
+
"""
|
|
416
|
+
从文本开头裁剪消息,保留尾部(最新消息优先)。
|
|
417
|
+
|
|
418
|
+
按行(即按消息)为单位裁剪,避免在消息中间截断。
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
text: 格式化后的对话文本
|
|
422
|
+
max_chars: 最大保留字符数
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
裁剪后的文本
|
|
426
|
+
"""
|
|
427
|
+
if len(text) <= max_chars:
|
|
428
|
+
return text
|
|
429
|
+
|
|
430
|
+
lines = text.split("\n")
|
|
431
|
+
result_lines: List[str] = []
|
|
432
|
+
total = 0
|
|
433
|
+
|
|
434
|
+
# 从后往前添加行,保留最新的消息
|
|
435
|
+
for line in reversed(lines):
|
|
436
|
+
if total + len(line) + 1 > max_chars:
|
|
437
|
+
break
|
|
438
|
+
result_lines.append(line)
|
|
439
|
+
total += len(line) + 1
|
|
440
|
+
|
|
441
|
+
result_lines.reverse()
|
|
442
|
+
return "\n".join(result_lines)
|
|
443
|
+
|
|
329
444
|
def _build_user_input(
|
|
330
445
|
self,
|
|
331
446
|
user_typed_text: str,
|
package/core/logger.py
CHANGED
|
@@ -148,6 +148,12 @@ def setup_logger(
|
|
|
148
148
|
# 控制台输出
|
|
149
149
|
if console:
|
|
150
150
|
ch = logging.StreamHandler(sys.stdout)
|
|
151
|
+
# Windows 兼容 UTF-8
|
|
152
|
+
if sys.platform == "win32":
|
|
153
|
+
try:
|
|
154
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|
|
151
157
|
ch.setFormatter(ColorFormatter(fmt, datefmt=datefmt))
|
|
152
158
|
ch.setLevel(logging.DEBUG) # 控制台始终显示所有级别
|
|
153
159
|
logger.addHandler(ch)
|
|
@@ -216,7 +222,16 @@ def get_logger(name: str = "myagent") -> logging.Logger:
|
|
|
216
222
|
"""获取已存在的 Logger,如果不存在则创建默认的"""
|
|
217
223
|
logger = logging.getLogger(name)
|
|
218
224
|
if not logger.handlers:
|
|
219
|
-
|
|
225
|
+
# 子 logger 继承父 logger 的 handlers(如文件 handler)
|
|
226
|
+
parent_logger = logging.getLogger()
|
|
227
|
+
if parent_logger.handlers:
|
|
228
|
+
for handler in parent_logger.handlers:
|
|
229
|
+
logger.addHandler(handler)
|
|
230
|
+
logger.setLevel(parent_logger.level)
|
|
231
|
+
logger.propagate = parent_logger.propagate
|
|
232
|
+
else:
|
|
233
|
+
# 没有父 logger,创建默认配置
|
|
234
|
+
return setup_logger(name)
|
|
220
235
|
return logger
|
|
221
236
|
|
|
222
237
|
|