From 0e084ef49443cdbf51bde3d99dec682809785446 Mon Sep 17 00:00:00 2001 From: keqing Date: Mon, 13 Jul 2026 22:42:17 +0800 Subject: [PATCH 1/8] step1 --- .../src/powermem_langchain/middleware.py | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/powermem-langchain/src/powermem_langchain/middleware.py b/packages/powermem-langchain/src/powermem_langchain/middleware.py index 59c43c99f5..6e14b23419 100644 --- a/packages/powermem-langchain/src/powermem_langchain/middleware.py +++ b/packages/powermem-langchain/src/powermem_langchain/middleware.py @@ -36,9 +36,28 @@ def __init__( user_id: str | None = None, search_limit: int = 5, save_interactions: bool = True, + system_template: str = ( + "\n" + "The following memories were retrieved from PowerMem and may be " + "relevant to the user's request. Use them when answering, but do " + "not mention them explicitly unless asked.\n" + "{memories}\n" + "" + ), + infer: bool = False, **kwargs: Any, ) -> None: - pass + if user_id is None: + raise ValueError( + "PowerMemMiddleware requires an explicit user_id; " + "do not rely on global or runtime-inferred user identity." + ) + self.memory = memory + self.user_id = user_id + self.search_limit = search_limit + self.save_interactions = save_interactions + self.system_template = system_template + self.infer = infer def before_agent(self, state: PowerMemState, runtime) -> PowerMemStateUpdate | None: pass From 1d48cac43d6491d887907d5cbe336c53376fe853 Mon Sep 17 00:00:00 2001 From: keqing Date: Mon, 13 Jul 2026 22:44:09 +0800 Subject: [PATCH 2/8] step2 --- .../src/powermem_langchain/middleware.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/packages/powermem-langchain/src/powermem_langchain/middleware.py b/packages/powermem-langchain/src/powermem_langchain/middleware.py index 6e14b23419..63c47254fc 100644 --- a/packages/powermem-langchain/src/powermem_langchain/middleware.py +++ b/packages/powermem-langchain/src/powermem_langchain/middleware.py @@ -7,11 +7,16 @@ from __future__ import annotations +import asyncio +import logging from typing import Any, NotRequired, TypedDict from langchain.agents.middleware import AgentMiddleware, AgentState +logger = logging.getLogger(__name__) + + class PowerMemState(AgentState): """State schema reserved for the PowerMem middleware implementation.""" @@ -59,6 +64,28 @@ def __init__( self.system_template = system_template self.infer = infer + def _retrieve(self, query: str) -> list[str]: + try: + result = self.memory.search( + query, + user_id=self.user_id, + limit=self.search_limit, + ) + except Exception as exc: + logger.warning( + "PowerMem search failed for user_id=%s: %s", self.user_id, exc + ) + return [] + memories = result.get("results", []) if isinstance(result, dict) else [] + return [ + item["memory"] + for item in memories + if isinstance(item, dict) and item.get("memory") + ] + + async def _aretrieve(self, query: str) -> list[str]: + return await asyncio.to_thread(self._retrieve, query) + def before_agent(self, state: PowerMemState, runtime) -> PowerMemStateUpdate | None: pass From 0a5c6bf5f70228b80417d54571866e32a9daccd2 Mon Sep 17 00:00:00 2001 From: keqing Date: Mon, 13 Jul 2026 22:45:09 +0800 Subject: [PATCH 3/8] step3 --- .../src/powermem_langchain/middleware.py | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/packages/powermem-langchain/src/powermem_langchain/middleware.py b/packages/powermem-langchain/src/powermem_langchain/middleware.py index 63c47254fc..e614186c0d 100644 --- a/packages/powermem-langchain/src/powermem_langchain/middleware.py +++ b/packages/powermem-langchain/src/powermem_langchain/middleware.py @@ -12,6 +12,7 @@ from typing import Any, NotRequired, TypedDict from langchain.agents.middleware import AgentMiddleware, AgentState +from langchain_core.messages import HumanMessage logger = logging.getLogger(__name__) @@ -86,15 +87,43 @@ def _retrieve(self, query: str) -> list[str]: async def _aretrieve(self, query: str) -> list[str]: return await asyncio.to_thread(self._retrieve, query) - def before_agent(self, state: PowerMemState, runtime) -> PowerMemStateUpdate | None: - pass + @staticmethod + def _latest_user_text(messages: list[Any] | None) -> str | None: + if not messages: + return None + for message in reversed(messages): + if isinstance(message, HumanMessage): + content = message.content + if isinstance(content, str) and content.strip(): + return content + if isinstance(content, list): + parts = [ + part.get("text", "") + for part in content + if isinstance(part, dict) and part.get("text") + ] + joined = "".join(parts).strip() + if joined: + return joined + return None + + def before_agent(self, state: PowerMemState, runtime) -> dict[str, Any] | None: + query = self._latest_user_text(state.get("messages")) + if not query: + return None + memories = self._retrieve(query) + return {"powermem_memories": memories} async def abefore_agent( self, state: PowerMemState, runtime, - ) -> PowerMemStateUpdate | None: - pass + ) -> dict[str, Any] | None: + query = self._latest_user_text(state.get("messages")) + if not query: + return None + memories = await self._aretrieve(query) + return {"powermem_memories": memories} def after_agent(self, state: PowerMemState, runtime) -> None: pass From 6cfa389eccac8a3124d5580a3b3d444223c89f0d Mon Sep 17 00:00:00 2001 From: keqing Date: Mon, 13 Jul 2026 22:50:45 +0800 Subject: [PATCH 4/8] step4 --- .../src/powermem_langchain/middleware.py | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/powermem-langchain/src/powermem_langchain/middleware.py b/packages/powermem-langchain/src/powermem_langchain/middleware.py index e614186c0d..4dbe18b348 100644 --- a/packages/powermem-langchain/src/powermem_langchain/middleware.py +++ b/packages/powermem-langchain/src/powermem_langchain/middleware.py @@ -12,7 +12,7 @@ from typing import Any, NotRequired, TypedDict from langchain.agents.middleware import AgentMiddleware, AgentState -from langchain_core.messages import HumanMessage +from langchain_core.messages import HumanMessage, SystemMessage logger = logging.getLogger(__name__) @@ -125,5 +125,25 @@ async def abefore_agent( memories = await self._aretrieve(query) return {"powermem_memories": memories} + def _format_system_message(self, memories: list[str]) -> SystemMessage: + body = "\n".join(f"- {memory}" for memory in memories) + return SystemMessage(content=self.system_template.format(memories=body)) + + def wrap_model_call(self, request, handler): + memories = request.state.get("powermem_memories") or [] + if not memories: + return handler(request) + system_message = self._format_system_message(memories) + request = request.override(messages=[system_message, *request.messages]) + return handler(request) + + async def awrap_model_call(self, request, handler): + memories = request.state.get("powermem_memories") or [] + if not memories: + return await handler(request) + system_message = self._format_system_message(memories) + request = request.override(messages=[system_message, *request.messages]) + return await handler(request) + def after_agent(self, state: PowerMemState, runtime) -> None: pass From 0312b2fbf97764b09a0f2f719a49255045b7f2a4 Mon Sep 17 00:00:00 2001 From: keqing Date: Mon, 13 Jul 2026 22:51:54 +0800 Subject: [PATCH 5/8] step5 --- .../src/powermem_langchain/middleware.py | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/powermem-langchain/src/powermem_langchain/middleware.py b/packages/powermem-langchain/src/powermem_langchain/middleware.py index 4dbe18b348..fdda966256 100644 --- a/packages/powermem-langchain/src/powermem_langchain/middleware.py +++ b/packages/powermem-langchain/src/powermem_langchain/middleware.py @@ -12,7 +12,7 @@ from typing import Any, NotRequired, TypedDict from langchain.agents.middleware import AgentMiddleware, AgentState -from langchain_core.messages import HumanMessage, SystemMessage +from langchain_core.messages import AIMessage, HumanMessage, SystemMessage logger = logging.getLogger(__name__) @@ -145,5 +145,45 @@ async def awrap_model_call(self, request, handler): request = request.override(messages=[system_message, *request.messages]) return await handler(request) + @staticmethod + def _last_of_type(messages: list[Any] | None, message_type: type) -> Any | None: + if not messages: + return None + for message in reversed(messages): + if isinstance(message, message_type): + return message + return None + + def _persist_interaction(self, user_message: Any, assistant_message: Any) -> None: + try: + self.memory.add( + messages=[user_message, assistant_message], + user_id=self.user_id, + infer=self.infer, + ) + except Exception as exc: + logger.warning( + "PowerMem add failed for user_id=%s: %s", self.user_id, exc + ) + def after_agent(self, state: PowerMemState, runtime) -> None: - pass + if not self.save_interactions: + return + messages = state.get("messages") or [] + user_message = self._last_of_type(messages, HumanMessage) + assistant_message = self._last_of_type(messages, AIMessage) + if user_message is None or assistant_message is None: + return + self._persist_interaction(user_message, assistant_message) + + async def aafter_agent(self, state: PowerMemState, runtime) -> None: + if not self.save_interactions: + return + messages = state.get("messages") or [] + user_message = self._last_of_type(messages, HumanMessage) + assistant_message = self._last_of_type(messages, AIMessage) + if user_message is None or assistant_message is None: + return + await asyncio.to_thread( + self._persist_interaction, user_message, assistant_message + ) From 13650bb6f9f945bba0a1c96b51acb176a1b3c944 Mon Sep 17 00:00:00 2001 From: keqing Date: Mon, 13 Jul 2026 22:52:53 +0800 Subject: [PATCH 6/8] step6 --- .../src/powermem_langchain/middleware.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/powermem-langchain/src/powermem_langchain/middleware.py b/packages/powermem-langchain/src/powermem_langchain/middleware.py index fdda966256..196b4d36e9 100644 --- a/packages/powermem-langchain/src/powermem_langchain/middleware.py +++ b/packages/powermem-langchain/src/powermem_langchain/middleware.py @@ -9,7 +9,7 @@ import asyncio import logging -from typing import Any, NotRequired, TypedDict +from typing import Any, NotRequired from langchain.agents.middleware import AgentMiddleware, AgentState from langchain_core.messages import AIMessage, HumanMessage, SystemMessage @@ -21,17 +21,12 @@ class PowerMemState(AgentState): """State schema reserved for the PowerMem middleware implementation.""" - powermem_context: NotRequired[str] - - -class PowerMemStateUpdate(TypedDict): - """State update returned by memory-loading middleware hooks.""" - - powermem_context: str + powermem_memories: NotRequired[list[str]] class PowerMemMiddleware(AgentMiddleware[PowerMemState, Any, Any]): - """Placeholder for the summer school implementation.""" + """LangChain v1 middleware that injects PowerMem retrieval into the model + context and persists the interaction after the agent run completes.""" state_schema = PowerMemState From c8a2c514dc684c7a73bf2ffe790332c6719d1d2d Mon Sep 17 00:00:00 2001 From: keqing Date: Mon, 13 Jul 2026 23:06:45 +0800 Subject: [PATCH 7/8] fix bug in step5 --- .../src/powermem_langchain/middleware.py | 42 ++++++++++++++----- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/packages/powermem-langchain/src/powermem_langchain/middleware.py b/packages/powermem-langchain/src/powermem_langchain/middleware.py index 196b4d36e9..0f4125df97 100644 --- a/packages/powermem-langchain/src/powermem_langchain/middleware.py +++ b/packages/powermem-langchain/src/powermem_langchain/middleware.py @@ -102,6 +102,29 @@ def _latest_user_text(messages: list[Any] | None) -> str | None: return joined return None + @staticmethod + def _last_of_type(messages: list[Any] | None, message_type: type) -> Any | None: + if not messages: + return None + for message in reversed(messages): + if isinstance(message, message_type): + return message + return None + + @staticmethod + def _message_text(message: Any) -> str: + content = getattr(message, "content", message) + if isinstance(content, str): + return content + if isinstance(content, list): + parts = [ + part.get("text", "") + for part in content + if isinstance(part, dict) and part.get("text") + ] + return "".join(parts) + return str(content) + def before_agent(self, state: PowerMemState, runtime) -> dict[str, Any] | None: query = self._latest_user_text(state.get("messages")) if not query: @@ -140,19 +163,16 @@ async def awrap_model_call(self, request, handler): request = request.override(messages=[system_message, *request.messages]) return await handler(request) - @staticmethod - def _last_of_type(messages: list[Any] | None, message_type: type) -> Any | None: - if not messages: - return None - for message in reversed(messages): - if isinstance(message, message_type): - return message - return None - def _persist_interaction(self, user_message: Any, assistant_message: Any) -> None: try: self.memory.add( - messages=[user_message, assistant_message], + messages=[ + {"role": "user", "content": self._message_text(user_message)}, + { + "role": "assistant", + "content": self._message_text(assistant_message), + }, + ], user_id=self.user_id, infer=self.infer, ) @@ -181,4 +201,4 @@ async def aafter_agent(self, state: PowerMemState, runtime) -> None: return await asyncio.to_thread( self._persist_interaction, user_message, assistant_message - ) + ) \ No newline at end of file From 8d90951ff8266d8810efead1450c4e3a3ac5fcba Mon Sep 17 00:00:00 2001 From: keqing Date: Tue, 14 Jul 2026 12:59:01 +0800 Subject: [PATCH 8/8] feat: implement powermem langchain middleware --- packages/powermem-langchain/examples/openai_agent.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/powermem-langchain/examples/openai_agent.py b/packages/powermem-langchain/examples/openai_agent.py index f06972a0bb..2bf7ab610d 100644 --- a/packages/powermem-langchain/examples/openai_agent.py +++ b/packages/powermem-langchain/examples/openai_agent.py @@ -9,6 +9,7 @@ from __future__ import annotations import argparse +import traceback from typing import Any from langchain.agents import create_agent @@ -123,6 +124,7 @@ def main() -> None: try: memory = create_memory() except Exception as exc: + traceback.print_exc() raise SystemExit( "Failed to create PowerMem memory. Configure PowerMem first, for " "example by setting LLM_PROVIDER, LLM_API_KEY, DATABASE_PROVIDER, "