Skip to content

Commit 9e0117b

Browse files
committed
fix(storage): 优化 SQL 存储兼容性
- 新增通用的 Content 清理逻辑,在 SQL Session 和 Memory 存储读写时过滤空的 Content Part。 - 修复 MySQL 下 DynamicPickleType 写入 LONGBLOB 时未序列化导致的类型错误。 - 支持通过 sessionmaker_kwargs 向 SqlStorage 传递 sessionmaker 配置,例如 expire_on_commit。 - 将 SQL Session 中显式更新 update_time 的逻辑改为使用 datetime.now(),避免 func.now() 带来的 ORM 状态问题。 - 统一通过当前事件循环创建 Session 和 Memory 服务的清理任务。 - 优化 SQL Session 示例输出,在助手回复前打印用户问题。 - 在 SQL Memory/Session 示例中关闭 thinking 输出,使演示结果更清晰。 - 增加 SQL Content 清理和 MySQL DynamicPickleType 序列化相关测试。
1 parent ca82991 commit 9e0117b

16 files changed

Lines changed: 172 additions & 74 deletions

File tree

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ tRPC-Agent-Python provides an end-to-end foundation for agent building, orchestr
1616

1717
- **Multi-paradigm agent orchestration**: Built-in orchestration supports `ChainAgent` / `ParallelAgent` / `CycleAgent` / `TransferAgent`, with `GraphAgent` for graph-based orchestration.
1818
- **Graph orchestration capability (`GraphAgent`)**: Use DSL to orchestrate `Agent` / `Tool` / `MCP` / `Knowledge` / `CodeExecutor` in one unified flow.
19-
- **Efficient integration with Python AI ecosystems**: Agent ecosystem extensions (`claude-agent-sdk` / `LangGraph`, etc.) / Tool ecosystem extensions (`mcp`, etc.) / Knowledge ecosystem extensions (`LangChain`, etc.) / Model ecosystem extensions (`LiteLLM`, etc.) / Memory ecosystem extensions (`Mem0`, etc.).
19+
- **Efficient integration with Python AI ecosystems**: Agent ecosystem extensions (`claude-agent-sdk` / `LangGraph`, etc.) / Tool ecosystem extensions (`mcp`, etc.) / Knowledge ecosystem extensions (`LangChain`, etc.) / Model ecosystem extensions (`LiteLLM`, etc.) / Memory ecosystem extensions (`Mem0`, `Mempalace`, etc.).
2020
- **Agent ecosystem extensions**: Supports `LangGraphAgent` / `ClaudeAgent` / `TeamAgent` (Agno-Like).
2121
- **Tool ecosystem extensions**: `FunctionTool` / File tools / `MCPToolset` / LangChain Tool / Agent-as-Tool.
22-
- **Complete memory capability (`Session` / `Memory`)**: `Session` manages messages and state within a single session, while `Memory` manages cross-session long-term memory and personalization. Persistence supports `InMemory` / `Redis` / `SQL`; `Memory` also supports `Mem0`.
22+
- **Complete memory capability (`Session` / `Memory`)**: `Session` manages messages and state within a single session, while `Memory` manages cross-session long-term memory and personalization. Persistence supports `InMemory` / `Redis` / `SQL`; `Memory` also supports `Mem0``Mempalace`.
2323
- **Production-grade knowledge capability**: Built on LangChain components with first-class RAG support.
2424
- **CodeExecutor extension capability**: Supports local / container executors for code execution and task grounding.
2525
- **Skills extension capability**: Supports `SKILL.md`-based skill systems for reusable capabilities and dynamic tooling.
@@ -84,7 +84,7 @@ pip install trpc-agent-py
8484
Install optional capabilities as needed:
8585

8686
```bash
87-
pip install trpc-agent-py[a2a,ag-ui,knowledge,agent-claude,mem0,langfuse]
87+
pip install trpc-agent-py[a2a,ag-ui,knowledge,agent-claude,mem0, Mempalace, langfuse]
8888
```
8989

9090
### Develop Weather Agent
@@ -457,7 +457,7 @@ Related docs:
457457
This group helps you:
458458

