From e55f5a9b13058381b7f47b5f7aec8da0df656ea1 Mon Sep 17 00:00:00 2001 From: raychen <815315825@qq.com> Date: Fri, 8 May 2026 10:17:15 +0800 Subject: [PATCH] =?UTF-8?q?feature:=20=E6=94=AF=E6=8C=81=20MemPalace=20?= =?UTF-8?q?=E8=AE=B0=E5=BF=86=E9=9B=86=E6=88=90=E5=B9=B6=E4=BC=98=E5=8C=96?= =?UTF-8?q?=20Agent=20=E8=BF=90=E8=A1=8C=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 MemPalaceMemoryService 和 mempalace_tool,支持 drawer、diary、KG 工具能力。 - 新增 MemPalace 示例、文档和单测,并完善中英文 memory 文档。 - 调整 LlmAgent 循环控制,支持通过 metadata 控制运行状态。 - 优化 A2A 长运行/鉴权事件转换和取消事件处理。 - 规范 memory/tool 输出、examples 环境变量模板,并修复相关 lint 问题。 --- README.md | 2 +- README.zh_CN.md | 2 +- docs/mkdocs/en/memory.md | 592 ++++++++- docs/mkdocs/zh/memory.md | 1058 ++++++++++++----- examples/dsl/classifier_mcp/.env | 5 + examples/llmagent/.env | 4 + examples/llmagent_with_branch_filtering/.env | 3 + examples/llmagent_with_cancel/.env | 3 + examples/llmagent_with_custom_agent/.env | 3 + examples/llmagent_with_custom_prompt/.env | 3 + examples/llmagent_with_human_in_the_loop/.env | 3 + .../llmagent_with_max_history_messages/.env | 3 + examples/llmagent_with_model_create_fn/.env | 3 + examples/llmagent_with_parallal_tools/.env | 3 + examples/llmagent_with_schema/.env | 3 + .../llmagent_with_streaming_tool_complex/.env | 4 + .../llmagent_with_streaming_tool_simple/.env | 4 + examples/llmagent_with_thinking/.env | 4 + .../llmagent_with_timeline_filtering/.env | 4 + examples/llmagent_with_tool_prompt/.env | 4 + examples/llmagent_with_user_history/.env | 4 + examples/mcp_tools/.env | 4 + examples/mem0_tools/.env | 4 + examples/{mem_0 => mem0_tools}/README.md | 14 +- .../{mem_0 => mem0_tools}/agent/__init__.py | 0 examples/{mem_0 => mem0_tools}/agent/agent.py | 4 +- .../{mem_0 => mem0_tools}/agent/config.py | 0 .../{mem_0 => mem0_tools}/agent/prompts.py | 0 examples/{mem_0 => mem0_tools}/agent/tools.py | 0 .../{mem_0 => mem0_tools}/images/mem0_ai.png | Bin .../images/mem0_plat.png | Bin .../images/mem0_result.png | Bin .../images/qdrant_dashboard.png | Bin .../images/qdrant_mem.png | Bin examples/{mem_0 => mem0_tools}/run_agent.py | 6 +- examples/mem_0/.env | 1 - examples/memory_service_with_in_memory/.env | 4 + examples/memory_service_with_mem0/.env | 5 +- examples/memory_service_with_mempalace/.env | 9 + .../memory_service_with_mempalace/README.md | 253 ++++ .../agent/__init__.py | 5 + .../agent/agent.py | 47 + .../agent/config.py | 61 + .../agent/prompts.py | 8 + .../agent/tools.py | 30 + .../run_agent.py | 133 +++ examples/memory_service_with_redis/.env | 5 +- examples/memory_service_with_sql/.env | 5 +- examples/mempalace_tools/.env | 13 + examples/mempalace_tools/README.md | 180 +++ examples/mempalace_tools/agent/__init__.py | 5 + examples/mempalace_tools/agent/agent.py | 60 + examples/mempalace_tools/agent/config.py | 29 + examples/mempalace_tools/agent/prompts.py | 26 + examples/mempalace_tools/agent/tools.py | 6 + examples/mempalace_tools/out.txt | 319 +++++ examples/mempalace_tools/run_agent.py | 183 +++ examples/multi_agent_chain/.env | 5 +- examples/multi_agent_compose/.env | 5 +- examples/multi_agent_cycle/.env | 5 +- examples/multi_agent_parallel/.env | 5 +- examples/multi_agent_start_from_last/.env | 5 +- examples/multi_agent_subagent/.env | 5 +- examples/quickstart/.env | 5 +- examples/session_service_with_in_memory/.env | 5 +- examples/session_service_with_redis/.env | 5 +- examples/session_service_with_sql/.env | 5 +- examples/session_state/.env | 5 +- examples/session_summarizer/.env | 5 +- examples/skills/.env | 5 +- examples/skills_with_container/.env | 5 +- examples/skills_with_dynamic_tools/.env | 5 +- examples/streaming_tools/.env | 5 +- examples/team/.env | 5 +- examples/team_as_sub_agent/.env | 5 +- examples/team_human_in_the_loop/.env | 5 +- examples/team_member_agent_claude/.env | 5 +- examples/team_member_agent_langgraph/.env | 5 +- examples/team_member_agent_team/.env | 5 +- examples/team_member_message_filter/.env | 5 +- examples/team_parallel_execution/.env | 5 +- examples/team_with_cancel/.env | 5 +- examples/team_with_skill/.env | 5 +- examples/tools/.env | 5 +- examples/toolsets/.env | 5 +- examples/transfer_agent/.env | 5 +- examples/webfetch_tool/.env | 6 +- examples/websearch_tool/.env | 6 +- pyproject.toml | 5 + requirements-test.txt | 1 + requirements.txt | 1 + tests/memory/test_mem0_memory_service.py | 9 +- tests/memory/test_mempalace_memory_service.py | 161 +++ tests/tools/test_mempalace_tool.py | 390 ++++++ trpc_agent_sdk/agents/_constants.py | 3 + trpc_agent_sdk/agents/_llm_agent.py | 6 +- trpc_agent_sdk/events/_long_running_event.py | 3 +- trpc_agent_sdk/filter/_registry.py | 1 - .../memory/_in_memory_memory_service.py | 2 + .../memory/_redis_memory_service.py | 2 +- trpc_agent_sdk/memory/_sql_memory_service.py | 2 + trpc_agent_sdk/memory/_utils.py | 16 + trpc_agent_sdk/memory/mem0_memory_service.py | 30 +- .../memory/mempalace_memory_service.py | 444 +++++++ .../planners/_plan_re_act_planner.py | 34 +- trpc_agent_sdk/server/a2a/_constants.py | 1 + .../server/a2a/converters/_event_converter.py | 14 +- .../a2a/executor/_a2a_agent_executor.py | 2 +- trpc_agent_sdk/tools/__init__.py | 16 + trpc_agent_sdk/tools/_agent_tool.py | 16 +- trpc_agent_sdk/tools/_load_memory_tool.py | 2 +- trpc_agent_sdk/tools/mempalace_tool.py | 527 ++++++++ trpc_agent_sdk/types/__init__.py | 2 +- 113 files changed, 4577 insertions(+), 403 deletions(-) create mode 100644 examples/mem0_tools/.env rename examples/{mem_0 => mem0_tools}/README.md (97%) rename examples/{mem_0 => mem0_tools}/agent/__init__.py (100%) rename examples/{mem_0 => mem0_tools}/agent/agent.py (93%) rename examples/{mem_0 => mem0_tools}/agent/config.py (100%) rename examples/{mem_0 => mem0_tools}/agent/prompts.py (100%) rename examples/{mem_0 => mem0_tools}/agent/tools.py (100%) rename examples/{mem_0 => mem0_tools}/images/mem0_ai.png (100%) rename examples/{mem_0 => mem0_tools}/images/mem0_plat.png (100%) rename examples/{mem_0 => mem0_tools}/images/mem0_result.png (100%) rename examples/{mem_0 => mem0_tools}/images/qdrant_dashboard.png (100%) rename examples/{mem_0 => mem0_tools}/images/qdrant_mem.png (100%) rename examples/{mem_0 => mem0_tools}/run_agent.py (96%) delete mode 100644 examples/mem_0/.env create mode 100644 examples/memory_service_with_mempalace/.env create mode 100644 examples/memory_service_with_mempalace/README.md create mode 100644 examples/memory_service_with_mempalace/agent/__init__.py create mode 100644 examples/memory_service_with_mempalace/agent/agent.py create mode 100644 examples/memory_service_with_mempalace/agent/config.py create mode 100644 examples/memory_service_with_mempalace/agent/prompts.py create mode 100644 examples/memory_service_with_mempalace/agent/tools.py create mode 100644 examples/memory_service_with_mempalace/run_agent.py create mode 100644 examples/mempalace_tools/.env create mode 100644 examples/mempalace_tools/README.md create mode 100644 examples/mempalace_tools/agent/__init__.py create mode 100644 examples/mempalace_tools/agent/agent.py create mode 100644 examples/mempalace_tools/agent/config.py create mode 100644 examples/mempalace_tools/agent/prompts.py create mode 100644 examples/mempalace_tools/agent/tools.py create mode 100644 examples/mempalace_tools/out.txt create mode 100644 examples/mempalace_tools/run_agent.py create mode 100644 tests/memory/test_mempalace_memory_service.py create mode 100644 tests/tools/test_mempalace_tool.py create mode 100644 trpc_agent_sdk/memory/mempalace_memory_service.py create mode 100644 trpc_agent_sdk/tools/mempalace_tool.py diff --git a/README.md b/README.md index b7b3cf1..5ffcbfe 100644 --- a/README.md +++ b/README.md @@ -446,7 +446,7 @@ This group helps you: Recommended first: - Session: [examples/session_service_with_in_memory](./examples/session_service_with_in_memory/README.md) / [examples/session_service_with_redis](./examples/session_service_with_redis/README.md) / [examples/session_service_with_sql](./examples/session_service_with_sql/README.md) / [examples/session_summarizer](./examples/session_summarizer/README.md) / [examples/session_state](./examples/session_state/README.md) -- Memory: [examples/memory_service_with_in_memory](./examples/memory_service_with_in_memory/README.md) / [examples/memory_service_with_redis](./examples/memory_service_with_redis/README.md) / [examples/memory_service_with_sql](./examples/memory_service_with_sql/README.md) / [examples/memory_service_with_mem0](./examples/memory_service_with_mem0/README.md) / [examples/mem_0](./examples/mem_0/README.md) +- Memory: [examples/memory_service_with_in_memory](./examples/memory_service_with_in_memory/README.md) / [examples/memory_service_with_redis](./examples/memory_service_with_redis/README.md) / [examples/memory_service_with_sql](./examples/memory_service_with_sql/README.md) / [examples/memory_service_with_mem0](./examples/memory_service_with_mem0/README.md) / [examples/memory_service_with_mempalace](./examples/memory_service_with_mempalace/README.md) - Knowledge: [examples/knowledge_with_documentloader](./examples/knowledge_with_documentloader/README.md) / [examples/knowledge_with_vectorstore](./examples/knowledge_with_vectorstore/README.md) / [examples/knowledge_with_rag_agent](./examples/knowledge_with_rag_agent/README.md) / [examples/knowledge_with_searchtool_rag_agent](./examples/knowledge_with_searchtool_rag_agent/README.md) / [examples/knowledge_with_prompt_template](./examples/knowledge_with_prompt_template/README.md) / [examples/knowledge_with_custom_components](./examples/knowledge_with_custom_components/README.md) Related docs: diff --git a/README.zh_CN.md b/README.zh_CN.md index 3a9e094..a4cf0e6 100644 --- a/README.zh_CN.md +++ b/README.zh_CN.md @@ -447,7 +447,7 @@ skill_tool_set = SkillToolSet(repository=repository, run_tool_kwargs=tool_kwargs 建议先看: - Session:[examples/session_service_with_in_memory](./examples/session_service_with_in_memory/README.md) / [examples/session_service_with_redis](./examples/session_service_with_redis/README.md) / [examples/session_service_with_sql](./examples/session_service_with_sql/README.md) / [examples/session_summarizer](./examples/session_summarizer/README.md) / [examples/session_state](./examples/session_state/README.md) -- Memory:[examples/memory_service_with_in_memory](./examples/memory_service_with_in_memory/README.md) / [examples/memory_service_with_redis](./examples/memory_service_with_redis/README.md) / [examples/memory_service_with_sql](./examples/memory_service_with_sql/README.md) / [examples/memory_service_with_mem0](./examples/memory_service_with_mem0/README.md) / [examples/mem_0](./examples/mem_0/README.md) +- Memory: [examples/memory_service_with_in_memory](./examples/memory_service_with_in_memory/README.md) / [examples/memory_service_with_redis](./examples/memory_service_with_redis/README.md) / [examples/memory_service_with_sql](./examples/memory_service_with_sql/README.md) / [examples/memory_service_with_mem0](./examples/memory_service_with_mem0/README.md) / [examples/memory_service_with_mempalace](./examples/memory_service_with_mempalace/README.md) - Knowledge:[examples/knowledge_with_documentloader](./examples/knowledge_with_documentloader/README.md) / [examples/knowledge_with_vectorstore](./examples/knowledge_with_vectorstore/README.md) / [examples/knowledge_with_rag_agent](./examples/knowledge_with_rag_agent/README.md) / [examples/knowledge_with_searchtool_rag_agent](./examples/knowledge_with_searchtool_rag_agent/README.md) / [examples/knowledge_with_prompt_template](./examples/knowledge_with_prompt_template/README.md) / [examples/knowledge_with_custom_components](./examples/knowledge_with_custom_components/README.md) 相关文档: diff --git a/docs/mkdocs/en/memory.md b/docs/mkdocs/en/memory.md index 61cd5fb..0c64168 100644 --- a/docs/mkdocs/en/memory.md +++ b/docs/mkdocs/en/memory.md @@ -28,6 +28,7 @@ Based on the implementation in [trpc_agent_sdk/memory/](../../../trpc_agent_sdk/ - **InMemoryMemoryService**: Stored in an in-process memory dictionary - **RedisMemoryService**: Stored in a Redis List (JSON format) - **SqlMemoryService**: Stored in the `mem_events` table in MySQL/PostgreSQL +- **MempalaceMemoryService**: Stored as MemPalace drawers in a local ChromaDB-backed palace **Code Example**: ```python @@ -54,7 +55,7 @@ async def store_session(self, session: Session, agent_context: Optional[AgentCon **Function**: Searches for related historical memories based on query keywords. -**Search Method**: **Keyword matching** (not semantic search) +**Search Method**: Built-in InMemory/Redis/SQL services use **keyword matching**; semantic memory services such as MemPalace and Mem0 use vector / semantic retrieval. **Implementation Logic** (using `InMemoryMemoryService` as an example): ```python @@ -117,6 +118,7 @@ for memory in response.memories: - **InMemoryMemoryService**: Background periodic cleanup task (`_cleanup_loop`) - **RedisMemoryService**: Redis native `EXPIRE` mechanism (automatic expiration) - **SqlMemoryService**: Background periodic cleanup task (batch SQL DELETE) +- **MempalaceMemoryService**: Background periodic cleanup task (batch drawer deletion by metadata timestamp) **TTL Configuration**: ```python @@ -135,6 +137,7 @@ memory_service_config = MemoryServiceConfig( **TTL Refresh Mechanism**: - **Refresh on access**: TTL is refreshed for matched events during `search_memory` - **Refresh on storage**: TTL is set for new events during `store_session` +- **Persistent semantic services**: Some services, such as MemPalace, delete expired drawers by stored event timestamp rather than refreshing TTL on every search. --- @@ -165,7 +168,7 @@ _session_events = { ## MemoryService Implementations -trpc-agent provides three `MemoryService` implementations, allowing you to choose the appropriate storage backend based on your scenario: +trpc-agent provides multiple `MemoryService` implementations, allowing you to choose the appropriate storage backend based on your scenario: ### InMemoryMemoryService @@ -1092,7 +1095,7 @@ await memory.delete(memory_id="memory-id") await memory.delete_all(user_id="alice") ``` -**More Advanced Usage:** [Advanced Usage Documentation](../../../examples/mem_0/README.md#高级用法) +**More Advanced Usage:** [Advanced Usage Documentation](../../../examples/mem0_tools/README.md#高级用法) --- @@ -1135,7 +1138,7 @@ ConnectionError: Cannot connect to Qdrant at localhost:6333 |---|---|---| | `Mem0MemoryService` complete example | [examples/memory_service_with_mem0/](../../../examples/memory_service_with_mem0/README.md) | Includes execution result analysis, FAQ | | `Mem0MemoryService` source code | [mem0_memory_service.py](../../../trpc_agent_sdk/memory/mem0_memory_service.py) | Service implementation | -| Tool-based integration source code | [mem0_tool.py](../../../trpc_agent_sdk/tools/mem0_tool.py) | `SearchMemoryTool` / `SaveMemoryTool` tool classes | +| Tool-based integration source code | [mem0_tools.py](../../../trpc_agent_sdk/tools/mem0_tools.py) | `SearchMemoryTool` / `SaveMemoryTool` tool classes | | infer parameter details | [README.md#infer-参数详解](../../../examples/memory_service_with_mem0/README.md#infer-参数详解) | True vs False comparison | | FAQ | [README.md#常见问题-qa](../../../examples/memory_service_with_mem0/README.md#常见问题-qa) | Error analysis and answers | @@ -1157,6 +1160,569 @@ ConnectionError: Cannot connect to Qdrant at localhost:6333 --- +## Integrating MemPalace + +### What is MemPalace? + +MemPalace is a local-first memory system for storing verbatim memories and retrieving historical context with semantic search. Its core storage hierarchy can be understood as: + +```text +Palace + └── Wing + └── Room + └── Drawer +``` + +In `MempalaceMemoryService`, each storable framework event is filed as a drawer. The drawer contains the original text and metadata such as `wing`, `room`, `session_id`, `event_id`, `author`, and `timestamp`. + +**Core Capabilities:** +- Local persistent storage in a MemPalace palace directory +- Semantic search through MemPalace / ChromaDB +- `wing` and `room` filters for memory isolation +- CLI inspection through `mempalace search` +- TTL cleanup managed by the framework memory service + +--- + +### tRPC-Agent Integration Methods + +The recommended integration path is the framework-level memory service: + +| Method | Class / Tool | Applicable Scenario | +|---|---|---| +| **Framework-level memory service** (recommended) | `MempalaceMemoryService` | The framework automatically writes cross-session memories; the Agent retrieves them through `load_memory` | +| **MemPalace tools** | `mempalace_search` / `mempalace_add_drawer`, etc. | The Agent needs direct access to MemPalace drawers, diary, KG, or other advanced capabilities | + +`MempalaceMemoryService` is the standard MemoryService integration for this project. The framework calls `store_session()` automatically after each turn to persist memory, while the Agent calls `load_memory` during response generation to retrieve historical memories through `search_memory()`. + +--- + +### MempalaceMemoryService (Recommended) + +`MempalaceMemoryService` is a framework-level memory service. The framework stores session memories automatically after each turn, while the Agent retrieves related memories through the built-in `load_memory` tool. + +**How It Works**: Stores memory data as MemPalace drawers in a local-first memory palace backed by ChromaDB. + +**Implementation Details** (based on `mempalace_memory_service.py`): +- **Data Structure**: MemPalace `Palace -> Wing -> Room -> Drawer` +- **Storage Location**: Local MemPalace palace directory, usually `~/.mempalace/palace` +- **Search Method**: MemPalace hybrid semantic search (`search_memories`) with `wing` / `room` filters +- **TTL Mechanism**: Background periodic cleanup task; expired drawers are deleted by metadata timestamp +- **Write Mode**: Incremental background writes; events already scheduled or stored in the current process are skipped +- **Cross-session sharing**: `session.save_key`, usually `{app_name}/{user_id}`, is used as the cross-session memory dimension + +**Persistence**: ✅ **Yes**. Data is persisted in the MemPalace palace directory and can be recovered after application restart. + +**Applicable Scenarios**: +- ✅ Local-first semantic memory +- ✅ Cross-session user profile and preference memory +- ✅ Development or private deployments that should keep memory data on local disk +- ✅ Scenarios where CLI inspection with `mempalace search` is useful + +#### Quick Integration + +**Step 1: Install dependencies** + +```bash +# Install through the trpc-agent extra +pip install -e ".[mempalace]" + +# Or install MemPalace directly +pip install mempalace +``` + +**Step 2: Create `MempalaceMemoryService`** + +```python +from trpc_agent_sdk.memory import MemoryServiceConfig +from trpc_agent_sdk.memory.mempalace_memory_service import MempalaceMemoryService + +memory_service = MempalaceMemoryService( + memory_service_config=MemoryServiceConfig( + enabled=True, + ttl=MemoryServiceConfig.create_ttl_config( + enable=True, + ttl_seconds=86400, + cleanup_interval_seconds=3600, + ), + ), + wing="my_app_user", + room="conversations", + store_only_model_visible=True, +) +``` + +**Step 3: Pass `memory_service` to `Runner`** + +```python +from trpc_agent_sdk.runners import Runner +from trpc_agent_sdk.sessions import InMemorySessionService +from trpc_agent_sdk.tools import load_memory_tool + +agent = LlmAgent( + name="assistant", + model=your_model, + tools=[load_memory_tool], + instruction="Use load_memory to recall relevant past conversations before answering.", +) + +runner = Runner( + app_name="my_app", + agent=agent, + session_service=InMemorySessionService(), + memory_service=memory_service, +) +``` + +**Step 4: Run the Agent; memories are persisted across sessions automatically** + +```python +# First conversation round (session_1) +async for event in runner.run_async(user_id="alice", session_id="session_1", new_message=...): + ... +# After the turn finishes, the framework calls store_session and writes storable events to MemPalace. + +# Second conversation round (session_2) — a new session can still retrieve memories from session_1. +async for event in runner.run_async(user_id="alice", session_id="session_2", new_message=...): + ... +``` + +**Complete Runnable Example:** [examples/memory_service_with_mempalace/run_agent.py](../../../examples/memory_service_with_mempalace/run_agent.py) + +--- + +#### MemPalace Hierarchy Mapping + +```text +session.save_key = "{app_name}/{user_id}" -> wing (when wing is not explicitly configured) +room -> room, defaults to conversations +Event -> drawer +session.id / event.id / author / timestamp -> drawer metadata +``` + +If `wing="trpc-agent"` is configured explicitly, all memories are written into that wing. If `wing` is omitted, the service derives the wing from `save_key`, which is usually the more natural isolation strategy for app/user-scoped long-term memory. + +--- + +#### Path and CLI Search + +MemPalace stores data under `MempalaceConfig().palace_path`. The default path is usually: + +```text +~/.mempalace/palace +``` + +You can configure a custom path through an environment variable: + +```bash +export MEMPALACE_PALACE_PATH=/path/to/palace +``` + +Or through `~/.mempalace/config.json`: + +```json +{ + "palace_path": "/path/to/palace", + "collection_name": "mempalace_drawers" +} +``` + +If the application is configured to use a custom palace path, CLI search must use the same path: + +```bash +mempalace --palace /path/to/palace search "user name" +``` + +Filter by `wing` and `room`: + +```bash +mempalace --palace /path/to/palace search "user name" \ + --wing my_app_user \ + --room conversations +``` + +If no custom path is configured, MemPalace uses its default config, and CLI search can omit `--palace`: + +```bash +mempalace search "user name" --wing my_app_user --room conversations +``` + +> `/path/to/palace` is the MemPalace data directory that contains `chroma.sqlite3`, not a single database file. + +--- + +#### TTL Configuration (Optional) + +```python +memory_service_config = MemoryServiceConfig( + enabled=True, + ttl=MemoryServiceConfig.create_ttl_config( + enable=True, + ttl_seconds=86400, # Keep memories for 24 hours + cleanup_interval_seconds=3600, # Run cleanup every hour + ), +) +``` + +Important notes: + +- MemPalace itself does not delete memories automatically just because they have not been used for a long time. +- `MempalaceMemoryService` implements TTL cleanup at the framework layer. +- Cleanup scans drawers written by this service and deletes expired records based on the `timestamp` metadata. +- This TTL policy is based on the original event timestamp; it is not an "extend expiration on access" policy. + +--- + +#### Direct Memory Management + +The service provides a helper to delete all drawers in a wing, or only drawers in a specific room: + +```python +await memory_service.delete_memory(wing="my_app_user") +await memory_service.delete_memory(wing="my_app_user", room="conversations") +``` + +> MemPalace CLI currently does not provide a direct command to delete all memories by `wing` / `room`; use the service helper or call the underlying collection `delete(where=...)`. + +--- + +#### Storage Content Policy + +In general, only ordinary text events with long-term value should be written to MemPalace. Intermediate tool calls, tool responses, and code execution results are usually poor long-term memories because they can cause: + +- `load_memory` results to be written back into memory again +- nested historical memory JSON inside newly stored memories +- tool logs polluting long-term memory and reducing retrieval quality + +`MempalaceMemoryService` is better suited for memories such as: + +```text +User: My name is Alice. +User: My favorite color is blue. +Assistant: Confirmed the user's name or preference. +``` + +Rather than: + +```text +[tool_call] load_memory: ... +[tool_response] load_memory: {"memories": [...]} +``` + +--- + +#### Typical Workflow + +```text +1. User: Do you remember my name? + ↓ + Agent calls: load_memory(query="user name") + ↓ + Result: {"memories": []} + ↓ + Agent: I don't know your name yet. + +2. User: My name is Alice + ↓ + After the turn, the framework automatically calls MempalaceMemoryService.store_session() + ↓ + The user message is written as a drawer under the configured wing/room + +3. User starts a new session: Do you remember my name? + ↓ + Agent calls: load_memory(query="user name") + ↓ + MemPalace returns a historical memory containing "My name is Alice" + ↓ + Agent: Yes, your name is Alice. +``` + +**Complete Demo Output (MempalaceMemoryService):** [examples/memory_service_with_mempalace/README.md](../../../examples/memory_service_with_mempalace/README.md) + +--- + +### Tool-based Integration (mempalace_tool) + +`mempalace_tool` is another way to integrate with MemPalace. It is not the recommended standard MemoryService path. Instead, it exposes MemPalace capabilities as Agent-callable tools, allowing the Agent to decide when to search, write drawers, read or write diary entries, or maintain KG facts. + +The difference from `MempalaceMemoryService` is: + +| Method | Write Timing | Retrieval Method | Applicable Scenario | +|---|---|---|---| +| `MempalaceMemoryService` | The framework writes automatically after each turn | `load_memory` indirectly calls `search_memory()` | Standard cross-session long-term memory | +| `mempalace_tool` | The Agent explicitly calls tools to write | The Agent explicitly calls `mempalace_search` | Fine-grained control over MemPalace drawers, diary, KG, or manual memory management | + +#### Available Tools + +| Tool Class | Tool Name | Function | Use Case | +|---|---|---|---| +| `MempalaceSearchTool` | `mempalace_search` | Semantically search saved drawer content | The Agent needs to recall user profiles, preferences, or historical facts | +| `MempalaceAddDrawerTool` | `mempalace_add_drawer` | Write a verbatim drawer under a specified `wing/room` | The user explicitly asks the Agent to remember long-term information | +| `MempalaceDiaryWriteTool` | `mempalace_diary_write` | Write an agent diary entry | Record runtime observations, task progress, or interim summaries | +| `MempalaceDiaryReadTool` | `mempalace_diary_read` | Read recent diary entries for an agent | The Agent needs to review previous task notes | +| `MempalaceKGAddTool` | `mempalace_kg_add` | Write a knowledge-graph triple fact | Structured facts such as `subject -> predicate -> object` | +| `MempalaceKGQueryTool` | `mempalace_kg_query` | Query relationships for a knowledge-graph entity | Query facts about Alice, project dependencies, or entity relationships | +| `MempalaceKGTimelineTool` | `mempalace_kg_timeline` | Read knowledge-graph facts as a timeline | Inspect how an entity's relationships change over time | +| `MempalaceKGInvalidateTool` | `mempalace_kg_invalidate` | Mark a current fact as no longer valid | Represent fact changes while keeping historical records | + +> **Note**: Like `mem0_tool`, `mempalace_tool` exposes tools to the Agent and lets the model decide when to call them. Unlike Mem0's two search/save tools, MemPalace tools also cover diary and KG operations. See the complete example at [examples/mempalace_tools/README.md](../../../examples/mempalace_tools/README.md), and the tool source at [mempalace_tool.py](../../../trpc_agent_sdk/tools/mempalace_tool.py). + +#### Integration Architecture + +``` +┌──────────────────────┐ +│ User Input │ +└──────────┬───────────┘ + │ + ▼ +┌──────────────────────┐ +│ tRPC-Agent │◄────────────────┐ +│ LlmAgent │ │ +└──────────┬───────────┘ │ + │ │ returns tool results + │ calls tools │ + ▼ │ +┌──────────────────────┐ │ +│ MemPalace Tools │─────────────────┘ +│ - mempalace_search │ +│ - add_drawer │ +│ - diary read/write │ +│ - KG tools │ +└──────────┬───────────┘ + │ + ▼ +┌──────────────────────┐ +│ MemPalace Backend │ +│ - Palace / ChromaDB │ +│ - KG SQLite │ +└──────────────────────┘ +``` + +#### Quick Integration + +```python +from trpc_agent_sdk.agents import LlmAgent +from trpc_agent_sdk.tools.mempalace_tool import MempalaceAddDrawerTool +from trpc_agent_sdk.tools.mempalace_tool import MempalaceDiaryReadTool +from trpc_agent_sdk.tools.mempalace_tool import MempalaceDiaryWriteTool +from trpc_agent_sdk.tools.mempalace_tool import MempalaceKGAddTool +from trpc_agent_sdk.tools.mempalace_tool import MempalaceKGInvalidateTool +from trpc_agent_sdk.tools.mempalace_tool import MempalaceKGQueryTool +from trpc_agent_sdk.tools.mempalace_tool import MempalaceKGTimelineTool +from trpc_agent_sdk.tools.mempalace_tool import MempalaceSearchTool + +palace_path = "/tmp/trpc-agent-mempalace-demo" +kg_path = "/tmp/trpc-agent-mempalace-demo/knowledge_graph.sqlite3" + +tools = [ + MempalaceSearchTool(palace_path=palace_path), + MempalaceAddDrawerTool(palace_path=palace_path), + MempalaceDiaryWriteTool(palace_path=palace_path), + MempalaceDiaryReadTool(palace_path=palace_path), + MempalaceKGAddTool(palace_path=palace_path, kg_path=kg_path), + MempalaceKGQueryTool(palace_path=palace_path, kg_path=kg_path), + MempalaceKGTimelineTool(palace_path=palace_path, kg_path=kg_path), + MempalaceKGInvalidateTool(palace_path=palace_path, kg_path=kg_path), +] + +agent = LlmAgent( + name="memory_assistant", + model=your_model, + instruction=""" + You are a helpful assistant with MemPalace tools. + - Use mempalace_search before answering questions that may require past memory. + - Use mempalace_add_drawer when the user explicitly asks you to remember stable facts. + - Use diary tools for agent diary entries. + - Use KG tools for structured facts such as Alice -> likes -> Italian food. + """, + tools=tools, +) +``` + +#### Specify the MemPalace Path + +Tool classes accept `palace_path`. If it is omitted, they use `MempalaceConfig().palace_path`. KG tools also accept `kg_path`; if `kg_path` is omitted and `palace_path` is provided, they default to `palace_path/knowledge_graph.sqlite3`: + +```python +mempalace_search_tool = MempalaceSearchTool(palace_path="/path/to/palace") +mempalace_add_drawer_tool = MempalaceAddDrawerTool(palace_path="/path/to/palace") +mempalace_kg_query_tool = MempalaceKGQueryTool( + palace_path="/path/to/palace", + kg_path="/path/to/palace/knowledge_graph.sqlite3", +) +``` + +Use the same path when inspecting memories from the CLI: + +```bash +mempalace --palace /path/to/palace search "user name" +``` + +The example manages paths through `.env`: + +```bash +MEMPALACE_PALACE_PATH=/tmp/trpc-agent-mempalace-demo +MEMPALACE_KG_PATH=/tmp/trpc-agent-mempalace-demo/knowledge_graph.sqlite3 +MEMPALACE_WING=personal_assistant_alice +MEMPALACE_ROOM=user_profile +``` + +#### Tool-based Workflow + +```text +1. User: Use mempalace_search to check whether you remember my name. + ↓ + Agent calls: mempalace_search( + query="name", + wing="personal_assistant_alice", + room="user_profile" + ) + ↓ + Result: No palace found or empty results + ↓ + Agent: I do not know your name yet. + +2. User: Use mempalace_add_drawer to remember that my name is Alice. + ↓ + Agent calls: mempalace_add_drawer( + wing="personal_assistant_alice", + room="user_profile", + content="User's name is Alice." + ) + ↓ + MemPalace writes the drawer + +3. User starts a new session: Use mempalace_search to recall my name. + ↓ + Agent calls: mempalace_search(query="name", wing="personal_assistant_alice", room="user_profile") + ↓ + MemPalace returns "User's name is Alice." + ↓ + Agent: Your name is Alice. + +4. User: Use mempalace_kg_add to add this fact: Alice likes Italian food. + ↓ + Agent calls: mempalace_kg_add(subject="Alice", predicate="likes", object="Italian food") + ↓ + KG writes the triple fact: Alice -> likes -> Italian food + +5. User: Use mempalace_kg_invalidate to mark the fact Alice likes Italian food as ended today. + ↓ + Agent calls: mempalace_kg_invalidate(subject="Alice", predicate="likes", object="Italian food") + ↓ + KG keeps the historical fact but marks current as false +``` + +**Complete tool-based demo and result analysis:** [examples/mempalace_tools/README.md](../../../examples/mempalace_tools/README.md) + +--- + +#### MempalaceSearchTool + +Semantically searches drawer content saved in MemPalace. + +**Constructor:** +```python +MempalaceSearchTool( + palace_path: str | None = None, + filters_name: list[str] | None = None, + filters: list[Any] | None = None, +) +``` + +**Agent Tool Parameters (callable by LLM):** +- `query` (string, required): Search query +- `limit` (integer, optional): Maximum number of results, defaults to 5 +- `wing` (string, optional): Filter by wing +- `room` (string, optional): Filter by room + +**Return Value Example:** +```python +{ + "query": "name favorite food", + "filters": {"wing": "personal_assistant_alice", "room": "user_profile"}, + "results": [ + {"text": "User's name is Alice.", "wing": "personal_assistant_alice", "room": "user_profile"}, + {"text": "My favorite food is Italian food.", "wing": "personal_assistant_alice", "room": "user_profile"}, + ], +} +``` + +--- + +#### MempalaceAddDrawerTool + +Writes a verbatim drawer under a specified `wing/room`. It is suitable for long-term facts that the user explicitly asks the Agent to remember. + +**Agent Tool Parameters (callable by LLM):** +- `wing` (string, required): Storage scope, for example `personal_assistant_alice` +- `room` (string, required): Memory topic, for example `user_profile` +- `content` (string, required): Verbatim content to save +- `source_file` (string, optional): Source identifier + +**Return Value Example:** +```python +{ + "success": True, + "drawer_id": "drawer_personal_assistant_alice_user_profile_xxx", + "wing": "personal_assistant_alice", + "room": "user_profile", +} +``` + +--- + +#### Diary Tools + +`MempalaceDiaryWriteTool` and `MempalaceDiaryReadTool` record and read agent diary entries. They are useful for "what happened in this task, what was observed, and what to watch next" style runtime notes, and should not replace user-profile memories. + +| Tool | Key Parameters | Return Highlights | +|---|---|---| +| `mempalace_diary_write` | `entry`, `agent_name`, `topic`, `wing` | `success`, `entry_id`, `agent`, `topic` | +| `mempalace_diary_read` | `agent_name`, `last_n`, `wing` | `entries`, `total`, `showing` | + +In the example output, after writing `Alice tested the MemPalace tools example today.`, a later new session can still read that diary entry, showing that diary data is persisted. + +--- + +#### KG Tools + +KG tools maintain structured facts. A fact is usually represented as a triple: + +```text +subject -> predicate -> object +Alice -> likes -> Italian food +``` + +| Tool | Key Parameters | Semantics | +|---|---|---| +| `mempalace_kg_add` | `subject`, `predicate`, `object`, `valid_from`, `valid_to`, `confidence` | Write a structured fact | +| `mempalace_kg_query` | `entity`, `as_of`, `direction` | Query facts related to an entity | +| `mempalace_kg_timeline` | `entity` | Inspect an entity's fact timeline | +| `mempalace_kg_invalidate` | `subject`, `predicate`, `object`, `ended` | Mark a fact as no longer valid | + +`mempalace_kg_invalidate` does not delete the historical fact. It sets `valid_to` and makes `current=False`. The example therefore runs invalidation after the second-phase persistence verification, so it does not alter the query result used to validate persistence. + +#### Recommendations + +- If you only need standard cross-session long-term memory, prefer `MempalaceMemoryService`. +- Use `mempalace_tool` when the Agent needs direct control over what to write, how to classify it, diary operations, or KG maintenance. +- For user profiles and preferences, write to a stable `wing/room`, such as `personal_assistant_alice/user_profile`. +- For KG fact changes, prefer `mempalace_kg_invalidate` to express "no longer true" instead of deleting history. +- Do not let the Agent write `load_memory` tool results, code execution outputs, or other intermediate traces directly into drawers, as this can pollute long-term memory. + +--- + +### MemPalace Resources + +| Resource | Path | Description | +|---|---|---| +| `MempalaceMemoryService` complete example | [examples/memory_service_with_mempalace/](../../../examples/memory_service_with_mempalace/README.md) | Installation, path configuration, CLI search, and execution result analysis | +| `MempalaceMemoryService` source code | [mempalace_memory_service.py](../../../trpc_agent_sdk/memory/mempalace_memory_service.py) | Recommended framework-level memory service implementation | +| MemPalace tools source code | [mempalace_tool.py](../../../trpc_agent_sdk/tools/mempalace_tool.py) | Optional tool-based integration: `mempalace_search`, `mempalace_add_drawer`, diary, KG tools | + +--- + ## Core Feature Summary ### 1. Cross-session Memory Sharing @@ -1165,17 +1731,19 @@ ConnectionError: Cannot connect to Qdrant at localhost:6333 - ✅ Uses `save_key` (`app_name/user_id`) as the memory key - ✅ Suitable for storing user profiles, long-term preferences, and other cross-session information -### 2. Keyword Search +### 2. Keyword or Semantic Search - ✅ Supports keyword extraction and matching for both Chinese and English - ✅ Uses `extract_words_lower` to extract English words and Chinese characters - ✅ Matching logic: returns on any query word match +- ✅ Semantic memory services such as `MempalaceMemoryService` and `Mem0MemoryService` use vector / semantic retrieval instead of simple keyword matching ### 3. TTL Cache Eviction - ✅ Automatically cleans up expired memories, preventing unlimited storage growth - ✅ Refreshes TTL on access (during `search_memory`) - ✅ Different implementations use different cleanup mechanisms +- ⚠️ Some persistent semantic services may use fixed event timestamps for TTL cleanup rather than refreshing TTL on every search ### 4. Automatic Storage @@ -1188,6 +1756,7 @@ ConnectionError: Cannot connect to Qdrant at localhost:6333 - ✅ Supports multiple implementations: In-Memory, Redis, SQL, Mem0, etc. - ✅ Supports TRPC Redis integration - ✅ Supports Mem0 semantic memory integration (vector search + LLM extraction) +- ✅ Supports MemPalace local-first semantic memory integration (ChromaDB-backed palace) - ✅ Choose the appropriate implementation based on your scenario --- @@ -1201,21 +1770,23 @@ ConnectionError: Cannot connect to Qdrant at localhost:6333 ### 2. Keyword Search Limitations -- The current implementation uses **keyword (token) matching**, not semantic search +- The built-in InMemory/Redis/SQL implementations use **keyword (token) matching**, not semantic search - After `extract_words_lower` (whole English words, individual Chinese characters), **any** query token that appears in the event's token set counts as a match (this is not full-sentence semantic similarity) - Suitable for rapid prototyping, not suitable for complex semantic retrieval requirements +- For semantic retrieval, use `MempalaceMemoryService` or `Mem0MemoryService` ### 3. TTL Configuration - `ttl_seconds`: Memory expiration time (in seconds) -- `cleanup_interval_seconds`: Cleanup interval (InMemory/SQL only; Redis handles expiration automatically) -- TTL is automatically refreshed on access, extending the memory's validity period +- `cleanup_interval_seconds`: Cleanup interval (InMemory/SQL/MemPalace; Redis handles expiration automatically) +- InMemory/Redis refresh TTL on access; persistent semantic services may use stored timestamps for expiration ### 4. Concurrency Safety - `InMemoryMemoryService`: Thread-safe within a single process - `RedisMemoryService`: Supports multi-process/multi-server concurrency - `SqlMemoryService`: Supports multi-process/multi-server concurrency (using database transactions) +- `MempalaceMemoryService`: Local-first storage; avoid multiple processes writing to the same palace unless the underlying MemPalace/ChromaDB deployment is managed carefully --- @@ -1225,9 +1796,9 @@ MemoryService provides powerful long-term memory management capabilities: - ✅ **Cross-session sharing**: Different sessions can access shared memories - ✅ **Automatic storage**: Automatically stores Session events when `enabled=True` -- ✅ **Keyword search**: Supports Chinese and English keyword matching +- ✅ **Search**: Supports keyword matching and semantic memory retrieval depending on implementation - ✅ **TTL eviction**: Automatically cleans up expired memories -- ✅ **Multiple implementations**: In-Memory, Redis, SQL, TRPC Redis, Mem0 +- ✅ **Multiple implementations**: In-Memory, Redis, SQL, TRPC Redis, Mem0, MemPalace Through proper use of MemoryService, you can achieve: - User profile construction @@ -1241,3 +1812,4 @@ For more detailed usage examples, please refer to the related examples in the [e - [examples/memory_service_with_redis/run_agent.py](../../../examples/memory_service_with_redis/run_agent.py) - [examples/memory_service_with_sql/run_agent.py](../../../examples/memory_service_with_sql/run_agent.py) - [examples/memory_service_with_mem0/run_agent.py](../../../examples/memory_service_with_mem0/run_agent.py) +- [examples/memory_service_with_mempalace/run_agent.py](../../../examples/memory_service_with_mempalace/run_agent.py) diff --git a/docs/mkdocs/zh/memory.md b/docs/mkdocs/zh/memory.md index b076453..13597d8 100644 --- a/docs/mkdocs/zh/memory.md +++ b/docs/mkdocs/zh/memory.md @@ -163,9 +163,9 @@ _session_events = { --- -## MemoryService 实现 +## 基本 MemoryService 实现 -trpc-agent 提供了三种 `MemoryService` 实现,方便根据场景选择合适的存储后端: +trpc-agent 内置提供了三种 `MemoryService` 实现,方便根据场景选择合适的存储后端: ### InMemoryMemoryService @@ -385,7 +385,7 @@ async def _cleanup_expired_async(self) -> None: --- -## 三种实现对比 +### 三种实现对比 | 特性 | InMemoryMemoryService | RedisMemoryService | SqlMemoryService | |-----|----------------------|-------------------|------------------| @@ -407,9 +407,9 @@ async def _cleanup_expired_async(self) -> None: --- -## 使用示例 +### 使用示例 -### 基本使用流程 +#### 基本使用流程 ```python from trpc_agent_sdk.sessions import InMemorySessionService @@ -452,7 +452,9 @@ async for event in runner.run_async( # Agent 会自动调用 memory_service.search_memory() ``` -### 手动存储和搜索 +--- + +#### 手动存储和搜索 ```python # 手动存储会话到 Memory @@ -478,59 +480,9 @@ for memory in response.memories: --- -## 集成 SessionService 和 MemoryService - -在实际应用中,通常需要同时使用 `SessionService` 和 `MemoryService`: - -```python -from trpc_agent_sdk.sessions import InMemorySessionService -from trpc_agent_sdk.memory import MemoryServiceConfig -from trpc_agent_sdk.memory import InMemoryMemoryService -from trpc_agent_sdk.runners import Runner - -# 创建服务实例 -session_service = InMemorySessionService() -memory_service_config = MemoryServiceConfig( - enabled=True, - ttl=MemoryServiceConfig.create_ttl_config( - enable=True, - ttl_seconds=86400, - ), -) -memory_service = InMemoryMemoryService(memory_service_config=memory_service_config) - -# 创建 Runner 并配置服务 -runner = Runner( - app_name="my_app", - agent=my_agent, - session_service=session_service, - memory_service=memory_service # 可选:配置 MemoryService -) - -# 运行 Agent -async for event in runner.run_async( - user_id=user_id, - session_id=session_id, - new_message=user_message -): - # 处理事件... - pass -``` - -**工作流程**: - -1. **SessionService** 管理当前会话的上下文(对话历史、状态等) -2. **MemoryService** 自动存储 Session 事件到长期记忆(如果 `enabled=True`) -3. **load_memory 工具** 调用 `memory_service.search_memory()` 检索相关记忆 -4. Agent 可以同时访问当前会话上下文和历史记忆,提供更连贯的对话体验 - ---- - -## 相关示例 +#### 相关实例 -以下示例展示了不同 MemoryService 实现的使用方式: - -### InMemoryMemoryService +##### InMemoryMemoryService 📁 **示例路径**:[`examples/memory_service_with_in_memory/run_agent.py`](../../../examples/memory_service_with_in_memory/run_agent.py) @@ -548,7 +500,7 @@ python3 run_agent.py --- -### RedisMemoryService +##### RedisMemoryService 📁 **示例路径**:[`examples/memory_service_with_redis/run_agent.py`](../../../examples/memory_service_with_redis/run_agent.py) @@ -566,7 +518,7 @@ python3 run_agent.py --- -### SqlMemoryService +##### SqlMemoryService 📁 **示例路径**:[`examples/memory_service_with_sql/run_agent.py`](../../../examples/memory_service_with_sql/run_agent.py) @@ -584,79 +536,98 @@ python3 run_agent.py --- -## 集成 Mem0 -### 什么是 Mem0? +--- -Mem0 是为 LLM 提供的智能、自我改进的记忆层,能够跨对话持久化和检索用户信息,实现更加个性化和连贯一致的用户体验。 +## 扩展 MemoryService 实现 -**核心能力:** -- 🧠 智能记忆提取和存储 -- 🔍 语义搜索历史对话 -- 🔄 自动记忆更新和去重 -- 🎯 用户级别的记忆隔离 +### 集成 Mempalace -**官方资源:** -- 官方文档:[https://docs.mem0.ai/introduction](https://docs.mem0.ai/introduction) -- GitHub:[https://github.com/mem0ai/mem0](https://github.com/mem0ai/mem0) +#### 什么是 Mempalace? + +Mempalace 是一个本地优先的记忆系统,用于保存原文记忆并通过语义搜索召回历史内容。它的核心存储层级可以理解为: + +```text +Palace + └── Wing + └── Room + └── Drawer +``` + +在 `MempalaceMemoryService` 中,每条可存储的框架事件会被写成一个 drawer,drawer 中包含原始文本和 metadata,例如 `wing`、`room`、`session_id`、`event_id`、`author`、`timestamp` 等。 + +**核心能力:** +- 本地持久化存储,默认数据目录通常是 `~/.mempalace/palace` +- 基于 Mempalace / ChromaDB 的语义检索 +- 通过 `wing` / `room` 做记忆空间和类别隔离 +- 支持使用 `mempalace search` 在命令行排查记忆 +- 支持框架侧 TTL 后台清理过期 drawer --- -### tRPC-Agent 集成方式 +#### tRPC-Agent 集成方式 -tRPC-Agent 提供两种集成 Mem0 的方式: +当前推荐使用框架级记忆服务: | 方式 | 类 / 工具 | 适用场景 | |---|---|---| -| **框架级记忆服务**(推荐) | `Mem0MemoryService` | 由框架自动完成跨会话记忆的存储与检索,Agent 无感知 | -| **工具式记忆** | `SearchMemoryTool` / `SaveMemoryTool` | Agent 通过工具主动调用 Mem0,灵活控制存取时机 | +| **框架级记忆服务**(推荐) | `MempalaceMemoryService` | 由框架自动完成跨会话记忆写入,Agent 通过 `load_memory` 检索 | +| **Mempalace 工具** | `mempalace_search` / `mempalace_add_drawer` 等 | Agent 需要直接操作 Mempalace 的 drawer、diary、KG 等能力 | + +`MempalaceMemoryService` 更适合作为本项目的标准 MemoryService 接入方式。它会在每轮对话结束后由框架自动调用 `store_session()` 写入记忆,Agent 在响应时通过 `load_memory` 工具调用 `search_memory()` 检索历史记忆。 --- -### Mem0MemoryService(推荐方式) +#### MempalaceMemoryService(推荐方式) -`Mem0MemoryService` 是 tRPC-Agent 的**框架级记忆服务**,由框架在每轮对话结束后自动调用 `store_session` 存储会话记忆,Agent 在响应时通过 `load_memory` 工具主动检索相关记忆,无需手动管理存取时机。 +##### 核心设计 -#### 核心设计 +- **跨会话共享**:MemoryService 使用 `session.save_key` 作为跨 session 的用户记忆维度,`save_key` 通常是 `{app_name}/{user_id}`。 +- **Mempalace 映射**:`save_key` 默认映射为 `wing`,`room` 默认是 `conversations`,单条 `Event` 映射为 drawer。 +- **增量后台写入**:只写入当前进程尚未调度或尚未存储的 drawer,避免每轮全量重写 session。 +- **语义检索**:查询时调用 Mempalace 的 `search_memories()`,并可通过 `wing` / `room` 过滤范围。 +- **TTL 自动清理**:后台定期扫描本服务写入的 drawer,根据 metadata 中的事件时间删除过期记忆。 -- **两级 Key 策略**:`session.save_key` → Mem0 `user_id`(用户维度);`session.id` → `run_id`(会话维度) -- **跨会话共享**:同一用户的不同 session 共享同一份记忆 -- **TTL 自动过期**:后台定期清理超时记忆 +##### 快速接入 -#### 快速接入 +**步骤 1:安装依赖** -**步骤 1:创建 `Mem0MemoryService`** +```bash +# 通过 trpc-agent extra 安装 +pip install -e ".[mempalace]" -```python -from mem0 import AsyncMemory, AsyncMemoryClient -from trpc_agent_sdk.memory import MemoryServiceConfig -from trpc_agent_sdk.memory.mem0_memory_service import Mem0MemoryService +# 或者只安装 Mempalace +pip install mempalace +``` -# 自托管模式(AsyncMemory + Qdrant) -from mem0.configs.base import MemoryConfig -mem0_client = AsyncMemory(config=MemoryConfig(**{ - "vector_store": {"provider": "qdrant", "config": {"host": "localhost", "port": 6333}}, # 向量数据库声明 - "llm": {"provider": "deepseek", "config": {"model": "...", "api_key": "..."}}, # 用于记忆摘要提炼(infer=True 时使用) - "embedder": {"provider": "huggingface", "config": {"model": "multi-qa-MiniLM-L6-cos-v1"}}, # 开源嵌入模型 -})) +**步骤 2:创建 `MempalaceMemoryService`** -# 或者:远端平台模式(AsyncMemoryClient),无需自建基础设施 -mem0_client = AsyncMemoryClient(api_key="your_mem0_api_key", host="https://api.mem0.ai") +```python +from trpc_agent_sdk.memory import MemoryServiceConfig +from trpc_agent_sdk.memory.mempalace_memory_service import MempalaceMemoryService -memory_service = Mem0MemoryService( - mem0_client=mem0_client, - memory_service_config=MemoryServiceConfig( - enabled=True, - ttl=MemoryServiceConfig.create_ttl_config(enable=False), # 不启用 TTL,记忆永久保留 +memory_service_config = MemoryServiceConfig( + enabled=True, + ttl=MemoryServiceConfig.create_ttl_config( + enable=True, + ttl_seconds=86400, # 记忆保留 24 小时 + cleanup_interval_seconds=3600, # 每小时清理一次 ), - infer=False, # False=原文存储(稳定),True=语义抽取(智能) +) + +memory_service = MempalaceMemoryService( + memory_service_config=memory_service_config, + wing="my_app_user", # 可选:记忆命名空间;不传则默认由 save_key 推导 + room="conversations", # 可选:记忆类别;默认 conversations + store_only_model_visible=True, ) ``` -**步骤 2:将 `memory_service` 传入 `Runner`** +**步骤 3:将 `memory_service` 传入 `Runner`** ```python from trpc_agent_sdk.runners import Runner +from trpc_agent_sdk.sessions import InMemorySessionService from trpc_agent_sdk.tools import load_memory_tool agent = LlmAgent( @@ -674,58 +645,203 @@ runner = Runner( ) ``` -**步骤 3:运行,记忆自动跨会话持久化** +**步骤 4:运行,记忆自动跨会话持久化** ```python # 第一轮对话(session_1) async for event in runner.run_async(user_id="alice", session_id="session_1", new_message=...): ... -# 框架在对话结束后自动调用 store_session,将本轮消息存入 Mem0 +# 框架在对话结束后自动调用 store_session,将本轮可存储事件写入 Mempalace -# 第二轮对话(session_2)——新会话,但能检索到 session_1 的记忆 +# 第二轮对话(session_2)——新会话,但能通过 load_memory 检索到 session_1 的记忆 async for event in runner.run_async(user_id="alice", session_id="session_2", new_message=...): ... ``` -#### `infer` 参数选择 +**完整可运行示例:** [examples/memory_service_with_mempalace/run_agent.py](../../../examples/memory_service_with_mempalace/run_agent.py) -| | `infer=False`(推荐) | `infer=True` | -|---|---|---| -| 存储内容 | 对话原文 | LLM 提炼后的语义事实 | -| 稳定性 | 高,每条必存 | 中,LLM 判断 NONE 时不存 | -| token 消耗 | 低(无 LLM 调用) | 高(每次写入调用 LLM) | -| 冲突消解 | 不做 | 自动(新事实覆盖旧事实) | -| 推荐场景 | 完整历史归档、生产环境 | 长期用户画像、偏好提炼 | +--- + +##### Mempalace 层级映射 + +```text +session.save_key = "{app_name}/{user_id}" -> wing(未显式配置 wing 时) +room -> room,默认 conversations +Event -> drawer +session.id / event.id / author / timestamp -> drawer metadata +``` + +如果显式传入 `wing="trpc-agent"`,则所有记忆会写入该 wing;如果不显式传入,框架会使用 `save_key` 推导 wing,从而更自然地按 app/user 隔离长期记忆。 + +--- + +##### 指定存储路径与命令行查询 + +Mempalace 的数据路径来自 `MempalaceConfig().palace_path`,默认通常是: + +```text +~/.mempalace/palace +``` + +可以通过环境变量指定: + +```bash +export MEMPALACE_PALACE_PATH=/path/to/palace +``` + +也可以通过 `~/.mempalace/config.json` 指定: + +```json +{ + "palace_path": "/path/to/palace", + "collection_name": "mempalace_drawers" +} +``` + +如果代码中指定或配置了自定义 palace 路径,命令行查询时也必须使用同一个路径: + +```bash +mempalace --palace /path/to/palace search "user name" +``` + +按 `wing` / `room` 过滤: + +```bash +mempalace --palace /path/to/palace search "user name" \ + --wing my_app_user \ + --room conversations +``` + +如果使用默认路径,可以省略 `--palace`: + +```bash +mempalace search "user name" --wing my_app_user --room conversations +``` + +> `/path/to/palace` 是包含 `chroma.sqlite3` 的 Mempalace 数据目录,不是某个单独文件。 + +--- -#### TTL 配置(可选) +##### TTL 配置(可选) ```python memory_service_config = MemoryServiceConfig( enabled=True, ttl=MemoryServiceConfig.create_ttl_config( enable=True, - ttl_seconds=86400, # 记忆保留 24 小时 + ttl_seconds=86400, # 记忆保留 24 小时 cleanup_interval_seconds=3600, # 每小时清理一次 ), ) ``` -> 详细说明、运行结果分析和常见问题解答:[examples/memory_service_with_mem0/README.md](../../../examples/memory_service_with_mem0/README.md) +需要注意: + +- Mempalace 本身没有“长时间不用自动删除”的默认 TTL 机制。 +- `MempalaceMemoryService` 的 TTL 是框架侧实现的后台清理。 +- 清理时会扫描本服务写入的 drawer,并根据 metadata 中的 `timestamp` 判断是否过期。 +- 该 TTL 不是“访问后刷新过期时间”的语义,而是基于原始事件时间进行过期删除。 --- -### 工具式集成(mem0_tool) +##### 删除记忆 -tRPC-Agent 通过 **工具(Tools)** 的方式集成 Mem0,为 Agent 提供记忆能力。框架提供了两个核心工具类: +Mempalace CLI 当前没有直接提供按 `wing` 或 `wing + room` 批量删除的命令。框架中提供了便捷方法: + +```python +# 删除整个 wing +await memory_service.delete_memory(wing="my_app_user") + +# 删除 wing 下指定 room +await memory_service.delete_memory(wing="my_app_user", room="conversations") +``` + +这会删除 drawer collection 中匹配 metadata 的记录,并清理当前进程内的去重缓存,避免删除后无法重新写入。 + +--- + +##### 存储内容策略 + +当前建议只把有长期价值的普通文本事件写入 Mempalace。工具调用、工具结果、代码执行结果等中间过程通常不适合作为长期记忆,否则容易出现: + +- `load_memory` 的工具结果再次被写入记忆 +- 记忆中嵌套旧的 memory JSON +- 长期记忆被工具日志污染,检索质量下降 + +因此 `MempalaceMemoryService` 更适合存储类似下面的信息: + +```text +用户:My name is Alice. +用户:My favorite color is blue. +助手:已确认用户姓名或偏好。 +``` + +而不是存储: + +```text +[tool_call] load_memory: ... +[tool_response] load_memory: {"memories": [...]} +``` + +--- + +##### 典型工作流 + +``` +1. 用户:Do you remember my name? + ↓ + Agent 调用: load_memory(query="user name") + ↓ + 结果:{"memories": []} + ↓ + Agent:I don't know your name yet. + +2. 用户:My name is Alice + ↓ + 本轮结束后,框架自动调用 MempalaceMemoryService.store_session() + ↓ + 用户消息被写入 wing/room 下的 drawer + +3. 用户开启新 session:Do you remember my name? + ↓ + Agent 调用: load_memory(query="user name") + ↓ + Mempalace 返回包含 "My name is Alice" 的历史记忆 + ↓ + Agent:Yes, your name is Alice. +``` + +**查看完整演示输出(MempalaceMemoryService):** [examples/memory_service_with_mempalace/README.md](../../../examples/memory_service_with_mempalace/README.md) + +--- + +#### 工具式集成(mempalace_tool) + +`mempalace_tool` 是另一种与 Mempalace 集成的方式。它不是推荐的标准 MemoryService 路径,而是把 Mempalace 的能力作为 Agent 可调用工具暴露出来,让 Agent 主动决定什么时候查询、写入、读取 diary 或维护 KG。 + +它与 `MempalaceMemoryService` 的区别是: + +| 方式 | 写入时机 | 检索方式 | 适用场景 | +|---|---|---|---| +| `MempalaceMemoryService` | 框架在每轮结束后自动写入 | `load_memory` 间接调用 `search_memory()` | 标准跨会话长期记忆 | +| `mempalace_tool` | Agent 主动调用工具写入 | Agent 主动调用 `mempalace_search` | 需要精细控制 Mempalace 功能、diary、KG 或手动 drawer 管理 | + +##### 可用工具 | 工具类 | 工具名 | 功能 | 使用场景 | -|--------|--------|------|---------| -| `SearchMemoryTool` | `search_memory` | 搜索历史记忆 | Agent 需要回忆过去的对话内容 | -| `SaveMemoryTool` | `save_memory` | 保存重要信息 | Agent 判断需要记住的用户信息 | +|---|---|---|---| +| `MempalaceSearchTool` | `mempalace_search` | 按语义搜索已保存的 drawer 内容 | Agent 需要回忆用户画像、偏好或历史事实 | +| `MempalaceAddDrawerTool` | `mempalace_add_drawer` | 向指定 `wing/room` 写入一条原文 drawer | 用户明确要求记住某个长期信息 | +| `MempalaceDiaryWriteTool` | `mempalace_diary_write` | 写入 agent diary | 记录运行观察、任务过程或阶段性总结 | +| `MempalaceDiaryReadTool` | `mempalace_diary_read` | 读取指定 agent 最近的 diary | Agent 需要回顾历史任务记录 | +| `MempalaceKGAddTool` | `mempalace_kg_add` | 写入知识图谱三元组事实 | 需要结构化表达 `subject -> predicate -> object` | +| `MempalaceKGQueryTool` | `mempalace_kg_query` | 查询某个实体的知识图谱关系 | 查询 Alice 相关事实、项目依赖、实体关系等 | +| `MempalaceKGTimelineTool` | `mempalace_kg_timeline` | 按时间线读取知识图谱事实 | 查看某个实体关系如何随时间变化 | +| `MempalaceKGInvalidateTool` | `mempalace_kg_invalidate` | 将一条当前事实标记为失效 | 表达事实变化,保留历史但不再视为当前事实 | -> **注意**:两个工具类需要在实例化时传入 Mem0 客户端,`user_id` 由框架通过 `InvocationContext` 自动注入,无需在工具参数中显式传递。 +> **注意**:`mempalace_tool` 与 `mem0_tool` 类似,都是通过 Tools 暴露给 Agent,由模型决定是否调用;但 MemPalace 工具不只覆盖“搜索/保存”两个动作,还覆盖 diary 和 KG。完整示例见 [examples/mempalace_tools/README.md](../../../examples/mempalace_tools/README.md),工具源码见 [mempalace_tool.py](../../../trpc_agent_sdk/tools/mempalace_tool.py)。 -#### 集成架构 +##### 集成架构 ``` ┌──────────────────────┐ @@ -734,153 +850,436 @@ tRPC-Agent 通过 **工具(Tools)** 的方式集成 Mem0,为 Agent 提供 │ ▼ ┌──────────────────────┐ -│ tRPC-Agent │◄─────────┐ -│ LlmAgent │ │ -└──────────┬───────────┘ │ - │ │ - │ 调用工具 │ 返回记忆 - │ │ - ▼ │ -┌──────────────────────┐ │ -│ Mem0 Tools │──────────┘ -│ - SearchMemoryTool │ -│ - SaveMemoryTool │ -└──────────┬───────────┘ - │ - ▼ -┌──────────────────────┐ -│ Mem0 Client │ -│ (AsyncMemory / │ -│ AsyncMemoryClient) │ +│ tRPC-Agent │◄────────────────┐ +│ LlmAgent │ │ +└──────────┬───────────┘ │ + │ │ 返回工具结果 + │ 调用工具 │ + ▼ │ +┌──────────────────────┐ │ +│ MemPalace Tools │─────────────────┘ +│ - mempalace_search │ +│ - add_drawer │ +│ - diary read/write │ +│ - KG tools │ └──────────┬───────────┘ │ ▼ ┌──────────────────────┐ -│ Storage │ -│ - Qdrant │ -│ - Mem0 Cloud │ +│ MemPalace Backend │ +│ - Palace / ChromaDB │ +│ - KG SQLite │ └──────────────────────┘ ``` ---- +##### 快速接入 -### 部署模式 +```python +from trpc_agent_sdk.agents import LlmAgent +from trpc_agent_sdk.tools.mempalace_tool import MempalaceAddDrawerTool +from trpc_agent_sdk.tools.mempalace_tool import MempalaceDiaryReadTool +from trpc_agent_sdk.tools.mempalace_tool import MempalaceDiaryWriteTool +from trpc_agent_sdk.tools.mempalace_tool import MempalaceKGAddTool +from trpc_agent_sdk.tools.mempalace_tool import MempalaceKGInvalidateTool +from trpc_agent_sdk.tools.mempalace_tool import MempalaceKGQueryTool +from trpc_agent_sdk.tools.mempalace_tool import MempalaceKGTimelineTool +from trpc_agent_sdk.tools.mempalace_tool import MempalaceSearchTool + +palace_path = "/tmp/trpc-agent-mempalace-demo" +kg_path = "/tmp/trpc-agent-mempalace-demo/knowledge_graph.sqlite3" + +tools = [ + MempalaceSearchTool(palace_path=palace_path), + MempalaceAddDrawerTool(palace_path=palace_path), + MempalaceDiaryWriteTool(palace_path=palace_path), + MempalaceDiaryReadTool(palace_path=palace_path), + MempalaceKGAddTool(palace_path=palace_path, kg_path=kg_path), + MempalaceKGQueryTool(palace_path=palace_path, kg_path=kg_path), + MempalaceKGTimelineTool(palace_path=palace_path, kg_path=kg_path), + MempalaceKGInvalidateTool(palace_path=palace_path, kg_path=kg_path), +] -tRPC-Agent 支持 Mem0 的两种部署模式:自托管模式和平台模式 +agent = LlmAgent( + name="memory_assistant", + model=your_model, + instruction=""" + You are a helpful assistant with MemPalace tools. + - Use mempalace_search before answering questions that may require past memory. + - Use mempalace_add_drawer when the user explicitly asks you to remember stable facts. + - Use diary tools for agent diary entries. + - Use KG tools for structured facts such as Alice -> likes -> Italian food. + """, + tools=tools, +) +``` -#### 模式对比 +##### 指定 Mempalace 路径 -| 特性 | 自托管模式 | 平台模式 | -|------|-----------|---------| -| **客户端类型** | `AsyncMemory` | `AsyncMemoryClient` | -| **存储位置** | 本地向量数据库(如 Qdrant) | Mem0 云端 | -| **依赖组件** | 向量数据库 + 嵌入模型 + LLM | 仅需 API Key | -| **数据控制** | 完全控制 | 托管服务 | -| **适用场景** | 开发测试、数据敏感、本地部署 | 生产环境、快速部署 | +工具类支持传入 `palace_path`。如果不传,则使用 `MempalaceConfig().palace_path`。KG 工具额外支持 `kg_path`;如果不传 `kg_path` 且传入了 `palace_path`,默认使用 `palace_path/knowledge_graph.sqlite3`: -#### 模式一:自托管(AsyncMemory) +```python +mempalace_search_tool = MempalaceSearchTool(palace_path="/path/to/palace") +mempalace_add_drawer_tool = MempalaceAddDrawerTool(palace_path="/path/to/palace") +mempalace_kg_query_tool = MempalaceKGQueryTool( + palace_path="/path/to/palace", + kg_path="/path/to/palace/knowledge_graph.sqlite3", +) +``` -适合需要完全控制数据和基础设施的场景。 +命令行排查时也要使用相同路径: -**核心组件:** -- **向量存储**:支持多种后端(见下方完整清单) -- **LLM**:用于生成记忆摘要(OpenAI / DeepSeek / Gemini 等) -- **嵌入模型**:用于向量化(HuggingFace / OpenAI 等) +```bash +mempalace --palace /path/to/palace search "user name" +``` -**自托管支持的向量存储(完整清单):** -- `azure_ai_search` -- `azure_mysql` -- `baidu` -- `cassandra` -- `chroma` -- `databricks` -- `elasticsearch` -- `faiss` -- `langchain` -- `milvus` -- `mongodb` -- `neptune_analytics` -- `opensearch` -- `pgvector` -- `pinecone` -- `qdrant` -- `redis` -- `s3_vectors` -- `supabase` -- `turbopuffer` -- `upstash_vector` -- `valkey` -- `vertex_ai_vector_search` -- `weaviate` +示例目录中通过 `.env` 管理路径: -> 官方向量存储实现列表(以 mem0 仓库为准):[mem0/vector_stores](https://github.com/mem0ai/mem0/tree/main/mem0/vector_stores) +```bash +MEMPALACE_PALACE_PATH=/tmp/trpc-agent-mempalace-demo +MEMPALACE_KG_PATH=/tmp/trpc-agent-mempalace-demo/knowledge_graph.sqlite3 +MEMPALACE_WING=personal_assistant_alice +MEMPALACE_ROOM=user_profile +``` -**示例代码:** +##### 工具式工作流 + +```text +1. 用户:Use mempalace_search to check whether you remember my name. + ↓ + Agent 调用: mempalace_search( + query="name", + wing="personal_assistant_alice", + room="user_profile" + ) + ↓ + 结果:No palace found 或空结果 + ↓ + Agent:暂时没有记住你的名字 + +2. 用户:Use mempalace_add_drawer to remember that my name is Alice. + ↓ + Agent 调用: mempalace_add_drawer( + wing="personal_assistant_alice", + room="user_profile", + content="User's name is Alice." + ) + ↓ + Mempalace 写入 drawer + +3. 用户开启新的 session:Use mempalace_search to recall my name. + ↓ + Agent 调用: mempalace_search(query="name", wing="personal_assistant_alice", room="user_profile") + ↓ + Mempalace 返回 "User's name is Alice." + ↓ + Agent:你的名字是 Alice + +4. 用户:Use mempalace_kg_add to add this fact: Alice likes Italian food. + ↓ + Agent 调用: mempalace_kg_add(subject="Alice", predicate="likes", object="Italian food") + ↓ + KG 写入三元组事实:Alice -> likes -> Italian food + +5. 用户:Use mempalace_kg_invalidate to mark the fact Alice likes Italian food as ended today. + ↓ + Agent 调用: mempalace_kg_invalidate(subject="Alice", predicate="likes", object="Italian food") + ↓ + KG 保留历史事实,但将 current 标记为 false +``` + +**查看完整工具式演示和运行结果分析:** [examples/mempalace_tools/README.md](../../../examples/mempalace_tools/README.md) + +--- + +##### MempalaceSearchTool 介绍 + +按语义搜索 MemPalace 中已保存的 drawer 内容。 + +**构造函数:** ```python -from mem0 import AsyncMemory -from trpc_agent_sdk.server.tools.mem0_tool import SearchMemoryTool, SaveMemoryTool +MempalaceSearchTool( + palace_path: str | None = None, + filters_name: list[str] | None = None, + filters: list[Any] | None = None, +) +``` -# 配置自定义组件 -config = { - "vector_store": {"provider": "qdrant", "config": {...}}, - "llm": {"provider": "deepseek", "config": {...}}, - "embedder": {"provider": "huggingface", "config": {...}} +**Agent 工具参数(LLM 可调用):** +- `query`(string,必填):搜索查询内容 +- `limit`(integer,可选):最多返回多少条结果,默认 5 +- `wing`(string,可选):按 wing 过滤 +- `room`(string,可选):按 room 过滤 + +**返回值示例:** +```python +{ + "query": "name favorite food", + "filters": {"wing": "personal_assistant_alice", "room": "user_profile"}, + "results": [ + {"text": "User's name is Alice.", "wing": "personal_assistant_alice", "room": "user_profile"}, + {"text": "My favorite food is Italian food.", "wing": "personal_assistant_alice", "room": "user_profile"}, + ], } +``` -# 创建 Mem0 客户端 -memory = await AsyncMemory.from_config(config) +--- -# 用客户端实例化工具 -search_memory_tool = SearchMemoryTool(client=memory) -save_memory_tool = SaveMemoryTool(client=memory) +##### MempalaceAddDrawerTool 介绍 + +向指定 `wing/room` 写入一条原文 drawer,适合保存用户明确要求记住的长期事实。 + +**Agent 工具参数(LLM 可调用):** +- `wing`(string,必填):存储作用域,例如 `personal_assistant_alice` +- `room`(string,必填):记忆主题,例如 `user_profile` +- `content`(string,必填):要保存的原文内容 +- `source_file`(string,可选):来源标识 + +**返回值示例:** +```python +{ + "success": True, + "drawer_id": "drawer_personal_assistant_alice_user_profile_xxx", + "wing": "personal_assistant_alice", + "room": "user_profile", +} ``` -**详细配置:** 参见 [完整示例 - 自托管模式](../../../examples/memory_service_with_mem0/README.md#自托管模式asyncmemory--qdrant) +--- -#### 模式二:平台(AsyncMemoryClient) +##### Diary 工具介绍 -适合快速部署和生产环境使用。 +`MempalaceDiaryWriteTool` 和 `MempalaceDiaryReadTool` 用于记录和读取 agent diary。它们适合保存“本次任务做了什么、观察到什么、后续要注意什么”这类运行记录,不建议替代用户画像记忆。 -**前置条件:** -- 注册 [Mem0 平台账号](https://app.mem0.ai/dashboard) -- 获取 API Key +| 工具 | 关键参数 | 返回重点 | +|---|---|---| +| `mempalace_diary_write` | `entry`、`agent_name`、`topic`、`wing` | `success`、`entry_id`、`agent`、`topic` | +| `mempalace_diary_read` | `agent_name`、`last_n`、`wing` | `entries`、`total`、`showing` | + +示例输出中,写入 `Alice tested the MemPalace tools example today.` 后,后续新的 session 仍能读取到这条 diary,说明 diary 数据也已经持久化。 + +--- + +##### KG 工具介绍 + +KG 工具用于维护结构化事实。这里的“事实”通常是一个三元组: + +```text +subject -> predicate -> object +Alice -> likes -> Italian food +``` + +| 工具 | 关键参数 | 语义 | +|---|---|---| +| `mempalace_kg_add` | `subject`、`predicate`、`object`、`valid_from`、`valid_to`、`confidence` | 写入一条结构化事实 | +| `mempalace_kg_query` | `entity`、`as_of`、`direction` | 查询实体相关事实 | +| `mempalace_kg_timeline` | `entity` | 查看实体事实时间线 | +| `mempalace_kg_invalidate` | `subject`、`predicate`、`object`、`ended` | 将事实标记为失效 | + +`mempalace_kg_invalidate` 不会直接删除历史事实,而是设置 `valid_to` 并让 `current=False`。因此示例中把 invalidate 放在第二阶段持久化读取验证之后执行,避免提前改变第二阶段查询结果。 + +##### 使用建议 + +- 如果只是需要标准的跨 session 长期记忆,优先使用 `MempalaceMemoryService`。 +- 如果希望 Agent 主动控制写入内容、写入分类、diary 或 KG,再使用 `mempalace_tool`。 +- 对用户画像、偏好等长期信息,建议写入稳定的 `wing/room`,例如 `personal_assistant_alice/user_profile`。 +- 对 KG 事实变化,优先用 `mempalace_kg_invalidate` 表达“已不再成立”,而不是直接删除历史。 +- 不建议让 Agent 把 `load_memory` 的工具返回、代码执行结果等中间过程直接写入 drawer,否则容易污染长期记忆。 + +--- + +#### Mempalace 资料 + +| 资源 | 路径 | 说明 | +|---|---|---| +| `MempalaceMemoryService` 完整示例 | [examples/memory_service_with_mempalace/](../../../examples/memory_service_with_mempalace/README.md) | 含安装、路径配置、CLI 查询和运行结果分析 | +| `MempalaceMemoryService` 源码 | [mempalace_memory_service.py](../../../trpc_agent_sdk/memory/mempalace_memory_service.py) | 推荐方式,框架级记忆服务实现 | +| Mempalace 工具源码 | [mempalace_tool.py](../../../trpc_agent_sdk/tools/mempalace_tool.py) | 可选方式,`mempalace_search` / `mempalace_add_drawer` / diary / KG 等工具 | + +--- + +### 集成 Mem0 + +#### 什么是 Mem0? + +Mem0 是为 LLM 提供的智能、自我改进的记忆层,能够跨对话持久化和检索用户信息,实现更加个性化和连贯一致的用户体验。 + +**核心能力:** +- 🧠 智能记忆提取和存储 +- 🔍 语义搜索历史对话 +- 🔄 自动记忆更新和去重 +- 🎯 用户级别的记忆隔离 + +**官方资源:** +- 官方文档:[https://docs.mem0.ai/introduction](https://docs.mem0.ai/introduction) +- GitHub:[https://github.com/mem0ai/mem0](https://github.com/mem0ai/mem0) + +--- + +#### tRPC-Agent 集成方式 + +tRPC-Agent 提供两种集成 Mem0 的方式: + +| 方式 | 类 / 工具 | 适用场景 | +|---|---|---| +| **框架级记忆服务**(推荐) | `Mem0MemoryService` | 由框架自动完成跨会话记忆的存储与检索,Agent 无感知 | +| **工具式记忆** | `SearchMemoryTool` / `SaveMemoryTool` | Agent 通过工具主动调用 Mem0,灵活控制存取时机 | + +--- + +#### Mem0MemoryService(推荐方式) + +`Mem0MemoryService` 是 tRPC-Agent 的**框架级记忆服务**,由框架在每轮对话结束后自动调用 `store_session` 存储会话记忆,Agent 在响应时通过 `load_memory` 工具主动检索相关记忆,无需手动管理存取时机。 + +##### 核心设计 + +- **两级 Key 策略**:`session.save_key` → Mem0 `user_id`(用户维度);`session.id` → `run_id`(会话维度) +- **跨会话共享**:同一用户的不同 session 共享同一份记忆 +- **TTL 自动过期**:后台定期清理超时记忆 + +##### 快速接入 + +**步骤 1:创建 `Mem0MemoryService`** -**示例代码:** ```python -from mem0 import AsyncMemoryClient -from trpc_agent_sdk.server.tools.mem0_tool import SearchMemoryTool, SaveMemoryTool +from mem0 import AsyncMemory, AsyncMemoryClient +from trpc_agent_sdk.memory import MemoryServiceConfig +from trpc_agent_sdk.memory.mem0_memory_service import Mem0MemoryService -# 创建平台客户端 -client = AsyncMemoryClient( - api_key="m0-your-api-key", - host="https://api.mem0.ai" +# 自托管模式(AsyncMemory + Qdrant) +from mem0.configs.base import MemoryConfig +mem0_client = AsyncMemory(config=MemoryConfig(**{ + "vector_store": {"provider": "qdrant", "config": {"host": "localhost", "port": 6333}}, # 向量数据库声明 + "llm": {"provider": "deepseek", "config": {"model": "...", "api_key": "..."}}, # 用于记忆摘要提炼(infer=True 时使用) + "embedder": {"provider": "huggingface", "config": {"model": "multi-qa-MiniLM-L6-cos-v1"}}, # 开源嵌入模型 +})) + +# 或者:远端平台模式(AsyncMemoryClient),无需自建基础设施 +mem0_client = AsyncMemoryClient(api_key="your_mem0_api_key", host="https://api.mem0.ai") + +memory_service = Mem0MemoryService( + mem0_client=mem0_client, + memory_service_config=MemoryServiceConfig( + enabled=True, + ttl=MemoryServiceConfig.create_ttl_config(enable=False), # 不启用 TTL,记忆永久保留 + ), + infer=False, # False=原文存储(稳定),True=语义抽取(智能) ) +``` -# 用客户端实例化工具 -search_memory_tool = SearchMemoryTool(client=client) -save_memory_tool = SaveMemoryTool(client=client) +**步骤 2:将 `memory_service` 传入 `Runner`** + +```python +from trpc_agent_sdk.runners import Runner +from trpc_agent_sdk.tools import load_memory_tool + +agent = LlmAgent( + name="assistant", + model=your_model, + tools=[load_memory_tool], # Agent 通过此工具主动检索记忆 + instruction="Use load_memory to recall relevant past conversations before answering.", +) + +runner = Runner( + app_name="my_app", + agent=agent, + session_service=InMemorySessionService(), + memory_service=memory_service, # 框架自动负责存储 +) ``` -**详细配置:** 参见 [完整示例 - 平台模式](../../../examples/memory_service_with_mem0/README.md#远端平台模式asyncmemoryclient) +**步骤 3:运行,记忆自动跨会话持久化** + +```python +# 第一轮对话(session_1) +async for event in runner.run_async(user_id="alice", session_id="session_1", new_message=...): + ... +# 框架在对话结束后自动调用 store_session,将本轮消息存入 Mem0 + +# 第二轮对话(session_2)——新会话,但能检索到 session_1 的记忆 +async for event in runner.run_async(user_id="alice", session_id="session_2", new_message=...): + ... +``` + +**完整可运行示例:** [examples/memory_service_with_mem0/run_agent.py](../../../examples/memory_service_with_mem0/run_agent.py) + +##### `infer` 参数选择 + +| | `infer=False`(推荐) | `infer=True` | +|---|---|---| +| 存储内容 | 对话原文 | LLM 提炼后的语义事实 | +| 稳定性 | 高,每条必存 | 中,LLM 判断 NONE 时不存 | +| token 消耗 | 低(无 LLM 调用) | 高(每次写入调用 LLM) | +| 冲突消解 | 不做 | 自动(新事实覆盖旧事实) | +| 推荐场景 | 完整历史归档、生产环境 | 长期用户画像、偏好提炼 | + +##### TTL 配置(可选) + +```python +memory_service_config = MemoryServiceConfig( + enabled=True, + ttl=MemoryServiceConfig.create_ttl_config( + enable=True, + ttl_seconds=86400, # 记忆保留 24 小时 + cleanup_interval_seconds=3600, # 每小时清理一次 + ), +) +``` + +> 详细说明、运行结果分析和常见问题解答:[examples/memory_service_with_mem0/README.md](../../../examples/memory_service_with_mem0/README.md) --- -### Mem0 快速开始 +#### 工具式集成(mem0_tool) -#### 1. 安装依赖 +tRPC-Agent 通过 **工具(Tools)** 的方式集成 Mem0,为 Agent 提供记忆能力。框架提供了两个核心工具类: -```bash -# 安装 Mem0 核心包 -pip install mem0ai +| 工具类 | 工具名 | 功能 | 使用场景 | +|--------|--------|------|---------| +| `SearchMemoryTool` | `search_memory` | 搜索历史记忆 | Agent 需要回忆过去的对话内容 | +| `SaveMemoryTool` | `save_memory` | 保存重要信息 | Agent 判断需要记住的用户信息 | -# 自托管模式额外依赖 -pip install sentence-transformers qdrant-client +> **注意**:两个工具类需要在实例化时传入 Mem0 客户端,`user_id` 由框架通过 `InvocationContext` 自动注入,无需在工具参数中显式传递。 + +##### 集成架构 -# 或使用 trpc-agent 扩展安装 -pip install -e ".[mem0]" +``` +┌──────────────────────┐ +│ User Input │ +└──────────┬───────────┘ + │ + ▼ +┌──────────────────────┐ +│ tRPC-Agent │◄─────────┐ +│ LlmAgent │ │ +└──────────┬───────────┘ │ + │ │ + │ 调用工具 │ 返回记忆 + │ │ + ▼ │ +┌──────────────────────┐ │ +│ Mem0 Tools │──────────┘ +│ - SearchMemoryTool │ +│ - SaveMemoryTool │ +└──────────┬───────────┘ + │ + ▼ +┌──────────────────────┐ +│ Mem0 Client │ +│ (AsyncMemory / │ +│ AsyncMemoryClient) │ +└──────────┬───────────┘ + │ + ▼ +┌──────────────────────┐ +│ Storage │ +│ - Qdrant │ +│ - Mem0 Cloud │ +└──────────────────────┘ ``` -#### 2. 创建 Agent +--- + +##### 快速接入 ```python from trpc_agent_sdk.agents import LlmAgent @@ -905,34 +1304,46 @@ agent = LlmAgent( ) ``` -#### 3. 运行 Agent +--- -```python -from trpc_agent_sdk.runners import Runner +##### 典型工作流 -runner = Runner( - app_name="memory_app", - agent=agent, - session_service=your_session_service -) +- 场景:个人助理记住用户偏好 -# 与 Agent 交互,自动使用记忆功能 -async for event in runner.run_async( - user_id="alice", - session_id="session_1", - new_message=user_input -): - # 处理响应 - pass ``` +1. 用户:Do you remember my name? + ↓ + Agent 调用: search_memory(query="user's name") + 框架自动注入 user_id="alice" + ↓ + 结果:no_memories + ↓ + Agent:I don't have your name. Could you tell me? -**完整可运行示例:** [examples/memory_service_with_mem0/run_agent.py](../../../examples/memory_service_with_mem0/run_agent.py) +2. 用户:My name is Alice + ↓ + Agent 调用: save_memory(content="User's name is Alice") + 框架自动注入 user_id="alice" + ↓ + 结果:success + ↓ + Agent:Thank you, Alice! I'll remember that. ---- +3. 用户:Do you remember my name? + ↓ + Agent 调用: search_memory(query="user's name") + 框架自动注入 user_id="alice" + ↓ + 结果:success, memories="- Name is Alice" + ↓ + Agent:Yes, your name is Alice! +``` + +**查看完整演示输出(Mem0MemoryService):** [运行结果分析](../../../examples/memory_service_with_mem0/README.md#运行结果分析) -### 工具 API +--- -#### SearchMemoryTool +##### SearchMemoryTool 介绍 搜索用户的历史记忆。 @@ -967,7 +1378,9 @@ SearchMemoryTool( } ``` -#### SaveMemoryTool +--- + +##### SaveMemoryTool 介绍 保存重要信息到用户记忆。 @@ -1011,46 +1424,113 @@ SaveMemoryTool( --- -### 典型工作流(工具式) -#### 场景:个人助理记住用户偏好 +#### 部署模式 + +tRPC-Agent 支持 Mem0 的两种部署模式:自托管模式和平台模式 + +##### 模式对比 + +| 特性 | 自托管模式 | 平台模式 | +|------|-----------|---------| +| **客户端类型** | `AsyncMemory` | `AsyncMemoryClient` | +| **存储位置** | 本地向量数据库(如 Qdrant) | Mem0 云端 | +| **依赖组件** | 向量数据库 + 嵌入模型 + LLM | 仅需 API Key | +| **数据控制** | 完全控制 | 托管服务 | +| **适用场景** | 开发测试、数据敏感、本地部署 | 生产环境、快速部署 | + +##### 模式一:自托管(AsyncMemory) + +适合需要完全控制数据和基础设施的场景。 + +**核心组件:** +- **向量存储**:支持多种后端(见下方完整清单) +- **LLM**:用于生成记忆摘要(OpenAI / DeepSeek / Gemini 等) +- **嵌入模型**:用于向量化(HuggingFace / OpenAI 等) +**自托管支持的向量存储(完整清单):** +- `azure_ai_search` +- `azure_mysql` +- `baidu` +- `cassandra` +- `chroma` +- `databricks` +- `elasticsearch` +- `faiss` +- `langchain` +- `milvus` +- `mongodb` +- `neptune_analytics` +- `opensearch` +- `pgvector` +- `pinecone` +- `qdrant` +- `redis` +- `s3_vectors` +- `supabase` +- `turbopuffer` +- `upstash_vector` +- `valkey` +- `vertex_ai_vector_search` +- `weaviate` + +> 官方向量存储实现列表(以 mem0 仓库为准):[mem0/vector_stores](https://github.com/mem0ai/mem0/tree/main/mem0/vector_stores) + +**示例代码:** +```python +from mem0 import AsyncMemory +from trpc_agent_sdk.server.tools.mem0_tool import SearchMemoryTool, SaveMemoryTool + +# 配置自定义组件 +config = { + "vector_store": {"provider": "qdrant", "config": {...}}, + "llm": {"provider": "deepseek", "config": {...}}, + "embedder": {"provider": "huggingface", "config": {...}} +} + +# 创建 Mem0 客户端 +memory = await AsyncMemory.from_config(config) + +# 用客户端实例化工具 +search_memory_tool = SearchMemoryTool(client=memory) +save_memory_tool = SaveMemoryTool(client=memory) ``` -1. 用户:Do you remember my name? - ↓ - Agent 调用: search_memory(query="user's name") - 框架自动注入 user_id="alice" - ↓ - 结果:no_memories - ↓ - Agent:I don't have your name. Could you tell me? -2. 用户:My name is Alice - ↓ - Agent 调用: save_memory(content="User's name is Alice") - 框架自动注入 user_id="alice" - ↓ - 结果:success - ↓ - Agent:Thank you, Alice! I'll remember that. +**详细配置:** 参见 [完整示例 - 自托管模式](../../../examples/memory_service_with_mem0/README.md#自托管模式asyncmemory--qdrant) -3. 用户:Do you remember my name? - ↓ - Agent 调用: search_memory(query="user's name") - 框架自动注入 user_id="alice" - ↓ - 结果:success, memories="- Name is Alice" - ↓ - Agent:Yes, your name is Alice! +##### 模式二:平台(AsyncMemoryClient) + +适合快速部署和生产环境使用。 + +**前置条件:** +- 注册 [Mem0 平台账号](https://app.mem0.ai/dashboard) +- 获取 API Key + +**示例代码:** +```python +from mem0 import AsyncMemoryClient +from trpc_agent_sdk.server.tools.mem0_tool import SearchMemoryTool, SaveMemoryTool + +# 创建平台客户端 +client = AsyncMemoryClient( + api_key="m0-your-api-key", + host="https://api.mem0.ai" +) + +# 用客户端实例化工具 +search_memory_tool = SearchMemoryTool(client=client) +save_memory_tool = SaveMemoryTool(client=client) ``` -**查看完整演示输出(Mem0MemoryService):** [运行结果分析](../../../examples/memory_service_with_mem0/README.md#运行结果分析) +**详细配置:** 参见 [完整示例 - 平台模式](../../../examples/memory_service_with_mem0/README.md#远端平台模式asyncmemoryclient) --- -### 高级特性 -#### 多用户记忆隔离 + +#### 高级特性 + +##### 多用户记忆隔离 通过 `user_id` 参数实现用户级别的记忆隔离: @@ -1062,7 +1542,7 @@ await runner.run_async(user_id="user_a", ...) await runner.run_async(user_id="user_b", ...) ``` -#### 记忆过滤和搜索 +##### 记忆过滤和搜索 通过 `filters` 参数可以对记忆进行精细化检索,支持按用户、类别等维度过滤,避免跨用户或无关记忆的干扰: @@ -1077,7 +1557,7 @@ memories = await mem0_client.search( ) ``` -#### 直接记忆管理 +##### 直接记忆管理 除了通过 Agent 工具间接操作外,也可以直接调用 Mem0 客户端 API 对记忆进行增删查管理: @@ -1092,13 +1572,13 @@ await memory.delete(memory_id="memory-id") await memory.delete_all(user_id="alice") ``` -**更多高级用法:** [高级用法文档](../../../examples/mem_0/README.md#高级用法) +**更多高级用法:** [高级用法文档](../../../examples/mem0_tools/README.md#高级用法) --- -### Mem0 常见问题 +#### Mem0 常见问题 -#### 如何选择部署模式? +##### 如何选择部署模式? | 考虑因素 | 自托管 | 平台 | |---------|-------|------| @@ -1108,7 +1588,7 @@ await memory.delete_all(user_id="alice") | 生产环境高可用 | ❌ | ✅ | | 成本敏感(小规模) | ✅ | ❌ | -#### 自托管模式常见错误 +##### 自托管模式常见错误 **向量维度不匹配:** ``` @@ -1127,19 +1607,19 @@ ConnectionError: Cannot connect to Qdrant at localhost:6333 --- -### Mem0 参考资料 +#### Mem0 参考资料 -#### 框架资源 +##### 框架资源 | 资源 | 路径 | 说明 | |---|---|---| | `Mem0MemoryService` 完整示例 | [examples/memory_service_with_mem0/](../../../examples/memory_service_with_mem0/README.md) | 含运行结果分析、QA | | `Mem0MemoryService` 源码 | [mem0_memory_service.py](../../../trpc_agent_sdk/memory/mem0_memory_service.py) | 服务实现 | -| 工具式集成源码 | [mem0_tool.py](../../../trpc_agent_sdk/tools/mem0_tool.py) | `SearchMemoryTool` / `SaveMemoryTool` 工具类 | +| 工具式集成源码 | [mem0_tools.py](../../../trpc_agent_sdk/tools/mem0_tools.py) | `SearchMemoryTool` / `SaveMemoryTool` 工具类 | | infer 参数详解 | [README.md#infer-参数详解](../../../examples/memory_service_with_mem0/README.md#infer-参数详解) | True vs False 对比 | | 常见问题 QA | [README.md#常见问题-qa](../../../examples/memory_service_with_mem0/README.md#常见问题-qa) | 错误分析与解答 | -#### Mem0 官方资源 +##### Mem0 官方资源 - **官方文档:** [https://docs.mem0.ai/introduction](https://docs.mem0.ai/introduction) - **GitHub:** [https://github.com/mem0ai/mem0](https://github.com/mem0ai/mem0) - **示例代码:** [https://github.com/mem0ai/mem0/tree/main/examples](https://github.com/mem0ai/mem0/tree/main/examples) @@ -1147,7 +1627,7 @@ ConnectionError: Cannot connect to Qdrant at localhost:6333 --- -### 下一步 +##### 其他资料 1. **快速上手(推荐):** 查看 [Mem0MemoryService 完整示例](../../../examples/memory_service_with_mem0/) 并运行 `run_agent.py` 2. **选择部署模式:** 参考 [自托管 vs 远端平台对比](../../../examples/memory_service_with_mem0/README.md#两种部署模式详解) diff --git a/examples/dsl/classifier_mcp/.env b/examples/dsl/classifier_mcp/.env index 23a7cf9..12626e6 100644 --- a/examples/dsl/classifier_mcp/.env +++ b/examples/dsl/classifier_mcp/.env @@ -1,3 +1,8 @@ +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name + # Generated environment variables for this workflow. # Update the values as needed. MODEL1_NAME="your-model-name-1" diff --git a/examples/llmagent/.env b/examples/llmagent/.env index d47b675..0a57a17 100644 --- a/examples/llmagent/.env +++ b/examples/llmagent/.env @@ -1 +1,5 @@ # Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name + diff --git a/examples/llmagent_with_branch_filtering/.env b/examples/llmagent_with_branch_filtering/.env index d47b675..dc79139 100644 --- a/examples/llmagent_with_branch_filtering/.env +++ b/examples/llmagent_with_branch_filtering/.env @@ -1 +1,4 @@ # Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/llmagent_with_cancel/.env b/examples/llmagent_with_cancel/.env index d47b675..dc79139 100644 --- a/examples/llmagent_with_cancel/.env +++ b/examples/llmagent_with_cancel/.env @@ -1 +1,4 @@ # Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/llmagent_with_custom_agent/.env b/examples/llmagent_with_custom_agent/.env index d47b675..dc79139 100644 --- a/examples/llmagent_with_custom_agent/.env +++ b/examples/llmagent_with_custom_agent/.env @@ -1 +1,4 @@ # Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/llmagent_with_custom_prompt/.env b/examples/llmagent_with_custom_prompt/.env index d47b675..dc79139 100644 --- a/examples/llmagent_with_custom_prompt/.env +++ b/examples/llmagent_with_custom_prompt/.env @@ -1 +1,4 @@ # Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/llmagent_with_human_in_the_loop/.env b/examples/llmagent_with_human_in_the_loop/.env index d47b675..dc79139 100644 --- a/examples/llmagent_with_human_in_the_loop/.env +++ b/examples/llmagent_with_human_in_the_loop/.env @@ -1 +1,4 @@ # Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/llmagent_with_max_history_messages/.env b/examples/llmagent_with_max_history_messages/.env index d47b675..dc79139 100644 --- a/examples/llmagent_with_max_history_messages/.env +++ b/examples/llmagent_with_max_history_messages/.env @@ -1 +1,4 @@ # Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/llmagent_with_model_create_fn/.env b/examples/llmagent_with_model_create_fn/.env index d47b675..dc79139 100644 --- a/examples/llmagent_with_model_create_fn/.env +++ b/examples/llmagent_with_model_create_fn/.env @@ -1 +1,4 @@ # Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/llmagent_with_parallal_tools/.env b/examples/llmagent_with_parallal_tools/.env index d47b675..dc79139 100644 --- a/examples/llmagent_with_parallal_tools/.env +++ b/examples/llmagent_with_parallal_tools/.env @@ -1 +1,4 @@ # Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/llmagent_with_schema/.env b/examples/llmagent_with_schema/.env index 65bab9c..0a57a17 100644 --- a/examples/llmagent_with_schema/.env +++ b/examples/llmagent_with_schema/.env @@ -1,2 +1,5 @@ # Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/llmagent_with_streaming_tool_complex/.env b/examples/llmagent_with_streaming_tool_complex/.env index 8b13789..0a57a17 100644 --- a/examples/llmagent_with_streaming_tool_complex/.env +++ b/examples/llmagent_with_streaming_tool_complex/.env @@ -1 +1,5 @@ +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/llmagent_with_streaming_tool_simple/.env b/examples/llmagent_with_streaming_tool_simple/.env index 8b13789..0a57a17 100644 --- a/examples/llmagent_with_streaming_tool_simple/.env +++ b/examples/llmagent_with_streaming_tool_simple/.env @@ -1 +1,5 @@ +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/llmagent_with_thinking/.env b/examples/llmagent_with_thinking/.env index 8b13789..0a57a17 100644 --- a/examples/llmagent_with_thinking/.env +++ b/examples/llmagent_with_thinking/.env @@ -1 +1,5 @@ +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/llmagent_with_timeline_filtering/.env b/examples/llmagent_with_timeline_filtering/.env index 8b13789..0a57a17 100644 --- a/examples/llmagent_with_timeline_filtering/.env +++ b/examples/llmagent_with_timeline_filtering/.env @@ -1 +1,5 @@ +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/llmagent_with_tool_prompt/.env b/examples/llmagent_with_tool_prompt/.env index 8b13789..0a57a17 100644 --- a/examples/llmagent_with_tool_prompt/.env +++ b/examples/llmagent_with_tool_prompt/.env @@ -1 +1,5 @@ +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/llmagent_with_user_history/.env b/examples/llmagent_with_user_history/.env index 8b13789..0a57a17 100644 --- a/examples/llmagent_with_user_history/.env +++ b/examples/llmagent_with_user_history/.env @@ -1 +1,5 @@ +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/mcp_tools/.env b/examples/mcp_tools/.env index 8b13789..0a57a17 100644 --- a/examples/mcp_tools/.env +++ b/examples/mcp_tools/.env @@ -1 +1,5 @@ +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/mem0_tools/.env b/examples/mem0_tools/.env new file mode 100644 index 0000000..dc79139 --- /dev/null +++ b/examples/mem0_tools/.env @@ -0,0 +1,4 @@ +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/mem_0/README.md b/examples/mem0_tools/README.md similarity index 97% rename from examples/mem_0/README.md rename to examples/mem0_tools/README.md index afac9e4..8eef052 100644 --- a/examples/mem_0/README.md +++ b/examples/mem0_tools/README.md @@ -25,10 +25,10 @@ personal_assistant (LlmAgent) 关键文件: -- [examples/mem_0/agent/agent.py](./agent/agent.py) -- [examples/mem_0/agent/config.py](./agent/config.py) -- [examples/mem_0/run_agent.py](./run_agent.py) -- `trpc_agent_sdk/tools/mem0_tool.py` +- [examples/mem0_tools/agent/agent.py](./agent/agent.py) +- [examples/mem0_tools/agent/config.py](./agent/config.py) +- [examples/mem0_tools/run_agent.py](./run_agent.py) +- `trpc_agent_sdk/tools/mem0_tools.py` ## 关键代码解释 @@ -79,7 +79,7 @@ pip3 install sentence-transformers qdrant-client ### 环境变量要求 -在 [examples/mem_0/.env](./.env) 中配置(或通过 `export`): +在 [examples/mem0_tools/.env](./.env) 中配置(或通过 `export`): - `TRPC_AGENT_API_KEY` - `TRPC_AGENT_BASE_URL` @@ -91,7 +91,7 @@ pip3 install sentence-transformers qdrant-client ### 运行命令 ```bash -cd examples/mem_0 +cd examples/mem0_tools python3 run_agent.py ``` @@ -369,4 +369,4 @@ AsyncMemoryClient 平台客户端参数 - [Mem0 Docs](https://docs.mem0.ai/introduction) - [Mem0 Examples](https://github.com/mem0ai/mem0/tree/main/examples) -- [tRPC-Agent Mem0 Tool](../../trpc_agent_ecosystem/tools/mem0_tool.py) +- [tRPC-Agent Mem0 Tool](../../trpc_agent_ecosystem/tools/mem0_tools.py) diff --git a/examples/mem_0/agent/__init__.py b/examples/mem0_tools/agent/__init__.py similarity index 100% rename from examples/mem_0/agent/__init__.py rename to examples/mem0_tools/agent/__init__.py diff --git a/examples/mem_0/agent/agent.py b/examples/mem0_tools/agent/agent.py similarity index 93% rename from examples/mem_0/agent/agent.py rename to examples/mem0_tools/agent/agent.py index 3457f44..af69d34 100644 --- a/examples/mem_0/agent/agent.py +++ b/examples/mem0_tools/agent/agent.py @@ -10,8 +10,8 @@ from trpc_agent_sdk.agents import LlmAgent from trpc_agent_sdk.models import LLMModel from trpc_agent_sdk.models import OpenAIModel -from trpc_agent_sdk.tools.mem0_tool import SaveMemoryTool -from trpc_agent_sdk.tools.mem0_tool import SearchMemoryTool +from trpc_agent_sdk.tools.mem0_tools import SaveMemoryTool +from trpc_agent_sdk.tools.mem0_tools import SearchMemoryTool from .config import get_mem0_platform_config from .config import get_memory_config diff --git a/examples/mem_0/agent/config.py b/examples/mem0_tools/agent/config.py similarity index 100% rename from examples/mem_0/agent/config.py rename to examples/mem0_tools/agent/config.py diff --git a/examples/mem_0/agent/prompts.py b/examples/mem0_tools/agent/prompts.py similarity index 100% rename from examples/mem_0/agent/prompts.py rename to examples/mem0_tools/agent/prompts.py diff --git a/examples/mem_0/agent/tools.py b/examples/mem0_tools/agent/tools.py similarity index 100% rename from examples/mem_0/agent/tools.py rename to examples/mem0_tools/agent/tools.py diff --git a/examples/mem_0/images/mem0_ai.png b/examples/mem0_tools/images/mem0_ai.png similarity index 100% rename from examples/mem_0/images/mem0_ai.png rename to examples/mem0_tools/images/mem0_ai.png diff --git a/examples/mem_0/images/mem0_plat.png b/examples/mem0_tools/images/mem0_plat.png similarity index 100% rename from examples/mem_0/images/mem0_plat.png rename to examples/mem0_tools/images/mem0_plat.png diff --git a/examples/mem_0/images/mem0_result.png b/examples/mem0_tools/images/mem0_result.png similarity index 100% rename from examples/mem_0/images/mem0_result.png rename to examples/mem0_tools/images/mem0_result.png diff --git a/examples/mem_0/images/qdrant_dashboard.png b/examples/mem0_tools/images/qdrant_dashboard.png similarity index 100% rename from examples/mem_0/images/qdrant_dashboard.png rename to examples/mem0_tools/images/qdrant_dashboard.png diff --git a/examples/mem_0/images/qdrant_mem.png b/examples/mem0_tools/images/qdrant_mem.png similarity index 100% rename from examples/mem_0/images/qdrant_mem.png rename to examples/mem0_tools/images/qdrant_mem.png diff --git a/examples/mem_0/run_agent.py b/examples/mem0_tools/run_agent.py similarity index 96% rename from examples/mem_0/run_agent.py rename to examples/mem0_tools/run_agent.py index 49b554f..afe9b15 100644 --- a/examples/mem_0/run_agent.py +++ b/examples/mem0_tools/run_agent.py @@ -17,7 +17,7 @@ load_dotenv() -async def run_mem_zero_agent(): +async def run_mem0_agent(): """Run the mem zero agent demo""" app_name = "memory_assistant" @@ -73,12 +73,12 @@ async def run_mem_zero_agent(): async def main(): - await run_mem_zero_agent() + await run_mem0_agent() # Sleep for 10 seconds, wait for user input print("Press Enter to continue...") await asyncio.sleep(10) print("Sleeping for 10 seconds...") - await run_mem_zero_agent() + await run_mem0_agent() if __name__ == "__main__": diff --git a/examples/mem_0/.env b/examples/mem_0/.env deleted file mode 100644 index 8b13789..0000000 --- a/examples/mem_0/.env +++ /dev/null @@ -1 +0,0 @@ - diff --git a/examples/memory_service_with_in_memory/.env b/examples/memory_service_with_in_memory/.env index 8b13789..0a57a17 100644 --- a/examples/memory_service_with_in_memory/.env +++ b/examples/memory_service_with_in_memory/.env @@ -1 +1,5 @@ +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/memory_service_with_mem0/.env b/examples/memory_service_with_mem0/.env index 8b13789..dc79139 100644 --- a/examples/memory_service_with_mem0/.env +++ b/examples/memory_service_with_mem0/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/memory_service_with_mempalace/.env b/examples/memory_service_with_mempalace/.env new file mode 100644 index 0000000..7abfc4d --- /dev/null +++ b/examples/memory_service_with_mempalace/.env @@ -0,0 +1,9 @@ +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name + + +# If you want to use the default MemPalace path, you can comment out the following two lines. +#MEMPALACE_PALACE_PATH=~/.mempalace +#MEMPALACE_KG_PATH=~/.mempalace/knowledge_graph.sqlite3 diff --git a/examples/memory_service_with_mempalace/README.md b/examples/memory_service_with_mempalace/README.md new file mode 100644 index 0000000..1318fec --- /dev/null +++ b/examples/memory_service_with_mempalace/README.md @@ -0,0 +1,253 @@ +# MemPalace Memory Service 使用指南 + +本示例演示如何在 `trpc-agent` 中使用 `MempalaceMemoryService` 实现跨 session 的长期记忆存储与检索。 + +MemPalace 是一个本地优先的记忆系统,底层使用 ChromaDB 存储 drawer 文本、metadata 和向量索引。当前示例会把会话中的可见文本事件写入 MemPalace,并通过 `load_memory` 工具在后续 session 中检索出来。 + +## 关键特性 + +- 使用 `MempalaceMemoryService` 接入本地 MemPalace。 +- 通过 `wing` 和 `room` 组织记忆。 +- 默认按 `save_key = {app}/{user}` 维度实现跨 session 检索。 +- 支持配置 MemPalace 存储路径。 +- 支持 TTL 后台定时清理过期 drawer。 +- 示例输出中会截断过长工具结果,避免 memory JSON 刷屏。 + +## 安装依赖 + +使用前需要安装本项目依赖和 MemPalace 可选依赖。 + +在项目根目录执行: + +```bash +git clone https://github.com/trpc-group/trpc-agent-python.git +cd trpc-agent-python +python3 -m venv .venv +source .venv/bin/activate +pip install -e ".[mempalace]" +``` + +如果你使用虚拟环境,请确保运行示例和执行 `mempalace search` 时使用的是同一个环境。 + +## 运行示例 + +在项目根目录执行: + +```bash +cd examples/memory_service_with_mempalace +python3 run_agent.py +``` + +示例会连续运行三轮,每轮包含多个不同 session: + +- 先询问是否记得姓名和喜欢的颜色。 +- 再告诉 agent:姓名是 Alice,喜欢的颜色是 blue。 +- 后续 session 再次询问时,agent 会通过 `load_memory` 检索长期记忆。 + +## 关键代码 + +`run_agent.py` 中创建 Memory Service: + +```python +memory_service_config = MemoryServiceConfig( + ttl=MemoryServiceConfig.create_ttl_config( + enable=True, + ttl_seconds=20, + cleanup_interval_seconds=20, + ), + enabled=True, +) + +memory_service = MempalaceMemoryService( + memory_service_config=memory_service_config, + wing="trpc-agent", + room="conversations", + store_only_model_visible=True, +) +``` + +这里的含义是: + +- `wing="trpc-agent"`:把示例记忆固定写入 `trpc-agent` 这个 wing。 +- `room="conversations"`:把普通对话记忆写入 `conversations` room。 +- `store_only_model_visible=True`:只存模型可见的事件。 +- `ttl_seconds=20`:超过 20 秒的记忆会被后台 cleanup 删除。 +- `cleanup_interval_seconds=20`:每 20 秒执行一次清理。 + +## MemPalace 层级映射 + +MemPalace 的主要存储层级是: + +```text +Palace + └── Wing + └── Room + └── Drawer +``` + +在当前示例中: + +```text +Palace = MempalaceConfig.palace_path +Wing = trpc-agent +Room = conversations +Drawer = 单条 Event 文本 +``` + +如果没有显式传入 `wing`,`MempalaceMemoryService` 会默认用 `session.save_key` 解析 wing。框架里的 `save_key` 通常是: + +```text +{app}/{user} +``` + +这样可以做到同一个 app/user 下跨 session 查询记忆。 + +## 指定存储路径 + +MemPalace 的默认存储路径来自 `MempalaceConfig().palace_path`,通常是: + +```text +~/.mempalace/palace +``` + +如果要指定路径,可以使用 MemPalace 自己支持的配置方式,例如环境变量: + +```bash +export MEMPALACE_PALACE_PATH=/path/to/palace +``` + +也可以使用 MemPalace 配置文件 `~/.mempalace/config.json` 指定: + +```json +{ + "palace_path": "/path/to/palace", + "collection_name": "mempalace_drawers" +} +``` + +注意:`/path/to/palace` 指的是 MemPalace 数据目录,也就是包含 `chroma.sqlite3` 的目录,不是某个单独文件。 + +## 使用 CLI 查询指定路径 + +如果代码里指定或配置了自定义 palace 路径,使用 `mempalace search` 查询时也必须指定同一个路径: + +```bash +mempalace --palace /path/to/palace search "user name" +``` + +如果还要限制到当前示例的 wing: + +```bash +mempalace --palace /path/to/palace search "user name" \ + --wing trpc-agent +``` + +如果还要限制到 room: + +```bash +mempalace --palace /path/to/palace search "user name" \ + --wing trpc-agent \ + --room conversations +``` + +如果没有指定自定义路径,也可以直接查询默认 palace: + +```bash +mempalace search "user name" --wing trpc-agent --room conversations +``` + +## 删除记忆 + +MemPalace CLI 当前没有直接提供按 `wing` 或 `wing + room` 删除的命令。框架里已经在 `MempalaceMemoryService` 提供了删除方法: + +```python +await memory_service.delete_memory(wing="trpc-agent") +await memory_service.delete_memory(wing="trpc-agent", room="conversations") +``` + +如果需要用命令行删除,可以写一个小脚本直接调用 MemPalace collection 的 `delete(where=...)`。 + +## 运行结果分析 + +### 1. 首次查询没有记忆 + +第一次运行开始时: + +```text +load_memory({'query': 'user name'}) +Tool Result: {"memories": []} +``` + +说明开始时 MemPalace 中没有可召回的姓名记忆,agent 正确回答“不知道用户姓名”。 + +### 2. 写入姓名后可以跨 session 召回 + +当用户输入: + +```text +Hello! My name is Alice. Please remember my name. +``` + +后续再问: + +```text +Now, do you still remember my name? +``` + +工具结果中出现: + +```text +[2026-05-07T20:19:27.141759] user: +Hello! My name is Alice. Please remember my name. +``` + +agent 随后回答能记得姓名是 Alice。说明 MemPalace 已经把前一个 session 的用户消息写入,并在后续 session 中成功检索。 + +### 3. favorite color 也可以被召回 + +当用户输入: + +```text +Hello! My favorite color is blue. Please remember my favorite color. +``` + +后续查询 `favorite color` 时,工具结果能召回对应文本,agent 回答喜欢的颜色是 blue。说明语义检索和跨 session 记忆对这个场景有效。 + +### 4. TTL 清理生效 + +输出中可以看到多次 cleanup 日志: + +```text +MemPalace cleanup: deleted 195 expired memories +MemPalace cleanup: deleted 13 expired memories +MemPalace cleanup: deleted 5 expired memories +``` + +这说明示例中配置的 TTL 清理任务已经运行,并删除了超过 `ttl_seconds=20` 的过期记忆。 + +第三轮开始时: + +```text +load_memory({'query': 'user name'}) +Tool Result: {"memories": []} +``` + +这是符合预期的,因为 `main()` 在第二轮后等待了 30 秒,而 TTL 只有 20 秒,旧记忆已经被后台清理。 + +### 5. 结果中的现象说明 + +输出里有时 agent 会说“我没有主动保存记忆的工具”。这是模型对工具能力的表述不够准确。实际框架是在每轮结束后由 `Runner` 调用 `memory_service.store_session()` 自动写入记忆,并不是通过一个显式的 `save_memory` 工具保存。 + +因此判断是否符合要求时,应看后续 `load_memory` 是否能召回历史内容,而不是看模型是否声称自己调用了保存工具。 + +## 结论 + +`out.txt` 体现了本示例的核心目标: + +- 初始无记忆时,查询返回空。 +- 用户提供姓名或偏好后,后续 session 可以通过 MemPalace 召回。 +- 记忆按 `wing=trpc-agent`、`room=conversations` 写入。 +- TTL 到期后,旧记忆会被定时清理。 +- CLI 查询自定义路径时,需要使用 `mempalace --palace /path/to/palace search "query"`。 + +所以该运行结果符合 MemPalace memory service 示例的预期。 diff --git a/examples/memory_service_with_mempalace/agent/__init__.py b/examples/memory_service_with_mempalace/agent/__init__.py new file mode 100644 index 0000000..bc6e483 --- /dev/null +++ b/examples/memory_service_with_mempalace/agent/__init__.py @@ -0,0 +1,5 @@ +# Tencent is pleased to support the open source community by making tRPC-Agent-Python available. +# +# Copyright (C) 2026 Tencent. All rights reserved. +# +# tRPC-Agent-Python is licensed under Apache-2.0. diff --git a/examples/memory_service_with_mempalace/agent/agent.py b/examples/memory_service_with_mempalace/agent/agent.py new file mode 100644 index 0000000..7990c93 --- /dev/null +++ b/examples/memory_service_with_mempalace/agent/agent.py @@ -0,0 +1,47 @@ +# Tencent is pleased to support the open source community by making tRPC-Agent-Python available. +# +# Copyright (C) 2026 Tencent. All rights reserved. +# +# tRPC-Agent-Python is licensed under Apache-2.0. +""" Agent module""" + +from trpc_agent_sdk.agents import LlmAgent +from trpc_agent_sdk.models import LLMModel +from trpc_agent_sdk.models import OpenAIModel +from trpc_agent_sdk.tools import FunctionTool +from trpc_agent_sdk.tools import load_memory_tool +from trpc_agent_sdk.types import GenerateContentConfig +from trpc_agent_sdk.types import HttpOptions + +from .config import get_model_config +from .prompts import INSTRUCTION +from .tools import get_weather_report + + +def _create_model() -> LLMModel: + """ Create a model""" + api_key, url, model_name = get_model_config() + model = OpenAIModel(model_name=model_name, api_key=api_key, base_url=url) + return model + + + +def create_agent() -> LlmAgent: + """ Create an agent""" + generate_content_config = GenerateContentConfig( + http_options=HttpOptions(extra_body={"chat_template_kwargs": { + "enable_thinking": False + }}), + ) + agent = LlmAgent( + name="assistant", + description="A helpful assistant for conversation", + model=_create_model(), # You can change this to your preferred model + instruction=INSTRUCTION, + tools=[FunctionTool(get_weather_report), load_memory_tool], + generate_content_config=generate_content_config, + ) + return agent + + +root_agent = create_agent() diff --git a/examples/memory_service_with_mempalace/agent/config.py b/examples/memory_service_with_mempalace/agent/config.py new file mode 100644 index 0000000..0a165f5 --- /dev/null +++ b/examples/memory_service_with_mempalace/agent/config.py @@ -0,0 +1,61 @@ +# Tencent is pleased to support the open source community by making tRPC-Agent-Python available. +# +# Copyright (C) 2026 Tencent. All rights reserved. +# +# tRPC-Agent-Python is licensed under Apache-2.0. +""" Agent config module""" + +import os + +from mem0.configs.base import MemoryConfig + + +def get_model_config() -> tuple[str, str, str]: + """Get model config from environment variables""" + api_key = os.getenv('TRPC_AGENT_API_KEY', '') + url = os.getenv('TRPC_AGENT_BASE_URL', '') + model_name = os.getenv('TRPC_AGENT_MODEL_NAME', '') + if not api_key or not url or not model_name: + raise ValueError('''TRPC_AGENT_API_KEY, TRPC_AGENT_BASE_URL, + and TRPC_AGENT_MODEL_NAME must be set in environment variables''') + return api_key, url, model_name + + +def get_memory_config() -> MemoryConfig: + """Get memory config from environment variables""" + memory_config = { + "vector_store": { + "provider": "qdrant", + "config": { + "host": "localhost", + "port": 6333, + "collection_name": "mem0", + } + }, + "llm": { + "provider": "deepseek", + "config": { + "model": os.getenv('TRPC_AGENT_MODEL_NAME', ''), + "api_key": os.getenv('TRPC_AGENT_API_KEY', ''), + "deepseek_base_url": os.getenv('TRPC_AGENT_BASE_URL', ''), + "temperature": 0.2, + "max_tokens": 2000, + } + }, + "embedder": { + "provider": "huggingface", + "config": { + "model": "multi-qa-MiniLM-L6-cos-v1" # Runs locally; no API key required + # "model": "text-embedding-3-small" # Requires API key + } + } + } + return MemoryConfig(**memory_config) + + +def get_mem0_platform_config() -> dict: + """Get mem0 platform config from environment variables""" + return { + "api_key": os.getenv('MEM0_API_KEY', ''), + "host": os.getenv('MEM0_BASE_URL', ''), + } diff --git a/examples/memory_service_with_mempalace/agent/prompts.py b/examples/memory_service_with_mempalace/agent/prompts.py new file mode 100644 index 0000000..ed0cdcc --- /dev/null +++ b/examples/memory_service_with_mempalace/agent/prompts.py @@ -0,0 +1,8 @@ +# Tencent is pleased to support the open source community by making tRPC-Agent-Python available. +# +# Copyright (C) 2026 Tencent. All rights reserved. +# +# tRPC-Agent-Python is licensed under Apache-2.0. +""" prompts for agent""" + +INSTRUCTION = "You are a helpful assistant for intelligence conversation." diff --git a/examples/memory_service_with_mempalace/agent/tools.py b/examples/memory_service_with_mempalace/agent/tools.py new file mode 100644 index 0000000..e469629 --- /dev/null +++ b/examples/memory_service_with_mempalace/agent/tools.py @@ -0,0 +1,30 @@ +# Tencent is pleased to support the open source community by making tRPC-Agent-Python available. +# +# Copyright (C) 2026 Tencent. All rights reserved. +# +# tRPC-Agent-Python is licensed under Apache-2.0. +""" Tools for the agent. """ + + +def get_weather_report(city: str) -> dict: + """Retrieves the current weather report for a specified city. + + Returns: + dict: A dictionary containing the weather information with a 'status' key ('success' or 'error') + and a 'report' key with the weather details if successful, or an 'error_message' if + an error occurred. + """ + if city.lower() == "london": + return { + "status": + "success", + "report": ("The current weather in London is cloudy with a temperature of " + "18 degrees Celsius and a chance of rain."), + } + elif city.lower() == "paris": + return { + "status": "success", + "report": "The weather in Paris is sunny with a temperature of 25 degrees Celsius.", + } + else: + return {"status": "error", "error_message": f"Weather information for '{city}' is not available."} diff --git a/examples/memory_service_with_mempalace/run_agent.py b/examples/memory_service_with_mempalace/run_agent.py new file mode 100644 index 0000000..6dbe3cc --- /dev/null +++ b/examples/memory_service_with_mempalace/run_agent.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 + +# Tencent is pleased to support the open source community by making tRPC-Agent-Python available. +# +# Copyright (C) 2026 Tencent. All rights reserved. +# +# tRPC-Agent-Python is licensed under Apache-2.0. +"""Run the weather query agent demo""" +import asyncio +import sys +from pathlib import Path + +from dotenv import load_dotenv +from trpc_agent_sdk.context import AgentContext +from trpc_agent_sdk.memory import MemoryServiceConfig +from trpc_agent_sdk.memory.mempalace_memory_service import MempalaceMemoryService +from trpc_agent_sdk.runners import Runner +from trpc_agent_sdk.sessions import InMemorySessionService +from trpc_agent_sdk.types import Content +from trpc_agent_sdk.types import Part + +# Load environment variables from the .env file +load_dotenv() + +sys.path.append(str(Path(__file__).parent)) + + +def _truncate_tool_response(response, max_length: int = 256) -> str: + """Limit verbose tool responses in demo output.""" + text = str(response) + if len(text) <= max_length: + return text + return text[:max_length] + + +def create_memory_service(): + """Create session service""" + + memory_service_config = MemoryServiceConfig( + ttl=MemoryServiceConfig.create_ttl_config(enable=True, ttl_seconds=20, cleanup_interval_seconds=20), + enabled=True, + ) + memory_service = MempalaceMemoryService( + memory_service_config=memory_service_config, + wing="trpc-agent", + room="conversations", + store_only_model_visible=True, + ) + return memory_service + + +async def run_weather_agent(memory_service: MempalaceMemoryService): + """Run the weather query agent demo""" + + app_name = "weather_agent_demo" + + from agent.agent import root_agent + session_service = InMemorySessionService() + runner = Runner(app_name=app_name, agent=root_agent, session_service=session_service, memory_service=memory_service) + + user_id = "mempalace_memory_user" + current_session_id = "mempalace_memory_session" + + # Demo query list + demo_queries = [ + "Do you remember my name?", + "Do you remember my favorite color?", + "What is the weather like in paris?", + "Hello! My name is Alice. Please remember my name.", + "Now, do you still remember my name?", + "Hello! My favorite color is blue. Please remember my favorite color.", + "Now, do you still remember my favorite color?", + ] + + for index, query in enumerate(demo_queries): + # Use a new session for each query + + user_content = Content(parts=[Part.from_text(text=query)]) + + print("🤖 Assistant: ", end="", flush=True) + agent_context = AgentContext() + session_id = f"{current_session_id}_{index}" + # set_mem0_filters(agent_context, {"session_id": session_id}) + async for event in runner.run_async(agent_context=agent_context, + user_id=user_id, + session_id=session_id, + new_message=user_content): + # Check if event.content exists + if not event.content or not event.content.parts: + continue + + if event.partial: + for part in event.content.parts: + if part.text: + print(part.text, end="", flush=True) + continue + + for part in event.content.parts: + # Skip the reasoning part; the output is already generated when partial=True + if part.thought: + continue + if part.function_call: + print(f"\n🔧 [Invoke Tool: {part.function_call.name}({part.function_call.args})]") + elif part.function_response: + print(f"📊 [Tool Result: {_truncate_tool_response(part.function_response.response)}]") + # Uncomment to get the full text output of the LLM + # elif part.text: + # print(f"\n✅ {part.text}") + + print("\n" + "-" * 40) + + +async def main(): + memory_service = create_memory_service() + print("=" * 60) + print("First run") + print("=" * 60) + await run_weather_agent(memory_service) + await asyncio.sleep(2) + print("=" * 60) + print("Second run") + print("=" * 60) + await run_weather_agent(memory_service) + await asyncio.sleep(30) + print("=" * 60) + print("Third run") + print("=" * 60) + await run_weather_agent(memory_service) + # await memory_service.delete_memory(wing="trpc-agent", room="conversations") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/memory_service_with_redis/.env b/examples/memory_service_with_redis/.env index 8b13789..dc79139 100644 --- a/examples/memory_service_with_redis/.env +++ b/examples/memory_service_with_redis/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/memory_service_with_sql/.env b/examples/memory_service_with_sql/.env index 8b13789..dc79139 100644 --- a/examples/memory_service_with_sql/.env +++ b/examples/memory_service_with_sql/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/mempalace_tools/.env b/examples/mempalace_tools/.env new file mode 100644 index 0000000..f360195 --- /dev/null +++ b/examples/mempalace_tools/.env @@ -0,0 +1,13 @@ +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name + +# MemPalace config, you can change the wing and room to your own. +MEMPALACE_WING=personal_assistant_alice # default is personal_assistant_alice +MEMPALACE_ROOM=user_profile # default is user_profile +# Optional. Use the same path with `mempalace --palace ... search`. +# If you want to use the default MemPalace path, you can comment out the following two lines. +MEMPALACE_PALACE_PATH=~/.mempalace +MEMPALACE_KG_PATH=~/.mempalace/knowledge_graph.sqlite3 + diff --git a/examples/mempalace_tools/README.md b/examples/mempalace_tools/README.md new file mode 100644 index 0000000..6b04f9d --- /dev/null +++ b/examples/mempalace_tools/README.md @@ -0,0 +1,180 @@ +# MemPalace 工具集成示例 + +本示例演示如何把 `trpc_agent_sdk.tools.mempalace_tool` 中的工具直接注入 `LlmAgent`,让模型在对话中主动调用 MemPalace 完成长期记忆检索、写入、日记和知识图谱操作。 + +> 如果希望框架自动保存会话记忆,推荐使用 `MempalaceMemoryService`。本目录展示的是“工具式集成”:是否搜索、写入什么内容,都由模型通过工具调用完成。 + +## 关键特性 + +- **完整工具覆盖**:示例注入 `mempalace_tool.py` 中的 search、add drawer、diary、KG 全部工具。 +- **本地持久化**:MemPalace 默认使用本地 palace 目录和 ChromaDB 保存向量与原文。 +- **可指定存储路径**:通过 `MEMPALACE_PALACE_PATH` 指定 palace 目录,便于和 CLI 查询同一份数据。 +- **跨 Session 记忆**:示例每轮使用不同 `session_id`,但 MemPalace 数据仍可被后续会话检索。 +- **自动清理测试数据**:运行前后会删除示例 `wing/room` 下的数据,避免影响下一次测试。 + +## Agent 结构 + +```text +personal_assistant (LlmAgent) +├── model: OpenAIModel (config from .env) +└── tools: + ├── MempalaceSearchTool (mempalace_search) + ├── MempalaceAddDrawerTool (mempalace_add_drawer) + ├── MempalaceDiaryWriteTool (mempalace_diary_write) + ├── MempalaceDiaryReadTool (mempalace_diary_read) + ├── MempalaceKGAddTool (mempalace_kg_add) + ├── MempalaceKGQueryTool (mempalace_kg_query) + ├── MempalaceKGInvalidateTool (mempalace_kg_invalidate) + └── MempalaceKGTimelineTool (mempalace_kg_timeline) + └── backend: + └── MemPalace / ChromaDB local palace +``` + +关键文件: + +- [agent/agent.py](./agent/agent.py) +- [agent/config.py](./agent/config.py) +- [agent/prompts.py](./agent/prompts.py) +- [run_agent.py](./run_agent.py) +- `trpc_agent_sdk/tools/mempalace_tool.py` + +## 工具说明 + +| 工具类 | 工具名 | 作用 | 关键参数 | 示例触发场景 | +|---|---|---|---|---| +| `MempalaceSearchTool` | `mempalace_search` | 从 MemPalace 中按语义检索已保存的 drawer 内容。 | `query`、`limit`、`wing`、`room` | 用户问“你还记得我的名字吗?”时,先搜索 `user name`。 | +| `MempalaceAddDrawerTool` | `mempalace_add_drawer` | 将原文内容写入指定 `wing/room`,作为可检索的长期记忆。 | `wing`、`room`、`content`、`source_file` | 用户说“请记住我的名字是 Alice”时,写入用户画像房间。 | +| `MempalaceDiaryWriteTool` | `mempalace_diary_write` | 写入一条 agent 日记,适合记录运行观察、任务过程或阶段性总结。 | `entry`、`agent_name`、`topic`、`wing` | 用户要求“写一条今天测试工具的日记”。 | +| `MempalaceDiaryReadTool` | `mempalace_diary_read` | 读取指定 agent 最近的日记记录。 | `agent_name`、`last_n`、`wing` | 用户要求“读取最近几条日记”。 | +| `MempalaceKGAddTool` | `mempalace_kg_add` | 向知识图谱写入一条三元组事实,并可带有效期、置信度和来源。 | `subject`、`predicate`、`object`、`valid_from`、`valid_to`、`confidence` | 用户要求记录“Alice likes Italian food”。 | +| `MempalaceKGQueryTool` | `mempalace_kg_query` | 查询某个实体的知识图谱关系,支持按日期和方向过滤。 | `entity`、`as_of`、`direction` | 用户要求“查询 Alice 相关事实”。 | +| `MempalaceKGTimelineTool` | `mempalace_kg_timeline` | 按时间线读取知识图谱事实,可限定某个实体。 | `entity` | 用户要求“展示 Alice 的知识图谱时间线”。 | +| `MempalaceKGInvalidateTool` | `mempalace_kg_invalidate` | 将一条当前事实标记为失效,用于表达事实变化,而不是直接删除历史。 | `subject`、`predicate`、`object`、`ended` | 用户要求“把 Alice likes Italian food 标记为今天结束”。 | + +## 安装 + +```bash +git clone https://github.com/trpc-group/trpc-agent-python.git +cd trpc-agent-python +python3 -m venv .venv +source .venv/bin/activate + +pip3 install -e . +pip3 install mempalace +``` + +如果你的 MemPalace 安装需要额外向量依赖,请按 MemPalace 官方说明补装对应 embedding 或 Chroma 依赖。 + +## 环境变量 + +在 `examples/mempalace_tools/.env` 中配置,或通过 `export` 设置: + +```bash +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=https://api.example.com/v1 +TRPC_AGENT_MODEL_NAME=your-model-name + +# Optional. If omitted, MemPalace uses its default palace path. +MEMPALACE_PALACE_PATH=/tmp/trpc-agent-mempalace-demo +MEMPALACE_KG_PATH= +MEMPALACE_WING=personal_assistant_alice +MEMPALACE_ROOM=user_profile +``` + +`wing` 和 `room` 是本示例给 MemPalace 的固定存储范围: + +- `wing`:建议映射到应用或用户级作用域,例如 `app/user`、`personal_assistant_alice`。 +- `room`:建议映射到记忆主题,例如 `user_profile`、`preferences`、`work_notes`。 + +## 运行 + +```bash +cd examples/mempalace_tools +python3 run_agent.py +``` + +示例分三个阶段执行。每条消息都会使用新的 `session_id`,用于验证不同 session 之间仍能通过 MemPalace 读到之前写入的数据。 + +第一阶段写入数据并立即用新 session 查询: + +```text +Use mempalace_search to check whether you remember my name. +Use mempalace_add_drawer to remember that my name is Alice. +Use mempalace_add_drawer to remember that my favorite food is Italian food. +Use mempalace_search to recall my name and favorite food. +Use mempalace_diary_write to write a diary entry... +Use mempalace_diary_read to read the latest diary entries. +Use mempalace_kg_add to add this fact: Alice likes Italian food. +Use mempalace_kg_query to query facts about Alice. +Use mempalace_kg_timeline to show Alice's knowledge graph timeline. +``` + +第二阶段只读取,不再写入,用于验证数据已经落盘,并且新的 session 仍能读到上一阶段的数据: + +```text +Use mempalace_search to recall my name and favorite food from the previous sessions. +Use mempalace_diary_read to read the latest diary entries from the previous sessions. +Use mempalace_kg_query to query facts about Alice from the previous sessions. +Use mempalace_kg_timeline to show Alice's knowledge graph timeline from the previous sessions. +``` + +第三阶段单独测试知识图谱失效能力。`mempalace_kg_invalidate` 会改变事实的当前状态,所以放在持久化读取验证之后执行,避免影响第二阶段判断: + +```text +Use mempalace_kg_invalidate to mark the fact Alice likes Italian food as ended today. +Use mempalace_kg_query to query facts about Alice again after invalidation. +``` + +运行时可以从日志看到: + +- 查询类问题会触发 `mempalace_search`。 +- 用户要求记住稳定信息时会触发 `mempalace_add_drawer`。 +- 日记类问题会触发 `mempalace_diary_write` / `mempalace_diary_read`。 +- 知识图谱类问题会触发 `mempalace_kg_add` / `mempalace_kg_query` / `mempalace_kg_timeline` / `mempalace_kg_invalidate`。 +- 每条消息都会换新的 `session_id`,但仍能从同一 MemPalace palace 中检索到之前写入的内容。 +- 第二阶段只读不写,用于验证 MemPalace 数据已经落盘。 +- 脚本开始和结束都会清理示例数据:drawer/diary 按 `MEMPALACE_WING`、`MEMPALACE_ROOM` 删除;KG 文件只会在设置了 `MEMPALACE_KG_PATH` 或 `MEMPALACE_PALACE_PATH` 时清理。 + +## 运行结果分析 + +以下分析基于 [out.txt](./out.txt) 中的实际输出。 + +| 阶段 | 验证目标 | 实际结果 | 是否符合预期 | +|---|---|---|---| +| 启动清理 | 运行前清理历史测试数据,避免影响本次结果。 | 首行出现 `Failed to clean MemPalace demo data: ~/.mempalace`,原因是首次运行时 palace 目录还不存在。 | 符合预期。首次运行没有历史数据可清理,不影响后续写入和查询。 | +| 第一阶段:首次搜索 | 测试 `mempalace_search` 在无记忆时的行为。 | 搜索 `name` 返回 `No palace found`,说明还没有已初始化/已写入的数据。 | 符合预期。此时尚未写入任何 drawer。 | +| 第一阶段:写入 drawer | 测试 `mempalace_add_drawer` 能写入用户画像。 | 分别写入 `User's name is Alice.` 和 `My favorite food is Italian food.`,工具返回 `success=True` 和对应 `drawer_id`。 | 符合预期。两个长期记忆都写入到 `personal_assistant_alice/user_profile`。 | +| 第一阶段:搜索 drawer | 测试不同 session 下能立即检索刚写入的 drawer。 | 搜索 `name favorite food` 返回 2 条结果,包含姓名和喜欢的食物。 | 符合预期。说明 drawer 写入后可以被语义检索命中。 | +| 第一阶段:写入日记 | 测试 `mempalace_diary_write`。 | 写入 `Alice tested the MemPalace tools example today.`,返回 `success=True` 和 `entry_id`。 | 符合预期。日记写入成功,并使用了配置的 `wing`。 | +| 第一阶段:读取日记 | 测试 `mempalace_diary_read`。 | 读取到 1 条日记,内容与刚写入的 entry 一致。 | 符合预期。说明 diary 写入和读取链路正常。 | +| 第一阶段:写入 KG 事实 | 测试 `mempalace_kg_add`。 | 写入 `Alice -> likes -> Italian food`,返回 `success=True` 和 `triple_id`。 | 符合预期。知识图谱三元组事实写入成功。 | +| 第一阶段:查询 KG 事实 | 测试 `mempalace_kg_query`。 | 查询 `Alice` 返回 1 条 outgoing fact:`Alice likes Italian food`,`current=True`。 | 符合预期。说明 KG 查询能按实体查到刚写入的事实。 | +| 第一阶段:KG 时间线 | 测试 `mempalace_kg_timeline`。 | 查询 `Alice` 的 timeline 返回同一条事实,`current=True`。 | 符合预期。说明时间线能展示实体相关事实。 | +| 第二阶段:跨 session 搜索 drawer | 验证只读阶段能读到上一阶段写入的数据。 | 使用新的 `session_id` 搜索,仍返回姓名和喜欢的食物 2 条结果。 | 符合预期。说明数据不依赖当前 session 内存,而是已经落到 MemPalace。 | +| 第二阶段:跨 session 读取日记 | 验证 diary 数据可跨 session 读取。 | 仍能读取到上一阶段写入的 1 条日记。 | 符合预期。说明 diary 数据持久化成功。 | +| 第二阶段:跨 session 查询 KG | 验证 KG 数据可跨 session 读取。 | 查询 `Alice` 仍返回 `Alice likes Italian food`,且 `current=True`。 | 符合预期。说明 KG 数据持久化成功,且在 invalidate 前仍是当前事实。 | +| 第三阶段:失效 KG 事实 | 测试 `mempalace_kg_invalidate` 的语义。 | 对 `Alice -> likes -> Italian food` 执行 invalidate,返回 `success=True`,`ended=2026-05-09`。 | 符合预期。invalidate 不删除事实,而是设置失效日期。 | +| 第三阶段:失效后查询 | 验证失效后的事实状态。 | 再次查询 `Alice`,事实仍存在,但 `valid_to=2026-05-09`,`current=False`。 | 符合预期。说明 KG 保留历史事实,同时标记其不再是当前事实。 | +| 结束清理 | 验证测试数据不会影响下次运行。 | 输出 `Cleaned MemPalace demo drawers: 3`,并删除 `knowledge_graph.sqlite3` 及 `-wal/-shm` 文件。 | 符合预期。drawer、diary 和 KG 文件都被清理。 | + +整体结论:`out.txt` 的结果符合本示例预期。它验证了每条消息使用不同 `session_id` 时,MemPalace 仍能从本地持久化数据中检索 drawer、diary 和 KG;同时也验证了 KG invalidate 的行为是“保留历史记录但标记为非当前事实”。 + +## 使用 CLI 查询 + +如果指定了 `MEMPALACE_PALACE_PATH`,需要用同一个路径查询: + +```bash +mempalace --palace /tmp/trpc-agent-mempalace-demo search "user name" +mempalace --palace /tmp/trpc-agent-mempalace-demo search "favorite food" +``` + +如果没有指定路径,CLI 需要使用 MemPalace 默认 palace 路径,或者先确认当前代码实际写入的路径。 + +## 和 MemoryService 的区别 + +| 方式 | 触发时机 | 适合场景 | +|---|---|---| +| `MempalaceMemoryService` | 框架自动在会话结束/记忆加载阶段处理 | 推荐用于稳定的长期记忆能力 | +| `mempalace_tool` | 模型主动调用工具搜索或写入 | 适合让模型显式控制“查什么、存什么” | + +本示例属于第二种方式,因此 prompt 中明确要求模型在需要回忆时调用 `mempalace_search`,在需要保存稳定事实时调用 `mempalace_add_drawer`,在需要日记或知识图谱能力时调用对应的 MemPalace 工具。 diff --git a/examples/mempalace_tools/agent/__init__.py b/examples/mempalace_tools/agent/__init__.py new file mode 100644 index 0000000..bc6e483 --- /dev/null +++ b/examples/mempalace_tools/agent/__init__.py @@ -0,0 +1,5 @@ +# Tencent is pleased to support the open source community by making tRPC-Agent-Python available. +# +# Copyright (C) 2026 Tencent. All rights reserved. +# +# tRPC-Agent-Python is licensed under Apache-2.0. diff --git a/examples/mempalace_tools/agent/agent.py b/examples/mempalace_tools/agent/agent.py new file mode 100644 index 0000000..5a7c1ed --- /dev/null +++ b/examples/mempalace_tools/agent/agent.py @@ -0,0 +1,60 @@ +# Tencent is pleased to support the open source community by making tRPC-Agent-Python available. +# +# Copyright (C) 2026 Tencent. All rights reserved. +# +# tRPC-Agent-Python is licensed under Apache-2.0. +""" Agent module""" + +from trpc_agent_sdk.agents import LlmAgent +from trpc_agent_sdk.models import LLMModel +from trpc_agent_sdk.models import OpenAIModel +from trpc_agent_sdk.tools.mempalace_tool import MempalaceAddDrawerTool +from trpc_agent_sdk.tools.mempalace_tool import MempalaceDiaryReadTool +from trpc_agent_sdk.tools.mempalace_tool import MempalaceDiaryWriteTool +from trpc_agent_sdk.tools.mempalace_tool import MempalaceKGAddTool +from trpc_agent_sdk.tools.mempalace_tool import MempalaceKGInvalidateTool +from trpc_agent_sdk.tools.mempalace_tool import MempalaceKGQueryTool +from trpc_agent_sdk.tools.mempalace_tool import MempalaceKGTimelineTool +from trpc_agent_sdk.tools.mempalace_tool import MempalaceSearchTool + +from .config import get_mempalace_config +from .config import get_model_config +from .prompts import build_instruction + + +def _create_model() -> LLMModel: + """ Create a model""" + api_key, url, model_name = get_model_config() + model = OpenAIModel(model_name=model_name, api_key=api_key, base_url=url) + return model + + +def create_agent() -> LlmAgent: + """Create an agent with MemPalace tools.""" + mempalace_config = get_mempalace_config() + palace_path = mempalace_config["palace_path"] + kg_path = mempalace_config["kg_path"] + tools = [ + MempalaceSearchTool(palace_path=palace_path), + MempalaceAddDrawerTool(palace_path=palace_path), + MempalaceDiaryWriteTool(palace_path=palace_path), + MempalaceDiaryReadTool(palace_path=palace_path), + MempalaceKGQueryTool(palace_path=palace_path, kg_path=kg_path), + MempalaceKGAddTool(palace_path=palace_path, kg_path=kg_path), + MempalaceKGInvalidateTool(palace_path=palace_path, kg_path=kg_path), + MempalaceKGTimelineTool(palace_path=palace_path, kg_path=kg_path), + ] + + return LlmAgent( + name="personal_assistant", + description="A personal assistant that remembers user preferences and past interactions", + model=_create_model(), + instruction=build_instruction( + wing=mempalace_config["wing"], + room=mempalace_config["room"], + ), + tools=tools, + ) + + +root_agent = create_agent() diff --git a/examples/mempalace_tools/agent/config.py b/examples/mempalace_tools/agent/config.py new file mode 100644 index 0000000..b741eab --- /dev/null +++ b/examples/mempalace_tools/agent/config.py @@ -0,0 +1,29 @@ +# Tencent is pleased to support the open source community by making tRPC-Agent-Python available. +# +# Copyright (C) 2026 Tencent. All rights reserved. +# +# tRPC-Agent-Python is licensed under Apache-2.0. +""" Agent config module""" + +import os + + +def get_model_config() -> tuple[str, str, str]: + """Get model config from environment variables""" + api_key = os.getenv('TRPC_AGENT_API_KEY', '') + url = os.getenv('TRPC_AGENT_BASE_URL', '') + model_name = os.getenv('TRPC_AGENT_MODEL_NAME', '') + if not api_key or not url or not model_name: + raise ValueError('''TRPC_AGENT_API_KEY, TRPC_AGENT_BASE_URL, + and TRPC_AGENT_MODEL_NAME must be set in environment variables''') + return api_key, url, model_name + + +def get_mempalace_config() -> dict: + """Get MemPalace tool configuration from environment variables.""" + return { + "palace_path": os.getenv("MEMPALACE_PALACE_PATH") or None, + "kg_path": os.getenv("MEMPALACE_KG_PATH") or None, + "wing": os.getenv("MEMPALACE_WING", "personal_assistant_alice"), + "room": os.getenv("MEMPALACE_ROOM", "user_profile"), + } diff --git a/examples/mempalace_tools/agent/prompts.py b/examples/mempalace_tools/agent/prompts.py new file mode 100644 index 0000000..737dcea --- /dev/null +++ b/examples/mempalace_tools/agent/prompts.py @@ -0,0 +1,26 @@ +# Tencent is pleased to support the open source community by making tRPC-Agent-Python available. +# +# Copyright (C) 2026 Tencent. All rights reserved. +# +# tRPC-Agent-Python is licensed under Apache-2.0. +""" prompts for agent""" + +def build_instruction(*, wing: str, room: str) -> str: + """Build the agent instruction with the configured MemPalace scope.""" + return f""" +You are a helpful personal assistant with MemPalace memory capabilities. + +Use MemPalace with this fixed scope: +- wing: {wing} +- room: {room} + +Memory policy: +- Before answering questions about remembered user information, call mempalace_search with a concise query. +- When the user tells you stable personal information or asks you to remember something, call mempalace_add_drawer. +- When the user asks you to write or read an agent diary, call mempalace_diary_write or mempalace_diary_read with the configured wing above. +- When the user asks you to add, query, invalidate, or list knowledge graph facts, call the matching KG tool: + mempalace_kg_add, mempalace_kg_query, mempalace_kg_invalidate, or mempalace_kg_timeline. +- Store only useful long-term facts. Do not store temporary tool results or implementation details. +- For mempalace_add_drawer, use the configured wing and room above, and write concise verbatim content. +- Personalize your final answer using the memory you retrieved or stored. +""" diff --git a/examples/mempalace_tools/agent/tools.py b/examples/mempalace_tools/agent/tools.py new file mode 100644 index 0000000..5fea90d --- /dev/null +++ b/examples/mempalace_tools/agent/tools.py @@ -0,0 +1,6 @@ +# Tencent is pleased to support the open source community by making tRPC-Agent-Python available. +# +# Copyright (C) 2026 Tencent. All rights reserved. +# +# tRPC-Agent-Python is licensed under Apache-2.0. +""" Tools for the agent. """ diff --git a/examples/mempalace_tools/out.txt b/examples/mempalace_tools/out.txt new file mode 100644 index 0000000..b97e85f --- /dev/null +++ b/examples/mempalace_tools/out.txt @@ -0,0 +1,319 @@ +(venv) [root@VM-186-194-centos trpc-agent-python]# python3 examples/mempalace_tools/run_agent.py +⚠️ Failed to clean MemPalace demo data: ~/.mempalace +============================================================ +First phase: write memories and verify cross-session reads +============================================================ +🆔 Session ID: d9118bff... +📝 User: Use mempalace_search to check whether you remember my name. +🤖 Assistant: The user is asking me to check if I remember their name using mempalace_search. I should search for their name in the memory system. I'll use a concise query to search for name-related information. + +I need to use mempalace_search with: +- query: something related to "name" +- I should use the configured wing: personal_assistant_alice +- room: user_profile (though this is optional in the search function) + +Let me search for "name" to see if I have any stored information about the user's name. +🔧 [Invoke Tool: mempalace_search({'query': 'name', 'wing': 'personal_assistant_alice', 'room': 'user_profile'})] +No palace found at ~/.mempalace: ~/.mempalace +📊 [Tool Result: {'error': 'No palace found', 'hint': 'Run: mempalace init && mempalace mine '}] +The search returned an error saying "No palace found". This means the MemPalace hasn't been initialized yet, or there's no stored memory. So I don't currently have any memory of the user's name. + +I should let the user know that I don't currently have any memory of their name stored, and perhaps ask them to tell me their name so I can remember it.I searched my memory for your name, but I don't have any stored information about you yet — the memory palace hasn't been initialized with any user profile data. + +Would you like to tell me your name so I can remember it for future conversations? 🧠 +---------------------------------------- +🆔 Session ID: c3b2b9b1... +📝 User: Use mempalace_add_drawer to remember that my name is Alice. +🤖 Assistant: The user wants me to remember that their name is Alice using the mempalace_add_drawer function. According to my memory policy, when the user tells me stable personal information, I should call mempalace_add_drawer. + +I need to use: +- wing: personal_assistant_alice (as configured in my scope) +- room: user_profile (as configured in my scope) +- content: Something concise about their name being Alice + +Let me call the function with these parameters. +🔧 [Invoke Tool: mempalace_add_drawer({'wing': 'personal_assistant_alice', 'room': 'user_profile', 'content': "User's name is Alice."})] +📊 [Tool Result: {'success': True, 'drawer_id': 'drawer_personal_assistant_alice_user_profile_d9b5c67e3df06a7c9a67df63', 'wing': 'personal_assistant_alice', 'room': 'user_profile'}] +The memory has been successfully stored. I should confirm this to Alice.Got it, Alice! I've stored your name in my memory. I'll remember that from now on. Is there anything else you'd like me to remember about yourself? +---------------------------------------- +🆔 Session ID: 992cd70a... +📝 User: Use mempalace_add_drawer to remember that my favorite food is Italian food. +🤖 Assistant: The user wants me to remember that their favorite food is Italian food. I need to use mempalace_add_drawer to store this information. According to my instructions, I should use the configured wing and room: +- wing: personal_assistant_alice +- room: user_profile + +I need to write concise verbatim content. The content should be "My favorite food is Italian food" or something similar and clear. +🔧 [Invoke Tool: mempalace_add_drawer({'wing': 'personal_assistant_alice', 'room': 'user_profile', 'content': 'My favorite food is Italian food.'})] +📊 [Tool Result: {'success': True, 'drawer_id': 'drawer_personal_assistant_alice_user_profile_c99ac7087bcafd299a0eda14', 'wing': 'personal_assistant_alice', 'room': 'user_profile'}] +The memory was successfully added. I should confirm this to the user in a friendly way.Got it! I've saved that your favorite food is Italian food. 🍝 I'll remember this for future conversations. If you ever want me to recall or update this information, just let me know! +---------------------------------------- +🆔 Session ID: 65dfeda4... +📝 User: Use mempalace_search to recall my name and favorite food. +🤖 Assistant: The user is asking me to recall their name and favorite food using mempalace_search. I need to search for this information in the MemPalace memory system. I should use the configured scope which is: +- wing: personal_assistant_alice +- room: user_profile + +I'll search for information about the user's name and favorite food. I can make a search query that should capture both pieces of information.I'll search for your name and favorite food in my memory. +🔧 [Invoke Tool: mempalace_search({'query': 'name favorite food', 'wing': 'personal_assistant_alice', 'room': 'user_profile'})] +📊 [Tool Result: {'query': 'name favorite food', 'filters': {'wing': 'personal_assistant_alice', 'room': 'user_profile'}, 'total_before_filter': 2, 'results': [{'text': 'My favorite food is Italian food.', 'wing': 'personal_assistant_alice', 'room': 'user_profile', 'source_file': '?', 'created_at': '2026-05-09T10:38:22.357784', 'similarity': 0.625, 'distance': 0.3747, 'effective_distance': 0.3747, 'closet_boost': 0.0, 'matched_via': 'drawer', 'bm25_score': 1.566}, {'text': "User's name is Alice.", 'wing': 'personal_assistan... ] +I found the information in memory. The search returned two results: +1. User's name is Alice +2. My favorite food is Italian food + +I can now answer the user with this recalled information.I found your information in my memory! Here's what I recall: + +- **Your name:** Alice +- **Your favorite food:** Italian food + +These were stored in my memory on May 9, 2026. Is there anything else you'd like me to remember for you? +---------------------------------------- +🆔 Session ID: 49bb2724... +📝 User: Use mempalace_diary_write to write a diary entry: Alice tested the MemPalace tools example today. +🤖 Assistant: The user wants me to write a diary entry using mempalace_diary_write. Looking at the function parameters: +- entry (required): "Alice tested the MemPalace tools example today." +- agent_name (optional): Defaults to the current agent when omitted, which would be "personal_assistant" +- topic (optional): Default is "general" +- wing (optional): Default is "wing_" which would be "wing_personal_assistant" + +But wait, looking at the memory policy at the top, it says to use a specific wing: "personal_assistant_alice" and the configured wing above. Let me re-read... + +The memory policy says: +- "Use MemPalace with this fixed scope: + - wing: personal_assistant_alice + - room: user_profile" +- "When the user asks you to write or read an agent diary, call mempalace_diary_write or mempalace_diary_read with the configured wing above." + +So I should use wing: "personal_assistant_alice" for the diary write. + +The entry is clearly provided: "Alice tested the MemPalace tools example today." + +I have all the required parameters, so I can proceed with the function call. +🔧 [Invoke Tool: mempalace_diary_write({'entry': 'Alice tested the MemPalace tools example today.', 'wing': 'personal_assistant_alice'})] +📊 [Tool Result: {'success': True, 'entry_id': 'diary_personal_assistant_alice_20260509_103835213651_ab0ba2fb772a', 'agent': 'personal_assistant', 'topic': 'general'}] +The diary entry has been successfully written to MemPalace. The entry was stored under the configured wing "personal_assistant_alice" and has been assigned a unique entry ID. I should confirm this to the user.I've successfully written the diary entry to MemPalace. The entry "Alice tested the MemPalace tools example today." has been recorded under the **personal_assistant_alice** wing with entry ID `diary_personal_assistant_alice_20260509_103835213651_ab0ba2fb772a`. +---------------------------------------- +🆔 Session ID: c2118706... +📝 User: Use mempalace_diary_read to read the latest diary entries. +🤖 Assistant: The user wants me to read the latest diary entries using mempalace_diary_read. According to the memory policy, I should use the configured wing which is "personal_assistant_alice". The function has optional parameters: +- agent_name: defaults to the current agent when omitted +- last_n: default is 10 +- wing: optional wing filter + +Since the user just wants the latest entries without specifying any parameters, I'll call the function with just the wing parameter set to the configured wing "personal_assistant_alice", and let the other parameters use their defaults. +🔧 [Invoke Tool: mempalace_diary_read({'wing': 'personal_assistant_alice'})] +📊 [Tool Result: {'agent': 'personal_assistant', 'entries': [{'date': '2026-05-09', 'timestamp': '2026-05-09T10:38:35.213651', 'topic': 'general', 'content': 'Alice tested the MemPalace tools example today.'}], 'total': 1, 'showing': 1}] +The diary read returned 1 entry. I should present this information clearly to the user.Here are the latest diary entries for the personal_assistant agent: + +| Date | Topic | Content | +|------|-------|---------| +| 2026-05-09 | general | Alice tested the MemPalace tools example today. | + +There is currently **1 entry** in the diary. Would you like me to write a new entry or perform any other actions? +---------------------------------------- +🆔 Session ID: 22cbd266... +📝 User: Use mempalace_kg_add to add this fact: Alice likes Italian food. +🤖 Assistant: The user wants me to add a fact to the knowledge graph. Let me break down what they want: + +- Subject: Alice +- Predicate: likes (or a similar relationship type) +- Object: Italian food + +I should use the mempalace_kg_add function with these parameters. The required parameters are: +- subject: "Alice" +- predicate: "likes" +- object: "Italian food" + +The optional parameters like confidence, source_file, source_drawer_id, valid_from, valid_to are not specified, so I won't include them.I'll add that fact to the MemPalace knowledge graph for you. +🔧 [Invoke Tool: mempalace_kg_add({'subject': 'Alice', 'predicate': 'likes', 'object': 'Italian food'})] +📊 [Tool Result: {'success': True, 'triple_id': 't_alice_likes_italian_food_a3f0581e2437', 'fact': 'Alice -> likes -> Italian food'}] +The fact has been successfully added to the knowledge graph. The triple was created with ID "t_alice_likes_italian_food_a3f0581e2437" and represents the relationship "Alice -> likes -> Italian food". I should confirm this success to the user.I've successfully added that fact to the MemPalace knowledge graph: + +**Fact added:** Alice → likes → Italian food + +The relationship has been stored and can now be queried using the knowledge graph tools. +---------------------------------------- +🆔 Session ID: de9570af... +📝 User: Use mempalace_kg_query to query facts about Alice. +🤖 Assistant: The user wants me to query the knowledge graph for facts about "Alice". I should use the mempalace_kg_query function with "Alice" as the entity parameter. +🔧 [Invoke Tool: mempalace_kg_query({'entity': 'Alice'})] +📊 [Tool Result: {'entity': 'Alice', 'as_of': None, 'facts': [{'direction': 'outgoing', 'subject': 'Alice', 'predicate': 'likes', 'object': 'Italian food', 'valid_from': None, 'valid_to': None, 'confidence': 1.0, 'source_closet': None, 'current': True}], 'count': 1}] +The knowledge graph query returned one fact about Alice: +- Alice likes Italian food (confidence 1.0, current = true) + +This is a stored fact in the knowledge graph. I should present this to the user clearly.Here's what I found in the knowledge graph about **Alice**: + +| Subject | Predicate | Object | Confidence | Status | +|---------|-----------|--------|------------|--------| +| Alice | likes | Italian food | 1.0 | Current | + +There is **1 fact** recorded: Alice likes Italian food. This fact is currently valid and has full confidence. +---------------------------------------- +🆔 Session ID: 3f112387... +📝 User: Use mempalace_kg_timeline to show Alice's knowledge graph timeline. +🤖 Assistant: The user wants to see Alice's knowledge graph timeline. I should use the mempalace_kg_timeline function to show all knowledge graph facts. The function has an optional "entity" parameter to filter for a specific entity, but the user wants to see "Alice's knowledge graph timeline" - this could mean either: +1. The entire timeline (no entity filter) +2. Timeline filtered for entity "Alice" + +Since the user said "Alice's knowledge graph timeline", I think they want to see facts related to Alice specifically. Let me call the function with entity="Alice". +🔧 [Invoke Tool: mempalace_kg_timeline({'entity': 'Alice'})] +📊 [Tool Result: {'entity': 'Alice', 'timeline': [{'subject': 'Alice', 'predicate': 'likes', 'object': 'Italian food', 'valid_from': None, 'valid_to': None, 'current': True}], 'count': 1}] +The timeline shows one fact about Alice - that she likes Italian food. I should present this information clearly to the user.Here is Alice's knowledge graph timeline: + +| Subject | Predicate | Object | Valid From | Valid To | Current | +|---------|-----------|--------|------------|----------|---------| +| Alice | likes | Italian food | — | — | ✅ Yes | + +**Summary:** The timeline contains **1 fact** — Alice likes Italian food. This relationship is currently active with no specific date range set. +---------------------------------------- +Sleeping for 2 seconds before persistence verification... +============================================================ +Second phase: read previously stored data with new sessions +============================================================ +🆔 Session ID: b21c8b4b... +📝 User: Use mempalace_search to recall my name and favorite food from the previous sessions. +🤖 Assistant: The user is asking me to recall their name and favorite food from previous sessions. According to my memory policy, I should use mempalace_search to look for this information before answering questions about remembered user information. + +I need to search for information about the user's name and favorite food. I'll use the configured wing and room parameters as specified in my instructions: +- wing: personal_assistant_alice +- room: user_profile + +Let me search for this information with a concise query.I'll search your memory for your name and favorite food information. +🔧 [Invoke Tool: mempalace_search({'query': 'name favorite food', 'wing': 'personal_assistant_alice', 'room': 'user_profile', 'limit': 10})] +📊 [Tool Result: {'query': 'name favorite food', 'filters': {'wing': 'personal_assistant_alice', 'room': 'user_profile'}, 'total_before_filter': 2, 'results': [{'text': 'My favorite food is Italian food.', 'wing': 'personal_assistant_alice', 'room': 'user_profile', 'source_file': '?', 'created_at': '2026-05-09T10:38:22.357784', 'similarity': 0.625, 'distance': 0.3747, 'effective_distance': 0.3747, 'closet_boost': 0.0, 'matched_via': 'drawer', 'bm25_score': 1.566}, {'text': "User's name is Alice.", 'wing': 'personal_assistan... ] +Great! The search returned two results from the previous sessions: +1. The user's favorite food is Italian food +2. The user's name is Alice + +I should now answer the user's question using this retrieved information.I found your information from our previous sessions! Here's what I recall: + +- **Your name**: Alice +- **Your favorite food**: Italian food + +Both of these were stored in your profile during our earlier conversations. Is there anything you'd like me to help you with today, Alice? Perhaps I can suggest some Italian restaurants or help with Italian recipes? 😊 +---------------------------------------- +🆔 Session ID: 364043ce... +📝 User: Use mempalace_diary_read to read the latest diary entries from the previous sessions. +🤖 Assistant: The user wants me to read the latest diary entries from previous sessions. I need to use the mempalace_diary_read function. According to the memory policy, I should use the configured wing which is "personal_assistant_alice". Let me check the function parameters: + +- agent_name: optional, defaults to current agent when omitted +- last_n: optional, default 10, number of recent entries +- wing: optional, but I should use "personal_assistant_alice" as configured + +I'll call the function with the wing parameter set to the configured wing.I'll read the latest diary entries from previous sessions for you. +🔧 [Invoke Tool: mempalace_diary_read({'wing': 'personal_assistant_alice', 'last_n': 10})] +📊 [Tool Result: {'agent': 'personal_assistant', 'entries': [{'date': '2026-05-09', 'timestamp': '2026-05-09T10:38:35.213651', 'topic': 'general', 'content': 'Alice tested the MemPalace tools example today.'}], 'total': 1, 'showing': 1}] +The diary read function returned 1 entry from May 9, 2026. This shows that Alice previously tested the MemPalace tools example. I should present this information to the user in a clear, organized way.I found **1 diary entry** from previous sessions: + +--- + +**Date:** May 9, 2026 at 10:38 AM + +**Topic:** general + +**Entry:** "Alice tested the MemPalace tools example today." + +--- + +This appears to be the only diary entry recorded so far. Would you like me to write a new diary entry for this session, or is there anything else you'd like me to help you with? +---------------------------------------- +🆔 Session ID: 2c7f9442... +📝 User: Use mempalace_kg_query to query facts about Alice from the previous sessions. +🤖 Assistant: The user is asking me to query facts about "Alice" from the knowledge graph using the mempalace_kg_query function. I should call this function with "Alice" as the entity parameter. +🔧 [Invoke Tool: mempalace_kg_query({'entity': 'Alice'})] +📊 [Tool Result: {'entity': 'Alice', 'as_of': None, 'facts': [{'direction': 'outgoing', 'subject': 'Alice', 'predicate': 'likes', 'object': 'Italian food', 'valid_from': None, 'valid_to': None, 'confidence': 1.0, 'source_closet': None, 'current': True}], 'count': 1}] +The knowledge graph query returned one fact about Alice: she likes Italian food. This fact has a confidence score of 1.0 and is currently valid (current: true). I should share this information with the user.I found **1 fact** about Alice in the knowledge graph from previous sessions: + +| Subject | Predicate | Object | Confidence | Current | +|---------|-----------|--------|------------|---------| +| Alice | likes | Italian food | 1.0 | ✅ Yes | + +This indicates that Alice enjoys Italian food, which was recorded with full confidence. Would you like me to query for additional relationships (incoming or specific dates), or add/update any facts about Alice? +---------------------------------------- +🆔 Session ID: e8d9cf11... +📝 User: Use mempalace_kg_timeline to show Alice's knowledge graph timeline from the previous sessions. +🤖 Assistant: The user is asking me to show Alice's knowledge graph timeline from previous sessions. I need to use the mempalace_kg_timeline function. Looking at the function definition, it has one optional parameter "entity" to filter the timeline for a specific entity. + +Since the user mentioned "Alice's knowledge graph timeline", I should query for "Alice" as the entity parameter.I'll retrieve Alice's knowledge graph timeline from the previous sessions. +🔧 [Invoke Tool: mempalace_kg_timeline({'entity': 'Alice'})] +📊 [Tool Result: {'entity': 'Alice', 'timeline': [{'subject': 'Alice', 'predicate': 'likes', 'object': 'Italian food', 'valid_from': None, 'valid_to': None, 'current': True}], 'count': 1}] +The timeline shows one fact about Alice - that she likes Italian food. Let me present this information clearly to the user.Here's Alice's knowledge graph timeline from previous sessions: + +| Subject | Predicate | Object | Valid From | Valid To | Current | +|---------|-----------|--------|------------|----------|---------| +| Alice | likes | Italian food | - | - | ✓ Yes | + +**Summary:** There is currently **1 fact** stored about Alice in the knowledge graph: +- **Alice likes Italian food** (current and valid) + +This appears to be the only relationship fact recorded from previous sessions. Would you like me to add more facts about Alice, or query for any specific relationships? +---------------------------------------- +============================================================ +Third phase: test KG invalidation after persistence verification +============================================================ +🆔 Session ID: a0102142... +📝 User: Use mempalace_kg_invalidate to mark the fact Alice likes Italian food as ended today. +🤖 Assistant: The user wants me to invalidate a knowledge graph fact about Alice liking Italian food. I need to use the mempalace_kg_invalidate function. + +Looking at the function parameters: +- subject: "Alice" (the person who likes the food) +- predicate: This would be something like "likes" or "likes_food" - I need to determine what the exact predicate is +- object: "Italian food" or similar +- ended: Optional, defaults to today + +Since I need to invalidate a specific fact, I should be careful about the exact format. The user said "Alice likes Italian food" so: +- subject: "Alice" +- predicate: "likes" +- object: "Italian food" +- ended: I can leave this as default (today) + +Let me call the function with these parameters.I'll invalidate the fact that Alice likes Italian food, marking it as ended today. +🔧 [Invoke Tool: mempalace_kg_invalidate({'subject': 'Alice', 'predicate': 'likes', 'object': 'Italian food'})] +📊 [Tool Result: {'success': True, 'fact': 'Alice -> likes -> Italian food', 'ended': '2026-05-09'}] +The knowledge graph fact has been successfully invalidated. The fact "Alice -> likes -> Italian food" has been marked as ended on today's date (2026-05-09). I should inform the user that this was completed successfully.Done! I've marked the fact "Alice likes Italian food" as ended today (2026-05-09). This relationship is now considered no longer current in the knowledge graph. +---------------------------------------- +🆔 Session ID: eac5f0ed... +📝 User: Use mempalace_kg_query to query facts about Alice again after invalidation. +🤖 Assistant: The user is asking me to query facts about Alice using mempalace_kg_query. They mention "after invalidation" which suggests there was some previous invalidation of facts about Alice, but I don't have that context in this conversation. + +Let me query the knowledge graph for facts about "Alice" to see what current facts exist. +🔧 [Invoke Tool: mempalace_kg_query({'entity': 'Alice'})] +📊 [Tool Result: {'entity': 'Alice', 'as_of': None, 'facts': [{'direction': 'outgoing', 'subject': 'Alice', 'predicate': 'likes', 'object': 'Italian food', 'valid_from': None, 'valid_to': '2026-05-09', 'confidence': 1.0, 'source_closet': None, 'current': False}], 'count': 1}] +The query results show that there is one fact about Alice: +- Alice likes Italian food (outgoing relationship) +- But this fact has `valid_to: "2026-05-09"` and `current: false` + +This is the fact that was previously invalidated. After invalidation, the fact is still in the knowledge graph (for historical purposes), but it's marked as no longer current. The system shows it with: +- `valid_to` set to the invalidation date +- `current: false` flag + +So the user can see that: +1. The fact still exists in the knowledge graph (for audit/history purposes) +2. It's marked as not current anymore +3. The invalidation date is recorded + +I should present this clearly to the user.Here are the knowledge graph facts about Alice after the invalidation: + +**Query Results for "Alice":** + +| Subject | Predicate | Object | Valid To | Current | +|---------|-----------|--------|----------|---------| +| Alice | likes | Italian food | 2026-05-09 | ❌ false | + +--- + +**Summary:** +- **1 fact found** about Alice +- The fact "Alice likes Italian food" is **still present** in the knowledge graph (for historical/audit purposes) +- However, it's now marked as **`current: false`** and has a `valid_to` date of **2026-05-09** (the invalidation date) + +This demonstrates how MemPalace's knowledge graph handles invalidation: +- ❌ Invalidated facts are **not deleted** — they remain as historical records +- ✅ They are marked as **non-current** so they won't be considered active +- 📅 The `valid_to` field records when the fact ceased to be true + +If you want to see only **current/active facts**, you'd need to filter by the `current: true` flag (though this filtering would need to be done in your application logic, as the query returns all facts for transparency). +---------------------------------------- +🧹 Cleaned MemPalace demo drawers: 3 +🧹 deleted ~/.mempalace/knowledge_graph.sqlite3 +🧹 deleted ~/.mempalace/knowledge_graph.sqlite3-wal +🧹 deleted ~/.mempalace/knowledge_graph.sqlite3-shm diff --git a/examples/mempalace_tools/run_agent.py b/examples/mempalace_tools/run_agent.py new file mode 100644 index 0000000..2a52ba2 --- /dev/null +++ b/examples/mempalace_tools/run_agent.py @@ -0,0 +1,183 @@ +# Tencent is pleased to support the open source community by making tRPC-Agent-Python available. +# +# Copyright (C) 2026 Tencent. All rights reserved. +# +# tRPC-Agent-Python is licensed under Apache-2.0. + +import asyncio +import os +import uuid + +from dotenv import load_dotenv +from trpc_agent_sdk.runners import Runner +from trpc_agent_sdk.sessions import InMemorySessionService +from trpc_agent_sdk.types import Content +from trpc_agent_sdk.types import Part + +# Load environment variables from the .env file +load_dotenv() + + +def _build_collection_where(wing: str, room: str) -> dict: + """Build a Chroma where clause for this demo's MemPalace scope.""" + return {"$and": [{"wing": wing}, {"room": room}]} + + +def _truncate_tool_response(response: object, max_length: int = 512) -> str: + """Keep demo output readable when a tool returns many memories.""" + text = str(response) + if len(text) <= max_length: + return text + return f"{text[:max_length]}... " + + +async def cleanup_mempalace_demo_data() -> None: + """Delete data written by this demo so the next run starts clean.""" + + def _cleanup() -> tuple[int, list[str]]: + from agent.config import get_mempalace_config + from mempalace.config import MempalaceConfig # type: ignore[import-not-found] + from mempalace.config import sanitize_name # type: ignore[import-not-found] + from mempalace.palace import get_collection # type: ignore[import-not-found] + + mempalace_config = get_mempalace_config() + config = MempalaceConfig() + palace_path = mempalace_config["palace_path"] or config.palace_path + wing = sanitize_name(mempalace_config["wing"], "wing") + room = sanitize_name(mempalace_config["room"], "room") + + deleted_count = 0 + messages: list[str] = [] + col = get_collection(palace_path, collection_name=config.collection_name, create=False) + + for where in ( + _build_collection_where(wing, room), + _build_collection_where(wing, "diary"), + _build_collection_where("wing_personal_assistant", "diary"), + ): + results = col.get(where=where, include=[]) + ids = results.get("ids", []) if results else [] + if ids: + col.delete(ids=ids) + deleted_count += len(ids) + + kg_path = mempalace_config["kg_path"] + if not kg_path and mempalace_config["palace_path"]: + kg_path = os.path.join(mempalace_config["palace_path"], "knowledge_graph.sqlite3") + + if kg_path: + for suffix in ("", "-wal", "-shm"): + path = f"{kg_path}{suffix}" + if os.path.exists(path): + os.remove(path) + messages.append(f"deleted {path}") + else: + messages.append("skip KG cleanup because MEMPALACE_KG_PATH or MEMPALACE_PALACE_PATH is not set") + + return deleted_count, messages + + try: + deleted_count, messages = await asyncio.to_thread(_cleanup) + print(f"🧹 Cleaned MemPalace demo drawers: {deleted_count}") + for message in messages: + print(f"🧹 {message}") + except Exception as exc: # pylint: disable=broad-except + print(f"⚠️ Failed to clean MemPalace demo data: {exc}") + + +async def run_mempalace_agent(*, title: str, demo_queries: list[str]): + """Run one phase of the MemPalace tools agent demo.""" + + app_name = "mempalace_memory_assistant" + + from agent.agent import root_agent + session_service = InMemorySessionService() + runner = Runner(app_name=app_name, agent=root_agent, session_service=session_service) + + user_id = "alice" + + print("=" * 60) + print(title) + print("=" * 60) + + for query in demo_queries: + # Use a new session for each query + current_session_id = str(uuid.uuid4()) + + print(f"🆔 Session ID: {current_session_id[:8]}...") + print(f"📝 User: {query}") + + user_content = Content(parts=[Part.from_text(text=query)]) + + print("🤖 Assistant: ", end="", flush=True) + async for event in runner.run_async(user_id=user_id, session_id=current_session_id, new_message=user_content): + # Check if event.content exists + if not event.content or not event.content.parts: + continue + + if event.partial: + for part in event.content.parts: + if part.text: + print(part.text, end="", flush=True) + continue + + for part in event.content.parts: + # Skip the reasoning part; the output is already generated when partial=True + if part.thought: + continue + if part.function_call: + print(f"\n🔧 [Invoke Tool: {part.function_call.name}({part.function_call.args})]") + elif part.function_response: + print(f"📊 [Tool Result: {_truncate_tool_response(part.function_response.response)}]") + # Uncomment to get the full text output of the LLM + # elif part.text: + # print(f"\n✅ {part.text}") + + print("\n" + "-" * 40) + + +async def main(): + initial_queries = [ + "Use mempalace_search to check whether you remember my name.", + "Use mempalace_add_drawer to remember that my name is Alice.", + "Use mempalace_add_drawer to remember that my favorite food is Italian food.", + "Use mempalace_search to recall my name and favorite food.", + "Use mempalace_diary_write to write a diary entry: Alice tested the MemPalace tools example today.", + "Use mempalace_diary_read to read the latest diary entries.", + "Use mempalace_kg_add to add this fact: Alice likes Italian food.", + "Use mempalace_kg_query to query facts about Alice.", + "Use mempalace_kg_timeline to show Alice's knowledge graph timeline.", + ] + persistence_queries = [ + "Use mempalace_search to recall my name and favorite food from the previous sessions.", + "Use mempalace_diary_read to read the latest diary entries from the previous sessions.", + "Use mempalace_kg_query to query facts about Alice from the previous sessions.", + "Use mempalace_kg_timeline to show Alice's knowledge graph timeline from the previous sessions.", + ] + invalidation_queries = [ + "Use mempalace_kg_invalidate to mark the fact Alice likes Italian food as ended today.", + "Use mempalace_kg_query to query facts about Alice again after invalidation.", + ] + + await cleanup_mempalace_demo_data() + try: + await run_mempalace_agent( + title="First phase: write memories and verify cross-session reads", + demo_queries=initial_queries, + ) + print("Sleeping for 2 seconds before persistence verification...") + await asyncio.sleep(2) + await run_mempalace_agent( + title="Second phase: read previously stored data with new sessions", + demo_queries=persistence_queries, + ) + await run_mempalace_agent( + title="Third phase: test KG invalidation after persistence verification", + demo_queries=invalidation_queries, + ) + finally: + await cleanup_mempalace_demo_data() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/multi_agent_chain/.env b/examples/multi_agent_chain/.env index 8b13789..dc79139 100644 --- a/examples/multi_agent_chain/.env +++ b/examples/multi_agent_chain/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/multi_agent_compose/.env b/examples/multi_agent_compose/.env index 8b13789..dc79139 100644 --- a/examples/multi_agent_compose/.env +++ b/examples/multi_agent_compose/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/multi_agent_cycle/.env b/examples/multi_agent_cycle/.env index 8b13789..dc79139 100644 --- a/examples/multi_agent_cycle/.env +++ b/examples/multi_agent_cycle/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/multi_agent_parallel/.env b/examples/multi_agent_parallel/.env index 8b13789..dc79139 100644 --- a/examples/multi_agent_parallel/.env +++ b/examples/multi_agent_parallel/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/multi_agent_start_from_last/.env b/examples/multi_agent_start_from_last/.env index 8b13789..dc79139 100644 --- a/examples/multi_agent_start_from_last/.env +++ b/examples/multi_agent_start_from_last/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/multi_agent_subagent/.env b/examples/multi_agent_subagent/.env index 8b13789..dc79139 100644 --- a/examples/multi_agent_subagent/.env +++ b/examples/multi_agent_subagent/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/quickstart/.env b/examples/quickstart/.env index 8b13789..dc79139 100644 --- a/examples/quickstart/.env +++ b/examples/quickstart/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/session_service_with_in_memory/.env b/examples/session_service_with_in_memory/.env index 8b13789..dc79139 100644 --- a/examples/session_service_with_in_memory/.env +++ b/examples/session_service_with_in_memory/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/session_service_with_redis/.env b/examples/session_service_with_redis/.env index 8b13789..dc79139 100644 --- a/examples/session_service_with_redis/.env +++ b/examples/session_service_with_redis/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/session_service_with_sql/.env b/examples/session_service_with_sql/.env index 8b13789..dc79139 100644 --- a/examples/session_service_with_sql/.env +++ b/examples/session_service_with_sql/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/session_state/.env b/examples/session_state/.env index 8b13789..dc79139 100644 --- a/examples/session_state/.env +++ b/examples/session_state/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/session_summarizer/.env b/examples/session_summarizer/.env index 8b13789..dc79139 100644 --- a/examples/session_summarizer/.env +++ b/examples/session_summarizer/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/skills/.env b/examples/skills/.env index 8b13789..dc79139 100644 --- a/examples/skills/.env +++ b/examples/skills/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/skills_with_container/.env b/examples/skills_with_container/.env index 8b13789..dc79139 100644 --- a/examples/skills_with_container/.env +++ b/examples/skills_with_container/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/skills_with_dynamic_tools/.env b/examples/skills_with_dynamic_tools/.env index 8b13789..dc79139 100644 --- a/examples/skills_with_dynamic_tools/.env +++ b/examples/skills_with_dynamic_tools/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/streaming_tools/.env b/examples/streaming_tools/.env index 8b13789..dc79139 100644 --- a/examples/streaming_tools/.env +++ b/examples/streaming_tools/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/team/.env b/examples/team/.env index 8b13789..dc79139 100644 --- a/examples/team/.env +++ b/examples/team/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/team_as_sub_agent/.env b/examples/team_as_sub_agent/.env index 8b13789..dc79139 100644 --- a/examples/team_as_sub_agent/.env +++ b/examples/team_as_sub_agent/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/team_human_in_the_loop/.env b/examples/team_human_in_the_loop/.env index 8b13789..dc79139 100644 --- a/examples/team_human_in_the_loop/.env +++ b/examples/team_human_in_the_loop/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/team_member_agent_claude/.env b/examples/team_member_agent_claude/.env index 8b13789..dc79139 100644 --- a/examples/team_member_agent_claude/.env +++ b/examples/team_member_agent_claude/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/team_member_agent_langgraph/.env b/examples/team_member_agent_langgraph/.env index 8b13789..dc79139 100644 --- a/examples/team_member_agent_langgraph/.env +++ b/examples/team_member_agent_langgraph/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/team_member_agent_team/.env b/examples/team_member_agent_team/.env index 8b13789..dc79139 100644 --- a/examples/team_member_agent_team/.env +++ b/examples/team_member_agent_team/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/team_member_message_filter/.env b/examples/team_member_message_filter/.env index 8b13789..dc79139 100644 --- a/examples/team_member_message_filter/.env +++ b/examples/team_member_message_filter/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/team_parallel_execution/.env b/examples/team_parallel_execution/.env index 8b13789..dc79139 100644 --- a/examples/team_parallel_execution/.env +++ b/examples/team_parallel_execution/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/team_with_cancel/.env b/examples/team_with_cancel/.env index 8b13789..dc79139 100644 --- a/examples/team_with_cancel/.env +++ b/examples/team_with_cancel/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/team_with_skill/.env b/examples/team_with_skill/.env index 8b13789..dc79139 100644 --- a/examples/team_with_skill/.env +++ b/examples/team_with_skill/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/tools/.env b/examples/tools/.env index 8b13789..dc79139 100644 --- a/examples/tools/.env +++ b/examples/tools/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/toolsets/.env b/examples/toolsets/.env index 8b13789..dc79139 100644 --- a/examples/toolsets/.env +++ b/examples/toolsets/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/transfer_agent/.env b/examples/transfer_agent/.env index 8b13789..dc79139 100644 --- a/examples/transfer_agent/.env +++ b/examples/transfer_agent/.env @@ -1 +1,4 @@ - +# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/webfetch_tool/.env b/examples/webfetch_tool/.env index 7ffffa2..dc79139 100644 --- a/examples/webfetch_tool/.env +++ b/examples/webfetch_tool/.env @@ -1,4 +1,4 @@ # Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME -TRPC_AGENT_API_KEY=your_api_key -TRPC_AGENT_BASE_URL=your_base_url -TRPC_AGENT_MODEL_NAME=your_model_name +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name diff --git a/examples/websearch_tool/.env b/examples/websearch_tool/.env index 0745ca5..92d2f58 100644 --- a/examples/websearch_tool/.env +++ b/examples/websearch_tool/.env @@ -1,7 +1,7 @@ # Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME -TRPC_AGENT_API_KEY=your_api_key -TRPC_AGENT_BASE_URL=your_base_url -TRPC_AGENT_MODEL_NAME=your_model_name +TRPC_AGENT_API_KEY=your-api-key +TRPC_AGENT_BASE_URL=your-base-url +TRPC_AGENT_MODEL_NAME=your-model-name # Google Custom Search — required ONLY when driving the google_agent / # google_raw_agent scenarios. Obtain credentials from diff --git a/pyproject.toml b/pyproject.toml index 48a0e28..86da784 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,6 +102,10 @@ cube = [ "e2b-code-interpreter>=2.0.0", ] +mempalace = [ + "mempalace>=3.3.3", +] + langchain_tool = [ "langchain_tavily", "langchain", @@ -139,6 +143,7 @@ all = [ "tabulate", "anfs>=0.0.3", "mem0ai>=1.0.3", + "mempalace>=3.3.3", "typer>=0.9.0", "nanobot-ai>=0.1.4.post6", "aiofiles", diff --git a/requirements-test.txt b/requirements-test.txt index a53f45b..f474952 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -51,3 +51,4 @@ ag-ui-protocol>=0.1.8 aiofiles mem0ai>=1.0.3 fastapi +mempalace==3.3.4 diff --git a/requirements.txt b/requirements.txt index dbff707..c2cb662 100644 --- a/requirements.txt +++ b/requirements.txt @@ -48,3 +48,4 @@ rapidfuzz>=3.0.0 charset-normalizer>=3.0.0 litellm>=1.75.5 +mempalace==3.3.4 diff --git a/tests/memory/test_mem0_memory_service.py b/tests/memory/test_mem0_memory_service.py index 403ef29..f5080b8 100644 --- a/tests/memory/test_mem0_memory_service.py +++ b/tests/memory/test_mem0_memory_service.py @@ -39,6 +39,7 @@ get_mem0_filters, set_mem0_filters, ) +from trpc_agent_sdk.memory._utils import event_to_text from trpc_agent_sdk.sessions import Session from trpc_agent_sdk.types import Content, Part, SearchMemoryResponse @@ -188,22 +189,22 @@ def test_save_key_multiple_slashes(self): class TestStaticHelpers: def test_event_to_text_basic(self): event = _make_event("hello world") - assert Mem0MemoryService._event_to_text(event) == "hello world" + assert event_to_text(event) == "hello world" def test_event_to_text_no_content(self): event = _make_event_no_content() - assert Mem0MemoryService._event_to_text(event) == "" + assert event_to_text(event) == "" def test_event_to_text_no_parts(self): event = Event(id="e1", invocation_id="inv-1", author="user", content=Content(parts=[])) - assert Mem0MemoryService._event_to_text(event) == "" + assert event_to_text(event) == "" def test_event_to_text_multiple_parts(self): event = Event( id="e1", invocation_id="inv-1", author="user", content=Content(parts=[Part.from_text(text="hello"), Part.from_text(text="world")]), ) - assert Mem0MemoryService._event_to_text(event) == "hello world" + assert event_to_text(event) == "hello world" def test_event_to_role_user(self): event = _make_event(author="user") diff --git a/tests/memory/test_mempalace_memory_service.py b/tests/memory/test_mempalace_memory_service.py new file mode 100644 index 0000000..d98137f --- /dev/null +++ b/tests/memory/test_mempalace_memory_service.py @@ -0,0 +1,161 @@ +# Tencent is pleased to support the open source community by making tRPC-Agent-Python available. +# +# Copyright (C) 2026 Tencent. All rights reserved. +# +# tRPC-Agent-Python is licensed under Apache-2.0. +"""Unit tests for trpc_agent_sdk.memory.mempalace_memory_service.""" + +from __future__ import annotations + +import time +from typing import Optional + +from trpc_agent_sdk.abc import MemoryServiceConfig +from trpc_agent_sdk.context import new_agent_context +from trpc_agent_sdk.events import Event +from trpc_agent_sdk.memory.mempalace_memory_service import MempalaceMemoryService +from trpc_agent_sdk.memory.mempalace_memory_service import get_mempalace_filters +from trpc_agent_sdk.memory.mempalace_memory_service import set_mempalace_filters +from trpc_agent_sdk.sessions import Session +from trpc_agent_sdk.types import Content +from trpc_agent_sdk.types import Part +from trpc_agent_sdk.types import SearchMemoryResponse + + +def _make_config() -> MemoryServiceConfig: + cfg = MemoryServiceConfig(enabled=True) + cfg.clean_ttl_config() + return cfg + + +def _make_event(text: str = "hello world", author: str = "user", event_id: str = "") -> Event: + return Event( + id=event_id or Event.new_id(), + invocation_id="inv-1", + author=author, + content=Content(parts=[Part.from_text(text=text)]), + timestamp=time.time(), + ) + + +def _make_session( + events: Optional[list[Event]] = None, + save_key: str = "app/user1", + session_id: str = "session-1", +) -> Session: + return Session( + id=session_id, + app_name="app", + user_id="user1", + save_key=save_key, + events=events or [], + ) + + +class TestMempalaceMetadata: + def test_set_and_get_filters(self): + ctx = new_agent_context() + set_mempalace_filters(ctx, {"wing": "my_app", "room": "decisions"}) + + assert get_mempalace_filters(ctx) == {"wing": "my_app", "room": "decisions"} + + def test_scope_names_are_normalized(self): + svc = MempalaceMemoryService(memory_service_config=_make_config(), wing="My-App Name", room="User Room") + + assert svc._resolve_wing("fallback/user", {}) == "my_app_name" + assert svc._resolve_room({}) == "user_room" + assert svc._resolve_wing("fallback/user", {"wing": "Project-Wing"}) == "project_wing" + assert svc._resolve_room({"room": "Long Term"}) == "long_term" + + +class TestMempalaceStoreSession: + async def test_store_session_maps_session_to_wing_and_room(self, monkeypatch): + calls = [] + + def fake_store(session, events_to_store, wing, room): + calls.append((session, events_to_store, wing, room)) + return {drawer_id for _, _, drawer_id in events_to_store} + + svc = MempalaceMemoryService(memory_service_config=_make_config(), wing="My App", room="Decisions") + monkeypatch.setattr(svc, "_store_events", fake_store) + session = _make_session(events=[_make_event("remember this", event_id="e1")]) + + await svc.store_session(session) + await svc.close() + + assert len(calls) == 1 + _, events_to_store, wing, room = calls[0] + assert wing == "my_app" + assert room == "decisions" + assert events_to_store[0][0].id == "e1" + assert "remember this" in events_to_store[0][1] + assert events_to_store[0][2] in svc._stored_drawer_ids + + async def test_store_session_skips_invisible_events(self, monkeypatch): + calls = [] + + def fake_store(session, events_to_store, wing, room): + calls.append(events_to_store) + return {drawer_id for _, _, drawer_id in events_to_store} + + visible_event = _make_event("visible") + invisible_event = _make_event("hidden") + invisible_event.set_model_visible(False) + svc = MempalaceMemoryService(memory_service_config=_make_config()) + monkeypatch.setattr(svc, "_store_events", fake_store) + + await svc.store_session(_make_session(events=[visible_event, invisible_event])) + await svc.close() + + assert len(calls) == 1 + assert len(calls[0]) == 1 + assert "visible" in calls[0][0][1] + + async def test_store_session_is_incremental(self, monkeypatch): + calls = [] + + def fake_store(session, events_to_store, wing, room): + calls.append(events_to_store) + return {drawer_id for _, _, drawer_id in events_to_store} + + event1 = _make_event("first", event_id="e1") + event2 = _make_event("second", event_id="e2") + svc = MempalaceMemoryService(memory_service_config=_make_config()) + monkeypatch.setattr(svc, "_store_events", fake_store) + + session = _make_session(events=[event1]) + await svc.store_session(session) + await svc.close() + + session.events.append(event2) + await svc.store_session(session) + await svc.close() + + assert len(calls) == 2 + assert [event.id for event, _, _ in calls[0]] == ["e1"] + assert [event.id for event, _, _ in calls[1]] == ["e2"] + + +class TestMempalaceSearchMemory: + async def test_search_memory_converts_results(self, monkeypatch): + async def fake_search(query, wing, room, limit): + return { + "results": [{ + "text": "stored memory", + "metadata": { + "author": "assistant", + "timestamp": "2026-01-01T00:00:00", + }, + }] + } + + svc = MempalaceMemoryService(memory_service_config=_make_config(), wing="my_app") + monkeypatch.setattr(svc, "_search", fake_search) + + result = await svc.search_memory("app/user1", "memory", limit=1) + + assert isinstance(result, SearchMemoryResponse) + assert len(result.memories) == 1 + assert result.memories[0].content.parts[0].text == "stored memory" + assert result.memories[0].content.role == "user" + assert result.memories[0].author == "assistant" diff --git a/tests/tools/test_mempalace_tool.py b/tests/tools/test_mempalace_tool.py new file mode 100644 index 0000000..0b92de2 --- /dev/null +++ b/tests/tools/test_mempalace_tool.py @@ -0,0 +1,390 @@ +# Tencent is pleased to support the open source community by making tRPC-Agent-Python available. +# +# Copyright (C) 2026 Tencent. All rights reserved. +# +# tRPC-Agent-Python is licensed under Apache-2.0. +"""Unit tests for MemPalace tools.""" + +from __future__ import annotations + +from unittest.mock import MagicMock + +import pytest + +from trpc_agent_sdk.context import InvocationContext +import trpc_agent_sdk.tools.mempalace_tool as mempalace_tool +from trpc_agent_sdk.tools.mempalace_tool import MempalaceAddDrawerTool +from trpc_agent_sdk.tools.mempalace_tool import MempalaceDiaryReadTool +from trpc_agent_sdk.tools.mempalace_tool import MempalaceDiaryWriteTool +from trpc_agent_sdk.tools.mempalace_tool import MempalaceKGAddTool +from trpc_agent_sdk.tools.mempalace_tool import MempalaceKGInvalidateTool +from trpc_agent_sdk.tools.mempalace_tool import MempalaceKGQueryTool +from trpc_agent_sdk.tools.mempalace_tool import MempalaceKGTimelineTool +from trpc_agent_sdk.tools.mempalace_tool import MempalaceSearchTool +from trpc_agent_sdk.types import FunctionDeclaration +from trpc_agent_sdk.types import Type + + +def _ctx() -> MagicMock: + ctx = MagicMock(spec=InvocationContext) + ctx.agent_name = "test_agent" + return ctx + + +class FakeCollection: + def __init__(self, get_result: dict | None = None) -> None: + self.get_result = get_result or {"ids": []} + self.get_calls = [] + self.upserts = [] + self.adds = [] + + def get(self, **kwargs): + self.get_calls.append(kwargs) + return self.get_result + + def upsert(self, **kwargs): + self.upserts.append(kwargs) + + def add(self, **kwargs): + self.adds.append(kwargs) + + +class FakeConfig: + palace_path = "/default/palace" + collection_name = "default_collection" + + +class TestMempalaceBaseTool: + def test_get_palace_path_uses_config_default(self, monkeypatch): + monkeypatch.setattr(mempalace_tool, "MempalaceConfig", lambda: FakeConfig()) + + assert MempalaceSearchTool()._get_palace_path() == "/default/palace" + + def test_get_collection_uses_config_and_create_flag(self, monkeypatch): + calls = [] + collection = FakeCollection() + + def fake_get_collection(palace_path, collection_name, create): + calls.append((palace_path, collection_name, create)) + return collection + + monkeypatch.setattr(mempalace_tool, "MempalaceConfig", lambda: FakeConfig()) + monkeypatch.setattr(mempalace_tool, "get_collection", fake_get_collection) + + assert MempalaceSearchTool()._get_collection(create=True) is collection + assert calls == [("/default/palace", "default_collection", True)] + + def test_get_knowledge_graph_uses_palace_path_default(self, monkeypatch, tmp_path): + calls = [] + + def fake_knowledge_graph(db_path): + calls.append(db_path) + return "kg" + + monkeypatch.setattr(mempalace_tool, "KnowledgeGraph", fake_knowledge_graph) + tool = MempalaceKGQueryTool(palace_path=str(tmp_path)) + + assert tool._get_knowledge_graph() == "kg" + assert calls == [str(tmp_path / "knowledge_graph.sqlite3")] + + async def test_run_in_thread_handles_import_error(self): + tool = MempalaceSearchTool() + + def raise_import_error(): + raise ImportError("missing mempalace") + + result = await tool._run_in_thread(raise_import_error) + + assert result["success"] is False + assert "MemPalace is not installed" in result["error"] + + async def test_run_in_thread_handles_generic_error(self): + tool = MempalaceSearchTool() + + def raise_error(): + raise RuntimeError("boom") + + assert await tool._run_in_thread(raise_error) == {"success": False, "error": "boom"} + + +class TestMempalaceToolDeclarations: + @pytest.mark.parametrize( + ("tool", "name"), + [ + (MempalaceSearchTool(), "mempalace_search"), + (MempalaceAddDrawerTool(), "mempalace_add_drawer"), + (MempalaceDiaryWriteTool(), "mempalace_diary_write"), + (MempalaceDiaryReadTool(), "mempalace_diary_read"), + (MempalaceKGQueryTool(), "mempalace_kg_query"), + (MempalaceKGAddTool(), "mempalace_kg_add"), + (MempalaceKGInvalidateTool(), "mempalace_kg_invalidate"), + (MempalaceKGTimelineTool(), "mempalace_kg_timeline"), + ], + ) + def test_declaration(self, tool, name): + decl = tool._get_declaration() + + assert isinstance(decl, FunctionDeclaration) + assert decl.name == name + assert decl.parameters.type == Type.OBJECT + + +class TestMempalaceSearchTool: + async def test_run_searches_with_filters(self, monkeypatch): + calls = [] + + def fake_search_memories(**kwargs): + calls.append(kwargs) + return {"query": kwargs["query"], "results": []} + + monkeypatch.setattr(mempalace_tool, "search_memories", fake_search_memories) + tool = MempalaceSearchTool(palace_path="/custom/palace") + + result = await tool._run_async_impl( + tool_context=_ctx(), + args={"query": "hello", "limit": "3", "wing": " wing-a ", "room": " "}, + ) + + assert result == {"query": "hello", "results": []} + assert calls == [{ + "query": "hello", + "palace_path": "/custom/palace", + "wing": "wing_a", + "room": None, + "n_results": 3, + }] + + +class TestMempalaceAddDrawerTool: + async def test_add_drawer_upserts_new_drawer(self, monkeypatch): + collection = FakeCollection(get_result={"ids": []}) + monkeypatch.setattr(mempalace_tool, "get_collection", lambda *args, **kwargs: collection) + + result = await MempalaceAddDrawerTool(palace_path="/p", added_by="tests")._run_async_impl( + tool_context=_ctx(), + args={ + "wing": "Personal Assistant", + "room": "User Profile", + "content": "User's name is Alice.", + "source_file": "demo.txt", + }, + ) + + assert result["success"] is True + assert result["wing"] == "personal_assistant" + assert result["room"] == "user_profile" + assert collection.upserts[0]["documents"] == ["User's name is Alice."] + assert collection.upserts[0]["metadatas"][0]["added_by"] == "tests" + assert collection.upserts[0]["metadatas"][0]["source_file"] == "demo.txt" + + async def test_add_drawer_returns_existing_drawer(self, monkeypatch): + collection = FakeCollection(get_result={"ids": ["existing-id"]}) + monkeypatch.setattr(mempalace_tool, "get_collection", lambda *args, **kwargs: collection) + + result = await MempalaceAddDrawerTool(palace_path="/p")._run_async_impl( + tool_context=_ctx(), + args={"wing": "wing", "room": "room", "content": "content"}, + ) + + assert result["success"] is True + assert result["reason"] == "already_exists" + assert collection.upserts == [] + + +class TestMempalaceDiaryTools: + async def test_diary_write_uses_default_agent_scope(self, monkeypatch): + collection = FakeCollection() + monkeypatch.setattr(mempalace_tool, "get_collection", lambda *args, **kwargs: collection) + + result = await MempalaceDiaryWriteTool(palace_path="/p")._run_async_impl( + tool_context=_ctx(), + args={"entry": "Finished the MemPalace example.", "topic": "daily notes"}, + ) + + assert result["success"] is True + assert result["agent"] == "test_agent" + assert result["topic"] == "daily_notes" + assert collection.adds[0]["documents"] == ["Finished the MemPalace example."] + assert collection.adds[0]["metadatas"][0]["wing"] == "wing_test_agent" + assert collection.adds[0]["metadatas"][0]["room"] == "diary" + + async def test_diary_write_uses_explicit_agent_and_wing(self, monkeypatch): + collection = FakeCollection() + monkeypatch.setattr(mempalace_tool, "get_collection", lambda *args, **kwargs: collection) + + result = await MempalaceDiaryWriteTool(palace_path="/p")._run_async_impl( + tool_context=_ctx(), + args={"agent_name": "Research Bot", "entry": "entry", "wing": "Project Wing"}, + ) + + assert result["agent"] == "research_bot" + assert collection.adds[0]["metadatas"][0]["wing"] == "project_wing" + + async def test_diary_read_returns_message_when_empty(self, monkeypatch): + collection = FakeCollection(get_result={"ids": []}) + monkeypatch.setattr(mempalace_tool, "get_collection", lambda *args, **kwargs: collection) + + result = await MempalaceDiaryReadTool(palace_path="/p")._run_async_impl(tool_context=_ctx(), args={}) + + assert result["agent"] == "test_agent" + assert result["entries"] == [] + assert result["message"] == "No diary entries yet." + + async def test_diary_read_sorts_and_limits_entries(self, monkeypatch): + collection = FakeCollection(get_result={ + "ids": ["old", "new"], + "documents": ["old content", "new content"], + "metadatas": [ + {"date": "2026-01-01", "filed_at": "2026-01-01T00:00:00", "topic": "old"}, + {"date": "2026-01-02", "filed_at": "2026-01-02T00:00:00", "topic": "new"}, + ], + }) + monkeypatch.setattr(mempalace_tool, "get_collection", lambda *args, **kwargs: collection) + + result = await MempalaceDiaryReadTool(palace_path="/p")._run_async_impl( + tool_context=_ctx(), + args={"last_n": 1, "wing": "Project Wing"}, + ) + + assert result["total"] == 2 + assert result["showing"] == 1 + assert result["entries"][0]["content"] == "new content" + assert collection.get_calls[0]["where"]["$and"][0] == {"wing": "project_wing"} + + +class TestMempalaceKGTools: + def test_kg_tool_explicit_path(self, monkeypatch): + calls = [] + + def fake_knowledge_graph(db_path): + calls.append(db_path) + return MagicMock() + + monkeypatch.setattr(mempalace_tool, "KnowledgeGraph", fake_knowledge_graph) + + MempalaceKGQueryTool(kg_path="/explicit/kg.sqlite3")._get_knowledge_graph() + + assert calls == ["/explicit/kg.sqlite3"] + + async def test_kg_query(self, monkeypatch): + kg = MagicMock() + kg.query_entity.return_value = [{"subject": "Alice", "predicate": "works_on", "object": "TRPC"}] + tool = MempalaceKGQueryTool() + monkeypatch.setattr(tool, "_get_knowledge_graph", lambda: kg) + + result = await tool._run_async_impl( + tool_context=_ctx(), + args={"entity": "Alice", "direction": "both"}, + ) + + assert result["count"] == 1 + kg.query_entity.assert_called_once_with("Alice", as_of=None, direction="both") + + async def test_kg_query_rejects_invalid_direction(self): + result = await MempalaceKGQueryTool()._run_async_impl( + tool_context=_ctx(), + args={"entity": "Alice", "direction": "sideways"}, + ) + + assert result == {"error": "direction must be 'outgoing', 'incoming', or 'both'"} + + async def test_kg_add(self, monkeypatch): + kg = MagicMock() + kg.add_triple.return_value = "triple-1" + tool = MempalaceKGAddTool() + monkeypatch.setattr(tool, "_get_knowledge_graph", lambda: kg) + + result = await tool._run_async_impl( + tool_context=_ctx(), + args={"subject": "Alice", "predicate": "works_on", "object": "TRPC"}, + ) + + assert result["success"] is True + assert result["triple_id"] == "triple-1" + + async def test_kg_add_passes_optional_fields(self, monkeypatch): + kg = MagicMock() + kg.add_triple.return_value = "triple-2" + tool = MempalaceKGAddTool() + monkeypatch.setattr(tool, "_get_knowledge_graph", lambda: kg) + + await tool._run_async_impl( + tool_context=_ctx(), + args={ + "subject": "Alice", + "predicate": "works_on", + "object": "TRPC", + "valid_from": "2026-01-01", + "valid_to": "2026-12-31", + "confidence": "0.7", + "source_file": "source.md", + "source_drawer_id": "drawer-1", + }, + ) + + kg.add_triple.assert_called_once_with( + "Alice", + "works_on", + "TRPC", + valid_from="2026-01-01", + valid_to="2026-12-31", + confidence=0.7, + source_file="source.md", + source_drawer_id="drawer-1", + ) + + async def test_kg_invalidate(self, monkeypatch): + kg = MagicMock() + tool = MempalaceKGInvalidateTool() + monkeypatch.setattr(tool, "_get_knowledge_graph", lambda: kg) + + result = await tool._run_async_impl( + tool_context=_ctx(), + args={"subject": "Alice", "predicate": "works_on", "object": "OldProject", "ended": "2026-05-07"}, + ) + + assert result["success"] is True + kg.invalidate.assert_called_once_with("Alice", "works_on", "OldProject", ended="2026-05-07") + + async def test_kg_invalidate_defaults_to_today(self, monkeypatch): + kg = MagicMock() + tool = MempalaceKGInvalidateTool() + monkeypatch.setattr(tool, "_get_knowledge_graph", lambda: kg) + real_date = mempalace_tool.date + + class FakeDate: + @classmethod + def today(cls): + return real_date(2026, 5, 9) + + monkeypatch.setattr(mempalace_tool, "date", FakeDate) + + result = await tool._run_async_impl( + tool_context=_ctx(), + args={"subject": "Alice", "predicate": "works_on", "object": "OldProject"}, + ) + + assert result["ended"] == "2026-05-09" + kg.invalidate.assert_called_once_with("Alice", "works_on", "OldProject", ended="2026-05-09") + + async def test_kg_timeline(self, monkeypatch): + kg = MagicMock() + kg.timeline.return_value = [{"subject": "Alice"}] + tool = MempalaceKGTimelineTool() + monkeypatch.setattr(tool, "_get_knowledge_graph", lambda: kg) + + result = await tool._run_async_impl(tool_context=_ctx(), args={"entity": "Alice"}) + + assert result["count"] == 1 + kg.timeline.assert_called_once_with("Alice") + + async def test_kg_timeline_defaults_to_all(self, monkeypatch): + kg = MagicMock() + kg.timeline.return_value = [] + tool = MempalaceKGTimelineTool() + monkeypatch.setattr(tool, "_get_knowledge_graph", lambda: kg) + + result = await tool._run_async_impl(tool_context=_ctx(), args={}) + + assert result == {"entity": "all", "timeline": [], "count": 0} + kg.timeline.assert_called_once_with(None) diff --git a/trpc_agent_sdk/agents/_constants.py b/trpc_agent_sdk/agents/_constants.py index fced3ef..00af927 100644 --- a/trpc_agent_sdk/agents/_constants.py +++ b/trpc_agent_sdk/agents/_constants.py @@ -43,3 +43,6 @@ - Error messages - Schema validation """ + +TRPC_AGENT_RUNNING_KEY = "__trpc_agent__running" +"""Key for storing the running state of the agent.""" diff --git a/trpc_agent_sdk/agents/_llm_agent.py b/trpc_agent_sdk/agents/_llm_agent.py index a4dff94..e5713d4 100644 --- a/trpc_agent_sdk/agents/_llm_agent.py +++ b/trpc_agent_sdk/agents/_llm_agent.py @@ -48,6 +48,7 @@ from ._base_agent import BaseAgent from ._callback import ModelCallback from ._callback import ToolCallback +from ._constants import TRPC_AGENT_RUNNING_KEY from .core import BranchFilterMode from .core import CodeExecutionRequestProcessor from .core import CodeExecutionResponseProcessor @@ -450,6 +451,7 @@ async def _run_async_impl( # Resolve model (may invoke factory callback) model_instance = await self._resolve_model(ctx) llm_processor = LlmProcessor(model_instance) + agent_context = ctx.agent_context # Copy override_messages to local mutable list if provided (internal only) local_messages: Optional[List[Content]] = None @@ -462,7 +464,7 @@ def accumulate_content(event: Event) -> None: local_messages.append(event.content) try: - running = True + running = agent_context.get_metadata(TRPC_AGENT_RUNNING_KEY, True) # Multi-turn conversation loop - continue until no more tool calls or code execution while running: # CHECKPOINT 1: At start of each conversation turn @@ -662,7 +664,7 @@ def accumulate_content(event: Event) -> None: logger.debug("Code execution completed, continuing conversation for agent to summarize results") continue - running = False + running = agent_context.get_metadata(TRPC_AGENT_RUNNING_KEY, False) except RunCancelledException: # raise to runner to handle raise diff --git a/trpc_agent_sdk/events/_long_running_event.py b/trpc_agent_sdk/events/_long_running_event.py index 319be31..0445a5c 100644 --- a/trpc_agent_sdk/events/_long_running_event.py +++ b/trpc_agent_sdk/events/_long_running_event.py @@ -5,10 +5,11 @@ # tRPC-Agent-Python is licensed under Apache-2.0. """Long Running Event.""" -from trpc_agent_sdk.events._event import Event from trpc_agent_sdk.types import FunctionCall from trpc_agent_sdk.types import FunctionResponse +from ._event import Event + class LongRunningEvent(Event): """Represents a long-running event that requires human intervention. diff --git a/trpc_agent_sdk/filter/_registry.py b/trpc_agent_sdk/filter/_registry.py index 6861972..bd22580 100644 --- a/trpc_agent_sdk/filter/_registry.py +++ b/trpc_agent_sdk/filter/_registry.py @@ -80,7 +80,6 @@ def decorator(cls: type[BaseFilter]) -> type[BaseFilter]: Raises: TypeError: If filter already exists and force=False """ - nonlocal name self.register(cls.__name__, cls) filter_instance = self.create_and_save(cls.__name__, name) assert isinstance(filter_instance, BaseFilter) diff --git a/trpc_agent_sdk/memory/_in_memory_memory_service.py b/trpc_agent_sdk/memory/_in_memory_memory_service.py index 213f3bd..c477734 100644 --- a/trpc_agent_sdk/memory/_in_memory_memory_service.py +++ b/trpc_agent_sdk/memory/_in_memory_memory_service.py @@ -111,6 +111,8 @@ async def search_memory(self, count = 0 for session_events in self._session_events[key].values(): for event_ttl in session_events: + if not event_ttl.event.is_model_visible(): + continue if not event_ttl.event.content or not event_ttl.event.content.parts: continue words_in_event = extract_words_lower(' '.join( diff --git a/trpc_agent_sdk/memory/_redis_memory_service.py b/trpc_agent_sdk/memory/_redis_memory_service.py index 0c51dc0..04e3aae 100644 --- a/trpc_agent_sdk/memory/_redis_memory_service.py +++ b/trpc_agent_sdk/memory/_redis_memory_service.py @@ -85,7 +85,7 @@ async def search_memory(self, except Exception as ex: # pylint: disable=broad-except logger.error("Error parsing event JSON: %s", ex) continue - if not event or not event.content or not event.content.parts: + if not event or not event.content or not event.content.parts or not event.is_model_visible(): continue words_in_event = extract_words_lower(' '.join( [part.text for part in event.content.parts if part.text])) diff --git a/trpc_agent_sdk/memory/_sql_memory_service.py b/trpc_agent_sdk/memory/_sql_memory_service.py index 6dbafa7..7ac2a42 100644 --- a/trpc_agent_sdk/memory/_sql_memory_service.py +++ b/trpc_agent_sdk/memory/_sql_memory_service.py @@ -200,6 +200,8 @@ async def store_session(self, session: Session, agent_context: Optional[AgentCon async with self._sql_storage.create_db_session() as sql_session: is_exist = False for event in session.events: + if not event.is_model_visible(): + continue if event.content and event.content.parts: is_exist = True # Check if the event already exists diff --git a/trpc_agent_sdk/memory/_utils.py b/trpc_agent_sdk/memory/_utils.py index 9ab2919..dfe98ae 100644 --- a/trpc_agent_sdk/memory/_utils.py +++ b/trpc_agent_sdk/memory/_utils.py @@ -6,6 +6,7 @@ """Utility functions for memory service.""" import re from datetime import datetime +from trpc_agent_sdk.events import Event def format_timestamp(timestamp: float) -> str: @@ -26,3 +27,18 @@ def extract_words_lower(text: str) -> set[str]: # Extract Chinese characters words.update(re.findall(r'[\u4e00-\u9fff]', text)) return words + + +def event_to_text(event: Event) -> str: + """Extract text from event content parts. + + Args: + event: The event to extract text from. + + Returns: + The text from the event content parts. + """ + if not event.content or not event.content.parts: + return "" + parts = [part.text for part in event.content.parts if part.text] + return " ".join(parts).strip() diff --git a/trpc_agent_sdk/memory/mem0_memory_service.py b/trpc_agent_sdk/memory/mem0_memory_service.py index 3a29768..9fccd4e 100644 --- a/trpc_agent_sdk/memory/mem0_memory_service.py +++ b/trpc_agent_sdk/memory/mem0_memory_service.py @@ -31,6 +31,8 @@ from trpc_agent_sdk.types import Part from trpc_agent_sdk.types import SearchMemoryResponse +from ._utils import event_to_text + _MEM0_KEY_METADATA = "metadata" @@ -83,7 +85,8 @@ def __init__( super().__init__(memory_service_config=memory_service_config) self._mem0 = mem0_client self._infer = infer - # only for AsyncMemoryClient, when async_mode is True, the platform will wait for the indexing to complete before returning + # only for AsyncMemoryClient, when async_mode is True, + # the platform will wait for the indexing to complete before returning self._async_mode = async_mode self._known_user_ids: Set[tuple[str, str]] = set() @@ -144,7 +147,9 @@ async def store_session(self, session: Session, agent_context: Optional[AgentCon level-1 key: session.save_key -> user_id level-2 key: session.id -> metadata["session_id"] """ - valid_events = [event for event in session.events if event.content and event.content.parts] + valid_events = [ + event for event in session.events if event.content and event.content.parts and event.is_model_visible() + ] if not valid_events: return @@ -161,7 +166,7 @@ async def store_session(self, session: Session, agent_context: Optional[AgentCon user_messages = [] assistant_messages = [] for event in valid_events: - text = self._event_to_text(event) + text = event_to_text(event) if not text: continue role = self._event_to_role(event) @@ -190,8 +195,15 @@ async def __mem0_search_memory(self, query: str, mem0_kwargs: Mem0Kwargs, limit: for key, value in mem0_kwargs.filters.items(): api_filters.append({key: value}) filters = {"AND": [{"OR": api_filters}, {"run_id": "*"}]} - search_fn = lambda: self._mem0.search( - query=query, user_id=mem0_kwargs.user_id, filters=filters, top_k=limit) + + async def search_fn(): + return await self._mem0.search( + query=query, + user_id=mem0_kwargs.user_id, + filters=filters, + top_k=limit, + ) + return await self._retry_transport("search", search_fn) kwargs: dict[str, Any] = {"user_id": mem0_kwargs.user_id, "limit": limit} if mem0_kwargs.agent_id: @@ -276,14 +288,6 @@ async def _retry_transport(self, op_name: str, call: Callable[..., Any], max_att # Internal helpers # ------------------------------------------------------------------ - @staticmethod - def _event_to_text(event: EventCls) -> str: - """Extract text from event content parts.""" - if not event.content or not event.content.parts: - return "" - parts = [part.text for part in event.content.parts if part.text] - return " ".join(parts).strip() - @staticmethod def _event_to_role(event: EventCls) -> str: """Map framework event author to Mem0 role.""" diff --git a/trpc_agent_sdk/memory/mempalace_memory_service.py b/trpc_agent_sdk/memory/mempalace_memory_service.py new file mode 100644 index 0000000..8070fd9 --- /dev/null +++ b/trpc_agent_sdk/memory/mempalace_memory_service.py @@ -0,0 +1,444 @@ +# Tencent is pleased to support the open source community by making tRPC-Agent-Python available. +# +# Copyright (C) 2026 Tencent. All rights reserved. +# +# tRPC-Agent-Python is licensed under Apache-2.0. +"""MemPalace-based memory service for local-first semantic memory.""" + +from __future__ import annotations + +import asyncio +import hashlib +import re +import time +from datetime import datetime +from typing import Any +from typing import Optional +from typing_extensions import override + +from mempalace.config import MempalaceConfig # type: ignore[import-not-found] +from mempalace.config import sanitize_content # type: ignore[import-not-found] +from mempalace.config import sanitize_name # type: ignore[import-not-found] +from mempalace.palace import get_collection # type: ignore[import-not-found] +from mempalace.searcher import search_memories # type: ignore[import-not-found] + +from trpc_agent_sdk.abc import MemoryEntry +from trpc_agent_sdk.abc import MemoryServiceABC as BaseMemoryService +from trpc_agent_sdk.abc import MemoryServiceConfig +from trpc_agent_sdk.context import AgentContext +from trpc_agent_sdk.events import Event +from trpc_agent_sdk.log import logger +from trpc_agent_sdk.sessions import Session +from trpc_agent_sdk.types import Content +from trpc_agent_sdk.types import Part +from trpc_agent_sdk.types import SearchMemoryResponse + +from ._utils import format_timestamp +from ._utils import event_to_text + +_MEMPALACE_KEY_METADATA = "mempalace_metadata" +_DEFAULT_ROOM = "conversations" +_DEFAULT_WING = "trpc_agent" +_DEFAULT_ADDED_BY = "trpc_agent" +_EVENT_TEXT_PREFIX_RE = re.compile(r"^\[([^\]]+)\]\s+([^:\n]+):\n") + +__all__ = [ + "MempalaceMemoryService", + "get_mempalace_filters", + "set_mempalace_filters", +] + + +def set_mempalace_filters(agent_context: AgentContext, filters: dict[str, Any]) -> None: + """Set MemPalace wing/room filters into agent_context.""" + if agent_context: + agent_context.with_metadata(_MEMPALACE_KEY_METADATA, filters) + + +def get_mempalace_filters(agent_context: Optional[AgentContext] = None) -> dict[str, Any]: + """Get MemPalace wing/room filters from agent_context.""" + filters: dict[str, Any] = {} + if agent_context: + filters.update(agent_context.get_metadata(_MEMPALACE_KEY_METADATA, {})) + return filters + + +def _slugify_name(value: str, default: str) -> str: + """Convert arbitrary framework keys into MemPalace-safe wing/room names.""" + value = (value or "").strip().lower() + value = re.sub(r"[^0-9a-zA-Z\u4e00-\u9fff .'-]+", "_", value) + value = re.sub(r"[_\s-]+", "_", value).strip("_. -'") + return value[:128] or default + + +class MempalaceMemoryService(BaseMemoryService): + """MemPalace-backed memory service. + + This implementation stores framework events as verbatim MemPalace drawers and + searches them with MemPalace semantic search. MemPalace is an optional + dependency; install it with ``pip install mempalace`` or the project extra. + """ + + def __init__( + self, + memory_service_config: Optional[MemoryServiceConfig] = None, + config: Optional[MempalaceConfig] = None, + wing: Optional[str] = None, + room: str = _DEFAULT_ROOM, + added_by: str = _DEFAULT_ADDED_BY, + store_only_model_visible: bool = True, + ) -> None: + super().__init__(memory_service_config=memory_service_config) + self._config = config or MempalaceConfig() + self._wing = wing + self._room = room + self._added_by = added_by + self._store_only_model_visible = store_only_model_visible + self._pending_tasks: set[asyncio.Task[None]] = set() + self._scheduled_drawer_ids: set[str] = set() + self._stored_drawer_ids: set[str] = set() + self.__cleanup_task: Optional[asyncio.Task] = None + self.__cleanup_stop_event: Optional[asyncio.Event] = None + self._start_cleanup_task() + + @override + async def store_session(self, session: Session, agent_context: Optional[AgentContext] = None) -> None: + """Store session events as verbatim MemPalace drawers.""" + filters = get_mempalace_filters(agent_context) + wing = self._resolve_wing(session.save_key, filters) + room = self._resolve_room(filters) + + events_to_store: list[tuple[Event, str, str]] = [] + for event in session.events: + if self._store_only_model_visible and not event.is_model_visible(): + continue + text = self._event_to_text(event) + if not text: + continue + drawer_id = self._drawer_id(wing, room, event.id, text) + if drawer_id in self._stored_drawer_ids or drawer_id in self._scheduled_drawer_ids: + continue + self._scheduled_drawer_ids.add(drawer_id) + events_to_store.append((event, text, drawer_id)) + if not events_to_store: + return + + task = asyncio.create_task(self._store_events_background(session, events_to_store, wing, room)) + self._pending_tasks.add(task) + task.add_done_callback(self._pending_tasks.discard) + + @override + async def search_memory( + self, + key: str, + query: str, + limit: int = 10, + agent_context: Optional[AgentContext] = None, + ) -> SearchMemoryResponse: + """Search MemPalace by primary framework key plus optional filters.""" + response = SearchMemoryResponse() + filters = get_mempalace_filters(agent_context) + wing = self._resolve_wing(key, filters) + room = filters.get("room", None) + + search_result = await self._search(query, wing, room, limit) + for item in search_result.get("results", []): + memory_text = item.get("text") + if not memory_text: + continue + metadata = item.get("metadata") or item + response.memories.append( + MemoryEntry( + content=Content(parts=[Part.from_text(text=memory_text)], role="user"), + author=self._memory_author(metadata, memory_text), + timestamp=self._memory_timestamp(metadata, memory_text), + )) + return response + + @override + async def close(self) -> None: + """Stop cleanup task and wait for pending background writes.""" + self._stop_cleanup_task() + await self._wait_pending_writes() + + async def _wait_pending_writes(self) -> None: + """Wait for pending background writes.""" + if self._pending_tasks: + await asyncio.gather(*self._pending_tasks, return_exceptions=True) + + async def delete_memory(self, wing: str, room: Optional[str] = None) -> int: + """Delete MemPalace drawers by wing, optionally limited to a room. + + Args: + wing: Wing to delete. For this service, this is usually the + slugified save_key, i.e. ``{app}/{user}``. + room: Optional room under the wing. If omitted, the whole wing is + deleted. + + Returns: + Number of matching drawers found before deletion. + """ + await self._wait_pending_writes() + try: + deleted_count = await asyncio.to_thread(self._delete_memory, wing, room) + except Exception as exc: # pylint: disable=broad-except + logger.warning("Failed to delete MemPalace memory. wing=%s, room=%s, err=%s", wing, room, exc) + return 0 + + # Deleting from storage invalidates the in-process dedupe cache. Keep it + # conservative so deleted events can be written again later if needed. + self._stored_drawer_ids.clear() + self._scheduled_drawer_ids.clear() + return deleted_count + + # ------------------------------------------------------------------ + # TTL eviction + # ------------------------------------------------------------------ + + def _start_cleanup_task(self) -> None: + """Start the background TTL cleanup task if TTL is configured.""" + if not self._memory_service_config.ttl.need_ttl_expire(): + logger.debug("MemPalace memory cleanup task disabled (ttl is disabled)") + return + + if self.__cleanup_task is not None: + logger.debug("MemPalace memory cleanup task is already running") + return + + self.__cleanup_stop_event = asyncio.Event() + self.__cleanup_task = asyncio.create_task(self._cleanup_loop()) + logger.debug("MemPalace memory cleanup task created") + + def _stop_cleanup_task(self) -> None: + """Stop the background TTL cleanup task.""" + if self.__cleanup_task is None: + return + + if self.__cleanup_stop_event is not None: + self.__cleanup_stop_event.set() + + if not self.__cleanup_task.done(): + self.__cleanup_task.cancel() + + self.__cleanup_task = None + self.__cleanup_stop_event = None + logger.debug("MemPalace memory cleanup task stopped") + + async def _cleanup_loop(self) -> None: + """Periodic background loop that evicts expired memories.""" + logger.debug("MemPalace memory cleanup task started with interval: %ss", + self._memory_service_config.ttl.cleanup_interval_seconds) + try: + while not self.__cleanup_stop_event.is_set(): + try: + await asyncio.wait_for( + self.__cleanup_stop_event.wait(), + timeout=self._memory_service_config.ttl.cleanup_interval_seconds, + ) + break + except asyncio.TimeoutError: + try: + await self._cleanup_expired_memories() + logger.debug("MemPalace memory cleanup cycle completed") + except Exception as exc: # pylint: disable=broad-except + logger.error("Error during MemPalace memory cleanup: %s", exc, exc_info=True) + except Exception as exc: # pylint: disable=broad-except + logger.error("MemPalace memory cleanup loop encountered error: %s", exc, exc_info=True) + finally: + logger.debug("MemPalace memory cleanup task stopped") + + async def _cleanup_expired_memories(self) -> None: + """Delete MemPalace drawers whose event timestamp has expired.""" + await self._wait_pending_writes() + deleted_ids = await asyncio.to_thread(self._cleanup_expired_memories_sync) + if deleted_ids: + self._stored_drawer_ids.difference_update(deleted_ids) + logger.info("MemPalace cleanup: deleted %s expired memories", len(deleted_ids)) + + async def _store_events_background( + self, + session: Session, + events_to_store: list[tuple[Event, str, str]], + wing: str, + room: str, + ) -> None: + """Store MemPalace drawers without blocking the caller.""" + drawer_ids = {drawer_id for _, _, drawer_id in events_to_store} + stored_drawer_ids: set[str] = set() + try: + stored_drawer_ids = await asyncio.to_thread(self._store_events, session, events_to_store, wing, room) + except Exception as exc: # pylint: disable=broad-except + logger.warning("Failed to store session in MemPalace. save_key=%s, session_id=%s, err=%s", session.save_key, + session.id, exc) + finally: + self._scheduled_drawer_ids.difference_update(drawer_ids) + self._stored_drawer_ids.update(stored_drawer_ids) + + def _store_events( + self, + session: Session, + events_to_store: list[tuple[Event, str, str]], + wing: str, + room: str, + ) -> set[str]: + """Synchronous MemPalace drawer upsert.""" + + collection_name = self._config.collection_name + col = get_collection(self._config.palace_path, collection_name=collection_name, create=True) + + stored_drawer_ids: set[str] = set() + safe_wing = sanitize_name(wing, "wing") + safe_room = sanitize_name(room, "room") + for event, text, drawer_id in events_to_store: + content = sanitize_content(text) + source_file = f"{session.save_key}/{session.id}/{event.id}" + metadata = { + "wing": safe_wing, + "room": safe_room, + "source_file": source_file, + "session_id": session.id, + "event_id": event.id, + "invocation_id": event.invocation_id, + "author": event.author, + "timestamp": format_timestamp(event.timestamp), + "added_by": self._added_by, + "filed_at": datetime.now().isoformat(), + "chunk_index": 0, + } + try: + existing = col.get(ids=[drawer_id]) + if existing and existing.get("ids"): + stored_drawer_ids.add(drawer_id) + continue + col.upsert(ids=[drawer_id], documents=[content], metadatas=[metadata]) + stored_drawer_ids.add(drawer_id) + except Exception as exc: # pylint: disable=broad-except + logger.warning("Failed to store MemPalace drawer. drawer_id=%s, err=%s", drawer_id, exc) + return stored_drawer_ids + + async def _search(self, query: str, wing: str, room: Optional[str], limit: int) -> dict[str, list[dict[str, Any]]]: + """Synchronous MemPalace semantic search.""" + try: + return await asyncio.to_thread( + search_memories, + query=query, + palace_path=self._config.palace_path, + wing=wing, + room=room, + n_results=limit, + ) + except Exception as exc: # pylint: disable=broad-except + logger.warning("Failed to search MemPalace. query=%s, wing=%s, room=%s, err=%s", query, wing, room, exc) + return {"results": []} + + def _delete_memory(self, wing: str, room: Optional[str] = None) -> int: + """Synchronously delete MemPalace drawers by wing/room.""" + safe_wing = sanitize_name(_slugify_name(wing, _DEFAULT_WING), "wing") + if room is None: + where: dict[str, Any] = {"wing": safe_wing} + else: + safe_room = sanitize_name(_slugify_name(room, _DEFAULT_ROOM), "room") + where = {"$and": [{"wing": safe_wing}, {"room": safe_room}]} + + col = get_collection(self._config.palace_path, collection_name=self._config.collection_name, create=False) + existing = col.get(where=where) + ids = existing.get("ids", []) if existing else [] + if ids: + col.delete(where=where) + return len(ids) + + def _cleanup_expired_memories_sync(self) -> set[str]: + """Synchronously delete expired MemPalace drawers written by this service.""" + now = time.time() + ttl_seconds = self._memory_service_config.ttl.ttl_seconds + col = get_collection(self._config.palace_path, collection_name=self._config.collection_name, create=False) + + expired_ids: list[str] = [] + offset = 0 + batch_size = 500 + while True: + batch = col.get( + where={"added_by": self._added_by}, + include=["metadatas"], + limit=batch_size, + offset=offset, + ) + ids = batch.get("ids", []) if batch else [] + metadatas = batch.get("metadatas", []) if batch else [] + if not ids: + break + + for drawer_id, metadata in zip(ids, metadatas): + metadata = metadata or {} + ts = self._parse_memory_timestamp(metadata.get("timestamp")) + if ts is not None and ts < now - ttl_seconds: + expired_ids.append(drawer_id) + + if len(ids) < batch_size: + break + offset += len(ids) + + for index in range(0, len(expired_ids), batch_size): + col.delete(ids=expired_ids[index:index + batch_size]) + + return set(expired_ids) + + def _resolve_wing(self, key: str, filters: dict[str, Any]) -> str: + return _slugify_name(filters.get("wing", self._wing or key), _DEFAULT_WING) + + def _resolve_room(self, filters: dict[str, Any]) -> str: + return _slugify_name(filters.get("room", self._room), _DEFAULT_ROOM) + + @staticmethod + def _drawer_id(wing: str, room: str, event_id: str, content: str) -> str: + digest = hashlib.sha256(f"{wing}|{room}|{event_id}|{content}".encode()).hexdigest()[:24] + return f"drawer_{wing}_{room}_{digest}" + + @staticmethod + def _memory_author(metadata: dict[str, Any], memory_text: str = "") -> str: + """Return a framework-friendly author for a retrieved memory.""" + author = metadata.get("author") + if isinstance(author, str) and author.strip(): + return author.strip() + role = metadata.get("role") + if isinstance(role, str) and role.strip(): + return role.strip() + match = _EVENT_TEXT_PREFIX_RE.match(memory_text) + if match: + return match.group(2).strip() + return "user" + + @staticmethod + def _memory_timestamp(metadata: dict[str, Any], memory_text: str = "") -> Optional[str]: + """Return the original event timestamp when available.""" + timestamp = metadata.get("timestamp") + if isinstance(timestamp, str) and timestamp.strip(): + return timestamp.strip() + match = _EVENT_TEXT_PREFIX_RE.match(memory_text) + if match: + return match.group(1).strip() + filed_at = metadata.get("filed_at") or metadata.get("created_at") + if isinstance(filed_at, str) and filed_at.strip(): + return filed_at.strip() + return None + + @staticmethod + def _parse_memory_timestamp(timestamp: Any) -> Optional[float]: + """Parse an ISO memory timestamp back to a Unix timestamp.""" + if not isinstance(timestamp, str) or not timestamp.strip(): + return None + try: + return datetime.fromisoformat(timestamp.strip()).timestamp() + except ValueError: + return None + + @classmethod + def _event_to_text(cls, event: Event) -> str: + """Extract verbatim text-like content from an event.""" + if not event.content or not event.content.parts: + return "" + text = event_to_text(event) + if not text: + return "" + timestamp = format_timestamp(event.timestamp) + return f"[{timestamp}] {event.author}:\n{text}" diff --git a/trpc_agent_sdk/planners/_plan_re_act_planner.py b/trpc_agent_sdk/planners/_plan_re_act_planner.py index a1f507d..e9fe2fb 100644 --- a/trpc_agent_sdk/planners/_plan_re_act_planner.py +++ b/trpc_agent_sdk/planners/_plan_re_act_planner.py @@ -269,27 +269,41 @@ def _build_nl_planner_instruction(self) -> str: NL planner system instruction with structured workflow guidance """ high_level_preamble = f""" -When answering the question, try to leverage the available tools to gather the information instead of your memorized knowledge. +When answering the question, try to leverage the available tools to gather the information instead of your memorized +knowledge. -Follow this process when answering the question: (1) first come up with a plan in natural language text format; (2) Then use tools to execute the plan and provide reasoning between tool code snippets to make a summary of current state and next step. Tool code snippets and reasoning should be interleaved with each other. (3) In the end, return one final answer. +Follow this process when answering the question: (1) first come up with a plan in natural language text format; +(2) Then use tools to execute the plan and provide reasoning between tool code snippets to make a summary of current +state and next step. Tool code snippets and reasoning should be interleaved with each other. (3) In the end, return one +final answer. -Follow this format when answering the question: (1) The planning part should be under {PLANNING_TAG}. (2) The tool code snippets should be under {ACTION_TAG}, and the reasoning parts should be under {REASONING_TAG}. (3) The final answer part should be under {FINAL_ANSWER_TAG}. +Follow this format when answering the question: (1) The planning part should be under {PLANNING_TAG}. (2) The tool +code snippets should be under {ACTION_TAG}, and the reasoning parts should be under {REASONING_TAG}. (3) The final +answer part should be under {FINAL_ANSWER_TAG}. """ planning_preamble = f""" Below are the requirements for the planning: -The plan is made to answer the user query if following the plan. The plan is coherent and covers all aspects of information from user query, and only involves the tools that are accessible by the agent. The plan contains the decomposed steps as a numbered list where each step should use one or multiple available tools. By reading the plan, you can intuitively know which tools to trigger or what actions to take. -If the initial plan cannot be successfully executed, you should learn from previous execution results and revise your plan. The revised plan should be under {REPLANNING_TAG}. Then use tools to follow the new plan. +The plan is made to answer the user query if following the plan. The plan is coherent and covers all aspects of +information from user query, and only involves the tools that are accessible by the agent. The plan contains the +decomposed steps as a numbered list where each step should use one or multiple available tools. By reading the plan, +you can intuitively know which tools to trigger or what actions to take. +If the initial plan cannot be successfully executed, you should learn from previous execution results and revise your +plan. The revised plan should be under {REPLANNING_TAG}. Then use tools to follow the new plan. """ reasoning_preamble = """ Below are the requirements for the reasoning: -The reasoning makes a summary of the current trajectory based on the user query and tool outputs. Based on the tool outputs and plan, the reasoning also comes up with instructions to the next steps, making the trajectory closer to the final answer. +The reasoning makes a summary of the current trajectory based on the user query and tool outputs. Based on the tool +outputs and plan, the reasoning also comes up with instructions to the next steps, making the trajectory closer to the +final answer. """ final_answer_preamble = """ Below are the requirements for the final answer: -The final answer should be precise and follow query formatting requirements. Some queries may not be answerable with the available tools and information. In those cases, inform the user why you cannot process their query and ask for more information. +The final answer should be precise and follow query formatting requirements. Some queries may not be answerable with +the available tools and information. In those cases, inform the user why you cannot process their query and ask for more +information. """ # Tool code requirements @@ -297,11 +311,13 @@ def _build_nl_planner_instruction(self) -> str: Below are the requirements for the tool code: **Custom Tools:** The available tools are described in the context and can be directly used. -- Code must be valid self-contained Python snippets with no imports and no references to tools or Python libraries that are not in the context. +- Code must be valid self-contained Python snippets with no imports and no references to tools or Python libraries that + are not in the context. - You cannot use any parameters or fields that are not explicitly defined in the APIs in the context. - The code snippets should be readable, efficient, and directly relevant to the user query and reasoning steps. - When using the tools, you should use the library name together with the function name, e.g., vertex_search.search(). -- If Python libraries are not provided in the context, NEVER write your own code other than the function calls using the provided tools. +- If Python libraries are not provided in the context, NEVER write your own code other than the function calls using the + provided tools. """ user_input_preamble = """ diff --git a/trpc_agent_sdk/server/a2a/_constants.py b/trpc_agent_sdk/server/a2a/_constants.py index 63b1a8b..8f3c279 100644 --- a/trpc_agent_sdk/server/a2a/_constants.py +++ b/trpc_agent_sdk/server/a2a/_constants.py @@ -99,3 +99,4 @@ """Constants for request euc function call name.""" TRPC_AGENT_CONTEXT_ID_SEPARATOR = "/" +"""Constants for trpc agent context id separator.""" diff --git a/trpc_agent_sdk/server/a2a/converters/_event_converter.py b/trpc_agent_sdk/server/a2a/converters/_event_converter.py index c0b4c06..0442932 100644 --- a/trpc_agent_sdk/server/a2a/converters/_event_converter.py +++ b/trpc_agent_sdk/server/a2a/converters/_event_converter.py @@ -612,11 +612,9 @@ def _a2a_part_requests_euc_auth(part: A2APart) -> bool: if not md: return False t = get_metadata(md, A2A_DATA_PART_METADATA_TYPE_KEY) - return all([ - t == A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL, - metadata_is_true(md, A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY), - root.data.get("name") == REQUEST_EUC_FUNCTION_CALL_NAME, - ]) + return (t == A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL + and metadata_is_true(md, A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY) + and root.data.get("name") == REQUEST_EUC_FUNCTION_CALL_NAME) def _a2a_part_is_long_running_function_call(part: A2APart) -> bool: @@ -625,10 +623,8 @@ def _a2a_part_is_long_running_function_call(part: A2APart) -> bool: if not md: return False t = get_metadata(md, A2A_DATA_PART_METADATA_TYPE_KEY) - return all([ - t == A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL, - metadata_is_true(md, A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY), - ]) + return (t == A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL + and metadata_is_true(md, A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY)) def _create_status_update_event( diff --git a/trpc_agent_sdk/server/a2a/executor/_a2a_agent_executor.py b/trpc_agent_sdk/server/a2a/executor/_a2a_agent_executor.py index 15bf4dc..17159bf 100644 --- a/trpc_agent_sdk/server/a2a/executor/_a2a_agent_executor.py +++ b/trpc_agent_sdk/server/a2a/executor/_a2a_agent_executor.py @@ -42,6 +42,7 @@ from trpc_agent_sdk.cancel import SessionKey from trpc_agent_sdk.cancel import is_run_cancelled from trpc_agent_sdk.context import new_agent_context +from trpc_agent_sdk.events import AgentCancelledEvent from trpc_agent_sdk.events import Event from trpc_agent_sdk.log import logger from trpc_agent_sdk.runners import Runner @@ -263,7 +264,6 @@ async def _handle_request(self, context: RequestContext, event_queue: EventQueue aggregator = TaskResultAggregator() event_callback = self._config.event_callback if self._config else None async for trpc_event in runner.run_async(**run_args): - from trpc_agent_sdk.events import AgentCancelledEvent if isinstance(trpc_event, AgentCancelledEvent): await event_queue.enqueue_event( create_cancellation_event( diff --git a/trpc_agent_sdk/tools/__init__.py b/trpc_agent_sdk/tools/__init__.py index 4412110..c38aa78 100644 --- a/trpc_agent_sdk/tools/__init__.py +++ b/trpc_agent_sdk/tools/__init__.py @@ -57,6 +57,14 @@ from .mcp_tool import StdioConnectionParams from .mcp_tool import StreamableHTTPConnectionParams from .mcp_tool import patch_mcp_cancel_scope_exit_issue +from .mempalace_tool import MempalaceAddDrawerTool +from .mempalace_tool import MempalaceDiaryReadTool +from .mempalace_tool import MempalaceDiaryWriteTool +from .mempalace_tool import MempalaceKGAddTool +from .mempalace_tool import MempalaceKGInvalidateTool +from .mempalace_tool import MempalaceKGQueryTool +from .mempalace_tool import MempalaceKGTimelineTool +from .mempalace_tool import MempalaceSearchTool from .utils import build_function_declaration from .utils import from_function_with_options from .utils import get_required_fields @@ -115,6 +123,14 @@ "StdioConnectionParams", "StreamableHTTPConnectionParams", "patch_mcp_cancel_scope_exit_issue", + "MempalaceAddDrawerTool", + "MempalaceDiaryReadTool", + "MempalaceDiaryWriteTool", + "MempalaceKGAddTool", + "MempalaceKGInvalidateTool", + "MempalaceKGQueryTool", + "MempalaceKGTimelineTool", + "MempalaceSearchTool", "build_function_declaration", "from_function_with_options", "get_required_fields", diff --git a/trpc_agent_sdk/tools/_agent_tool.py b/trpc_agent_sdk/tools/_agent_tool.py index f1ac882..80fd5d1 100644 --- a/trpc_agent_sdk/tools/_agent_tool.py +++ b/trpc_agent_sdk/tools/_agent_tool.py @@ -73,14 +73,14 @@ class AgentTool(BaseTool): """A tool that wraps an agent. - This tool allows an agent to be called as a tool within a larger application. - The agent's input schema is used to define the tool's input parameters, and - the agent's output is returned as the tool's result. - - Attributes: - agent: The agent to wrap. - skip_summarization: Whether to skip summarization of the agent output. - """ + This tool allows an agent to be called as a tool within a larger application. + The agent's input schema is used to define the tool's input parameters, and + the agent's output is returned as the tool's result. + + Attributes: + agent: The agent to wrap. + skip_summarization: Whether to skip summarization of the agent output. + """ def __init__(self, agent: AgentABC, diff --git a/trpc_agent_sdk/tools/_load_memory_tool.py b/trpc_agent_sdk/tools/_load_memory_tool.py index 2599121..411fa2d 100644 --- a/trpc_agent_sdk/tools/_load_memory_tool.py +++ b/trpc_agent_sdk/tools/_load_memory_tool.py @@ -58,7 +58,7 @@ async def load_memory(query: str, tool_context: InvocationContext) -> dict[str, """ search_memory_response = await tool_context.search_memory(query) rsp = LoadMemoryResponse(memories=search_memory_response.memories) - return json.dumps(rsp.model_dump()) + return json.dumps(rsp.model_dump(exclude_none=True), ensure_ascii=False) class LoadMemoryTool(FunctionTool): diff --git a/trpc_agent_sdk/tools/mempalace_tool.py b/trpc_agent_sdk/tools/mempalace_tool.py new file mode 100644 index 0000000..2a79f22 --- /dev/null +++ b/trpc_agent_sdk/tools/mempalace_tool.py @@ -0,0 +1,527 @@ +# Tencent is pleased to support the open source community by making tRPC-Agent-Python available. +# +# Copyright (C) 2026 Tencent. All rights reserved. +# +# tRPC-Agent-Python is licensed under Apache-2.0. +"""MemPalace tools with MCP-compatible names.""" + +from __future__ import annotations + +import asyncio +import hashlib +import os +import re +from datetime import date +from datetime import datetime +from typing import Any +from typing import Optional +from typing_extensions import override + +from mempalace.config import MempalaceConfig # type: ignore[import-not-found] +from mempalace.config import sanitize_content # type: ignore[import-not-found] +from mempalace.config import sanitize_name # type: ignore[import-not-found] +from mempalace.knowledge_graph import DEFAULT_KG_PATH # type: ignore[import-not-found] +from mempalace.knowledge_graph import KnowledgeGraph # type: ignore[import-not-found] +from mempalace.palace import get_collection # type: ignore[import-not-found] +from mempalace.searcher import search_memories # type: ignore[import-not-found] + +from trpc_agent_sdk.context import InvocationContext +from trpc_agent_sdk.types import FunctionDeclaration +from trpc_agent_sdk.types import Schema +from trpc_agent_sdk.types import Type + +from ._base_tool import BaseTool + +__all__ = [ + "MempalaceSearchTool", + "MempalaceAddDrawerTool", + "MempalaceDiaryWriteTool", + "MempalaceDiaryReadTool", + "MempalaceKGQueryTool", + "MempalaceKGAddTool", + "MempalaceKGInvalidateTool", + "MempalaceKGTimelineTool", +] + + +def _string_schema(description: str) -> Schema: + return Schema(type=Type.STRING, description=description) + + +def _integer_schema(description: str) -> Schema: + return Schema(type=Type.INTEGER, description=description) + + +def _number_schema(description: str) -> Schema: + return Schema(type=Type.NUMBER, description=description) + + +def _optional_str(args: dict[str, Any], key: str) -> Optional[str]: + value = args.get(key) + if value is None: + return None + value = str(value).strip() + return value or None + + +def _scope_name(value: str, field_name: str) -> str: + """Normalize MemPalace scope-like names to avoid duplicate logical scopes.""" + value = sanitize_name(value, field_name) + return sanitize_name(re.sub(r"[\s-]+", "_", value.lower()), field_name) + + +class _MempalaceBaseTool(BaseTool): + """Base class for local MemPalace-backed tools.""" + + def __init__( + self, + *, + name: str, + description: str, + palace_path: Optional[str] = None, + kg_path: Optional[str] = None, + filters_name: Optional[list[str]] = None, + filters: Optional[list[Any]] = None, + ) -> None: + super().__init__(name=name, description=description, filters_name=filters_name, filters=filters) + self._palace_path = palace_path + self._kg_path = kg_path + + def _get_palace_path(self) -> str: + return self._palace_path or MempalaceConfig().palace_path + + def _get_collection(self, *, create: bool = False): + config = MempalaceConfig() + return get_collection( + self._palace_path or config.palace_path, + collection_name=config.collection_name, + create=create, + ) + + def _get_knowledge_graph(self): + kg_path = self._kg_path + if kg_path is None and self._palace_path: + kg_path = os.path.join(self._palace_path, "knowledge_graph.sqlite3") + return KnowledgeGraph(db_path=kg_path or DEFAULT_KG_PATH) + + async def _run_in_thread(self, fn, *args, **kwargs): + try: + return await asyncio.to_thread(fn, *args, **kwargs) + except ImportError as exc: + return { + "success": False, + "error": f"MemPalace is not installed: {exc}. Install with `pip install mempalace`.", + } + except Exception as exc: # pylint: disable=broad-except + return {"success": False, "error": str(exc)} + + +class MempalaceSearchTool(_MempalaceBaseTool): + """Search MemPalace semantic memory.""" + + def __init__(self, palace_path: Optional[str] = None, **kwargs: Any) -> None: + super().__init__( + name="mempalace_search", + description="Search MemPalace semantic memory. Returns verbatim drawer content with metadata.", + palace_path=palace_path, + filters_name=kwargs.pop("filters_name", None), + filters=kwargs.pop("filters", None), + ) + + @override + def _get_declaration(self) -> FunctionDeclaration | None: + return FunctionDeclaration( + name=self.name, + description=self.description, + parameters=Schema( + type=Type.OBJECT, + properties={ + "query": _string_schema("What to search for."), + "limit": _integer_schema("Maximum number of results. Default: 5."), + "wing": _string_schema("Optional wing/project/user scope filter."), + "room": _string_schema("Optional room/topic filter."), + }, + required=["query"], + ), + ) + + @override + async def _run_async_impl(self, *, tool_context: InvocationContext, args: dict[str, Any]) -> dict: + + def _search(): + return search_memories( + query=args["query"], + palace_path=self._get_palace_path(), + wing=_scope_name(wing, "wing") if (wing := _optional_str(args, "wing")) else None, + room=_scope_name(room, "room") if (room := _optional_str(args, "room")) else None, + n_results=int(args.get("limit") or 5), + ) + + return await self._run_in_thread(_search) + + +class MempalaceAddDrawerTool(_MempalaceBaseTool): + """Add a verbatim drawer to MemPalace.""" + + def __init__(self, palace_path: Optional[str] = None, added_by: str = "trpc-agent", **kwargs: Any) -> None: + super().__init__( + name="mempalace_add_drawer", + description="File verbatim content into MemPalace under a wing and room.", + palace_path=palace_path, + filters_name=kwargs.pop("filters_name", None), + filters=kwargs.pop("filters", None), + ) + self._added_by = added_by + + @override + def _get_declaration(self) -> FunctionDeclaration | None: + return FunctionDeclaration( + name=self.name, + description=self.description, + parameters=Schema( + type=Type.OBJECT, + properties={ + "wing": _string_schema("Wing/project/user scope to store under."), + "room": _string_schema("Room/topic to store under."), + "content": _string_schema("Verbatim content to store."), + "source_file": _string_schema("Optional source identifier."), + }, + required=["wing", "room", "content"], + ), + ) + + @override + async def _run_async_impl(self, *, tool_context: InvocationContext, args: dict[str, Any]) -> dict: + + def _add_drawer(): + wing = _scope_name(args["wing"], "wing") + room = _scope_name(args["room"], "room") + content = sanitize_content(args["content"]) + source_file = _optional_str(args, "source_file") or "" + col = self._get_collection(create=True) + drawer_id = f"drawer_{wing}_{room}_{hashlib.sha256((wing + room + content).encode()).hexdigest()[:24]}" + existing = col.get(ids=[drawer_id]) + if existing and existing.get("ids"): + return {"success": True, "reason": "already_exists", "drawer_id": drawer_id} + col.upsert( + ids=[drawer_id], + documents=[content], + metadatas=[{ + "wing": wing, + "room": room, + "source_file": source_file, + "chunk_index": 0, + "added_by": self._added_by, + "filed_at": datetime.now().isoformat(), + }], + ) + return {"success": True, "drawer_id": drawer_id, "wing": wing, "room": room} + + return await self._run_in_thread(_add_drawer) + + +class MempalaceDiaryWriteTool(_MempalaceBaseTool): + """Write an agent diary entry.""" + + def __init__(self, palace_path: Optional[str] = None, **kwargs: Any) -> None: + super().__init__( + name="mempalace_diary_write", + description="Write an agent diary entry into MemPalace.", + palace_path=palace_path, + filters_name=kwargs.pop("filters_name", None), + filters=kwargs.pop("filters", None), + ) + + @override + def _get_declaration(self) -> FunctionDeclaration | None: + return FunctionDeclaration( + name=self.name, + description=self.description, + parameters=Schema( + type=Type.OBJECT, + properties={ + "agent_name": _string_schema("Agent name. Defaults to the current agent when omitted."), + "entry": _string_schema("Diary entry content."), + "topic": _string_schema("Topic tag. Default: general."), + "wing": _string_schema("Optional wing. Default: wing_."), + }, + required=["entry"], + ), + ) + + @override + async def _run_async_impl(self, *, tool_context: InvocationContext, args: dict[str, Any]) -> dict: + + def _write(): + agent_name = _scope_name(_optional_str(args, "agent_name") or tool_context.agent_name, "agent_name") + entry = sanitize_content(args["entry"]) + topic = _scope_name(_optional_str(args, "topic") or "general", "topic") + wing = _optional_str(args, "wing") + wing = _scope_name(wing, "wing") if wing else f"wing_{agent_name}" + now = datetime.now() + entry_id = (f"diary_{wing}_{now.strftime('%Y%m%d_%H%M%S%f')}_" + f"{hashlib.sha256(entry.encode()).hexdigest()[:12]}") + col = self._get_collection(create=True) + col.add( + ids=[entry_id], + documents=[entry], + metadatas=[{ + "wing": wing, + "room": "diary", + "hall": "hall_diary", + "topic": topic, + "type": "diary_entry", + "agent": agent_name, + "filed_at": now.isoformat(), + "date": now.strftime("%Y-%m-%d"), + }], + ) + return {"success": True, "entry_id": entry_id, "agent": agent_name, "topic": topic} + + return await self._run_in_thread(_write) + + +class MempalaceDiaryReadTool(_MempalaceBaseTool): + """Read recent agent diary entries.""" + + def __init__(self, palace_path: Optional[str] = None, **kwargs: Any) -> None: + super().__init__( + name="mempalace_diary_read", + description="Read recent MemPalace diary entries for an agent.", + palace_path=palace_path, + filters_name=kwargs.pop("filters_name", None), + filters=kwargs.pop("filters", None), + ) + + @override + def _get_declaration(self) -> FunctionDeclaration | None: + return FunctionDeclaration( + name=self.name, + description=self.description, + parameters=Schema( + type=Type.OBJECT, + properties={ + "agent_name": _string_schema("Agent name. Defaults to the current agent when omitted."), + "last_n": _integer_schema("Number of recent entries. Default: 10."), + "wing": _string_schema("Optional wing filter."), + }, + ), + ) + + @override + async def _run_async_impl(self, *, tool_context: InvocationContext, args: dict[str, Any]) -> dict: + + def _read(): + agent_name = _scope_name(_optional_str(args, "agent_name") or tool_context.agent_name, "agent_name") + wing = _optional_str(args, "wing") + if wing: + wing = _scope_name(wing, "wing") + last_n = max(1, min(int(args.get("last_n") or 10), 100)) + col = self._get_collection(create=False) + conditions = [{"room": "diary"}, {"agent": agent_name}] + if wing: + conditions.insert(0, {"wing": wing}) + results = col.get(where={"$and": conditions}, include=["documents", "metadatas"], limit=10000) + if not results.get("ids"): + return {"agent": agent_name, "entries": [], "message": "No diary entries yet."} + entries = [] + for doc, meta in zip(results.get("documents", []), results.get("metadatas", [])): + meta = meta or {} + entries.append({ + "date": meta.get("date", ""), + "timestamp": meta.get("filed_at", ""), + "topic": meta.get("topic", ""), + "content": doc, + }) + entries.sort(key=lambda item: item["timestamp"], reverse=True) + entries = entries[:last_n] + return {"agent": agent_name, "entries": entries, "total": len(results["ids"]), "showing": len(entries)} + + return await self._run_in_thread(_read) + + +class MempalaceKGQueryTool(_MempalaceBaseTool): + """Query MemPalace knowledge graph relationships.""" + + def __init__(self, palace_path: Optional[str] = None, kg_path: Optional[str] = None, **kwargs: Any) -> None: + super().__init__( + name="mempalace_kg_query", + description="Query the MemPalace knowledge graph for an entity's relationships.", + palace_path=palace_path, + kg_path=kg_path, + filters_name=kwargs.pop("filters_name", None), + filters=kwargs.pop("filters", None), + ) + + @override + def _get_declaration(self) -> FunctionDeclaration | None: + return FunctionDeclaration( + name=self.name, + description=self.description, + parameters=Schema( + type=Type.OBJECT, + properties={ + "entity": _string_schema("Entity to query."), + "as_of": _string_schema("Optional date filter in YYYY-MM-DD."), + "direction": _string_schema("outgoing, incoming, or both. Default: both."), + }, + required=["entity"], + ), + ) + + @override + async def _run_async_impl(self, *, tool_context: InvocationContext, args: dict[str, Any]) -> dict: + + def _query(): + direction = _optional_str(args, "direction") or "both" + if direction not in ("outgoing", "incoming", "both"): + return {"error": "direction must be 'outgoing', 'incoming', or 'both'"} + kg = self._get_knowledge_graph() + entity = args["entity"] + as_of = _optional_str(args, "as_of") + facts = kg.query_entity(entity, as_of=as_of, direction=direction) + return {"entity": entity, "as_of": as_of, "facts": facts, "count": len(facts)} + + return await self._run_in_thread(_query) + + +class MempalaceKGAddTool(_MempalaceBaseTool): + """Add a fact to the MemPalace knowledge graph.""" + + def __init__(self, palace_path: Optional[str] = None, kg_path: Optional[str] = None, **kwargs: Any) -> None: + super().__init__( + name="mempalace_kg_add", + description="Add a relationship fact to the MemPalace knowledge graph.", + palace_path=palace_path, + kg_path=kg_path, + filters_name=kwargs.pop("filters_name", None), + filters=kwargs.pop("filters", None), + ) + + @override + def _get_declaration(self) -> FunctionDeclaration | None: + return FunctionDeclaration( + name=self.name, + description=self.description, + parameters=Schema( + type=Type.OBJECT, + properties={ + "subject": _string_schema("Subject entity."), + "predicate": _string_schema("Relationship type."), + "object": _string_schema("Object entity."), + "valid_from": _string_schema("Optional start date in YYYY-MM-DD."), + "valid_to": _string_schema("Optional end date in YYYY-MM-DD."), + "confidence": _number_schema("Optional confidence score 0.0-1.0."), + "source_file": _string_schema("Optional provenance source file."), + "source_drawer_id": _string_schema("Optional provenance drawer ID."), + }, + required=["subject", "predicate", "object"], + ), + ) + + @override + async def _run_async_impl(self, *, tool_context: InvocationContext, args: dict[str, Any]) -> dict: + + def _add(): + kg = self._get_knowledge_graph() + triple_id = kg.add_triple( + args["subject"], + args["predicate"], + args["object"], + valid_from=_optional_str(args, "valid_from"), + valid_to=_optional_str(args, "valid_to"), + confidence=float(args.get("confidence") or 1.0), + source_file=_optional_str(args, "source_file"), + source_drawer_id=_optional_str(args, "source_drawer_id"), + ) + return { + "success": True, + "triple_id": triple_id, + "fact": f"{args['subject']} -> {args['predicate']} -> {args['object']}", + } + + return await self._run_in_thread(_add) + + +class MempalaceKGInvalidateTool(_MempalaceBaseTool): + """Invalidate a current MemPalace knowledge graph fact.""" + + def __init__(self, palace_path: Optional[str] = None, kg_path: Optional[str] = None, **kwargs: Any) -> None: + super().__init__( + name="mempalace_kg_invalidate", + description="Mark a MemPalace knowledge graph fact as no longer current.", + palace_path=palace_path, + kg_path=kg_path, + filters_name=kwargs.pop("filters_name", None), + filters=kwargs.pop("filters", None), + ) + + @override + def _get_declaration(self) -> FunctionDeclaration | None: + return FunctionDeclaration( + name=self.name, + description=self.description, + parameters=Schema( + type=Type.OBJECT, + properties={ + "subject": _string_schema("Subject entity."), + "predicate": _string_schema("Relationship type."), + "object": _string_schema("Object entity."), + "ended": _string_schema("Optional end date in YYYY-MM-DD. Default: today."), + }, + required=["subject", "predicate", "object"], + ), + ) + + @override + async def _run_async_impl(self, *, tool_context: InvocationContext, args: dict[str, Any]) -> dict: + + def _invalidate(): + ended = _optional_str(args, "ended") or date.today().isoformat() + kg = self._get_knowledge_graph() + kg.invalidate(args["subject"], args["predicate"], args["object"], ended=ended) + return { + "success": True, + "fact": f"{args['subject']} -> {args['predicate']} -> {args['object']}", + "ended": ended, + } + + return await self._run_in_thread(_invalidate) + + +class MempalaceKGTimelineTool(_MempalaceBaseTool): + """Read the MemPalace knowledge graph timeline.""" + + def __init__(self, palace_path: Optional[str] = None, kg_path: Optional[str] = None, **kwargs: Any) -> None: + super().__init__( + name="mempalace_kg_timeline", + description="Get chronological MemPalace knowledge graph facts, optionally for one entity.", + palace_path=palace_path, + kg_path=kg_path, + filters_name=kwargs.pop("filters_name", None), + filters=kwargs.pop("filters", None), + ) + + @override + def _get_declaration(self) -> FunctionDeclaration | None: + return FunctionDeclaration( + name=self.name, + description=self.description, + parameters=Schema( + type=Type.OBJECT, + properties={ + "entity": _string_schema("Optional entity to filter timeline for."), + }, + ), + ) + + @override + async def _run_async_impl(self, *, tool_context: InvocationContext, args: dict[str, Any]) -> dict: + + def _timeline(): + entity = _optional_str(args, "entity") + kg = self._get_knowledge_graph() + timeline = kg.timeline(entity) + return {"entity": entity or "all", "timeline": timeline, "count": len(timeline)} + + return await self._run_in_thread(_timeline) diff --git a/trpc_agent_sdk/types/__init__.py b/trpc_agent_sdk/types/__init__.py index 75daa59..06d26a3 100644 --- a/trpc_agent_sdk/types/__init__.py +++ b/trpc_agent_sdk/types/__init__.py @@ -9,7 +9,7 @@ # All exported types are available in __all__ """Types module for TRPC Agent framework.""" -from google.genai.types import * +from google.genai.types import * # noqa: F401,F403 from ._agent_types import ActiveStreamingTool from ._agent_types import LiveRequest