Skip to content

Commit 34fab2f

Browse files
committed
fix: local run setup
1 parent aef4e09 commit 34fab2f

File tree

4 files changed

+265
-131
lines changed

4 files changed

+265
-131
lines changed

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22
*.bin
33
.env*
44

5+
# Sqlite
6+
*.db
7+
*.sqlite
8+
*.sqlite3
9+
10+
chroma_index/
11+
512
# Byte-compiled / optimized / DLL files
613
__pycache__/
714
*.py[cod]

api.py

Lines changed: 141 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -99,63 +99,82 @@ def mock_embedding(text: str) -> List[float]:
9999
async 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
147171
class 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

docker-compose.local.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
version: "3.8"
2+
3+
services:
4+
# Chroma Vector Database
5+
chroma:
6+
image: chromadb/chroma:latest
7+
volumes:
8+
- ./chroma_index:/chroma/chroma
9+
environment:
10+
- ALLOW_RESET=true
11+
- ANONYMIZED_TELEMETRY=false
12+
healthcheck:
13+
test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/heartbeat"]
14+
interval: 10s
15+
timeout: 5s
16+
retries: 5
17+
ports:
18+
- "8000:8000" # Map to default port so host apps can connect
19+
networks:
20+
- ignition-network
21+
22+
networks:
23+
ignition-network:
24+
driver: bridge

0 commit comments

Comments
 (0)