1+ import base64
12import logging
23import uuid
34from datetime import datetime , timedelta
5+ from io import BytesIO
46from pathlib import Path
57
68from fastapi import UploadFile
9+ from PIL import Image
710
8- from app .config import USER_ID , VECTOR_INDEX_NAME , VECTOR_NUM_CANDIDATES
11+ from app .config import IMAGE_SIZE , USER_ID , VECTOR_INDEX_NAME , VECTOR_NUM_CANDIDATES
912from app .services .anthropic import extract_memories
1013from app .services .voyage import get_multimodal_embedding , get_text_embedding
1114
1821UPLOADS_DIR .mkdir (parents = True , exist_ok = True )
1922
2023
21- def get_conversation_history (db , entry_id : str ) -> list [dict ]:
22- """Get conversation history for an entry."""
23- history = list (
24- db .messages .find (
25- {"entry_id" : entry_id }, {"role" : 1 , "content" : 1 , "_id" : 0 }
26- ).sort ("created_at" , 1 )
27- )
28- return [
29- {"role" : msg ["role" ], "content" : msg ["content" ]}
30- for msg in history
31- if msg .get ("content" )
32- ]
33-
34-
3524def save_user_message (
3625 db , entry_id : str , content : str | Path , version : int , msg_date : datetime
3726) -> None :
@@ -62,6 +51,28 @@ def save_user_message(
6251 logger .info (f"Saved message for entry { entry_id } " )
6352
6453
54+ def extract_and_save_memories (
55+ db , entry_id : str , conversation : list [dict ], entry_date : datetime
56+ ) -> None :
57+ """Extract memories from conversation and save them."""
58+ context = "\n " .join (f"{ msg ['role' ]} : { msg ['content' ]} " for msg in conversation )
59+ memories = extract_memories (context )
60+
61+ if memories :
62+ memory_docs = [
63+ {
64+ "user_id" : USER_ID ,
65+ "entry_id" : entry_id ,
66+ "content" : memory_content ,
67+ "embedding" : get_text_embedding (memory_content , input_type = "document" ),
68+ "created_at" : entry_date ,
69+ }
70+ for memory_content in memories
71+ ]
72+ db .memories .insert_many (memory_docs )
73+ logger .info (f"Extracted and saved { len (memories )} memories: { memories } " )
74+
75+
6576def retrieve_relevant_memories (db , query : str ) -> list [str ]:
6677 """Retrieve relevant memories via vector search."""
6778 query_embedding = get_text_embedding (query , input_type = "query" )
@@ -84,26 +95,42 @@ def retrieve_relevant_memories(db, query: str) -> list[str]:
8495 return memories
8596
8697
87- def extract_and_save_memories (
88- db , entry_id : str , conversation : list [dict ], entry_date : datetime
89- ) -> None :
90- """Extract memories from conversation and save them."""
91- context = "\n " .join (f"{ msg ['role' ]} : { msg ['content' ]} " for msg in conversation )
92- memories = extract_memories (context )
98+ def get_conversation_history (db , entry_id : str , include_images : bool = True ) -> list [dict ]:
99+ """Get conversation history for an entry."""
100+ history = list (
101+ db .messages .find (
102+ {"entry_id" : entry_id }, {"role" : 1 , "content" : 1 , "image" : 1 , "_id" : 0 }
103+ ).sort ("created_at" , 1 )
104+ )
93105
94- if memories :
95- memory_docs = [
96- {
97- "user_id" : USER_ID ,
98- "entry_id" : entry_id ,
99- "content" : memory_content ,
100- "embedding" : get_text_embedding (memory_content , input_type = "document" ),
101- "created_at" : entry_date ,
102- }
103- for memory_content in memories
104- ]
105- db .memories .insert_many (memory_docs )
106- logger .info (f"Extracted and saved { len (memories )} memories: { memories } " )
106+ messages = []
107+ for msg in history :
108+ if msg .get ("content" ):
109+ messages .append ({"role" : msg ["role" ], "content" : msg ["content" ]})
110+ elif msg .get ("image" ) and include_images :
111+ image_path = UPLOADS_DIR / msg ["image" ]
112+ if image_path .exists ():
113+ messages .append (
114+ {
115+ "role" : msg ["role" ],
116+ "content" : [image_to_base64 (image_path )],
117+ }
118+ )
119+ return messages
120+
121+
122+ def image_to_base64 (image_path : Path ) -> dict :
123+ """Convert an image file to Claude's base64 format, resizing to fit limits."""
124+ with Image .open (image_path ) as img :
125+ img = img .resize (IMAGE_SIZE , Image .Resampling .LANCZOS )
126+ buffer = BytesIO ()
127+ img .save (buffer , format = "JPEG" , quality = 85 )
128+ data = base64 .standard_b64encode (buffer .getvalue ()).decode ("utf-8" )
129+
130+ return {
131+ "type" : "image" ,
132+ "source" : {"type" : "base64" , "media_type" : "image/jpeg" , "data" : data },
133+ }
107134
108135
109136def save_assistant_message (db , entry_id : str , content : str , msg_date : datetime ) -> None :
@@ -120,7 +147,7 @@ def save_assistant_message(db, entry_id: str, content: str, msg_date: datetime)
120147
121148
122149def save_image_file (image_file : UploadFile ) -> Path :
123- """Save uploaded image file and return the path ."""
150+ """Save uploaded image file."""
124151 filename = f"{ uuid .uuid4 ()} { Path (image_file .filename ).suffix or '.jpg' } "
125152 image_path = UPLOADS_DIR / filename
126153 with open (image_path , "wb" ) as f :
0 commit comments