diff --git a/docs/mkdocs/en/memory.md b/docs/mkdocs/en/memory.md index 96dce630..c69c4fe6 100644 --- a/docs/mkdocs/en/memory.md +++ b/docs/mkdocs/en/memory.md @@ -594,8 +594,9 @@ python3 run_agent.py [TencentDB Agent Memory](https://github.com/TencentCloud/TencentDB-Agent-Memory) V3 gateway. After each completed turn, the framework incrementally sends new text events to `/v3/conversation/add`. An Agent equipped with -`load_memory_tool` searches extracted L1 atomic memories through -`/v3/atomic/search`. +`load_memory_tool` recalls L1 atomic memories, L2 scenario navigation, and L3 +core memory in parallel. If all three layers have no usable content, the +service searches raw L0 conversations through `/v3/conversation/search`. ```python from trpc_agent_sdk.memory.tencentdb_memory_service import ( @@ -630,6 +631,11 @@ Operational notes: - L1 extraction is asynchronous, so a memory may not be searchable immediately after a successful write. +- Recall requests `/v3/atomic/search`, `/v3/scenario/ls`, and `/v3/core/read` + concurrently. A failure in one layer does not discard results from the other + layers. +- L0 conversation search is used only when L1, L2, and L3 all have no usable + content. - Successfully accepted event IDs are checkpointed in process. Delivery is at-least-once across restarts. - TencentDB Agent Memory controls retention; framework TTL settings do not diff --git a/docs/mkdocs/zh/memory.md b/docs/mkdocs/zh/memory.md index 35715aa6..dbc4e9bb 100644 --- a/docs/mkdocs/zh/memory.md +++ b/docs/mkdocs/zh/memory.md @@ -547,8 +547,9 @@ python3 run_agent.py `TencentDBMemoryService` 用于对接 [TencentDB Agent Memory](https://github.com/TencentCloud/TencentDB-Agent-Memory) V3 网关。每轮对话结束后,框架会将新增文本事件增量写入 -`/v3/conversation/add`;配置了 `load_memory_tool` 的 Agent 会通过 -`/v3/atomic/search` 检索异步提取出的 L1 原子记忆。 +`/v3/conversation/add`;配置了 `load_memory_tool` 的 Agent 会并行召回 +L1 原子记忆、L2 场景导航和 L3 核心记忆。三层均无可用内容时,再通过 +`/v3/conversation/search` 搜索 L0 原始对话。 ```python from trpc_agent_sdk.memory.tencentdb_memory_service import ( @@ -581,6 +582,9 @@ memory_service = TencentDBMemoryService( 使用时需要注意: - L1 记忆提取是异步的,写入成功后不保证立即可以搜索到。 +- 召回请求并行访问 `/v3/atomic/search`、`/v3/scenario/ls` 和 + `/v3/core/read`;单层失败不会丢弃其他层的有效结果。 +- 只有 L1、L2、L3 均无可用内容时才回退搜索 L0。 - 进程内会记录已成功写入的事件 ID;进程重启后采用至少一次投递语义。 - 记忆保留策略由 TencentDB Agent Memory 管理,框架 TTL 配置不适用于该服务。 - 使用前需要部署 V3 网关和记忆提取流水线。 diff --git a/examples/memory_service_with_tencentdb/.env b/examples/memory_service_with_tencentdb/.env index c9f66673..74e37a61 100644 --- a/examples/memory_service_with_tencentdb/.env +++ b/examples/memory_service_with_tencentdb/.env @@ -3,13 +3,15 @@ TRPC_AGENT_API_KEY=your-llm-api-key TRPC_AGENT_BASE_URL=https://your-llm-endpoint TRPC_AGENT_MODEL_NAME=your-model-name -# TencentDB Agent Memory V3 gateway -TENCENTDB_MEMORY_ENDPOINT=http://127.0.0.1:8420 -TENCENTDB_MEMORY_API_KEY=local +# TencentDB Agent Memory cloud values from the verified L1 demo. +# Do not commit a real API key; provide it locally before running. +TENCENTDB_MEMORY_ENDPOINT=https://memory.ap-beijing.tencenttdai.com +TENCENTDB_MEMORY_API_KEY=your-memory-api-key TENCENTDB_MEMORY_SERVICE_ID=your-memory-service-id TENCENTDB_MEMORY_TEAM_ID=your-team-id -TENCENTDB_MEMORY_AGENT_ID=weather-assistant +# This Agent/User scope already produced "favorite color is blue" as L1. +TENCENTDB_MEMORY_AGENT_ID=your-agent-id +TENCENTDB_MEMORY_USER_ID=usr-your-user-id -# L1 extraction is asynchronous. Increase this for slower deployments. -TENCENTDB_MEMORY_PIPELINE_WAIT_SECONDS=5 -MEMORY_PROMPT_MODE=chat +# L1 extraction is asynchronous. This delay is an allowance, not a readiness guarantee. +TENCENTDB_MEMORY_PIPELINE_WAIT_SECONDS=60 diff --git a/examples/memory_service_with_tencentdb/README.md b/examples/memory_service_with_tencentdb/README.md index 68a872e7..f7957868 100644 --- a/examples/memory_service_with_tencentdb/README.md +++ b/examples/memory_service_with_tencentdb/README.md @@ -2,7 +2,8 @@ 本示例演示如何通过 `TencentDBMemoryService` 将 tRPC-Agent 接入 [TencentDB Agent Memory](https://github.com/TencentCloud/TencentDB-Agent-Memory), -在一个 Session 中写入用户偏好,并在另一个 Session 中召回服务端提取的 L1 长期记忆。 +在一个 Session 中写入带唯一验证标识的办公场景信息,并在另一个 Session 中召回 +服务端提取的长期记忆。 ## 工作流程 @@ -15,8 +16,8 @@ 第二轮对话 -> Agent 调用 load_memory -> TencentDBMemoryService.search_memory() - -> POST /v3/atomic/search 搜索 L1 记忆 - -> L1 无结果时,POST /v3/conversation/search 搜索 L0 原始对话 + -> 并行读取 L1 /v3/atomic/search、L2 /v3/scenario/ls 和 L3 /v3/core/read + -> L1/L2/L3 均无可用内容时,POST /v3/conversation/search 搜索 L0 原始对话 ``` 写入记忆由框架自动完成,不需要给 Agent 添加 `save_memory` 工具。Agent 只需通过 @@ -32,7 +33,7 @@ > - `chat`:个人偏好、用户画像、对话经历、教学和通用助手场景。 > - `code`:项目事实、工程任务、技术决策、SOP 和团队协作场景。 > -> 本示例写入“喜欢的颜色”。本地部署需要在 Memory Core 服务端选择 `chat` +> 本示例写入工程任务和发版评审信息。本地部署需要在 Memory Core 服务端选择 `code` > 模式。腾讯云托管实例的提取策略由产品服务端管理,本客户端不会读取或发送 > `MEMORY_PROMPT_MODE`。 @@ -78,11 +79,11 @@ cd TencentDB-Agent-Memory/deploy/global-images cp .env.example .env ``` -本示例保存的是“喜欢的颜色”这类个人偏好,服务端必须使用 `chat` 提取模式。在 +本示例保存的是工程任务和发版评审信息,服务端应使用 `code` 提取模式。在 `TencentDB-Agent-Memory/deploy/global-images/.env` 中确认: ```dotenv -MEMORY_PROMPT_MODE=chat +MEMORY_PROMPT_MODE=code ``` 然后运行启动脚本。脚本会交互式要求填写 Memory 和 Proxy 使用的 LLM 地址、 @@ -259,28 +260,31 @@ python3 examples/memory_service_with_tencentdb/run_agent.py 示例执行以下流程: -1. `session-write` 告诉 Agent:“My favorite color is blue.” -2. 第一轮结束后,Runner 自动将新增事件写入 L0。 -3. Memory Core 异步将该偏好提取为 L1 记忆。 -4. 等待提取完成后,`session-recall` 在另一个 Session 中询问喜欢的颜色。 -5. Agent 调用 `load_memory`,跨 Session 搜索 `alice` 的长期记忆。 +1. 为本次运行生成唯一的 `验证项目-` 标识。 +2. 写入 Agent 不加载历史记忆,只发送本轮办公场景对话并由 Runner 写入 L0。 +3. Memory Core 异步提取 L1/L2/L3。 +4. 等待配置的提取时间后,通过公开 `search_memory()` 输出各层召回结果。 +5. 召回 Agent 在新的 Session 中根据唯一标识询问评审会最终安排。 ## 测试结果 一次成功运行的关键输出如下,模型的具体措辞可能不同: ```text -User (session-write): My favorite color is blue. Please remember it. -Assistant: ... I've noted that your favorite color is blue. +TencentDB verification marker: 验证项目-1234abcd +User (office-write-1234abcd): 验证项目-1234abcd 的 Q4 发版评审会原定于... Waiting s for asynchronous memory extraction... -User (session-recall): What is my favorite color? -Assistant: Your favorite color is blue! +TencentDB recall verification summary: layers=L1,L2,L3, memories=, +current_run_l1_found=True, current_run_final_found=True + +User (office-recall-1234abcd): 验证项目-1234abcd 的 Q4 发版评审会最终安排... +Assistant: 11 月 6 日上午 10 点,会议室 3B。 ``` -实际写入发生在对话结束后的 Runner 阶段,对 Agent 是透明的。第二个 Session 能 -回答 `blue`,说明记忆检索链路可用;但由于当前实现会在 L1 无结果时搜索 L0 原始 -对话,仅凭回答正确不能证明 L1 已成功提取。 +`current_run_final_found=True` 表示包含本次唯一标识的 L1 已提取出最终改期信息, +不会因为历史记录中恰好存在相同日期和会议室而误判。若 L1、L2、L3 均无结果, +服务仍会搜索 L0 原始对话作为兜底。 ## 故障排查 @@ -314,8 +318,7 @@ l1-empty reason=empty_scenes 1. Gateway 地址、实例 ID、Team ID 和 Agent ID 来自同一个实例。 2. `run_agent.py` 的 `user_id` 与查询时使用的业务用户一致。 -3. 当前实例的服务端提取策略适合测试内容。默认编码场景可能不会沉淀“喜欢的颜色” - 这类个人偏好。 +3. 当前实例的服务端提取策略适合测试内容。本示例属于工程协作场景。 4. 已等待足够时间;L0 写入成功不代表 L1 已经生成。 如果仍然无法生成或检索 L1,请参考腾讯云官方文档检查服务端配置: @@ -352,8 +355,10 @@ POST /v3/atomic/search status=200 - `service_id/team_id/agent_id/user_id` 共同构成记忆隔离边界。 - `session_id` 仅在写入时发送;搜索时不限定 Session,因此支持跨 Session 召回。 -- `/v3/atomic/search` 是 L1 语义检索接口;如果 L1 没有命中,当前实现会使用 - `/v3/conversation/search` 对 L0 原始对话做语义兜底。 +- 召回时会并行读取 L1 `/v3/atomic/search`、L2 `/v3/scenario/ls` 和 L3 + `/v3/core/read`;只要任意一层有可用内容就返回组合结果。 +- L1/L2/L3 均无可用内容时,使用 `/v3/conversation/search` 对 L0 原始对话 + 做语义兜底。 - 服务只发送当前进程中尚未成功写入的事件。 - 进程重启后采用至少一次投递语义,因为 V3 写入接口没有调用方提供的幂等键。 - L1 提取是异步的,写入成功不代表记忆可以立即搜索。 diff --git a/examples/memory_service_with_tencentdb/agent/agent.py b/examples/memory_service_with_tencentdb/agent/agent.py index dca76fcc..8c4e54c7 100644 --- a/examples/memory_service_with_tencentdb/agent/agent.py +++ b/examples/memory_service_with_tencentdb/agent/agent.py @@ -12,8 +12,8 @@ from .config import get_model_config -def create_agent() -> LlmAgent: - """Create an assistant that can recall cross-session memory.""" +def create_agent(*, recall_enabled: bool = True) -> LlmAgent: + """Create an assistant, optionally with cross-session recall.""" api_key, base_url, model_name = get_model_config() return LlmAgent( name="memory_assistant", @@ -24,8 +24,9 @@ def create_agent() -> LlmAgent: base_url=base_url, ), instruction=("Use load_memory before answering questions about information the " - "user may have shared in earlier conversations."), - tools=[load_memory_tool], + "user may have shared in earlier conversations." + if recall_enabled else "Acknowledge the user's new information without recalling prior memory."), + tools=[load_memory_tool] if recall_enabled else [], ) diff --git a/examples/memory_service_with_tencentdb/run_agent.py b/examples/memory_service_with_tencentdb/run_agent.py index 2486c1ba..13b9bfee 100644 --- a/examples/memory_service_with_tencentdb/run_agent.py +++ b/examples/memory_service_with_tencentdb/run_agent.py @@ -11,6 +11,7 @@ import os import sys from pathlib import Path +from uuid import uuid4 from dotenv import load_dotenv from trpc_agent_sdk.context import AgentContext @@ -26,6 +27,21 @@ load_dotenv() sys.path.append(str(Path(__file__).parent)) +_FINAL_DATE_MARKERS = ("11月6", "11/6", "2026-11-06") +_FINAL_ROOM_MARKER = "3b" + + +def _office_messages(run_marker: str) -> tuple[str, ...]: + return ( + f"{run_marker} 的 Q4 发版评审会原定于 11 月 5 日下午 3 点,会议室 3A。", + "我这周负责修 pay-service 的订单超时 bug,已经在 TAPD 建了单 TAPD-88231。", + "以后给我写 commit message 都用英文,标题不超过 72 字符,正文用 bullet 列改动。", + "刚定位到根因是连接池 maxIdle 配成 2 太小,改成 20 后本地复现不出来了。", + f"{run_marker} 的评审会最终改到 11 月 6 日上午 10 点,会议室 3B;" + "原定的 11 月 5 日下午 3 点、会议室 3A 作废。", + "发版前必须跑一遍全量回归,这是我定的硬规矩,别跳过。", + ) + def _required_env(name: str) -> str: value = os.getenv(name, "").strip() @@ -73,39 +89,109 @@ async def _run_turn( print() +async def _print_recall_verification( + memory_service: TencentDBMemoryService, + *, + user_id: str, + run_marker: str, + recall_query: str, +) -> None: + """Print the public recall result and its inferred memory layers.""" + result = await memory_service.search_memory( + key=user_id, + query=recall_query, + limit=10, + ) + found_layers: set[str] = set() + current_run_l1_found = False + current_run_final_found = False + normalized_marker = "".join(run_marker.split()).lower() + print("\nTencentDB recall verification:") + for index, memory in enumerate(result.memories): + if memory.author == "scenario": + layer = "L2" + elif memory.author == "core": + layer = "L3" + elif memory.author in {"user", "assistant", "message"}: + layer = "L0" + else: + layer = "L1" + found_layers.add(layer) + text = "".join(part.text or "" for part in memory.content.parts) + normalized = "".join(text.split()).lower() + belongs_to_current_run = layer == "L1" and normalized_marker in normalized + current_run_l1_found = current_run_l1_found or belongs_to_current_run + if (belongs_to_current_run and any(marker in normalized for marker in _FINAL_DATE_MARKERS) + and _FINAL_ROOM_MARKER in normalized): + current_run_final_found = True + preview = text if len(text) <= 500 else f"{text[:500]}...[truncated]" + print(f" [{index}] {layer}:{memory.author} {preview}") + layers = ",".join(sorted(found_layers)) if found_layers else "none" + print( + "TencentDB recall verification summary: " + f"layers={layers}, memories={len(result.memories)}, " + f"current_run_l1_found={current_run_l1_found}, " + f"current_run_final_found={current_run_final_found}", ) + + async def main() -> None: - """Write a fact in one session and recall it from another.""" + """Write office facts, wait for extraction, then recall across sessions.""" + from agent.agent import create_agent from agent.agent import root_agent memory_service = create_memory_service() - runner = Runner( + session_service = InMemorySessionService() + write_runner = Runner( + app_name="tencentdb_memory_demo", + agent=create_agent(recall_enabled=False), + session_service=session_service, + memory_service=memory_service, + close_session_service_on_close=False, + close_memory_service_on_close=False, + ) + recall_runner = Runner( app_name="tencentdb_memory_demo", agent=root_agent, - session_service=InMemorySessionService(), + session_service=session_service, memory_service=memory_service, ) - user_id = "alice" + user_id = _required_env("TENCENTDB_MEMORY_USER_ID") + run_id = uuid4().hex[:8] + run_marker = f"验证项目-{run_id}" + recall_query = f"{run_marker} 的 Q4 发版评审会最终安排在什么时间和会议室?" + write_session_id = f"office-write-{run_id}" + recall_session_id = f"office-recall-{run_id}" + print(f"TencentDB verification marker: {run_marker}") try: - await _run_turn( - runner, - user_id=user_id, - session_id="session-write", - text="My favorite color is blue. Please remember it.", - ) - - wait_seconds = float(os.getenv("TENCENTDB_MEMORY_PIPELINE_WAIT_SECONDS", "5"), ) + for text in _office_messages(run_marker): + await _run_turn( + write_runner, + user_id=user_id, + session_id=write_session_id, + text=text, + ) + + wait_seconds = float(os.getenv("TENCENTDB_MEMORY_PIPELINE_WAIT_SECONDS", "10"), ) print(f"Waiting {wait_seconds:g}s for asynchronous memory extraction...", ) await asyncio.sleep(wait_seconds) + await _print_recall_verification( + memory_service, + user_id=user_id, + run_marker=run_marker, + recall_query=recall_query, + ) + await _run_turn( - runner, + recall_runner, user_id=user_id, - session_id="session-recall", - text="What is my favorite color?", + session_id=recall_session_id, + text=recall_query, ) finally: - await runner.close() + await write_runner.close() + await recall_runner.close() if __name__ == "__main__": diff --git a/tests/memory/test_tencentdb_memory_service.py b/tests/memory/test_tencentdb_memory_service.py index 55aee501..a5e4a7dc 100644 --- a/tests/memory/test_tencentdb_memory_service.py +++ b/tests/memory/test_tencentdb_memory_service.py @@ -266,27 +266,32 @@ async def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.asyncio -async def test_search_memory_maps_v3_atomic_items_across_sessions(): - captured_body: dict[str, Any] = {} +async def test_search_memory_combines_v3_l1_l2_l3_across_sessions(): + captured_bodies: dict[str, dict[str, Any]] = {} async def handler(request: httpx.Request) -> httpx.Response: - captured_body.update(json.loads(request.content)) - return _json_response({ - "items": [ - { + captured_bodies[request.url.path] = json.loads(request.content) + if request.url.path == "/v3/atomic/search": + return _json_response({ + "items": [{ "id": "atomic-1", "type": "persona", "content": "The user's favorite color is blue.", "score": 0.97, "created_at": "2026-09-24T10:00:00Z", "updated_at": "2026-09-24T10:01:00Z", - }, - { - "id": "atomic-2", - "type": "episodic", - "content": "", - }, - ], + }], + }) + if request.url.path == "/v3/scenario/ls": + return _json_response({ + "entries": [{ + "path": "projects/release-review.md", + "updated_at": "2026-09-24T10:02:00Z", + }], + }) + return _json_response({ + "content": "Always run the full regression suite before release.", + "updated_at": "2026-09-24T10:03:00Z", }) client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) @@ -298,17 +303,60 @@ async def handler(request: httpx.Request) -> httpx.Response: limit=5, ) - assert captured_body == { + assert captured_bodies["/v3/atomic/search"] == { "query": "favorite color", "limit": 5, "team_id": "team-1", "agent_id": "agent-1", "user_id": "user-1", } - assert len(result.memories) == 1 + assert captured_bodies["/v3/scenario/ls"] == { + "team_id": "team-1", + "agent_id": "agent-1", + "user_id": "user-1", + } + assert captured_bodies["/v3/core/read"] == captured_bodies["/v3/scenario/ls"] + assert len(result.memories) == 3 assert result.memories[0].content.parts[0].text == ("The user's favorite color is blue.") assert result.memories[0].author == "persona" assert result.memories[0].timestamp == "2026-09-24T10:01:00Z" + assert result.memories[1].content.parts[0].text == "projects/release-review.md" + assert result.memories[1].author == "scenario" + assert result.memories[2].content.parts[0].text == ("Always run the full regression suite before release.") + assert result.memories[2].author == "core" + await client.aclose() + + +@pytest.mark.asyncio +async def test_search_memory_keeps_successful_layer_on_partial_recall_failure(): + paths: list[str] = [] + + async def handler(request: httpx.Request) -> httpx.Response: + paths.append(request.url.path) + if request.url.path == "/v3/scenario/ls": + return _json_response({ + "entries": [{ + "path": "projects/release-review.md", + }], + }) + return httpx.Response(503, text="unavailable") + + client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) + service = TencentDBMemoryService(_config(), client=client) + + result = await service.search_memory( + key="memory-demo/user-1", + query="release review", + ) + + assert paths == [ + "/v3/atomic/search", + "/v3/scenario/ls", + "/v3/core/read", + ] + assert len(result.memories) == 1 + assert result.memories[0].author == "scenario" + assert result.memories[0].content.parts[0].text == "projects/release-review.md" await client.aclose() @@ -320,6 +368,10 @@ async def handler(request: httpx.Request) -> httpx.Response: requests.append(request) if request.url.path == "/v3/atomic/search": return _json_response({"items": []}) + if request.url.path == "/v3/scenario/ls": + return _json_response({"entries": []}) + if request.url.path == "/v3/core/read": + return _json_response({"content": ""}) return _json_response({ "messages": [{ "id": "message-1", @@ -341,9 +393,11 @@ async def handler(request: httpx.Request) -> httpx.Response: assert [request.url.path for request in requests] == [ "/v3/atomic/search", + "/v3/scenario/ls", + "/v3/core/read", "/v3/conversation/search", ] - assert json.loads(requests[1].content)["query"] == "What color do I like?" + assert json.loads(requests[3].content)["query"] == "What color do I like?" assert result.memories[0].content.parts[0].text == "My favorite color is blue." assert result.memories[0].author == "user" assert result.memories[0].timestamp == "2026-09-24T10:00:00Z" diff --git a/trpc_agent_sdk/memory/tencentdb_memory_service.py b/trpc_agent_sdk/memory/tencentdb_memory_service.py index 6ce3203d..1c036cc1 100644 --- a/trpc_agent_sdk/memory/tencentdb_memory_service.py +++ b/trpc_agent_sdk/memory/tencentdb_memory_service.py @@ -35,6 +35,8 @@ _CONVERSATION_ADD_PATH = "/v3/conversation/add" _CONVERSATION_SEARCH_PATH = "/v3/conversation/search" _ATOMIC_SEARCH_PATH = "/v3/atomic/search" +_SCENARIO_LIST_PATH = "/v3/scenario/ls" +_CORE_READ_PATH = "/v3/core/read" _MAX_MESSAGES_PER_REQUEST = 100 _MAX_MESSAGE_UTF16_UNITS = 8192 @@ -110,9 +112,10 @@ class TencentDBMemoryService(BaseMemoryService): at-least-once across process restarts because the V3 API does not accept a caller-provided idempotency key. - L0 writes use ``/v3/conversation/add`` with session isolation. Searches - use ``/v3/atomic/search`` without a session ID so memories can be recalled - across sessions for the same user. + L0 writes use ``/v3/conversation/add`` with session isolation. Recall + combines L1 atomic memories, L2 scenario navigation and L3 core memory + without a session ID so memories can be recalled across sessions. L0 + conversation search is used only when none of those layers has content. """ def __init__( @@ -181,29 +184,42 @@ async def search_memory( limit: int = 10, agent_context: Optional[AgentContext] = None, ) -> SearchMemoryResponse: - """Search V3 L1 atomic memories across the user's sessions.""" + """Recall V3 L1/L2/L3 memories, falling back to L0.""" del agent_context response = SearchMemoryResponse() user_id = self._user_id_from_key(key) - body = { + isolation = self._isolation_body(user_id) + search_body = { "query": query, "limit": limit, - **self._isolation_body(user_id), + **isolation, } + layer_results = await asyncio.gather( + self._post(_ATOMIC_SEARCH_PATH, search_body), + self._post(_SCENARIO_LIST_PATH, isolation), + self._post(_CORE_READ_PATH, isolation), + return_exceptions=True, + ) + + self._append_layer_items( + response, + layer_results[0], + field="items", + layer="L1", + ) + self._append_scenario_entries(response, layer_results[1]) + self._append_core_memory(response, layer_results[2]) + if response.memories: + return response + try: - # search atomic memories for the user L1 - data = await self._post(_ATOMIC_SEARCH_PATH, body) - items = data.get("items", []) - if not items: - # search conversation memories for the user L0 - data = await self._post(_CONVERSATION_SEARCH_PATH, body) - items = data.get("messages", []) - if not isinstance(items, list): - raise ValueError("TencentDB Agent Memory search response must contain a list") - for item in items: - entry = self._to_memory_entry(item) - if entry is not None: - response.memories.append(entry) + data = await self._post(_CONVERSATION_SEARCH_PATH, search_body) + self._append_layer_items( + response, + data, + field="messages", + layer="L0", + ) except Exception as exc: # pylint: disable=broad-except logger.warning( "Failed to search TencentDB Agent Memory. key=%s, query=%s, err=%s", @@ -213,6 +229,87 @@ async def search_memory( ) return response + @classmethod + def _append_layer_items( + cls, + response: SearchMemoryResponse, + result: Any, + *, + field: str, + layer: str, + ) -> None: + if isinstance(result, Exception): + logger.warning( + "Failed to recall TencentDB Agent Memory %s. err=%s", + layer, + result, + ) + return + items = result.get(field, []) + if not isinstance(items, list): + logger.warning( + "TencentDB Agent Memory %s response field %s must be a list", + layer, + field, + ) + return + for item in items: + entry = cls._to_memory_entry(item) + if entry is not None: + response.memories.append(entry) + + @classmethod + def _append_scenario_entries( + cls, + response: SearchMemoryResponse, + result: Any, + ) -> None: + if isinstance(result, Exception): + logger.warning( + "Failed to recall TencentDB Agent Memory L2. err=%s", + result, + ) + return + entries = result.get("entries", []) + if not isinstance(entries, list): + logger.warning("TencentDB Agent Memory L2 response field entries must be a list", ) + return + for item in entries: + if not isinstance(item, dict): + continue + path = item.get("path") + if not isinstance(path, str) or not path.strip(): + continue + entry = cls._to_memory_entry({ + "type": "scenario", + "content": path.strip(), + "created_at": item.get("created_at"), + "updated_at": item.get("updated_at"), + }) + if entry is not None: + response.memories.append(entry) + + @classmethod + def _append_core_memory( + cls, + response: SearchMemoryResponse, + result: Any, + ) -> None: + if isinstance(result, Exception): + logger.warning( + "Failed to recall TencentDB Agent Memory L3. err=%s", + result, + ) + return + entry = cls._to_memory_entry({ + "type": "core", + "content": result.get("content"), + "created_at": result.get("created_at"), + "updated_at": result.get("updated_at"), + }) + if entry is not None: + response.memories.append(entry) + @override async def close(self) -> None: """Release the owned HTTP client and local delivery checkpoints."""