Skip to content

Commit 4499ef5

Browse files
author
ajosh0504
committed
handling multimodal entries
1 parent 055631f commit 4499ef5

File tree

3 files changed

+78
-40
lines changed

3 files changed

+78
-40
lines changed

apps/interactive-journal/backend/app/routers/helpers.py

Lines changed: 62 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
import base64
12
import logging
23
import uuid
34
from datetime import datetime, timedelta
5+
from io import BytesIO
46
from pathlib import Path
57

68
from 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
912
from app.services.anthropic import extract_memories
1013
from app.services.voyage import get_multimodal_embedding, get_text_embedding
1114

@@ -18,20 +21,6 @@
1821
UPLOADS_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-
3524
def 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+
6576
def 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

109136
def 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

122149
def 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:

apps/interactive-journal/backend/app/routers/routes.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
get_mood_distribution,
1414
get_themes,
1515
get_total_entries,
16+
image_to_base64,
1617
retrieve_relevant_memories,
1718
save_assistant_message,
1819
save_image_file,
@@ -62,10 +63,17 @@ def send_message(
6263
# Save image files to disk before streaming (file handles close after)
6364
image_paths = [save_image_file(image) for image in images]
6465

66+
# Build current message (text, images, or both)
67+
messages = []
68+
if content:
69+
messages.append({"type": "text", "text": content})
70+
for path in image_paths:
71+
messages.append(image_to_base64(path))
72+
6573
# Get conversation history and add current message
6674
conversation = get_conversation_history(db, entry_id)
67-
if content:
68-
conversation.append({"role": "user", "content": content})
75+
if messages:
76+
conversation.append({"role": "user", "content": messages})
6977

7078
# Retrieve relevant memories for context (V2 only)
7179
memories = retrieve_relevant_memories(db, content) if is_v2 and content else []
@@ -144,7 +152,7 @@ def search_entries(q: str, version: int = 1):
144152
def save_entry(entry_id: str, entry_date: str = Form(...)):
145153
"""Analyze entry for sentiment/themes and extract memories."""
146154
db = get_database()
147-
conversation = get_conversation_history(db, entry_id)
155+
conversation = get_conversation_history(db, entry_id, include_images=False)
148156

149157
if not conversation:
150158
return {"error": "No messages in entry"}

apps/interactive-journal/frontend/src/components/Entry.jsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ function Entry({ messages, onSendMessage, hasActiveEntry, activeEntry, entries,
3232
}
3333

3434
const handleSaveEntry = async () => {
35+
setSaveStatus('saving')
3536
try {
3637
const activeEntryObj = entries.find(e => e._id === activeEntry)
3738
const formData = new FormData()
@@ -45,6 +46,7 @@ function Entry({ messages, onSendMessage, hasActiveEntry, activeEntry, entries,
4546
setTimeout(() => setSaveStatus(null), 2000)
4647
} catch (error) {
4748
console.error('Failed to save entry:', error)
49+
setSaveStatus(null)
4850
}
4951
}
5052

@@ -291,10 +293,11 @@ function Entry({ messages, onSendMessage, hasActiveEntry, activeEntry, entries,
291293
{isV2 && messages.length > 0 && (
292294
<div className="save-entry">
293295
<button
294-
className={`save-btn ${saveStatus === 'saved' ? 'saved' : ''}`}
296+
className={`save-btn ${saveStatus || ''}`}
295297
onClick={handleSaveEntry}
298+
disabled={saveStatus === 'saving'}
296299
>
297-
{saveStatus === 'saved' ? 'Saved ✓' : 'Save'}
300+
{saveStatus === 'saving' ? 'Saving...' : saveStatus === 'saved' ? 'Saved ✓' : 'Save'}
298301
</button>
299302
</div>
300303
)}

0 commit comments

Comments
 (0)