@@ -99,63 +99,82 @@ def mock_embedding(text: str) -> List[float]:
9999async def global_exception_handler (request : Request , exc : Exception ):
100100 """Global exception handler to log all unhandled exceptions."""
101101 error_id = f"error-{ time .time ()} "
102- logger .error (f"Unhandled exception: { error_id } - { str (exc )} " , exc_info = True )
102+ logger .error (f"Error { error_id } : { exc } " )
103+ logger .error (traceback .format_exc ())
103104 return JSONResponse (
104105 status_code = status .HTTP_500_INTERNAL_SERVER_ERROR ,
105106 content = {
106- "error " : "An unexpected error occurred" ,
107+ "detail " : "An unexpected error occurred" ,
107108 "error_id" : error_id ,
108- "detail " : str (exc ) if app . debug else "Internal Server Error" ,
109+ "message " : str (exc ),
109110 },
110111 )
111112
112113
113- # Initialize Chroma client - either local or remote
114- try :
115- if CHROMA_HOST and CHROMA_PORT and not USE_PERSISTENT_CHROMA :
116- logger .info (f"Connecting to Chroma server at { CHROMA_HOST } :{ CHROMA_PORT } " )
117- chroma_client = chromadb .HttpClient (host = CHROMA_HOST , port = int (CHROMA_PORT ))
118- else :
119- logger .info (f"Using local Chroma with persistence at { PERSIST_DIRECTORY } " )
120- # Updated client initialization for newer ChromaDB versions
121- chroma_client = chromadb .PersistentClient (path = PERSIST_DIRECTORY )
122-
123- # Get collection or create if it doesn't exist
124- collection = chroma_client .get_collection (name = COLLECTION_NAME )
125- logger .info (
126- f"Connected to collection '{ COLLECTION_NAME } ' with { collection .count ()} documents"
127- )
128- except Exception as e :
129- logger .error (f"Error connecting to Chroma: { e } " , exc_info = True )
130- logger .info ("Creating a new collection. Please run indexer.py to populate it." )
114+ # Initialize Chroma client
115+ def get_chroma_client ():
116+ """Initialize and return a Chroma client."""
131117 try :
132- # Try to create the collection if it doesn't exist
133- if CHROMA_HOST and CHROMA_PORT and not USE_PERSISTENT_CHROMA :
134- chroma_client = chromadb .HttpClient (host = CHROMA_HOST , port = int (CHROMA_PORT ))
118+ # Check if external Chroma server is specified
119+ if CHROMA_HOST :
120+ logger .info (f"Connecting to external Chroma at { CHROMA_HOST } :{ CHROMA_PORT } " )
121+ client = chromadb .HttpClient (
122+ host = CHROMA_HOST ,
123+ port = int (CHROMA_PORT ) if CHROMA_PORT else 8000 ,
124+ settings = Settings (anonymized_telemetry = False ),
125+ )
135126 else :
136- # Updated client initialization for newer ChromaDB versions
137- chroma_client = chromadb .PersistentClient (path = PERSIST_DIRECTORY )
138- collection = chroma_client .create_collection (name = COLLECTION_NAME )
139- except Exception as create_error :
140- logger .critical (
141- f"Failed to create Chroma collection: { create_error } " , exc_info = True
127+ # Use local persistent Chroma
128+ logger .info (f"Using local Chroma with persistence at { PERSIST_DIRECTORY } " )
129+ client = chromadb .PersistentClient (
130+ path = PERSIST_DIRECTORY , settings = Settings (anonymized_telemetry = False )
131+ )
132+
133+ # Test connection works
134+ client .heartbeat ()
135+ return client
136+ except Exception as e :
137+ logger .error (f"Failed to connect to Chroma: { e } " )
138+ logger .error (
139+ f"Check if Chroma is running at { CHROMA_HOST or 'localhost' } :{ CHROMA_PORT or '8000' } "
142140 )
143- # We'll continue execution, but API calls that use the collection will fail
141+ # Create an in-memory client for fallback
142+ logger .warning ("Using in-memory Chroma as fallback (no persistence!)" )
143+ return chromadb .Client (Settings (anonymized_telemetry = False ))
144+
145+
146+ # Initialize Chroma collection
147+ def get_collection ():
148+ """Get or create the collection for storing Ignition project data."""
149+ try :
150+ client = get_chroma_client ()
151+ # Check if collection exists, create it if it doesn't
152+ try :
153+ collection = client .get_collection (COLLECTION_NAME )
154+ doc_count = collection .count ()
155+ logger .info (
156+ f"Connected to collection '{ COLLECTION_NAME } ' with { doc_count } documents"
157+ )
158+ except ValueError :
159+ logger .info (f"Creating new collection '{ COLLECTION_NAME } '" )
160+ collection = client .create_collection (COLLECTION_NAME )
161+
162+ return collection
163+ except Exception as e :
164+ logger .error (f"Error connecting to collection: { e } " )
165+ # For API to start even without Chroma, return None
166+ # Endpoints will need to check if collection is None
167+ return None
144168
145169
146170# Define query request model
147171class QueryRequest (BaseModel ):
148- query : str = Field (..., description = "The natural language query to search for" )
149- top_k : int = Field (5 , description = "Number of results to return" , ge = 1 , le = 20 )
150- filter_type : Optional [str ] = Field (
151- None , description = "Filter results by document type (perspective or tag)"
152- )
153- filter_path : Optional [str ] = Field (
154- None , description = "Filter results by file path pattern"
155- )
156- use_mock : Optional [bool ] = Field (
157- None ,
158- description = "Use mock embeddings for testing (overrides environment variable)" ,
172+ """Request model for querying the vector database."""
173+
174+ query : str = Field (..., description = "Query text to search for" )
175+ top_k : int = Field (3 , description = "Number of results to return" )
176+ filter_metadata : Dict [str , Any ] = Field (
177+ default = None , description = "Optional metadata filters for the query"
159178 )
160179
161180
@@ -224,105 +243,96 @@ async def health_check(deps: None = Depends(verify_dependencies)):
224243 }
225244
226245
227- @app .post ("/query" , response_model = QueryResponse )
228- async def query_vector_store (
229- req : QueryRequest , deps : None = Depends (verify_dependencies )
230- ):
231- """Query the vector database for similar content."""
232- start_time = time .time ()
233- logger .info (
234- f"Query request received: '{ req .query } ', top_k={ req .top_k } , filters={ req .filter_type } /{ req .filter_path } "
235- )
236-
237- # Check if mock mode is requested for this query
238- use_mock = MOCK_EMBEDDINGS
239- if req .use_mock is not None :
240- use_mock = req .use_mock
241- logger .info (f"Mock mode overridden to: { use_mock } " )
246+ @app .post ("/query" , summary = "Query the vector database" )
247+ async def query (request : QueryRequest ):
248+ """
249+ Search for relevant context from Ignition project files.
242250
251+ This endpoint performs semantic search using the query text.
252+ """
243253 try :
244- # Prepare filter if any
245- where_filter = {}
246- if req .filter_type :
247- where_filter ["type" ] = req .filter_type
248- if req .filter_path :
249- where_filter ["filepath" ] = {"$contains" : req .filter_path }
250-
251- # Use where_filter only if it has any conditions
252- where_document = where_filter if where_filter else None
253-
254- # Get embedding for the query
255- try :
256- embedding_start = time .time ()
254+ # Get collection (may be None if Chroma connection fails)
255+ collection = get_collection ()
256+ if collection is None :
257+ logger .error ("Failed to connect to the vector database" )
258+ raise HTTPException (
259+ status_code = 503 ,
260+ detail = "Vector database is not available. Please check Chroma connection." ,
261+ )
257262
258- if use_mock :
259- # Use mock embedding if in mock mode
260- query_vector = mock_embedding (req .query )
261- logger .debug ("Using mock embedding for query" )
262- else :
263- # Use OpenAI API for real embedding
264- response = openai_client .embeddings .create (
265- model = "text-embedding-ada-002" , input = [req .query ]
263+ # Check if collection is empty
264+ if collection .count () == 0 :
265+ logger .warning ("Empty collection, no results will be returned" )
266+ return {
267+ "results" : [],
268+ "metadata" : {
269+ "total_chunks" : 0 ,
270+ "query" : request .query ,
271+ "message" : "The collection is empty. Please run the indexer to populate it." ,
272+ },
273+ }
274+
275+ # Generate embedding for the query
276+ query_embedding = None
277+ if MOCK_EMBEDDINGS or not openai_client :
278+ logger .info ("Using mock embedding for query" )
279+ query_embedding = mock_embedding (request .query )
280+ else :
281+ try :
282+ # Use OpenAI API to generate embedding
283+ embedding_response = openai_client .embeddings .create (
284+ input = request .query , model = "text-embedding-ada-002"
266285 )
267- query_vector = response .data [0 ].embedding
268-
269- logger .debug (f"Generated embedding in { time .time () - embedding_start :.2f} s" )
270- except Exception as e :
271- logger .error (f"Error generating embedding: { e } " , exc_info = True )
272- if not use_mock :
286+ query_embedding = embedding_response .data [0 ].embedding
287+ except Exception as e :
288+ logger .error (f"Error generating embedding: { e } " )
289+ # Fallback to mock embedding
273290 logger .info ("Falling back to mock embedding" )
274- query_vector = mock_embedding (req .query )
275- use_mock = True
276- else :
277- raise HTTPException (
278- status_code = status .HTTP_503_SERVICE_UNAVAILABLE ,
279- detail = f"Error generating embedding: { str (e )} " ,
280- )
291+ query_embedding = mock_embedding (request .query )
292+
293+ # Query the collection
294+ results = collection .query (
295+ query_embeddings = query_embedding ,
296+ n_results = request .top_k ,
297+ where = request .filter_metadata or None ,
298+ include = ["metadatas" , "documents" , "distances" ],
299+ )
281300
282- # Perform similarity search in Chroma
283- try :
284- query_start = time .time ()
285- results = collection .query (
286- query_embeddings = [query_vector ],
287- n_results = req .top_k ,
288- where = where_document ,
301+ # Process and format results
302+ processed_results = []
303+ for i in range (len (results ["ids" ][0 ])):
304+ metadata = results ["metadatas" ][0 ][i ] if results ["metadatas" ] else {}
305+ document = results ["documents" ][0 ][i ] if results ["documents" ] else ""
306+ distance = results ["distances" ][0 ][i ] if results ["distances" ] else 0
307+
308+ # Convert distance to similarity score (cosine similarity)
309+ similarity = 1.0 - distance if distance <= 2.0 else 0
310+
311+ # Add result
312+ processed_results .append (
313+ {
314+ "content" : document ,
315+ "metadata" : metadata ,
316+ "similarity" : similarity ,
317+ "id" : results ["ids" ][0 ][i ] if results ["ids" ] else None ,
318+ }
289319 )
290- logger .debug (f"Chroma query completed in { time .time () - query_start :.2f} s" )
291- except Exception as e :
292- logger .error (f"Error querying vector database: { e } " , exc_info = True )
293- raise HTTPException (
294- status_code = status .HTTP_500_INTERNAL_SERVER_ERROR ,
295- detail = f"Error querying vector database: { str (e )} " ,
296- )
297-
298- chunks = []
299- # Check if we got any results
300- if results and "documents" in results and results ["documents" ]:
301- # results["documents"][0] is list of document texts, [0] because we passed one query
302- for doc_text , meta , distance in zip (
303- results ["documents" ][0 ],
304- results ["metadatas" ][0 ],
305- results ["distances" ][0 ],
306- ):
307- chunks .append (
308- Chunk (content = doc_text , metadata = meta , similarity = distance )
309- )
310320
311- response_time = time .time () - start_time
312- logger .info (
313- f"Query completed in { response_time :.2f} s, found { len (chunks )} results, mock_mode={ use_mock } "
314- )
315- return QueryResponse (results = chunks , total = len (chunks ), mock_used = use_mock )
321+ return {
322+ "results" : processed_results ,
323+ "metadata" : {
324+ "total_chunks" : collection .count (),
325+ "query" : request .query ,
326+ "embedding_type" : (
327+ "mock" if MOCK_EMBEDDINGS or not openai_client else "openai"
328+ ),
329+ },
330+ }
316331
317- except HTTPException :
318- # Re-raise HTTP exceptions to preserve status code
319- raise
320332 except Exception as e :
321- error_msg = f"Error processing query: { str (e )} "
322- logger .error (error_msg , exc_info = True )
323- raise HTTPException (
324- status_code = status .HTTP_500_INTERNAL_SERVER_ERROR , detail = error_msg
325- )
333+ logger .error (f"Error in query endpoint: { e } " )
334+ logger .error (traceback .format_exc ())
335+ raise HTTPException (status_code = 500 , detail = str (e ))
326336
327337
328338# Agent-optimized query for Cursor integration
0 commit comments