66
77from __future__ import annotations
88
9+ import json
910from typing import Any
1011
1112from multiverse_note .nodes .base import BaseNode
@@ -75,13 +76,32 @@ def _handle_user_input(self, payload: dict) -> None:
7576 if not content or self ._current_branch_id is None :
7677 return
7778
79+ attachments = payload .get ("attachments" , "" )
80+ image_data = payload .get ("image_data" , [])
81+
7882 # Store user message as a ConversationNode
7983 user_node = self ._node_store .create (
8084 branch_id = self ._current_branch_id ,
8185 role = "user" ,
8286 content = content ,
87+ attachments = attachments ,
8388 )
8489 self ._branch_store .add_node (self ._current_branch_id , user_node .id )
90+
91+ # Store image attachment data at correct indices matching the attachments list
92+ if image_data :
93+ try :
94+ att_list = json .loads (attachments ) if attachments else []
95+ except (json .JSONDecodeError , TypeError ):
96+ att_list = []
97+ image_idx = 0
98+ for i , att in enumerate (att_list ):
99+ if att .get ("content_type" ) == "image" and image_idx < len (image_data ):
100+ self ._node_store .set_attachment_data (
101+ user_node .id , i , image_data [image_idx ].get ("base64" , "" ),
102+ )
103+ image_idx += 1
104+
85105 self .logger .info ("Stored user node %d on branch %d" , user_node .id , self ._current_branch_id )
86106
87107 # Assemble context and publish LLM request
@@ -95,13 +115,14 @@ def _handle_user_input(self, payload: dict) -> None:
95115 },
96116 })
97117
98- def _assemble_context (self ) -> list [dict [str , str ]]:
99- """Build an ordered list of {role, content} messages for the LLM.
118+ def _assemble_context (self ) -> list [dict [str , Any ]]:
119+ """Build an ordered list of messages for the LLM.
100120
101121 Includes conversation history from referenced branches (context refs)
102122 followed by the current branch's conversation.
123+ Supports multimodal messages (text + images) for litellm.
103124 """
104- messages : list [dict [str , str ]] = []
125+ messages : list [dict [str , Any ]] = []
105126
106127 if self ._current_branch_id is None :
107128 return messages
@@ -124,14 +145,46 @@ def _assemble_context(self) -> list[dict[str, str]]:
124145 messages .extend (self ._load_branch_messages (self ._current_branch_id ))
125146 return messages
126147
127- def _load_branch_messages (self , branch_id : int ) -> list [dict [str , str ]]:
128- """Load all conversation nodes for a branch as {role, content} dicts."""
148+ def _load_branch_messages (self , branch_id : int ) -> list [dict [str , Any ]]:
149+ """Load all conversation nodes for a branch as message dicts.
150+
151+ For nodes with image attachments, builds multimodal content arrays
152+ per litellm format.
153+ """
129154 node_ids = self ._branch_store .get_nodes (branch_id )
130- messages : list [dict [str , str ]] = []
155+ messages : list [dict [str , Any ]] = []
131156 for nid in node_ids :
132157 node = self ._node_store .get (nid )
133- if node is not None :
134- messages .append ({"role" : node .role , "content" : node .content })
158+ if node is None :
159+ continue
160+
161+ # Check for image attachments
162+ if node .attachments :
163+ try :
164+ att_list = json .loads (node .attachments )
165+ except (json .JSONDecodeError , TypeError ):
166+ att_list = []
167+
168+ image_parts = []
169+ for i , att in enumerate (att_list ):
170+ if att .get ("content_type" ) == "image" :
171+ b64 = self ._node_store .get_attachment_data (nid , i )
172+ if b64 :
173+ mime = att .get ("mime_type" , "image/png" )
174+ image_parts .append ({
175+ "type" : "image_url" ,
176+ "image_url" : {"url" : f"data:{ mime } ;base64,{ b64 } " },
177+ })
178+
179+ if image_parts :
180+ content_parts : list [dict [str , Any ]] = [
181+ {"type" : "text" , "text" : node .content },
182+ ]
183+ content_parts .extend (image_parts )
184+ messages .append ({"role" : node .role , "content" : content_parts })
185+ continue
186+
187+ messages .append ({"role" : node .role , "content" : node .content })
135188 return messages
136189
137190 # --- Branch event handling ---
0 commit comments