Add user memory feature for cross-conversation recall

Give the LangChain agent a save_user_memory tool so users can ask it to
remember preferences and personal facts. Memories are stored per-user in
a new user_memories table and injected into the system prompt on each
conversation turn.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-09 21:53:14 -04:00
parent 90372a6a6d
commit c95800e65d
6 changed files with 203 additions and 8 deletions

View File

@@ -22,6 +22,7 @@ from .logic import (
get_conversation_by_id,
rename_conversation,
)
from .memory import get_memories_for_user
from .models import (
Conversation,
PydConversation,
@@ -36,15 +37,27 @@ conversation_blueprint = Blueprint(
_SYSTEM_PROMPT = SIMBA_SYSTEM_PROMPT
async def _build_system_prompt_with_memories(user_id: str) -> str:
"""Append user memories to the base system prompt."""
memories = await get_memories_for_user(user_id)
if not memories:
return _SYSTEM_PROMPT
memory_block = "\n".join(f"- {m}" for m in memories)
return f"{_SYSTEM_PROMPT}\n\nUSER MEMORIES (facts the user has asked you to remember):\n{memory_block}"
def _build_messages_payload(
conversation, query_text: str, image_description: str | None = None
conversation,
query_text: str,
image_description: str | None = None,
system_prompt: str | None = None,
) -> list:
recent_messages = (
conversation.messages[-10:]
if len(conversation.messages) > 10
else conversation.messages
)
messages_payload = [{"role": "system", "content": _SYSTEM_PROMPT}]
messages_payload = [{"role": "system", "content": system_prompt or _SYSTEM_PROMPT}]
for msg in recent_messages[:-1]: # Exclude the message we just added
role = "user" if msg.speaker == "user" else "assistant"
text = msg.text
@@ -80,10 +93,14 @@ async def query():
user=user,
)
messages_payload = _build_messages_payload(conversation, query)
system_prompt = await _build_system_prompt_with_memories(str(user.id))
messages_payload = _build_messages_payload(
conversation, query, system_prompt=system_prompt
)
payload = {"messages": messages_payload}
agent_config = {"configurable": {"user_id": str(user.id)}}
response = await main_agent.ainvoke(payload)
response = await main_agent.ainvoke(payload, config=agent_config)
message = response.get("messages", [])[-1].content
await add_message_to_conversation(
conversation=conversation,
@@ -163,15 +180,19 @@ async def stream_query():
logging.error(f"Failed to analyze image: {e}")
image_description = "[Image could not be analyzed]"
system_prompt = await _build_system_prompt_with_memories(str(user.id))
messages_payload = _build_messages_payload(
conversation, query_text or "", image_description
conversation, query_text or "", image_description, system_prompt=system_prompt
)
payload = {"messages": messages_payload}
agent_config = {"configurable": {"user_id": str(user.id)}}
async def event_generator():
final_message = None
try:
async for event in main_agent.astream_events(payload, version="v2"):
async for event in main_agent.astream_events(
payload, version="v2", config=agent_config
):
event_type = event.get("event")
if event_type == "on_tool_start":