diff --git a/surfsense_backend/app/agents/new_chat/tools/user_memory.py b/surfsense_backend/app/agents/new_chat/tools/user_memory.py index 3cefa2b02..32230394e 100644 --- a/surfsense_backend/app/agents/new_chat/tools/user_memory.py +++ b/surfsense_backend/app/agents/new_chat/tools/user_memory.py @@ -158,46 +158,79 @@ def create_save_memory_tool( Returns: A dictionary with the save status and memory details """ - # Validate category + # Log at the very start + logger.info(f">>> SAVE_MEMORY TOOL CALLED: content='{content}', category='{category}'") + print(f">>> SAVE_MEMORY TOOL CALLED: content='{content}', category='{category}'") + + # Normalize and validate category (LLMs may send uppercase) + category = category.lower() if category else "fact" valid_categories = ["preference", "fact", "instruction", "context"] if category not in valid_categories: category = "fact" try: + logger.info(f"save_memory called: user_id={user_id}, search_space_id={search_space_id}, content={content[:50]}...") + # Convert user_id to UUID uuid_user_id = _to_uuid(user_id) + logger.info(f"UUID conversion successful: {uuid_user_id}") # Check if we've hit the memory limit memory_count = await get_user_memory_count( db_session, user_id, search_space_id ) + logger.info(f"Current memory count: {memory_count}") + if memory_count >= MAX_MEMORIES_PER_USER: # Delete oldest memory to make room await delete_oldest_memory(db_session, user_id, search_space_id) # Generate embedding for the memory + logger.info("Generating embedding...") embedding = config.embedding_model_instance.embed(content) + logger.info(f"Embedding generated, type: {type(embedding)}, len: {len(embedding) if hasattr(embedding, '__len__') else 'N/A'}") - # Map string category to enum - category_enum = MemoryCategory(category) + # Convert numpy array to list of Python floats for PostgreSQL + import numpy as np + if isinstance(embedding, np.ndarray): + embedding_list = embedding.tolist() + else: + embedding_list = list(embedding) - # Create new memory - new_memory = UserMemory( - user_id=uuid_user_id, - search_space_id=search_space_id, - memory_text=content, - category=category_enum, - embedding=embedding, - updated_at=datetime.now(UTC), + # Create new memory using ORM with proper enum handling + # Use the enum's value attribute directly + from sqlalchemy import text as sql_text + + now = datetime.now(UTC) + + # Use raw SQL with proper parameter binding for asyncpg + insert_sql = sql_text(""" + INSERT INTO user_memories (user_id, search_space_id, memory_text, category, embedding, updated_at, created_at) + VALUES (:user_id, :search_space_id, :memory_text, CAST(:category AS memorycategory), :embedding, :updated_at, :created_at) + RETURNING id + """) + + result = await db_session.execute( + insert_sql, + { + "user_id": uuid_user_id, + "search_space_id": search_space_id, + "memory_text": content, + "category": category, # Already lowercase string + "embedding": str(embedding_list), # Convert to string format for pgvector + "updated_at": now, + "created_at": now, + } ) - - db_session.add(new_memory) + new_memory_id = result.scalar_one() + + logger.info("Committing...") await db_session.commit() - await db_session.refresh(new_memory) + logger.info(f"Memory saved successfully with id: {new_memory_id}") return { "status": "saved", - "memory_id": new_memory.id, + "memory_id": new_memory_id, "memory_text": content, "category": category, "message": f"I'll remember: {content}", @@ -205,6 +238,8 @@ def create_save_memory_tool( except Exception as e: logger.exception(f"Failed to save memory for user {user_id}: {e}") + # Rollback the session to clear any failed transaction state + await db_session.rollback() return { "status": "error", "error": str(e), @@ -260,10 +295,15 @@ def create_recall_memory_tool( A dictionary containing relevant memories and formatted context """ top_k = min(max(top_k, 1), 20) # Clamp between 1 and 20 + + # Log at the very start + logger.info(f">>> RECALL_MEMORY TOOL CALLED: query='{query}', category='{category}', top_k={top_k}") + print(f">>> RECALL_MEMORY TOOL CALLED: query='{query}', category='{category}', top_k={top_k}") try: # Convert user_id to UUID uuid_user_id = _to_uuid(user_id) + logger.info(f"Recall memory for user: {uuid_user_id}, search_space: {search_space_id}") if query: # Semantic search using embeddings @@ -307,6 +347,8 @@ def create_recall_memory_tool( result = await db_session.execute(stmt) memories = result.scalars().all() + + logger.info(f"Found {len(memories)} memories") # Format memories for response memory_list = [ @@ -318,8 +360,12 @@ def create_recall_memory_tool( } for m in memories ] + + logger.info(f"Formatted memory list: {memory_list}") formatted_context = format_memories_for_context(memory_list) + + logger.info(f"Returning {len(memory_list)} memories") return { "status": "success", @@ -329,6 +375,8 @@ def create_recall_memory_tool( } except Exception as e: + logger.exception(f"Failed to recall memories for user {user_id}: {e}") + await db_session.rollback() return { "status": "error", "error": str(e), diff --git a/surfsense_backend/app/db.py b/surfsense_backend/app/db.py index c23b133e2..550f0a704 100644 --- a/surfsense_backend/app/db.py +++ b/surfsense_backend/app/db.py @@ -475,10 +475,11 @@ class ChatCommentMention(BaseModel, TimestampMixin): class MemoryCategory(str, Enum): """Categories for user memories.""" - PREFERENCE = "preference" # User preferences (e.g., "prefers dark mode") - FACT = "fact" # Facts about the user (e.g., "is a Python developer") - INSTRUCTION = "instruction" # Standing instructions (e.g., "always respond in bullet points") - CONTEXT = "context" # Contextual information (e.g., "working on project X") + # Using lowercase keys to match PostgreSQL enum values + preference = "preference" # User preferences (e.g., "prefers dark mode") + fact = "fact" # Facts about the user (e.g., "is a Python developer") + instruction = "instruction" # Standing instructions (e.g., "always respond in bullet points") + context = "context" # Contextual information (e.g., "working on project X") class UserMemory(BaseModel, TimestampMixin): @@ -510,7 +511,7 @@ class UserMemory(BaseModel, TimestampMixin): category = Column( SQLAlchemyEnum(MemoryCategory), nullable=False, - default=MemoryCategory.FACT, + default=MemoryCategory.fact, ) # Vector embedding for semantic search embedding = Column(Vector(config.embedding_model_instance.dimension)) diff --git a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx index 43c33ba5a..742ccc9b8 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx @@ -32,6 +32,7 @@ import { DisplayImageToolUI } from "@/components/tool-ui/display-image"; import { GeneratePodcastToolUI } from "@/components/tool-ui/generate-podcast"; import { LinkPreviewToolUI } from "@/components/tool-ui/link-preview"; import { ScrapeWebpageToolUI } from "@/components/tool-ui/scrape-webpage"; +import { SaveMemoryToolUI, RecallMemoryToolUI } from "@/components/tool-ui/user-memory"; // import { WriteTodosToolUI } from "@/components/tool-ui/write-todos"; import { getBearerToken } from "@/lib/auth-utils"; import { createAttachmentAdapter, extractAttachmentContent } from "@/lib/chat/attachment-adapter"; @@ -1056,6 +1057,8 @@ export default function NewChatPage() { + + {/* Disabled for now */}