mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-30 01:51:28 +00:00
fix(livekit): recall on text turns that skip the voice hook
session.run and generate_reply never call on_user_turn_completed, so a text turn reached the model with no memory. SupermemoryAgent now recalls inside llm_node and skips the fetch when the voice hook already injected.
This commit is contained in:
parent
2d3c706a3e
commit
9b8308beee
4 changed files with 55 additions and 1 deletions
|
|
@ -89,7 +89,7 @@ if __name__ == "__main__":
|
|||
agents.cli.run_app(server)
|
||||
```
|
||||
|
||||
`preload` puts the caller's profile into the first turn, so the greeting can use it. `attach` stores user and assistant turns when the session closes, including a caller who hangs up mid-turn. `SupermemoryAgent` recalls before each reply and adds the memory tools.
|
||||
`preload` puts the caller's profile into the first turn, so the greeting can use it. `attach` stores user and assistant turns when the session closes, including a caller who hangs up mid-turn. `SupermemoryAgent` recalls before each reply and adds the memory tools. Voice turns use `on_user_turn_completed`. Text turns from `generate_reply` or `session.run` skip that hook, so the agent also recalls inside `llm_node`. A turn that already has memory is not fetched again.
|
||||
|
||||
## Your own agent
|
||||
|
||||
|
|
|
|||
|
|
@ -23,3 +23,7 @@ class SupermemoryAgent(Agent):
|
|||
|
||||
async def on_user_turn_completed(self, turn_ctx: Any, new_message: Any) -> None:
|
||||
await self.memory.on_user_turn_completed(turn_ctx, new_message)
|
||||
|
||||
async def llm_node(self, chat_ctx: Any, tools: list[Any], model_settings: Any) -> Any:
|
||||
await self.memory.enrich(chat_ctx)
|
||||
return Agent.default.llm_node(self, chat_ctx, tools, model_settings)
|
||||
|
|
|
|||
|
|
@ -150,6 +150,29 @@ class SupermemoryLiveKit:
|
|||
except Exception:
|
||||
logger.warning("memory inject failed", exc_info=True)
|
||||
|
||||
async def enrich(self, chat_ctx: Any) -> None:
|
||||
"""Recall for a context that already contains the user message.
|
||||
|
||||
``session.run`` and ``generate_reply`` skip ``on_user_turn_completed``, so the
|
||||
agent calls this from ``llm_node``. A voice turn that already injected memory
|
||||
is left alone.
|
||||
"""
|
||||
if self._has_injection(chat_ctx):
|
||||
return
|
||||
user = self._last_user_item(chat_ctx)
|
||||
query = message_text(user) if user is not None else None
|
||||
if not query:
|
||||
return
|
||||
try:
|
||||
text = await self._recall_text(query=query)
|
||||
if not text:
|
||||
return
|
||||
created_at = getattr(user, "created_at", None)
|
||||
before = created_at - 0.001 if isinstance(created_at, (int, float)) else None
|
||||
self._inject(chat_ctx, text, created_at=before)
|
||||
except Exception:
|
||||
logger.warning("memory inject failed", exc_info=True)
|
||||
|
||||
def attach(
|
||||
self,
|
||||
session: Any,
|
||||
|
|
@ -315,6 +338,19 @@ class SupermemoryLiveKit:
|
|||
except TypeError:
|
||||
chat_ctx.add_message(role="assistant", content=wrapped)
|
||||
|
||||
def _last_user_item(self, chat_ctx: Any) -> Any:
|
||||
for item in reversed(list(getattr(chat_ctx, "items", []) or [])):
|
||||
if message_role(item) == "user" and message_text(item):
|
||||
return item
|
||||
return None
|
||||
|
||||
def _has_injection(self, chat_ctx: Any) -> bool:
|
||||
for item in getattr(chat_ctx, "items", []) or []:
|
||||
text = message_text(item)
|
||||
if text and is_injected_memory(text):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _strip_injected(self, chat_ctx: Any) -> None:
|
||||
items = list(getattr(chat_ctx, "items", []) or [])
|
||||
for item in items:
|
||||
|
|
|
|||
|
|
@ -174,6 +174,20 @@ class MemoryTests(unittest.TestCase):
|
|||
self.assertIn("Lives in Lisbon", ctx.items[0].content)
|
||||
self.assertNotIn("Name is Ada", ctx.items[0].content)
|
||||
|
||||
def test_enrich_injects_when_the_user_message_is_already_present(self):
|
||||
client = FakeClient(FakeProfile(static=["The secret word is kelp"]))
|
||||
plugin = memory(client, container_tag="user_1")
|
||||
ctx = ChatCtx()
|
||||
ctx.add_message(role="user", content="what is the secret word?", created_at=8.0)
|
||||
|
||||
asyncio.run(plugin.enrich(ctx))
|
||||
|
||||
self.assertEqual(len(ctx.items), 2)
|
||||
self.assertIn("kelp", ctx.items[1].content)
|
||||
self.assertAlmostEqual(ctx.items[1].created_at, 7.999)
|
||||
asyncio.run(plugin.enrich(ctx))
|
||||
self.assertEqual(len(client.profile.calls), 1)
|
||||
|
||||
def test_timeout_and_errors_do_not_fail_the_turn(self):
|
||||
slow = FakeClient(FakeProfile(static=["Name is Ada"], delay=0.05))
|
||||
plugin = memory(slow, container_tag="user_1", params=InputParams(recall_timeout=0.01))
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue