feat:新增记忆测试、检索工具与服务
新增完整的长期记忆支持及测试:引入中文记忆检索提示词、query_long_term_memory 检索工具、记忆服务与记忆流程服务,以及 WebUI 的记忆路由。新增大规模测试套件(包括单元测试与基准/在线测试),覆盖聊天历史摘要、知识获取器、事件(episode)生成、写回机制以及用户画像检索等功能。 更新多个模块以集成记忆检索能力(包括 knowledge fetcher、chat summarizer、memory_retrieval、person_info、config/legacy 迁移以及 WebUI 路由),并移除遗留的 lpmm 知识模块。这些变更完成了记忆运行时的接入,同时为基准测试提供嵌入适配器的 mock,并支持新测试与工具所需的导入与 episode 处理流程。
This commit is contained in:
275
src/services/memory_flow_service.py
Normal file
275
src/services/memory_flow_service.py
Normal file
@@ -0,0 +1,275 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from json_repair import repair_json
|
||||
|
||||
from src.chat.utils.utils import is_bot_self
|
||||
from src.common.message_repository import find_messages
|
||||
from src.common.logger import get_logger
|
||||
from src.config.config import global_config, model_config
|
||||
from src.llm_models.utils_model import LLMRequest
|
||||
from src.memory_system.chat_history_summarizer import ChatHistorySummarizer
|
||||
from src.person_info.person_info import Person, get_person_id, store_person_memory_from_answer
|
||||
|
||||
logger = get_logger("memory_flow_service")
|
||||
|
||||
|
||||
class LongTermMemorySessionManager:
|
||||
def __init__(self) -> None:
|
||||
self._lock = asyncio.Lock()
|
||||
self._summarizers: Dict[str, ChatHistorySummarizer] = {}
|
||||
|
||||
async def on_message(self, message: Any) -> None:
|
||||
if not bool(getattr(global_config.memory, "long_term_auto_summary_enabled", True)):
|
||||
return
|
||||
session_id = str(getattr(message, "session_id", "") or "").strip()
|
||||
if not session_id:
|
||||
return
|
||||
|
||||
created = False
|
||||
async with self._lock:
|
||||
summarizer = self._summarizers.get(session_id)
|
||||
if summarizer is None:
|
||||
summarizer = ChatHistorySummarizer(session_id=session_id)
|
||||
self._summarizers[session_id] = summarizer
|
||||
created = True
|
||||
if created:
|
||||
await summarizer.start()
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
async with self._lock:
|
||||
items = list(self._summarizers.items())
|
||||
self._summarizers.clear()
|
||||
for session_id, summarizer in items:
|
||||
try:
|
||||
await summarizer.stop()
|
||||
except Exception as exc:
|
||||
logger.warning("停止聊天总结器失败: session=%s err=%s", session_id, exc)
|
||||
|
||||
|
||||
class PersonFactWritebackService:
|
||||
def __init__(self) -> None:
|
||||
self._queue: asyncio.Queue[Any] = asyncio.Queue(maxsize=256)
|
||||
self._worker_task: Optional[asyncio.Task] = None
|
||||
self._stopping = False
|
||||
self._extractor = LLMRequest(
|
||||
model_set=model_config.model_task_config.utils,
|
||||
request_type="person_fact_writeback",
|
||||
)
|
||||
|
||||
async def start(self) -> None:
|
||||
if self._worker_task is not None and not self._worker_task.done():
|
||||
return
|
||||
self._stopping = False
|
||||
self._worker_task = asyncio.create_task(self._worker_loop(), name="memory_person_fact_writeback")
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
self._stopping = True
|
||||
worker = self._worker_task
|
||||
self._worker_task = None
|
||||
if worker is None:
|
||||
return
|
||||
worker.cancel()
|
||||
try:
|
||||
await worker
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
except Exception as exc:
|
||||
logger.warning("关闭人物事实写回 worker 失败: %s", exc)
|
||||
|
||||
async def enqueue(self, message: Any) -> None:
|
||||
if not bool(getattr(global_config.memory, "person_fact_writeback_enabled", True)):
|
||||
return
|
||||
if self._stopping:
|
||||
return
|
||||
try:
|
||||
self._queue.put_nowait(message)
|
||||
except asyncio.QueueFull:
|
||||
logger.warning("人物事实写回队列已满,跳过本次回复")
|
||||
|
||||
async def _worker_loop(self) -> None:
|
||||
try:
|
||||
while not self._stopping:
|
||||
message = await self._queue.get()
|
||||
try:
|
||||
await self._handle_message(message)
|
||||
except Exception as exc:
|
||||
logger.warning("人物事实写回处理失败: %s", exc, exc_info=True)
|
||||
finally:
|
||||
self._queue.task_done()
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
|
||||
async def _handle_message(self, message: Any) -> None:
|
||||
reply_text = str(getattr(message, "processed_plain_text", "") or "").strip()
|
||||
if not reply_text:
|
||||
return
|
||||
if self._looks_ephemeral(reply_text):
|
||||
return
|
||||
|
||||
target_person = self._resolve_target_person(message)
|
||||
if target_person is None or not target_person.is_known:
|
||||
return
|
||||
|
||||
facts = await self._extract_facts(target_person, reply_text)
|
||||
if not facts:
|
||||
return
|
||||
|
||||
session_id = str(
|
||||
getattr(message, "session_id", "")
|
||||
or getattr(getattr(message, "session", None), "session_id", "")
|
||||
or ""
|
||||
).strip()
|
||||
if not session_id:
|
||||
return
|
||||
|
||||
person_name = str(getattr(target_person, "person_name", "") or getattr(target_person, "nickname", "") or "").strip()
|
||||
if not person_name:
|
||||
return
|
||||
|
||||
for fact in facts:
|
||||
await store_person_memory_from_answer(person_name, fact, session_id)
|
||||
|
||||
def _resolve_target_person(self, message: Any) -> Optional[Person]:
|
||||
session = getattr(message, "session", None)
|
||||
session_platform = str(getattr(session, "platform", "") or getattr(message, "platform", "") or "").strip()
|
||||
session_user_id = str(getattr(session, "user_id", "") or "").strip()
|
||||
group_id = str(getattr(session, "group_id", "") or "").strip()
|
||||
|
||||
if session_platform and session_user_id and not group_id:
|
||||
if is_bot_self(session_platform, session_user_id):
|
||||
return None
|
||||
person_id = get_person_id(session_platform, session_user_id)
|
||||
person = Person(person_id=person_id)
|
||||
return person if person.is_known else None
|
||||
|
||||
reply_to = str(getattr(message, "reply_to", "") or "").strip()
|
||||
if not reply_to:
|
||||
return None
|
||||
try:
|
||||
replies = find_messages(message_id=reply_to, limit=1)
|
||||
except Exception as exc:
|
||||
logger.debug("查询 reply_to 目标失败: %s", exc)
|
||||
return None
|
||||
if not replies:
|
||||
return None
|
||||
reply_message = replies[0]
|
||||
reply_platform = str(getattr(reply_message, "platform", "") or session_platform or "").strip()
|
||||
reply_user_info = getattr(getattr(reply_message, "message_info", None), "user_info", None)
|
||||
reply_user_id = str(getattr(reply_user_info, "user_id", "") or "").strip()
|
||||
if not reply_platform or not reply_user_id or is_bot_self(reply_platform, reply_user_id):
|
||||
return None
|
||||
person_id = get_person_id(reply_platform, reply_user_id)
|
||||
person = Person(person_id=person_id)
|
||||
return person if person.is_known else None
|
||||
|
||||
async def _extract_facts(self, person: Person, reply_text: str) -> List[str]:
|
||||
person_name = str(getattr(person, "person_name", "") or getattr(person, "nickname", "") or person.person_id)
|
||||
prompt = f"""你要从一条机器人刚刚发送的回复中,提取“关于{person_name}的稳定事实”。
|
||||
|
||||
目标人物:{person_name}
|
||||
机器人回复:
|
||||
{reply_text}
|
||||
|
||||
请只提取满足以下条件的事实:
|
||||
1. 明确是关于目标人物本人的信息。
|
||||
2. 具有相对稳定性,可以作为长期记忆保存。
|
||||
3. 用简洁中文陈述句表达。
|
||||
|
||||
不要提取:
|
||||
- 机器人的情绪、计划、临时动作、客套话
|
||||
- 只适用于当前时刻的短期安排
|
||||
- 不确定、猜测、反问
|
||||
- 与目标人物无关的信息
|
||||
|
||||
严格输出 JSON 数组,例如:
|
||||
["他喜欢深夜打游戏", "他养了一只猫"]
|
||||
如果没有可写入的事实,输出 []"""
|
||||
try:
|
||||
response, _ = await self._extractor.generate_response_async(prompt)
|
||||
except Exception as exc:
|
||||
logger.debug("人物事实提取模型调用失败: %s", exc)
|
||||
return []
|
||||
return self._parse_fact_list(response)
|
||||
|
||||
@staticmethod
|
||||
def _parse_fact_list(raw: str) -> List[str]:
|
||||
text = str(raw or "").strip()
|
||||
if not text:
|
||||
return []
|
||||
try:
|
||||
repaired = repair_json(text)
|
||||
payload = json.loads(repaired) if isinstance(repaired, str) else repaired
|
||||
except Exception:
|
||||
payload = None
|
||||
if not isinstance(payload, list):
|
||||
return []
|
||||
|
||||
items: List[str] = []
|
||||
seen = set()
|
||||
for item in payload:
|
||||
fact = str(item or "").strip().strip("- ")
|
||||
if not fact or len(fact) < 4:
|
||||
continue
|
||||
if fact in seen:
|
||||
continue
|
||||
seen.add(fact)
|
||||
items.append(fact)
|
||||
return items[:5]
|
||||
|
||||
@staticmethod
|
||||
def _looks_ephemeral(text: str) -> bool:
|
||||
content = str(text or "").strip()
|
||||
if not content:
|
||||
return True
|
||||
ephemeral_markers = (
|
||||
"哈哈",
|
||||
"好的",
|
||||
"收到",
|
||||
"嗯嗯",
|
||||
"晚安",
|
||||
"早安",
|
||||
"拜拜",
|
||||
"谢谢",
|
||||
"在吗",
|
||||
"?",
|
||||
)
|
||||
if len(content) <= 8 and any(marker in content for marker in ephemeral_markers):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class MemoryAutomationService:
|
||||
def __init__(self) -> None:
|
||||
self.session_manager = LongTermMemorySessionManager()
|
||||
self.fact_writeback = PersonFactWritebackService()
|
||||
self._started = False
|
||||
|
||||
async def start(self) -> None:
|
||||
if self._started:
|
||||
return
|
||||
await self.fact_writeback.start()
|
||||
self._started = True
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
if not self._started:
|
||||
return
|
||||
await self.session_manager.shutdown()
|
||||
await self.fact_writeback.shutdown()
|
||||
self._started = False
|
||||
|
||||
async def on_incoming_message(self, message: Any) -> None:
|
||||
if not self._started:
|
||||
await self.start()
|
||||
await self.session_manager.on_message(message)
|
||||
|
||||
async def on_message_sent(self, message: Any) -> None:
|
||||
if not self._started:
|
||||
await self.start()
|
||||
await self.fact_writeback.enqueue(message)
|
||||
|
||||
|
||||
memory_automation_service = MemoryAutomationService()
|
||||
428
src/services/memory_service.py
Normal file
428
src/services/memory_service.py
Normal file
@@ -0,0 +1,428 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.plugin_runtime.integration import get_plugin_runtime_manager
|
||||
|
||||
|
||||
logger = get_logger("memory_service")
|
||||
|
||||
PLUGIN_ID = "A_Memorix"
|
||||
|
||||
|
||||
@dataclass
|
||||
class MemoryHit:
|
||||
content: str
|
||||
score: float = 0.0
|
||||
hit_type: str = ""
|
||||
source: str = ""
|
||||
hash_value: str = ""
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
episode_id: str = ""
|
||||
title: str = ""
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"content": self.content,
|
||||
"score": self.score,
|
||||
"type": self.hit_type,
|
||||
"source": self.source,
|
||||
"hash": self.hash_value,
|
||||
"metadata": self.metadata,
|
||||
"episode_id": self.episode_id,
|
||||
"title": self.title,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class MemorySearchResult:
|
||||
summary: str = ""
|
||||
hits: List[MemoryHit] = field(default_factory=list)
|
||||
filtered: bool = False
|
||||
|
||||
def to_text(self, limit: int = 5) -> str:
|
||||
if not self.hits:
|
||||
return ""
|
||||
lines = []
|
||||
for index, item in enumerate(self.hits[: max(1, int(limit))], start=1):
|
||||
content = item.content.strip().replace("\n", " ")
|
||||
if len(content) > 160:
|
||||
content = content[:160] + "..."
|
||||
lines.append(f"{index}. {content}")
|
||||
return "\n".join(lines)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"summary": self.summary,
|
||||
"hits": [item.to_dict() for item in self.hits],
|
||||
"filtered": self.filtered,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class MemoryWriteResult:
|
||||
success: bool
|
||||
stored_ids: List[str] = field(default_factory=list)
|
||||
skipped_ids: List[str] = field(default_factory=list)
|
||||
detail: str = ""
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"success": self.success,
|
||||
"stored_ids": self.stored_ids,
|
||||
"skipped_ids": self.skipped_ids,
|
||||
"detail": self.detail,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class PersonProfileResult:
|
||||
summary: str = ""
|
||||
traits: List[str] = field(default_factory=list)
|
||||
evidence: List[Dict[str, Any]] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {"summary": self.summary, "traits": self.traits, "evidence": self.evidence}
|
||||
|
||||
|
||||
class MemoryService:
|
||||
async def _invoke(self, component_name: str, args: Optional[Dict[str, Any]] = None, *, timeout_ms: int = 30000) -> Any:
|
||||
runtime = get_plugin_runtime_manager()
|
||||
if not runtime.is_running:
|
||||
raise RuntimeError("plugin_runtime 未启动")
|
||||
return await runtime.invoke_plugin(
|
||||
method="plugin.invoke_tool",
|
||||
plugin_id=PLUGIN_ID,
|
||||
component_name=component_name,
|
||||
args=args or {},
|
||||
timeout_ms=max(1000, int(timeout_ms or 30000)),
|
||||
)
|
||||
|
||||
async def _invoke_admin(
|
||||
self,
|
||||
component_name: str,
|
||||
*,
|
||||
action: str,
|
||||
timeout_ms: int = 30000,
|
||||
**kwargs,
|
||||
) -> Dict[str, Any]:
|
||||
payload = await self._invoke(component_name, {"action": action, **kwargs}, timeout_ms=timeout_ms)
|
||||
return payload if isinstance(payload, dict) else {"success": False, "error": "invalid_payload"}
|
||||
|
||||
@staticmethod
|
||||
def _coerce_write_result(payload: Any) -> MemoryWriteResult:
|
||||
if not isinstance(payload, dict):
|
||||
return MemoryWriteResult(success=False, detail="invalid_payload")
|
||||
stored_ids = [str(item) for item in (payload.get("stored_ids") or []) if str(item).strip()]
|
||||
skipped_ids = [str(item) for item in (payload.get("skipped_ids") or []) if str(item).strip()]
|
||||
detail = str(payload.get("detail") or payload.get("reason") or "")
|
||||
if stored_ids or skipped_ids:
|
||||
success = True
|
||||
elif "success" in payload:
|
||||
success = bool(payload.get("success"))
|
||||
else:
|
||||
success = not bool(detail)
|
||||
return MemoryWriteResult(
|
||||
success=success,
|
||||
stored_ids=stored_ids,
|
||||
skipped_ids=skipped_ids,
|
||||
detail=detail,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _coerce_search_result(payload: Any) -> MemorySearchResult:
|
||||
if not isinstance(payload, dict):
|
||||
return MemorySearchResult()
|
||||
hits: List[MemoryHit] = []
|
||||
for item in payload.get("hits", []) or []:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
metadata = item.get("metadata", {}) or {}
|
||||
if not isinstance(metadata, dict):
|
||||
metadata = {}
|
||||
if "source_branches" in item and "source_branches" not in metadata:
|
||||
metadata["source_branches"] = item.get("source_branches") or []
|
||||
if "rank" in item and "rank" not in metadata:
|
||||
metadata["rank"] = item.get("rank")
|
||||
hits.append(
|
||||
MemoryHit(
|
||||
content=str(item.get("content", "") or ""),
|
||||
score=float(item.get("score", 0.0) or 0.0),
|
||||
hit_type=str(item.get("type", "") or ""),
|
||||
source=str(item.get("source", "") or ""),
|
||||
hash_value=str(item.get("hash", "") or ""),
|
||||
metadata=metadata,
|
||||
episode_id=str(item.get("episode_id", "") or ""),
|
||||
title=str(item.get("title", "") or ""),
|
||||
)
|
||||
)
|
||||
return MemorySearchResult(
|
||||
summary=str(payload.get("summary", "") or ""),
|
||||
hits=hits,
|
||||
filtered=bool(payload.get("filtered", False)),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _coerce_profile_result(payload: Any) -> PersonProfileResult:
|
||||
if not isinstance(payload, dict):
|
||||
return PersonProfileResult()
|
||||
return PersonProfileResult(
|
||||
summary=str(payload.get("summary", "") or ""),
|
||||
traits=[str(item) for item in (payload.get("traits") or []) if str(item).strip()],
|
||||
evidence=[item for item in (payload.get("evidence") or []) if isinstance(item, dict)],
|
||||
)
|
||||
|
||||
async def search(
|
||||
self,
|
||||
query: str,
|
||||
*,
|
||||
limit: int = 5,
|
||||
mode: str = "hybrid",
|
||||
chat_id: str = "",
|
||||
person_id: str = "",
|
||||
time_start: str | float | None = None,
|
||||
time_end: str | float | None = None,
|
||||
respect_filter: bool = True,
|
||||
user_id: str = "",
|
||||
group_id: str = "",
|
||||
) -> MemorySearchResult:
|
||||
clean_query = str(query or "").strip()
|
||||
normalized_time_start = None if time_start in {None, ""} else time_start
|
||||
normalized_time_end = None if time_end in {None, ""} else time_end
|
||||
if not clean_query and normalized_time_start is None and normalized_time_end is None:
|
||||
return MemorySearchResult()
|
||||
try:
|
||||
payload = await self._invoke(
|
||||
"search_memory",
|
||||
{
|
||||
"query": clean_query,
|
||||
"limit": max(1, int(limit)),
|
||||
"mode": mode,
|
||||
"chat_id": chat_id,
|
||||
"person_id": person_id,
|
||||
"time_start": normalized_time_start,
|
||||
"time_end": normalized_time_end,
|
||||
"respect_filter": bool(respect_filter),
|
||||
"user_id": str(user_id or "").strip(),
|
||||
"group_id": str(group_id or "").strip(),
|
||||
},
|
||||
)
|
||||
return self._coerce_search_result(payload)
|
||||
except Exception as exc:
|
||||
logger.warning("长期记忆搜索失败: %s", exc)
|
||||
return MemorySearchResult()
|
||||
|
||||
async def ingest_summary(
|
||||
self,
|
||||
*,
|
||||
external_id: str,
|
||||
chat_id: str,
|
||||
text: str,
|
||||
participants: Optional[List[str]] = None,
|
||||
time_start: float | None = None,
|
||||
time_end: float | None = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
respect_filter: bool = True,
|
||||
user_id: str = "",
|
||||
group_id: str = "",
|
||||
) -> MemoryWriteResult:
|
||||
try:
|
||||
payload = await self._invoke(
|
||||
"ingest_summary",
|
||||
{
|
||||
"external_id": external_id,
|
||||
"chat_id": chat_id,
|
||||
"text": text,
|
||||
"participants": participants or [],
|
||||
"time_start": time_start,
|
||||
"time_end": time_end,
|
||||
"tags": tags or [],
|
||||
"metadata": metadata or {},
|
||||
"respect_filter": bool(respect_filter),
|
||||
"user_id": str(user_id or "").strip(),
|
||||
"group_id": str(group_id or "").strip(),
|
||||
},
|
||||
)
|
||||
return self._coerce_write_result(payload)
|
||||
except Exception as exc:
|
||||
logger.warning("长期记忆写入摘要失败: %s", exc)
|
||||
return MemoryWriteResult(success=False, detail=str(exc))
|
||||
|
||||
async def ingest_text(
|
||||
self,
|
||||
*,
|
||||
external_id: str,
|
||||
source_type: str,
|
||||
text: str,
|
||||
chat_id: str = "",
|
||||
person_ids: Optional[List[str]] = None,
|
||||
participants: Optional[List[str]] = None,
|
||||
timestamp: float | None = None,
|
||||
time_start: float | None = None,
|
||||
time_end: float | None = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
entities: Optional[List[str]] = None,
|
||||
relations: Optional[List[Dict[str, Any]]] = None,
|
||||
respect_filter: bool = True,
|
||||
user_id: str = "",
|
||||
group_id: str = "",
|
||||
) -> MemoryWriteResult:
|
||||
try:
|
||||
payload = await self._invoke(
|
||||
"ingest_text",
|
||||
{
|
||||
"external_id": external_id,
|
||||
"source_type": source_type,
|
||||
"text": text,
|
||||
"chat_id": chat_id,
|
||||
"person_ids": person_ids or [],
|
||||
"participants": participants or [],
|
||||
"timestamp": timestamp,
|
||||
"time_start": time_start,
|
||||
"time_end": time_end,
|
||||
"tags": tags or [],
|
||||
"metadata": metadata or {},
|
||||
"entities": entities or [],
|
||||
"relations": relations or [],
|
||||
"respect_filter": bool(respect_filter),
|
||||
"user_id": str(user_id or "").strip(),
|
||||
"group_id": str(group_id or "").strip(),
|
||||
},
|
||||
)
|
||||
return self._coerce_write_result(payload)
|
||||
except Exception as exc:
|
||||
logger.warning("长期记忆写入文本失败: %s", exc)
|
||||
return MemoryWriteResult(success=False, detail=str(exc))
|
||||
|
||||
async def get_person_profile(self, person_id: str, *, chat_id: str = "", limit: int = 10) -> PersonProfileResult:
|
||||
clean_person_id = str(person_id or "").strip()
|
||||
if not clean_person_id:
|
||||
return PersonProfileResult()
|
||||
try:
|
||||
payload = await self._invoke(
|
||||
"get_person_profile",
|
||||
{"person_id": clean_person_id, "chat_id": chat_id, "limit": max(1, int(limit))},
|
||||
)
|
||||
return self._coerce_profile_result(payload)
|
||||
except Exception as exc:
|
||||
logger.warning("获取人物画像失败: %s", exc)
|
||||
return PersonProfileResult()
|
||||
|
||||
async def maintain_memory(
|
||||
self,
|
||||
*,
|
||||
action: str,
|
||||
target: str = "",
|
||||
hours: float | None = None,
|
||||
reason: str = "",
|
||||
limit: int = 50,
|
||||
) -> MemoryWriteResult:
|
||||
try:
|
||||
payload = await self._invoke(
|
||||
"maintain_memory",
|
||||
{"action": action, "target": target, "hours": hours, "reason": reason, "limit": limit},
|
||||
)
|
||||
if not isinstance(payload, dict):
|
||||
return MemoryWriteResult(success=False, detail="invalid_payload")
|
||||
return MemoryWriteResult(success=bool(payload.get("success")), detail=str(payload.get("detail", "") or ""))
|
||||
except Exception as exc:
|
||||
logger.warning("记忆维护失败: %s", exc)
|
||||
return MemoryWriteResult(success=False, detail=str(exc))
|
||||
|
||||
async def memory_stats(self) -> Dict[str, Any]:
|
||||
try:
|
||||
payload = await self._invoke("memory_stats", {})
|
||||
return payload if isinstance(payload, dict) else {}
|
||||
except Exception as exc:
|
||||
logger.warning("获取记忆统计失败: %s", exc)
|
||||
return {}
|
||||
|
||||
async def graph_admin(self, *, action: str, **kwargs) -> Dict[str, Any]:
|
||||
try:
|
||||
return await self._invoke_admin("memory_graph_admin", action=action, **kwargs)
|
||||
except Exception as exc:
|
||||
logger.warning("图谱管理调用失败: %s", exc)
|
||||
return {"success": False, "error": str(exc)}
|
||||
|
||||
async def source_admin(self, *, action: str, **kwargs) -> Dict[str, Any]:
|
||||
try:
|
||||
return await self._invoke_admin("memory_source_admin", action=action, **kwargs)
|
||||
except Exception as exc:
|
||||
logger.warning("来源管理调用失败: %s", exc)
|
||||
return {"success": False, "error": str(exc)}
|
||||
|
||||
async def episode_admin(self, *, action: str, **kwargs) -> Dict[str, Any]:
|
||||
try:
|
||||
return await self._invoke_admin("memory_episode_admin", action=action, **kwargs)
|
||||
except Exception as exc:
|
||||
logger.warning("Episode 管理调用失败: %s", exc)
|
||||
return {"success": False, "error": str(exc)}
|
||||
|
||||
async def profile_admin(self, *, action: str, **kwargs) -> Dict[str, Any]:
|
||||
try:
|
||||
return await self._invoke_admin("memory_profile_admin", action=action, **kwargs)
|
||||
except Exception as exc:
|
||||
logger.warning("画像管理调用失败: %s", exc)
|
||||
return {"success": False, "error": str(exc)}
|
||||
|
||||
async def runtime_admin(self, *, action: str, **kwargs) -> Dict[str, Any]:
|
||||
try:
|
||||
return await self._invoke_admin("memory_runtime_admin", action=action, **kwargs)
|
||||
except Exception as exc:
|
||||
logger.warning("运行时管理调用失败: %s", exc)
|
||||
return {"success": False, "error": str(exc)}
|
||||
|
||||
async def import_admin(self, *, action: str, timeout_ms: int = 120000, **kwargs) -> Dict[str, Any]:
|
||||
try:
|
||||
return await self._invoke_admin("memory_import_admin", action=action, timeout_ms=timeout_ms, **kwargs)
|
||||
except Exception as exc:
|
||||
logger.warning("导入管理调用失败: %s", exc)
|
||||
return {"success": False, "error": str(exc)}
|
||||
|
||||
async def tuning_admin(self, *, action: str, timeout_ms: int = 120000, **kwargs) -> Dict[str, Any]:
|
||||
try:
|
||||
return await self._invoke_admin("memory_tuning_admin", action=action, timeout_ms=timeout_ms, **kwargs)
|
||||
except Exception as exc:
|
||||
logger.warning("调优管理调用失败: %s", exc)
|
||||
return {"success": False, "error": str(exc)}
|
||||
|
||||
async def v5_admin(self, *, action: str, timeout_ms: int = 30000, **kwargs) -> Dict[str, Any]:
|
||||
try:
|
||||
return await self._invoke_admin("memory_v5_admin", action=action, timeout_ms=timeout_ms, **kwargs)
|
||||
except Exception as exc:
|
||||
logger.warning("V5 记忆管理调用失败: %s", exc)
|
||||
return {"success": False, "error": str(exc)}
|
||||
|
||||
async def delete_admin(self, *, action: str, timeout_ms: int = 120000, **kwargs) -> Dict[str, Any]:
|
||||
try:
|
||||
return await self._invoke_admin("memory_delete_admin", action=action, timeout_ms=timeout_ms, **kwargs)
|
||||
except Exception as exc:
|
||||
logger.warning("删除管理调用失败: %s", exc)
|
||||
return {"success": False, "error": str(exc)}
|
||||
|
||||
async def get_recycle_bin(self, *, limit: int = 50) -> Dict[str, Any]:
|
||||
try:
|
||||
payload = await self._invoke("maintain_memory", {"action": "recycle_bin", "limit": max(1, int(limit or 50))})
|
||||
return payload if isinstance(payload, dict) else {"success": False, "error": "invalid_payload"}
|
||||
except Exception as exc:
|
||||
logger.warning("获取回收站失败: %s", exc)
|
||||
return {"success": False, "error": str(exc)}
|
||||
|
||||
async def restore_memory(self, *, target: str) -> MemoryWriteResult:
|
||||
return await self.maintain_memory(action="restore", target=target)
|
||||
|
||||
async def reinforce_memory(self, *, target: str) -> MemoryWriteResult:
|
||||
return await self.maintain_memory(action="reinforce", target=target)
|
||||
|
||||
async def freeze_memory(self, *, target: str) -> MemoryWriteResult:
|
||||
return await self.maintain_memory(action="freeze", target=target)
|
||||
|
||||
async def protect_memory(self, *, target: str, hours: float | None = None) -> MemoryWriteResult:
|
||||
return await self.maintain_memory(action="protect", target=target, hours=hours)
|
||||
|
||||
|
||||
memory_service = MemoryService()
|
||||
Reference in New Issue
Block a user