Files
Hy-Memory/hy_memory/agent/mem_agent.py
T
alvinfeiandClaude Opus 5.5 dc762df78b feat(sdk): switch to turn the pipeline log off, keep conversation text out of INFO logs (2.0.8)
MEMORY_PIPELINE_LOG_ENABLED (default true) turns off the pipeline JSONL
log. With the DB trace also off, store_pipeline_log becomes a no-op that
does no timing, redaction, background work or database write. The init line
states the real on/off state.

Lines that carried conversation or memory text at INFO/WARNING now log ids,
counts and lengths; the text goes to DEBUG: [add] input, search queries and
rewrites, coding tasks and fact statements/subjects, reconcile op detail,
extracted info, embedding inputs and unparsable model output.

Bump hy-memory to 2.0.8 (not yet published).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-28 15:03:21 +08:00

334 lines
12 KiB
Python

"""
Agent Memory - MemAgent 智能体协调器
统一入口,协调调度其他智能体(Extractor、Reflector)。
功能:
- 接收处理请求
- 根据任务类型调度对应智能体
- 聚合处理结果
- 错误处理和降级
示例:
agent = MemAgent()
# 处理添加记忆请求
result = await agent.process_add(
content="用户说他喜欢川菜,尤其是麻婆豆腐",
context={"uid": "user_456"}
)
# 结果包含提取的信息
print(result.extracted_info) # {"preferences": {"food": "川菜"}}
print(result.suggested_layer) # "profile"
"""
from typing import Dict, Any, Optional, List
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
import logging
from .extractor import Extractor
from .reflector import Reflector
from .llm_provider import LLMProvider
from ..config import MemoryConfig
logger = logging.getLogger(__name__)
class ProcessMode(Enum):
"""处理模式"""
FULL = "full" # 完整处理(提取+摘要+反思)
EXTRACT_ONLY = "extract" # 仅提取
SKIP = "skip" # 跳过 Agent 处理
@dataclass
class AgentResult:
"""
智能体处理结果
"""
success: bool
# 提取结果
extracted_info: Dict[str, Any] = field(default_factory=dict)
suggested_layer: Optional[str] = None
# 摘要结果
summary: Optional[str] = None # LLM 原始摘要文本(已 strip),直接用于存储
# 反思结果
conflicts: List[Dict[str, Any]] = field(default_factory=list)
should_merge: bool = False
merge_target_id: Optional[str] = None
# 元信息
processing_time_ms: float = 0
tokens_used: int = 0
error: Optional[str] = None
error_code: Optional[str] = None # "EMPTY_RESPONSE" / "JSON_PARSE_FAILED" / "LLM_ERROR"
extract_raw_response: Optional[str] = None # LLM 原始返回(失败时保留)
# 细分统计(各步骤的 token 和耗时)
extract_tokens_used: int = 0
extract_prompt_tokens: int = 0
extract_completion_tokens: int = 0
extract_elapsed_ms: float = 0
summary_tokens_used: int = 0
summary_prompt_tokens: int = 0
summary_completion_tokens: int = 0
summary_elapsed_ms: float = 0
def to_dict(self) -> Dict[str, Any]:
return {
"success": self.success,
"extracted_info": self.extracted_info,
"suggested_layer": self.suggested_layer,
"summary": self.summary,
"conflicts": self.conflicts,
"should_merge": self.should_merge,
"processing_time_ms": self.processing_time_ms,
"tokens_used": self.tokens_used,
"error": self.error,
"extract_tokens_used": self.extract_tokens_used,
"extract_prompt_tokens": self.extract_prompt_tokens,
"extract_completion_tokens": self.extract_completion_tokens,
"extract_elapsed_ms": self.extract_elapsed_ms,
"summary_tokens_used": self.summary_tokens_used,
"summary_prompt_tokens": self.summary_prompt_tokens,
"summary_completion_tokens": self.summary_completion_tokens,
"summary_elapsed_ms": self.summary_elapsed_ms,
}
class MemAgent:
"""
记忆智能体协调器
负责协调 Extractor、Reflector 完成记忆处理。
"""
def __init__(
self,
config: Optional[MemoryConfig] = None,
llm_provider: Optional[LLMProvider] = None,
):
"""
初始化 MemAgent
Args:
config: 配置对象
llm_provider: LLM 提供器
"""
self.config = config or MemoryConfig.from_env()
self.llm_provider = llm_provider or LLMProvider(self.config)
# 初始化子智能体(传入 llm_config 以支持各场景 max_tokens 配置)
llm_config = self.config.llm
self.extractor = Extractor(self.llm_provider, llm_config)
self.reflector = Reflector(self.llm_provider, llm_config)
logger.debug("MemAgent initialized")
async def process_add(
self,
content: str,
context: Dict[str, Any],
mode: ProcessMode = ProcessMode.FULL,
existing_memories: List[Dict] = None,
memory_at: Optional[datetime] = None,
existing_tags: Optional[List[str]] = None,
tools: Optional[List[Dict[str, Any]]] = None,
tool_registry: Optional[Any] = None,
tool_context: Optional[Dict[str, Any]] = None,
history_context: str = "",
basic_profile_fields: Optional[Dict[str, str]] = None,
) -> AgentResult:
"""
处理添加记忆请求
Args:
content: 记忆内容
context: 上下文信息(uid, agent_id 等)
mode: 处理模式
existing_memories: 已有相似记忆(用于冲突检测)
memory_at: 记忆发生时间(用作 extract prompt 的 current_time 基准)
existing_tags: 可选,该用户已有的所有 tags(供 extractor 传给 LLM 优先复用)
tools: **DEPRECATED** v0.1.5.13+ 不再透传到 LLM;保留参数仅向后兼容
tool_registry: **DEPRECATED** 同上
tool_context: 保留(未使用)
basic_profile_fields:
{field_name: description} 字段表,由调用方从 MemoryConfig.basic_profile.fields
透传过来,extractor 会渲染到 prompt 中要求 LLM 在 JSON basic_info 字段返回。
Returns:
处理结果
"""
start_time = datetime.now()
total_tokens = 0
# 确定 extract prompt 的时间基准:memory_at 优先,不传则为空字符串(prompt 中留空)
current_time_str = memory_at.isoformat(timespec="seconds") if memory_at else ""
try:
result = AgentResult(success=True)
# 提取结构化信息;旧 rolling-summary 路径已移除。
_do_extract = mode in (ProcessMode.FULL, ProcessMode.EXTRACT_ONLY)
async def _run_extract():
return await self.extractor.extract(
content, context,
current_time=current_time_str,
existing_tags=existing_tags,
tools=tools,
tool_registry=tool_registry,
tool_context=tool_context,
history_context=history_context,
basic_profile_fields=basic_profile_fields,
)
_t_parallel = datetime.now()
extract_result = await _run_extract() if _do_extract else None
# 处理 extract 结果
if _do_extract and extract_result:
_extract_elapsed = (datetime.now() - _t_parallel).total_seconds() * 1000
result.extracted_info = extract_result.info
result.suggested_layer = extract_result.suggested_layer
total_tokens += extract_result.tokens_used
result.extract_tokens_used = extract_result.tokens_used
result.extract_prompt_tokens = extract_result.prompt_tokens
result.extract_completion_tokens = extract_result.completion_tokens
result.extract_elapsed_ms = _extract_elapsed
result.extract_raw_response = extract_result.raw_response
result._extract_result = extract_result # 保留完整结果供 trace 日志使用
# Extract 失败时提前返回
if not extract_result.success:
result.success = False
result.error = extract_result.error
result.error_code = extract_result.error_code
result.processing_time_ms = (
datetime.now() - start_time
).total_seconds() * 1000
result.tokens_used = total_tokens
logger.warning(
f"MemAgent: extract failed: code={extract_result.error_code} "
f"error={extract_result.error}"
)
return result
# INFO: 提取结果摘要
info = extract_result.info or {}
identity = info.get("identity", []) or []
facts = info.get("facts", []) or []
basic_info = info.get("basic_info", {}) or {}
basic_field_count = sum(
1 for k, v in basic_info.items() if v and v != "null"
)
logger.info(
f"MemAgent: extraction → "
f"identity={len(identity)} facts={len(facts)} "
f"basic_info={basic_field_count}fields"
)
logger.debug(f"MemAgent: extracted_info={extract_result.info}")
# 3. 冲突检测(如果有已有记忆)
if mode == ProcessMode.FULL and existing_memories:
reflect_result = await self.reflector.check_conflicts(
content,
existing_memories,
context
)
result.conflicts = reflect_result.conflicts
result.should_merge = reflect_result.should_merge
result.merge_target_id = reflect_result.merge_target_id
total_tokens += reflect_result.tokens_used
logger.info(
f"MemAgent: conflict detection → "
f"conflicts={len(reflect_result.conflicts)} "
f"should_merge={reflect_result.should_merge}"
)
# 计算处理时间
result.processing_time_ms = (
datetime.now() - start_time
).total_seconds() * 1000
result.tokens_used = total_tokens
logger.debug(f"MemAgent: tokens_used={total_tokens}")
return result
except Exception as e:
logger.error(f"MemAgent process_add failed: {e}")
return AgentResult(
success=False,
error=str(e),
processing_time_ms=(datetime.now() - start_time).total_seconds() * 1000,
)
async def process_recall(
self,
query: str,
memories: List[Dict],
context: Dict[str, Any],
) -> AgentResult:
"""
处理召回请求(可选的后处理)
可以对召回结果进行:
- 重排序
- 摘要生成
- 答案生成
Args:
query: 查询内容
memories: 召回的记忆列表
context: 上下文信息
Returns:
处理结果
"""
start_time = datetime.now()
try:
# 生成综合摘要
combined_content = "\n".join([m.get("content", "") for m in memories])
if combined_content:
abstract_result = await self.abstractor.abstract(
combined_content,
context,
focus=query # 聚焦于查询内容
)
return AgentResult(
success=True,
summary=abstract_result.summary,
tokens_used=abstract_result.tokens_used,
processing_time_ms=(datetime.now() - start_time).total_seconds() * 1000,
)
return AgentResult(
success=True,
processing_time_ms=(datetime.now() - start_time).total_seconds() * 1000,
)
except Exception as e:
logger.error(f"MemAgent process_recall failed: {e}")
return AgentResult(
success=False,
error=str(e),
)
def get_stats(self) -> Dict[str, Any]:
"""获取智能体统计信息"""
return {
"extractor": self.extractor.get_stats(),
"reflector": self.reflector.get_stats(),
}