459459
- Session: manage per-session messages, summaries, and state
460-
- Memory: manage cross-session long-term memory (including Mem0)
460+
- Memory: manage cross-session long-term memory (including Mem0, Mempalace)
461461
- Knowledge: cover document loading, retrieval, RAG, and prompt templates
462462

463463
### 10. Serving and Protocols
@@ -545,12 +545,12 @@ The framework is organized in an event-driven architecture where each layer can
545545
- **Runner layer**: Unified execution entry, coordinating Session / Memory / Artifact services
546546
- **Tool layer**: FunctionTool / file tools / MCPToolset / Skill tools
547547
- **Model layer**: OpenAIModel / AnthropicModel / LiteLLMModel
548-
- **Memory layer**: SessionService / MemoryService / SessionSummarizer / Mem0MemoryService
548+
- **Memory layer**: SessionService / MemoryService / SessionSummarizer / Mem0MemoryService / MempalaceMemoryService
549549
- **Knowledge layer**: Production-grade LangChain-based knowledge and RAG capability
550550
- **Execution and skill layer**: CodeExecutor (local / container) / Skills
551551
- **Service layer**: FastAPI / A2A / AG-UI
552552
- **Observability layer**: OpenTelemetry tracing/metrics, integrable with platforms like Langfuse
553-
- **Ecosystem adapter layer**: claude-agent-sdk / mcp / LangChain / LiteLLM / Mem0 plugged into the main chain through model/tool/memory adapters
553+
- **Ecosystem adapter layer**: claude-agent-sdk / mcp / LangChain / LiteLLM / Mem0 / Mempalace plugged into the main chain through model/tool/memory adapters
554554

555555
Key packages:
556556

README.zh_CN.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ tRPC-Agent-Python 提供从 Agent 构建、编排、工具接入、会话记忆
1616

