@@ -38,11 +38,12 @@ def get_system_prompt(thinking_mode: ThinkingMode) -> str:
3838 base_prompt = """You are an LCARS computer system with access to Star Trek Memory Alpha records.
3939
4040CRITICAL INSTRUCTIONS:
41- - You MUST answer ONLY using information from the provided records below
41+ - You MUST answer ONLY using information from the provided records
4242- If the records don't contain relevant information, say "I don't have information about that in my records"
4343- DO NOT make up information, invent characters, or hallucinate details
4444- DO NOT use external knowledge about Star Trek - only use the provided records
4545- If asked about something not in the records, be honest about the limitation
46+ - Stay in character as an LCARS computer system at all times
4647
4748"""
4849
@@ -57,14 +58,14 @@ def get_user_prompt(context_text: str, query: str) -> str:
5758 """Format user prompt with context and query"""
5859
5960 if not context_text .strip ():
60- return f"I have no relevant records for this query . Please ask about Star Trek topics that are documented in Memory Alpha .\n \n Query : { query } "
61+ return f"Starfleet database records contain no relevant information for this inquiry . Please inquire about documented Star Trek topics.\n \n INQUIRY : { query } "
6162
6263 return f"""MEMORY ALPHA RECORDS:
6364{ context_text }
6465
65- QUESTION : { query }
66+ INQUIRY : { query }
6667
67- Answer using ONLY the information in the records above. If the records don't contain the information needed to answer this question, say so clearly ."""
68+ Accessing Starfleet database records. Provide analysis using ONLY the information in the records above. If the records don't contain the information needed to answer this inquiry, state that the information is not available in current records ."""
6869
6970class MemoryAlphaRAG :
7071 def __init__ (self ,
@@ -88,42 +89,92 @@ def __init__(self,
8889 self .thinking_text = thinking_text
8990 self .conversation_history : List [Dict [str , str ]] = []
9091
91- # Initialize Ollama client first
92+ # Initialize lightweight components
9293 self .ollama_client = ollama .Client (host = self .ollama_url )
93-
94- # Initialize ChromaDB
9594 self .client = chromadb .PersistentClient (
9695 path = self .chroma_db_path ,
9796 settings = Settings (allow_reset = False )
9897 )
9998
100- # Initialize text collection
101- logger .info ("Loading text embedding model..." )
102- self .text_model = SentenceTransformer ('all-MiniLM-L6-v2' )
103- logger .info ("Text model loaded successfully" )
104-
105- from chromadb .utils import embedding_functions
106- class TextEmbeddingFunction (embedding_functions .EmbeddingFunction ):
107- def __init__ (self , text_model ):
108- self .text_model = text_model
109- def __call__ (self , input ):
110- embeddings = []
111- for text in input :
112- embedding = self .text_model .encode (text )
113- embeddings .append (embedding .tolist ())
114- return embeddings
115-
116- self .text_ef = TextEmbeddingFunction (self .text_model )
117- self .text_collection = self .client .get_or_create_collection ("memoryalpha_text" , embedding_function = self .text_ef )
118-
119- # Initialize cross-encoder for reranking
120- try :
121- logger .info ("Loading cross-encoder model..." )
122- self .cross_encoder = CrossEncoder ('BAAI/bge-reranker-v2-m3' )
123- logger .info ("Cross-encoder model loaded successfully" )
124- except Exception :
125- logger .warning ("Could not load cross-encoder, using basic search only" )
126- self .cross_encoder = None
99+ # Lazy-loaded components
100+ self ._text_model = None
101+ self ._cross_encoder = None
102+ self ._clip_model = None
103+ self ._text_collection = None
104+ self ._image_collection = None
105+ self ._text_ef = None
106+ self ._clip_ef = None
107+
108+ @property
109+ def text_model (self ):
110+ """Lazy load text embedding model."""
111+ if self ._text_model is None :
112+ logger .info ("Loading text embedding model..." )
113+ self ._text_model = SentenceTransformer ('all-MiniLM-L6-v2' )
114+ logger .info ("Text model loaded successfully" )
115+ return self ._text_model
116+
117+ @property
118+ def cross_encoder (self ):
119+ """Lazy load cross-encoder model."""
120+ if self ._cross_encoder is None :
121+ try :
122+ logger .info ("Loading cross-encoder model..." )
123+ self ._cross_encoder = CrossEncoder ('BAAI/bge-reranker-v2-m3' )
124+ logger .info ("Cross-encoder model loaded successfully" )
125+ except Exception as e :
126+ logger .warning (f"Could not load cross-encoder: { e } " )
127+ self ._cross_encoder = None
128+ return self ._cross_encoder
129+
130+ @property
131+ def clip_model (self ):
132+ """Lazy load CLIP model for image search."""
133+ if self ._clip_model is None :
134+ logger .info ("Loading CLIP model for image search..." )
135+ self ._clip_model = SentenceTransformer ('clip-ViT-B-32' )
136+ logger .info ("CLIP model loaded successfully" )
137+ return self ._clip_model
138+
139+ @property
140+ def text_collection (self ):
141+ """Lazy load text collection."""
142+ if self ._text_collection is None :
143+ from chromadb .utils import embedding_functions
144+
145+ class TextEmbeddingFunction (embedding_functions .EmbeddingFunction ):
146+ def __init__ (self , text_model ):
147+ self .text_model = text_model
148+ def __call__ (self , input ):
149+ embeddings = []
150+ for text in input :
151+ embedding = self .text_model .encode (text )
152+ embeddings .append (embedding .tolist ())
153+ return embeddings
154+
155+ self ._text_ef = TextEmbeddingFunction (self .text_model )
156+ self ._text_collection = self .client .get_or_create_collection ("memoryalpha_text" , embedding_function = self ._text_ef )
157+ return self ._text_collection
158+
159+ @property
160+ def image_collection (self ):
161+ """Lazy load image collection."""
162+ if self ._image_collection is None :
163+ from chromadb .utils import embedding_functions
164+
165+ class CLIPEmbeddingFunction (embedding_functions .EmbeddingFunction ):
166+ def __init__ (self , clip_model ):
167+ self .clip_model = clip_model
168+ def __call__ (self , input ):
169+ embeddings = []
170+ for img in input :
171+ embedding = self .clip_model .encode (img )
172+ embeddings .append (embedding .tolist ())
173+ return embeddings
174+
175+ self ._clip_ef = CLIPEmbeddingFunction (self .clip_model )
176+ self ._image_collection = self .client .get_or_create_collection ("memoryalpha_images" , embedding_function = self ._clip_ef )
177+ return self ._image_collection
127178
128179 def search (self , query : str , top_k : int = 10 ) -> List [Dict [str , Any ]]:
129180 """Search the Memory Alpha database for relevant documents."""
@@ -257,4 +308,70 @@ def _replace_thinking_tags(self, answer: str) -> str:
257308 def _update_history (self , question : str , answer : str ):
258309 """Update conversation history."""
259310 self .conversation_history .append ({"question" : question , "answer" : answer })
260- self .conversation_history = self .conversation_history [- self .max_history_turns :]
311+ self .conversation_history = self .conversation_history [- self .max_history_turns :]
312+
313+ def search_image (self , image_path : str , top_k : int = 5 ,
314+ model : str = os .getenv ("DEFAULT_IMAGE_MODEL" )) -> Dict [str , Any ]:
315+ """
316+ Search for images similar to the provided image.
317+ """
318+ from PIL import Image
319+ import requests
320+ import tempfile
321+ import os
322+
323+ if not model :
324+ raise ValueError ("model must be provided or set in DEFAULT_IMAGE_MODEL environment variable." )
325+
326+ try :
327+ # Load image and generate embedding
328+ image = Image .open (image_path ).convert ('RGB' )
329+ image_embedding = self .clip_model .encode (image )
330+ image_embedding = image_embedding .tolist ()
331+
332+ # Search image collection
333+ image_results = self .image_collection .query (
334+ query_embeddings = [image_embedding ],
335+ n_results = top_k
336+ )
337+
338+ # Process results
339+ if not image_results ["documents" ] or not image_results ["documents" ][0 ]:
340+ return {"model_answer" : "No matching visual records found in Starfleet archives." }
341+
342+ # Format results for the model
343+ formatted_results = []
344+ for i , (doc , meta , dist ) in enumerate (zip (
345+ image_results ['documents' ][0 ],
346+ image_results ['metadatas' ][0 ],
347+ image_results ['distances' ][0 ]
348+ ), 1 ):
349+ record_name = meta .get ('image_name' , 'Unknown visual record' )
350+ formatted_results .append (f"Visual Record { i } : { record_name } " )
351+
352+ result_text = "\n " .join (formatted_results )
353+
354+ # Use LLM to provide a natural language summary
355+ prompt = f"""You are an LCARS computer system analyzing visual records from Starfleet archives.
356+
357+ Based on these visual record matches, identify what subject or scene is being depicted:
358+
359+ { result_text }
360+
361+ Provide a direct identification of the subject without referencing images, searches, or technical processes. Stay in character as an LCARS computer system."""
362+
363+ result = self .ollama_client .chat (
364+ model = model ,
365+ messages = [
366+ {"role" : "system" , "content" : "You are an LCARS computer system. Respond in character without breaking the Star Trek universe immersion. Do not reference images, searches, or technical processes." },
367+ {"role" : "user" , "content" : prompt }
368+ ],
369+ stream = False ,
370+ options = {"temperature" : 0.3 , "num_predict" : 200 }
371+ )
372+
373+ return {"model_answer" : result ['message' ]['content' ]}
374+
375+ except Exception as e :
376+ logger .error (f"Image search failed: { e } " )
377+ return {"model_answer" : "Error accessing visual records database." }
0 commit comments