1717
- **多范式 Agent 编排**:预设编排支持 ChainAgent / ParallelAgent / CycleAgent / TransferAgent,同时支持 GraphAgent 图编排
1818
- **图编排能力(GraphAgent)**:通过 DSL 统一编排 Agent / Tool / MCP / Knowledge / CodeExecutor
19-
- **高效接入 Python AI 生态扩展**:Agent 生态扩展(claude-agent-sdk / LangGraph 等)/ 工具生态扩展(mcp 等)/ 知识库生态扩展(LangChain 等)/ 模型生态扩展(LiteLLM 等)/ 记忆生态扩展(Mem0
19+
- **高效接入 Python AI 生态扩展**:Agent 生态扩展(claude-agent-sdk / LangGraph 等)/ 工具生态扩展(mcp 等)/ 知识库生态扩展(LangChain 等)/ 模型生态扩展(LiteLLM 等)/ 记忆生态扩展(Mem0、Mempalace等
2020
- **Agent 生态扩展**:支持 LangGraphAgent / ClaudeAgent / TeamAgent(Agno-Like)
2121
- **Tool 生态扩展**:FunctionTool / 文件工具 / MCPToolset / LangChain Tool / Agent-as-Tool
22-
- **完善的记忆能力(Session / Memory)**:Session 负责单会话内的消息与状态管理,Memory 负责跨会话长期记忆与个性化信息沉淀。持久化支持 InMemory / Redis / SQL,Memory 还支持 Mem0
22+
- **完善的记忆能力(Session / Memory)**:Session 负责单会话内的消息与状态管理,Memory 负责跨会话长期记忆与个性化信息沉淀。持久化支持 InMemory / Redis / SQL,Memory 还支持 Mem0、Mempalace
2323
- **生产级知识库能力**:知识库能力基于 LangChain 组件构建,支持 RAG 场景
2424
- **CodeExecutor 扩展能力**:支持本地 / 容器执行器,用于支持 Agent 的代码执行与任务落地能力
2525
- **Skills 扩展能力**:支持 SKILL.md 技能体系,用于支持 Agent 的技能复用与动态工具化能力
@@ -84,7 +84,7 @@ pip install trpc-agent-py
8484
按需安装扩展能力:
8585

8686
```bash
87-
pip install trpc-agent-py[a2a,ag-ui,knowledge,agent-claude,mem0,langfuse]
87+
pip install trpc-agent-py[a2a,ag-ui,knowledge,agent-claude,mem0, Mempalace, langfuse]
8888
```
8989

9090

@@ -458,7 +458,7 @@ skill_tool_set = SkillToolSet(repository=repository, run_tool_kwargs=tool_kwargs
458458
这组示例可以帮你:
459459

460460
- Session:管理单会话的消息、摘要与状态
461-
- Memory:管理跨会话长期记忆(含 Mem0)
461+
- Memory:管理跨会话长期记忆(含 Mem0, Mempalace
462462
- Knowledge:覆盖文档加载、检索、RAG、提示模板等链路
463463

464464
### 10. 服务化与协议
@@ -546,12 +546,12 @@ skill_tool_set = SkillToolSet(repository=repository, run_tool_kwargs=tool_kwargs
546546
- **Runner 层**:统一执行入口,负责 Session/Memory/Artifact 等服务协同
547547
- **Tool 层**:FunctionTool / 文件工具 / MCPToolset / Skill 工具
548548
- **Model 层**:OpenAIModel / AnthropicModel / LiteLLMModel
549-
- **Memory 层**:SessionService / MemoryService / SessionSummarizer / Mem0MemoryService
549+
- **Memory 层**:SessionService / MemoryService / SessionSummarizer / Mem0MemoryService / MempalaceMemoryService
550550
- **Knowledge 层**:基于 LangChain 的生产级知识库能力(RAG)
551551
- **执行与技能层**:CodeExecutor(本地/容器)/ Skills
552552
- **服务层**:FastAPI / A2A / AG-UI
553553
- **观测层**:OpenTelemetry tracing/metrics,可对接 Langfuse 等平台
554-
- **生态适配层**:claude-agent-sdk / mcp / LangChain / LiteLLM / Mem0,通过模型/工具/记忆适配器接入主链路
554+
- **生态适配层**:claude-agent-sdk / mcp / LangChain / LiteLLM / Mem0 / MemoryService,通过模型/工具/记忆适配器接入主链路
555555

556556
关键包一览:
557557

examples/memory_service_with_sql/agent/agent.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from trpc_agent_sdk.models import OpenAIModel
1111
from trpc_agent_sdk.tools import FunctionTool
1212
from trpc_agent_sdk.tools import load_memory_tool
13+
from trpc_agent_sdk.types import GenerateContentConfig
14+
from trpc_agent_sdk.types import HttpOptions
1315

1416
from .config import get_model_config
1517
from .prompts import INSTRUCTION
@@ -25,12 +27,18 @@ def _create_model() -> LLMModel:
2527

2628
def create_agent() -> LlmAgent:
2729
""" Create an agent"""
30+
generate_content_config = GenerateContentConfig(
31+
http_options=HttpOptions(extra_body={"chat_template_kwargs": {
32+
"enable_thinking": False
33+
}}),
34+
)
2835
agent = LlmAgent(
2936
name="assistant",
3037
description="A helpful assistant for conversation",
3138
model=_create_model(), # You can change this to your preferred model
3239
instruction=INSTRUCTION,
3340
tools=[FunctionTool(get_weather_report), load_memory_tool],
41+
generate_content_config=generate_content_config,
3442
)
3543
return agent
3644

examples/session_service_with_sql/agent/agent.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from trpc_agent_sdk.models import LLMModel
1010
from trpc_agent_sdk.models import OpenAIModel
1111
from trpc_agent_sdk.tools import FunctionTool
12+
from trpc_agent_sdk.types import GenerateContentConfig
13+
from trpc_agent_sdk.types import HttpOptions
1214

1315
from .config import get_model_config
1416
from .prompts import INSTRUCTION
@@ -24,12 +26,18 @@ def _create_model() -> LLMModel:
2426

2527
def create_agent() -> LlmAgent:
2628
""" Create an agent"""
29+
generate_content_config = GenerateContentConfig(
30+
http_options=HttpOptions(extra_body={"chat_template_kwargs": {
31+
"enable_thinking": False
32+
}}),
33+
)
2734
agent = LlmAgent(
2835
name="assistant",
2936
description="A helpful assistant for conversation",
3037
model=_create_model(), # You can change this to your preferred model
3138
instruction=INSTRUCTION,
3239
tools=[FunctionTool(get_weather_report)],
40+
generate_content_config=generate_content_config,
3341
)
3442
return agent
3543

examples/session_service_with_sql/run_agent.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,9 @@ async def run_weather_agent():
7676
]
7777

7878
for query in demo_queries:
79-
# Use a new session for each query
80-
8179
user_content = Content(parts=[Part.from_text(text=query)])
8280

81+
print(f"👤 User: {query}")
8382
print("🤖 Assistant: ", end="", flush=True)
8483
async for event in runner.run_async(user_id=user_id, session_id=current_session_id, new_message=user_content):
8584
# Check if event.content exists

tests/memory/test_sql_memory_service.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -278,17 +278,6 @@ async def test_store_skips_events_without_content(self):
278278
svc._sql_storage.add.assert_not_called()
279279
svc._sql_storage.commit.assert_not_called()
280280

281-
async def test_store_raises_on_non_session(self):
282-
svc = SqlMemoryService.__new__(SqlMemoryService)
283-
svc._memory_service_config = _make_config_no_ttl()
284-
svc._sql_storage = _patch_sql_storage()
285-
svc._SqlMemoryService__cleanup_task = None
286-
svc._SqlMemoryService__cleanup_stop_event = None
287-
288-
with pytest.raises(TypeError, match="Content must be a Session"):
289-
await svc.store_session("not a session")
290-
291-
292281
# ---------------------------------------------------------------------------
293282
# SqlMemoryService — search_memory
294283
# ---------------------------------------------------------------------------

tests/sessions/test_sql_session_service.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,39 @@ def test_from_event_with_function_call(self):
9090
storage_event = SessionStorageEvent.from_event(session, event)
9191
assert storage_event.content is not None
9292

93+
def test_from_event_drops_empty_parts(self):
94+
session = Session(id="s1", app_name="app", user_id="user", save_key="k")
95+
event = Event(
96+
invocation_id="inv-1",
97+
author="agent",
98+
content=Content(parts=[Part()]),
99+
)
100+
storage_event = SessionStorageEvent.from_event(session, event)
101+
assert storage_event.content is None
102+
93103
def test_from_event_no_content(self):
94104
session = Session(id="s1", app_name="app", user_id="user", save_key="k")
95105
event = Event(invocation_id="inv-1", author="agent", actions=EventActions())
96106
storage_event = SessionStorageEvent.from_event(session, event)
97107
assert storage_event.content is None
98108

109+
def test_to_event_drops_legacy_empty_parts(self):
110+
storage_event = SessionStorageEvent(
111+
id="e1",
112+
app_name="app",
113+
user_id="user",
114+
session_id="s1",
115+
invocation_id="inv-1",
116+
author="agent",
117+
actions=EventActions(),
118+
long_running_tool_ids=set(),
119+
timestamp=datetime.now(),
120+
model_flags=1,
121+
content={"parts": [{}], "role": "model"},
122+
)
123+
event = storage_event.to_event()
124+
assert event.content is None
125+
99126
def test_long_running_tool_ids_property(self):
100127
session = Session(id="s1", app_name="app", user_id="user", save_key="k")
101128
event = _make_event()

tests/storage/test_sql_common.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,14 @@ def test_process_bind_param_spanner(self):
332332
result = dpt.process_bind_param(value, dialect)
333333
assert pickle.loads(result) == value
334334

335+
def test_process_bind_param_mysql(self):
336+
dpt = DynamicPickleType()
337+
dialect = _make_dialect("mysql")
338+
value = {"key": "value", "nums": [1, 2, 3]}
339+
result = dpt.process_bind_param(value, dialect)
340+
assert isinstance(result, bytes)
341+
assert pickle.loads(result) == value
342+
335343
def test_process_bind_param_non_spanner(self):
336344
dpt = DynamicPickleType()
337345
dialect = _make_dialect("sqlite")
@@ -352,6 +360,14 @@ def test_process_result_value_spanner(self):
352360
result = dpt.process_result_value(pickled, dialect)
353361
assert result == original
354362

363+
def test_process_result_value_mysql(self):
364+
dpt = DynamicPickleType()
365+
dialect = _make_dialect("mysql")
366+
original = {"key": "value", "nums": [1, 2, 3]}
367+
pickled = pickle.dumps(original)
368+
result = dpt.process_result_value(pickled, dialect)
369+
assert result == original
370+
355371
def test_process_result_value_non_spanner(self):
356372
dpt = DynamicPickleType()
357373
dialect = _make_dialect("sqlite")

trpc_agent_sdk/memory/_in_memory_memory_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ def _start_cleanup_task(self) -> None:
200200
return
201201

202202
self.__cleanup_stop_event = asyncio.Event()
203-
self.__cleanup_task = asyncio.create_task(self._cleanup_loop())
203+
self.__cleanup_task = asyncio.get_event_loop().create_task(self._cleanup_loop())
204204
logger.debug("Cleanup task created")
205205

206206
def _stop_cleanup_task(self) -> None:

trpc_agent_sdk/memory/_sql_memory_service.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from trpc_agent_sdk.storage import SqlStorage
4444
from trpc_agent_sdk.storage import decode_content
4545
from trpc_agent_sdk.storage import decode_grounding_metadata
46+
from trpc_agent_sdk.storage import sanitize_content_json
4647

4748
from ._utils import extract_words_lower
4849
from ._utils import format_timestamp
@@ -110,7 +111,7 @@ def update_event(self, session: Session, event: Event):
110111
self.error_message = event.error_message
111112
self.interrupted = event.interrupted
112113
if event.content:
113-
self.content = event.content.model_dump(exclude_none=True, mode="json")
114+
self.content = sanitize_content_json(event.content.model_dump(exclude_none=True, mode="json"))
114115
if event.grounding_metadata:
115116
self.grounding_metadata = event.grounding_metadata.model_dump(exclude_none=True, mode="json")
116117
if event.custom_metadata:
@@ -135,7 +136,7 @@ def from_event(cls, session: Session, event: Event) -> MemStorageEvent:
135136
interrupted=event.interrupted,
136137
)
137138
if event.content:
138-
storage_event.content = event.content.model_dump(exclude_none=True, mode="json")
139+
storage_event.content = sanitize_content_json(event.content.model_dump(exclude_none=True, mode="json"))
139140
if event.grounding_metadata:
140141
storage_event.grounding_metadata = event.grounding_metadata.model_dump(exclude_none=True, mode="json")
141142
if event.custom_metadata:
@@ -150,7 +151,7 @@ def to_event(self) -> Event:
150151
branch=self.branch,
151152
actions=self.actions, # type: ignore
152153
timestamp=self.timestamp.timestamp(),
153-
content=decode_content(self.content),
154+
content=decode_content(sanitize_content_json(self.content)),
154155
long_running_tool_ids=self.long_running_tool_ids,
155156
partial=self.partial,
156157
turn_complete=self.turn_complete,
@@ -194,15 +195,15 @@ async def store_session(self, session: Session, agent_context: Optional[AgentCon
194195
195196
Only stores events that are not expired based on event_ttl_seconds.
196197
"""
197-
if not isinstance(session, Session):
198-
raise TypeError(f"Content must be a Session, got {type(session)}")
199-
200198
async with self._sql_storage.create_db_session() as sql_session:
201199
is_exist = False
202200
for event in session.events:
203201
if not event.is_model_visible():
204202
continue
205-
if event.content and event.content.parts:
203+
if not event.content or not event.content.parts:
204+
continue
205+
content = sanitize_content_json(event.content.model_dump(exclude_none=True, mode="json"))
206+
if content:
206207
is_exist = True
207208
# Check if the event already exists
208209
event_key = SqlKey(key=(event.id, session.save_key, session.id), storage_cls=MemStorageEvent)
@@ -324,7 +325,7 @@ def _start_cleanup_task(self) -> None:
324325
return
325326

326327
self.__cleanup_stop_event = asyncio.Event()
327-
self.__cleanup_task = asyncio.create_task(self._cleanup_loop())
328+
self.__cleanup_task = asyncio.get_event_loop().create_task(self._cleanup_loop())
328329
logger.debug("Memory cleanup task created")
329330

330331
def _stop_cleanup_task(self) -> None:

0 commit comments

Comments
 (0)