bhoomika19 commited on
Commit
6eeecf2
·
verified ·
1 Parent(s): 2800165

Upload 23 files

Browse files
backend/__pycache__/main.cpython-311.pyc ADDED
Binary file (4.86 kB). View file
 
backend/main.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FastAPI backend for Math Agentic RAG system.
3
+ """
4
+ import sys
5
+ import os
6
+ from pathlib import Path
7
+
8
+ # Add the parent directory to Python path to import database module
9
+ parent_dir = Path(__file__).parent.parent
10
+ sys.path.append(str(parent_dir))
11
+
12
+ from fastapi import FastAPI, HTTPException
13
+ from fastapi.middleware.cors import CORSMiddleware
14
+ from contextlib import asynccontextmanager
15
+ import logging
16
+ import structlog
17
+ from dotenv import load_dotenv
18
+
19
+ # Import routes
20
+ from routes.search import router as search_router
21
+ from routes.feedback import router as feedback_router
22
+
23
+ # Load environment variables
24
+ load_dotenv()
25
+
26
+ # Configure structured logging
27
+ structlog.configure(
28
+ processors=[
29
+ structlog.stdlib.filter_by_level,
30
+ structlog.stdlib.add_logger_name,
31
+ structlog.stdlib.add_log_level,
32
+ structlog.stdlib.PositionalArgumentsFormatter(),
33
+ structlog.processors.TimeStamper(fmt="iso"),
34
+ structlog.processors.StackInfoRenderer(),
35
+ structlog.processors.format_exc_info,
36
+ structlog.processors.UnicodeDecoder(),
37
+ structlog.processors.JSONRenderer()
38
+ ],
39
+ context_class=dict,
40
+ logger_factory=structlog.stdlib.LoggerFactory(),
41
+ cache_logger_on_first_use=True,
42
+ )
43
+
44
+ logger = structlog.get_logger()
45
+
46
+ @asynccontextmanager
47
+ async def lifespan(app: FastAPI):
48
+ """Application lifespan manager."""
49
+ logger.info("Starting Math Agentic RAG Backend...")
50
+
51
+ # Startup
52
+ try:
53
+ # Initialize services here if needed
54
+ logger.info("Backend services initialized successfully")
55
+ yield
56
+ except Exception as e:
57
+ logger.error("Failed to initialize backend services", error=str(e))
58
+ raise
59
+ finally:
60
+ # Cleanup
61
+ logger.info("Shutting down Math Agentic RAG Backend...")
62
+
63
+ # Create FastAPI application
64
+ app = FastAPI(
65
+ title="Math Agentic RAG API",
66
+ description="Backend API for Math-focused Agentic RAG system with knowledge base and web search capabilities",
67
+ version="1.0.0",
68
+ docs_url="/docs",
69
+ redoc_url="/redoc",
70
+ lifespan=lifespan
71
+ )
72
+
73
+ # Add CORS middleware
74
+ app.add_middleware(
75
+ CORSMiddleware,
76
+ allow_origins=["*"], # Configure this properly for production
77
+ allow_credentials=True,
78
+ allow_methods=["*"],
79
+ allow_headers=["*"],
80
+ )
81
+
82
+ # Include routers
83
+ app.include_router(search_router, prefix="/api", tags=["search"])
84
+ app.include_router(feedback_router, prefix="/api", tags=["feedback"])
85
+
86
+ @app.get("/")
87
+ async def root():
88
+ """Root endpoint for health check."""
89
+ return {
90
+ "message": "Math Agentic RAG Backend API",
91
+ "status": "running",
92
+ "version": "1.0.0",
93
+ "docs": "/docs"
94
+ }
95
+
96
+ @app.get("/health")
97
+ async def health_check():
98
+ """Health check endpoint."""
99
+ return {
100
+ "status": "healthy",
101
+ "timestamp": structlog.processors.TimeStamper(fmt="iso")._stamper(),
102
+ "services": {
103
+ "api": "running",
104
+ "database": "connected", # Will be updated with actual checks
105
+ "mcp": "available" # Will be updated with actual checks
106
+ }
107
+ }
108
+
109
+ if __name__ == "__main__":
110
+ import uvicorn
111
+ uvicorn.run(
112
+ "main:app",
113
+ host="0.0.0.0",
114
+ port=8000,
115
+ reload=True,
116
+ log_level="info"
117
+ )
backend/models/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Empty __init__.py file to make this a Python package
backend/models/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (204 Bytes). View file
 
backend/models/__pycache__/schemas.cpython-311.pyc ADDED
Binary file (6.59 kB). View file
 
backend/models/schemas.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Pydantic models for API request/response schemas.
3
+ """
4
+ from pydantic import BaseModel, Field
5
+ from typing import List, Optional, Literal
6
+ from datetime import datetime
7
+ import uuid
8
+
9
+ # Request Models
10
+ class SearchRequest(BaseModel):
11
+ """Request model for search endpoint."""
12
+ question: str = Field(..., description="Math question to search for", max_length=200)
13
+
14
+ class FeedbackRequest(BaseModel):
15
+ """Request model for feedback endpoint."""
16
+ question: str = Field(..., description="Original question")
17
+ response_id: str = Field(..., description="UUID of the response")
18
+ correctness_rating: int = Field(..., ge=1, le=5, description="Rating from 1-5")
19
+ comment: str = Field("", description="Optional feedback comment")
20
+
21
+ # Response Models
22
+ class SearchResult(BaseModel):
23
+ """Individual search result."""
24
+ problem: str = Field(..., description="Math problem statement")
25
+ solution: str = Field(..., description="Solution to the problem")
26
+ score: float = Field(..., description="Similarity score")
27
+
28
+ class SearchResponse(BaseModel):
29
+ """Response model for search endpoint."""
30
+ response_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
31
+ final_answer: str = Field(..., description="The main answer to the question")
32
+ source: Literal["KB", "MCP", "Gemini"] = Field(..., description="Source of the answer")
33
+ explanation: Optional[str] = Field(None, description="Optional explanation")
34
+ results: List[SearchResult] = Field(default_factory=list, description="Detailed search results")
35
+ metadata: dict = Field(default_factory=dict, description="Additional metadata")
36
+ response_time_ms: Optional[float] = Field(None, description="Response time in milliseconds")
37
+
38
+ class FeedbackResponse(BaseModel):
39
+ """Response model for feedback endpoint."""
40
+ message: str = Field(..., description="Confirmation message")
41
+ feedback_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
42
+
43
+ # Internal Models
44
+ class APILogEntry(BaseModel):
45
+ """Model for logging API requests and responses."""
46
+ request_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
47
+ timestamp: datetime = Field(default_factory=datetime.utcnow)
48
+ endpoint: str = Field(..., description="API endpoint called")
49
+ method: str = Field(..., description="HTTP method")
50
+ request_data: dict = Field(..., description="Request payload")
51
+ response_data: dict = Field(..., description="Response payload")
52
+ response_time_ms: float = Field(..., description="Response time in milliseconds")
53
+ source: Literal["KB", "MCP", "Gemini"] = Field(..., description="Source of the answer")
54
+ feedback_received: bool = Field(default=False, description="Whether feedback was received")
55
+ status_code: int = Field(..., description="HTTP status code")
56
+
57
+ class ErrorResponse(BaseModel):
58
+ """Standard error response model."""
59
+ error: str = Field(..., description="Error message")
60
+ detail: Optional[str] = Field(None, description="Detailed error information")
61
+ request_id: Optional[str] = Field(None, description="Request ID for tracking")
backend/requirements.txt ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # FastAPI and web server
2
+ fastapi==0.104.1
3
+ uvicorn[standard]==0.24.0
4
+
5
+ # HTTP client for MCP calls
6
+ httpx==0.25.2
7
+
8
+ # Qdrant vector database
9
+ qdrant-client==1.8.0
10
+
11
+ # AI Guardrails
12
+ guardrails-ai==0.4.5
13
+
14
+ # Google Generative AI (Gemini)
15
+ google-generativeai==0.8.3
16
+
17
+ # Environment management
18
+ python-dotenv==1.0.0
19
+
20
+ # Structured logging
21
+ structlog==23.2.0
22
+
23
+ # Data processing and embeddings (reusing from database module)
24
+ sentence-transformers==2.2.2
25
+ datasets==2.18.0
26
+ pandas==2.1.4
27
+
28
+ # MCP client (for web search integration)
29
+ fastmcp==0.3.0
30
+
31
+ # Logging and monitoring
32
+ structlog==23.2.0
33
+
34
+ # Data validation
35
+ pydantic==2.5.0
36
+
37
+ # Async support
38
+ asyncio==3.4.3
39
+
40
+ # UUID generation (built-in, but listed for clarity)
41
+ # uuid (built-in)
42
+
43
+ # JSON handling
44
+ orjson==3.9.10
backend/routes/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Empty __init__.py file to make this a Python package
backend/routes/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (204 Bytes). View file
 
backend/routes/__pycache__/feedback.cpython-311.pyc ADDED
Binary file (3.87 kB). View file
 
backend/routes/__pycache__/search.cpython-311.pyc ADDED
Binary file (18.4 kB). View file
 
backend/routes/feedback.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Feedback endpoint for the Math Agentic RAG system.
3
+ """
4
+ from fastapi import APIRouter, HTTPException, BackgroundTasks
5
+ import structlog
6
+ import time
7
+ from typing import Dict, Any
8
+
9
+ from models.schemas import FeedbackRequest, FeedbackResponse, ErrorResponse
10
+
11
+ router = APIRouter()
12
+ logger = structlog.get_logger()
13
+
14
+ @router.post("/feedback", response_model=FeedbackResponse)
15
+ async def submit_feedback(
16
+ feedback: FeedbackRequest,
17
+ background_tasks: BackgroundTasks
18
+ ) -> FeedbackResponse:
19
+ """
20
+ Submit user feedback for search results.
21
+
22
+ Args:
23
+ feedback: Feedback data including response_id, rating, and comments
24
+ background_tasks: Background tasks for processing
25
+
26
+ Returns:
27
+ FeedbackResponse confirming feedback receipt
28
+ """
29
+ start_time = time.time()
30
+
31
+ try:
32
+ logger.info("Processing feedback submission",
33
+ response_id=feedback.response_id,
34
+ rating=feedback.rating,
35
+ has_comments=bool(feedback.comments))
36
+
37
+ # Process feedback in background
38
+ background_tasks.add_task(
39
+ process_feedback,
40
+ feedback.dict()
41
+ )
42
+
43
+ response_time_ms = (time.time() - start_time) * 1000
44
+
45
+ response = FeedbackResponse(
46
+ message="Feedback received successfully",
47
+ feedback_id=feedback.response_id, # Using response_id as feedback_id for traceability
48
+ status="received"
49
+ )
50
+
51
+ logger.info("Feedback submission completed",
52
+ response_id=feedback.response_id,
53
+ response_time_ms=response_time_ms)
54
+
55
+ return response
56
+
57
+ except Exception as e:
58
+ logger.error("Feedback submission failed",
59
+ response_id=feedback.response_id,
60
+ error=str(e))
61
+
62
+ raise HTTPException(
63
+ status_code=500,
64
+ detail=f"Failed to process feedback: {str(e)}"
65
+ )
66
+
67
+ async def process_feedback(feedback_data: Dict[str, Any]):
68
+ """
69
+ Process feedback data in the background.
70
+
71
+ This function will:
72
+ 1. Store feedback in Qdrant for analysis
73
+ 2. Update system metrics
74
+ 3. Trigger retraining if needed (future enhancement)
75
+ """
76
+ try:
77
+ logger.info("Processing feedback in background",
78
+ response_id=feedback_data.get("response_id"))
79
+
80
+ # TODO: Implement feedback storage in Qdrant
81
+ # TODO: Update system performance metrics
82
+ # TODO: Implement feedback-based model improvements
83
+
84
+ # For now, just log the feedback
85
+ logger.info("Feedback processed successfully",
86
+ feedback_data=feedback_data)
87
+
88
+ except Exception as e:
89
+ logger.error("Background feedback processing failed",
90
+ error=str(e),
91
+ feedback_data=feedback_data)
backend/routes/search.py ADDED
@@ -0,0 +1,452 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Search endpoint for the Math Agentic RAG system.
3
+ """
4
+ from fastapi import APIRouter, HTTPException, BackgroundTasks
5
+ import sys
6
+ from pathlib import Path
7
+ import structlog
8
+ import time
9
+ import uuid
10
+
11
+ # Add parent directory to import database module
12
+ parent_dir = Path(__file__).parent.parent.parent
13
+ sys.path.append(str(parent_dir))
14
+
15
+ from models.schemas import SearchRequest, SearchResponse, ErrorResponse, SearchResult
16
+ from services.qdrant_service import QdrantService
17
+ from services.mcp_service import MCPService
18
+ from services.guardrails_service import GuardrailsService
19
+ from services.gemini_service import GeminiService
20
+
21
+ router = APIRouter()
22
+ logger = structlog.get_logger()
23
+
24
+ # Initialize services (will be properly initialized when packages are installed)
25
+ qdrant_service = None
26
+ mcp_service = None
27
+ guardrails_service = None
28
+ gemini_service = None
29
+
30
+ def initialize_services():
31
+ """Initialize services on first request."""
32
+ global qdrant_service, mcp_service, guardrails_service, gemini_service
33
+
34
+ if qdrant_service is None:
35
+ qdrant_service = QdrantService()
36
+ mcp_service = MCPService()
37
+ guardrails_service = GuardrailsService()
38
+ gemini_service = GeminiService()
39
+
40
+ @router.post("/search", response_model=SearchResponse)
41
+ async def search_math_problems(
42
+ request: SearchRequest,
43
+ background_tasks: BackgroundTasks
44
+ ) -> SearchResponse:
45
+ """
46
+ Search for math problems in knowledge base or web.
47
+
48
+ Args:
49
+ request: Search request containing the math question
50
+ background_tasks: Background tasks for logging
51
+
52
+ Returns:
53
+ SearchResponse with results and metadata
54
+ """
55
+ start_time = time.time()
56
+ response_id = str(uuid.uuid4())
57
+
58
+ try:
59
+ # Initialize services if not already done
60
+ initialize_services()
61
+
62
+ logger.info("Processing search request",
63
+ request_id=response_id,
64
+ question=request.question)
65
+
66
+ # Step 1: Validate input with guardrails
67
+ validated_question = guardrails_service.validate_input(request.question)
68
+
69
+ # Step 2: Search knowledge base (Qdrant)
70
+ kb_results = await qdrant_service.search_similar(validated_question)
71
+
72
+ # Step 3: Determine if we need web search fallback with enhanced logic
73
+ confidence_threshold = 0.8 # Increased from 0.5 to 0.8 for higher confidence requirement
74
+ best_score = kb_results[0].score if kb_results else 0.0
75
+
76
+ logger.info("Evaluating search results",
77
+ kb_results_found=len(kb_results) if kb_results else 0,
78
+ best_score=best_score,
79
+ threshold=confidence_threshold)
80
+
81
+ if best_score >= confidence_threshold:
82
+ # Use knowledge base results - high confidence match found
83
+ source = "KB"
84
+ final_answer = kb_results[0].solution if kb_results else "No solution found"
85
+ explanation = f"High confidence match found (score: {best_score:.3f} ≥ {confidence_threshold})"
86
+ results = kb_results[:3] # Return top 3 results
87
+
88
+ logger.info("Using knowledge base results",
89
+ confidence_score=best_score,
90
+ results_returned=len(results))
91
+
92
+ else:
93
+ # First fallback: Web search via MCP
94
+ logger.info("Low confidence KB results, trying web search fallback",
95
+ best_score=best_score,
96
+ threshold=confidence_threshold)
97
+
98
+ try:
99
+ web_results = await mcp_service.search_web(validated_question)
100
+ mcp_answer = web_results.get("answer", "")
101
+ mcp_confidence = web_results.get("confidence", 0.6) # Default MCP confidence
102
+
103
+ logger.info("MCP web search completed",
104
+ answer_length=len(mcp_answer),
105
+ mcp_confidence=mcp_confidence)
106
+
107
+ # Check if MCP results meet confidence threshold
108
+ if mcp_confidence >= confidence_threshold and mcp_answer:
109
+ # Use MCP results - sufficient confidence
110
+ source = "MCP"
111
+ final_answer = mcp_answer
112
+ explanation = f"KB confidence too low ({best_score:.3f} < {confidence_threshold}), used web search (confidence: {mcp_confidence:.3f})"
113
+
114
+ results = [SearchResult(
115
+ problem=validated_question,
116
+ solution=final_answer,
117
+ score=mcp_confidence
118
+ )]
119
+
120
+ logger.info("Using MCP web search results",
121
+ mcp_confidence=mcp_confidence)
122
+
123
+ else:
124
+ # Second fallback: Gemini LLM when both KB and MCP have low confidence
125
+ logger.info("Both KB and MCP have low confidence, falling back to Gemini LLM",
126
+ kb_score=best_score,
127
+ mcp_confidence=mcp_confidence,
128
+ threshold=confidence_threshold)
129
+
130
+ try:
131
+ if gemini_service and gemini_service.is_available():
132
+ gemini_result = await gemini_service.solve_math_problem(validated_question)
133
+
134
+ source = "Gemini"
135
+ final_answer = gemini_result.get("answer", "No solution generated")
136
+ gemini_confidence = gemini_result.get("confidence", 0.75)
137
+ explanation = f"Both KB ({best_score:.3f}) and MCP ({mcp_confidence:.3f}) below threshold ({confidence_threshold}), used Gemini LLM"
138
+
139
+ results = [SearchResult(
140
+ problem=validated_question,
141
+ solution=final_answer,
142
+ score=gemini_confidence
143
+ )]
144
+
145
+ logger.info("Gemini LLM response generated successfully",
146
+ answer_length=len(final_answer),
147
+ gemini_confidence=gemini_confidence)
148
+
149
+ else:
150
+ # Ultimate fallback: Use best available result
151
+ logger.warning("Gemini service unavailable, using best available result")
152
+
153
+ if mcp_answer and len(mcp_answer) > 20: # Prefer MCP if it has substantial content
154
+ source = "MCP"
155
+ final_answer = mcp_answer
156
+ explanation = f"All services below threshold, using MCP result (confidence: {mcp_confidence:.3f})"
157
+ results = [SearchResult(problem=validated_question, solution=final_answer, score=mcp_confidence)]
158
+ else:
159
+ source = "KB"
160
+ final_answer = kb_results[0].solution if kb_results else "No solution available"
161
+ explanation = f"All services below threshold, using best KB result (score: {best_score:.3f})"
162
+ results = kb_results[:1] if kb_results else []
163
+
164
+ except Exception as gemini_error:
165
+ logger.error("Gemini LLM failed, using MCP results", error=str(gemini_error))
166
+ source = "MCP"
167
+ final_answer = mcp_answer if mcp_answer else "No solution available"
168
+ explanation = f"Gemini failed, used MCP result (confidence: {mcp_confidence:.3f})"
169
+ results = [SearchResult(problem=validated_question, solution=final_answer, score=mcp_confidence)] if mcp_answer else []
170
+
171
+ except Exception as mcp_error:
172
+ logger.error("MCP web search failed, trying Gemini fallback", error=str(mcp_error))
173
+
174
+ # If MCP fails, try Gemini directly
175
+ try:
176
+ if gemini_service and gemini_service.is_available():
177
+ gemini_result = await gemini_service.solve_math_problem(validated_question)
178
+
179
+ source = "Gemini"
180
+ final_answer = gemini_result.get("answer", "No solution generated")
181
+ gemini_confidence = gemini_result.get("confidence", 0.75)
182
+ explanation = f"KB confidence low ({best_score:.3f}), MCP failed, used Gemini LLM"
183
+
184
+ results = [SearchResult(
185
+ problem=validated_question,
186
+ solution=final_answer,
187
+ score=gemini_confidence
188
+ )]
189
+
190
+ logger.info("Gemini LLM used after MCP failure",
191
+ answer_length=len(final_answer))
192
+
193
+ else:
194
+ # Final fallback to KB results
195
+ logger.warning("Both MCP and Gemini failed, using KB results")
196
+ source = "KB"
197
+ final_answer = kb_results[0].solution if kb_results else "No solution available"
198
+ explanation = f"MCP and Gemini failed, using best KB result (score: {best_score:.3f})"
199
+ results = kb_results[:1] if kb_results else []
200
+
201
+ except Exception as final_error:
202
+ logger.error("All fallbacks failed, using KB results", error=str(final_error))
203
+ source = "KB"
204
+ final_answer = kb_results[0].solution if kb_results else "No solution available"
205
+ explanation = f"All services failed, using best KB result (score: {best_score:.3f})"
206
+ results = kb_results[:1] if kb_results else []
207
+
208
+
209
+ # Step 4: Validate output with guardrails and create comprehensive response
210
+ logger.info("Validating final answer with guardrails",
211
+ answer_length=len(final_answer),
212
+ source=source)
213
+
214
+ try:
215
+ validated_response = guardrails_service.validate_output(final_answer)
216
+
217
+ # Check if validation changed the response
218
+ if validated_response != final_answer:
219
+ logger.warning("Guardrails modified the response",
220
+ original_length=len(final_answer),
221
+ validated_length=len(validated_response))
222
+
223
+ except Exception as e:
224
+ logger.error("Guardrails validation failed, using original response", error=str(e))
225
+ validated_response = final_answer
226
+
227
+ # Calculate response time
228
+ response_time_ms = (time.time() - start_time) * 1000
229
+
230
+ # Create comprehensive response with enhanced metadata
231
+ response = SearchResponse(
232
+ response_id=response_id,
233
+ final_answer=validated_response,
234
+ source=source,
235
+ explanation=explanation,
236
+ results=results,
237
+ metadata={
238
+ "confidence_score": best_score,
239
+ "threshold_used": confidence_threshold,
240
+ "kb_results_count": len(kb_results) if kb_results else 0,
241
+ "search_strategy": "semantic_similarity" if source == "KB" else "web_search",
242
+ "guardrails_applied": validated_response != final_answer,
243
+ "processing_time_ms": response_time_ms
244
+ },
245
+ response_time_ms=response_time_ms
246
+ )
247
+
248
+ logger.info("Response created successfully",
249
+ response_id=response_id,
250
+ final_answer_length=len(validated_response),
251
+ results_count=len(results),
252
+ metadata_fields=len(response.metadata))
253
+
254
+ # Step 5: Post-processing, analytics, and optimization
255
+ logger.info("Starting post-processing and analytics",
256
+ response_id=response_id,
257
+ source=source)
258
+
259
+ try:
260
+ # 5.1: Performance optimization - cache high-confidence results
261
+ if source == "KB" and best_score >= 0.9:
262
+ logger.info("High confidence result detected for potential caching",
263
+ confidence_score=best_score,
264
+ question_hash=hash(validated_question))
265
+
266
+ # 5.2: Quality assessment
267
+ response_quality = assess_response_quality(
268
+ question=validated_question,
269
+ answer=validated_response,
270
+ source=source,
271
+ confidence=best_score
272
+ )
273
+
274
+ # 5.3: Add quality metrics to metadata
275
+ response.metadata.update({
276
+ "response_quality": response_quality,
277
+ "optimization_applied": best_score >= 0.9,
278
+ "search_efficiency": calculate_search_efficiency(
279
+ kb_results_count=len(kb_results) if kb_results else 0,
280
+ source=source,
281
+ response_time_ms=response_time_ms
282
+ )
283
+ })
284
+
285
+ # 5.4: Trigger analytics and learning
286
+ background_tasks.add_task(
287
+ update_analytics,
288
+ question=validated_question,
289
+ response_data=response.dict(),
290
+ performance_metrics={
291
+ "kb_hit": source == "KB",
292
+ "confidence_score": best_score,
293
+ "response_time_ms": response_time_ms,
294
+ "quality_score": response_quality
295
+ }
296
+ )
297
+
298
+ logger.info("Post-processing completed successfully",
299
+ response_id=response_id,
300
+ quality_score=response_quality,
301
+ total_metadata_fields=len(response.metadata))
302
+
303
+ except Exception as e:
304
+ logger.warning("Post-processing failed, but response is still valid",
305
+ error=str(e), response_id=response_id)
306
+
307
+ # Log API call in background for analytics
308
+ background_tasks.add_task(
309
+ log_api_call,
310
+ request=request.dict(),
311
+ response=response.dict(),
312
+ response_time_ms=response_time_ms,
313
+ source=source
314
+ )
315
+
316
+ # Final completion log with comprehensive metrics
317
+ logger.info("Search request completed successfully",
318
+ request_id=response_id,
319
+ source=source,
320
+ confidence_score=best_score,
321
+ threshold_used=confidence_threshold,
322
+ kb_results_count=len(kb_results) if kb_results else 0,
323
+ final_results_count=len(results),
324
+ response_time_ms=response_time_ms,
325
+ guardrails_applied=response.metadata.get("guardrails_applied", False))
326
+
327
+ return response
328
+
329
+ except Exception as e:
330
+ logger.error("Search request failed",
331
+ request_id=response_id,
332
+ error=str(e))
333
+
334
+ raise HTTPException(
335
+ status_code=500,
336
+ detail=f"Internal server error: {str(e)}"
337
+ )
338
+
339
+ async def log_api_call(
340
+ request: dict,
341
+ response: dict,
342
+ response_time_ms: float,
343
+ source: str
344
+ ):
345
+ """Log API call to Qdrant for analytics."""
346
+ try:
347
+ if qdrant_service:
348
+ await qdrant_service.log_api_call(
349
+ endpoint="/search",
350
+ method="POST",
351
+ request_data=request,
352
+ response_data=response,
353
+ response_time_ms=response_time_ms,
354
+ source=source
355
+ )
356
+ except Exception as e:
357
+ logger.warning("Failed to log API call", error=str(e))
358
+
359
+ def assess_response_quality(question: str, answer: str, source: str, confidence: float) -> float:
360
+ """
361
+ Assess the quality of the response based on multiple factors.
362
+
363
+ Returns:
364
+ Quality score between 0.0 and 1.0
365
+ """
366
+ try:
367
+ quality_score = 0.0
368
+
369
+ # Factor 1: Answer length (not too short, not too long)
370
+ answer_length = len(answer.strip())
371
+ if 50 <= answer_length <= 2000:
372
+ quality_score += 0.3
373
+ elif answer_length > 20:
374
+ quality_score += 0.1
375
+
376
+ # Factor 2: Source reliability
377
+ if source == "KB":
378
+ quality_score += 0.4 * confidence # Scale by confidence
379
+ else:
380
+ quality_score += 0.3 # Web search baseline
381
+
382
+ # Factor 3: Mathematical content indicators
383
+ math_indicators = ['=', '+', '-', '*', '/', '^', '√', '∫', '∑', 'x', 'y', 'equation']
384
+ math_content = sum(1 for indicator in math_indicators if indicator in answer.lower())
385
+ quality_score += min(0.3, math_content * 0.05)
386
+
387
+ return min(1.0, quality_score)
388
+
389
+ except Exception as e:
390
+ logger.warning("Quality assessment failed", error=str(e))
391
+ return 0.5 # Default neutral score
392
+
393
+ def calculate_search_efficiency(kb_results_count: int, source: str, response_time_ms: float) -> float:
394
+ """
395
+ Calculate search efficiency based on results and performance.
396
+
397
+ Returns:
398
+ Efficiency score between 0.0 and 1.0
399
+ """
400
+ try:
401
+ efficiency = 0.0
402
+
403
+ # Factor 1: Speed (faster is better)
404
+ if response_time_ms < 1000:
405
+ efficiency += 0.5
406
+ elif response_time_ms < 3000:
407
+ efficiency += 0.3
408
+ else:
409
+ efficiency += 0.1
410
+
411
+ # Factor 2: Result availability
412
+ if kb_results_count > 0:
413
+ efficiency += 0.3
414
+
415
+ # Factor 3: Source efficiency (KB is more efficient)
416
+ if source == "KB":
417
+ efficiency += 0.2
418
+
419
+ return min(1.0, efficiency)
420
+
421
+ except Exception as e:
422
+ logger.warning("Efficiency calculation failed", error=str(e))
423
+ return 0.5
424
+
425
+ async def update_analytics(question: str, response_data: dict, performance_metrics: dict):
426
+ """
427
+ Update analytics and learning systems with search data.
428
+ """
429
+ try:
430
+ logger.info("Updating analytics",
431
+ kb_hit=performance_metrics.get("kb_hit", False),
432
+ confidence=performance_metrics.get("confidence_score", 0),
433
+ quality=performance_metrics.get("quality_score", 0))
434
+
435
+ # Future: Could integrate with ML systems for:
436
+ # - Query pattern analysis
437
+ # - Response quality improvement
438
+ # - Automatic threshold adjustment
439
+ # - Usage pattern detection
440
+
441
+ # For now, just comprehensive logging
442
+ analytics_data = {
443
+ "question_length": len(question),
444
+ "question_hash": hash(question),
445
+ "timestamp": time.time(),
446
+ **performance_metrics
447
+ }
448
+
449
+ logger.info("Analytics updated", **analytics_data)
450
+
451
+ except Exception as e:
452
+ logger.warning("Analytics update failed", error=str(e))
backend/services/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Empty __init__.py file to make this a Python package
backend/services/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (206 Bytes). View file
 
backend/services/__pycache__/gemini_service.cpython-311.pyc ADDED
Binary file (10.8 kB). View file
 
backend/services/__pycache__/guardrails_service.cpython-311.pyc ADDED
Binary file (7.72 kB). View file
 
backend/services/__pycache__/mcp_service.cpython-311.pyc ADDED
Binary file (6.65 kB). View file
 
backend/services/__pycache__/qdrant_service.cpython-311.pyc ADDED
Binary file (7.15 kB). View file
 
backend/services/gemini_service.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gemini LLM service for final fallback when both KB and MCP have low confidence.
3
+ """
4
+ import os
5
+ import re
6
+ import structlog
7
+ import google.generativeai as genai
8
+ from typing import Dict, Optional
9
+
10
+ logger = structlog.get_logger()
11
+
12
+
13
+ class GeminiService:
14
+ """Service for interacting with Google Gemini 2.5 Pro."""
15
+
16
+ def __init__(self):
17
+ """Initialize Gemini service."""
18
+ self.api_key = os.getenv("GEMINI_API_KEY")
19
+ if not self.api_key:
20
+ logger.warning("GEMINI_API_KEY not found in environment variables")
21
+ self.client = None
22
+ return
23
+
24
+ try:
25
+ genai.configure(api_key=self.api_key)
26
+ self.model = genai.GenerativeModel('gemini-2.0-flash-exp')
27
+ logger.info("Gemini service initialized successfully")
28
+ except Exception as e:
29
+ logger.error("Failed to initialize Gemini service", error=str(e))
30
+ self.client = None
31
+
32
+ async def solve_math_problem(self, question: str) -> Dict[str, any]:
33
+ """
34
+ Solve a math problem using Gemini 2.5 Pro.
35
+
36
+ Args:
37
+ question: The math question to solve
38
+
39
+ Returns:
40
+ Dict containing the solution and metadata
41
+ """
42
+ if not self.model:
43
+ raise Exception("Gemini service not properly initialized")
44
+
45
+ try:
46
+ # Create a comprehensive prompt for math problem solving
47
+ prompt = self._create_math_prompt(question)
48
+
49
+ logger.info("Sending request to Gemini", question_length=len(question))
50
+
51
+ # Generate response
52
+ response = await self._generate_response(prompt)
53
+
54
+ # Parse and validate the response
55
+ result = self._parse_response(response, question)
56
+
57
+ logger.info("Gemini response generated successfully",
58
+ answer_length=len(result.get("answer", "")))
59
+
60
+ return result
61
+
62
+ except Exception as e:
63
+ logger.error("Error in Gemini math problem solving", error=str(e))
64
+ raise
65
+
66
+ def _create_math_prompt(self, question: str) -> str:
67
+ """Create a comprehensive prompt for math problem solving."""
68
+ return f"""You are an expert mathematics tutor. Solve this math problem with precision and clarity.
69
+
70
+ QUESTION: {question}
71
+
72
+ CRITICAL FORMATTING REQUIREMENT - THIS IS MANDATORY:
73
+ You MUST wrap every single mathematical expression in dollar signs ($). No exceptions.
74
+
75
+ RESPONSE FORMAT:
76
+ Solution Steps:
77
+ [Provide numbered steps with clear explanations]
78
+
79
+ Final Answer:
80
+ [State the final answer clearly and concisely]
81
+
82
+ Verification (if applicable):
83
+ [Show verification using an alternative method or substitution]
84
+
85
+ MANDATORY MATH FORMATTING EXAMPLES - COPY THIS STYLE EXACTLY:
86
+ - Write: "For the term $3x^2$, we have $a = 3$ and $n = 2$"
87
+ - Write: "The function $f(x) = 3x^2 + 2x - 1$"
88
+ - Write: "The derivative is $f'(x) = 6x + 2$"
89
+ - Write: "Apply the power rule: if $f(x) = ax^n$, then $f'(x) = nax^{{n-1}}$"
90
+
91
+ NEVER WRITE MATH WITHOUT DOLLAR SIGNS:
92
+ - WRONG: "For the term 3x^2, we have a = 3 and n = 2"
93
+ - WRONG: "The function f(x) = 3x^2 + 2x - 1"
94
+ - WRONG: "The derivative is f'(x) = 6x + 2"
95
+
96
+ EVERYTHING mathematical must have $ around it: variables, numbers in math context, equations, expressions.
97
+
98
+ Begin your solution now, remembering to wrap ALL math in $ signs:"""
99
+
100
+ async def _generate_response(self, prompt: str) -> str:
101
+ """Generate response from Gemini."""
102
+ try:
103
+ # Generate content using the configured model
104
+ response = self.model.generate_content(prompt)
105
+
106
+ if not response.text:
107
+ raise Exception("Empty response from Gemini")
108
+
109
+ return response.text
110
+
111
+ except Exception as e:
112
+ logger.error("Error generating Gemini response", error=str(e))
113
+ raise
114
+
115
+ def _parse_response(self, response: str, original_question: str) -> Dict[str, any]:
116
+ """Parse Gemini response into structured format."""
117
+ try:
118
+ # Clean up the response
119
+ cleaned_response = self._clean_response(response)
120
+
121
+ return {
122
+ "answer": cleaned_response,
123
+ "confidence": 0.85, # Increased confidence for better structured responses
124
+ "source": "Gemini",
125
+ "original_question": original_question,
126
+ "response_length": len(cleaned_response),
127
+ "model": "gemini-2.0-flash-exp"
128
+ }
129
+
130
+ except Exception as e:
131
+ logger.error("Error parsing Gemini response", error=str(e))
132
+ return {
133
+ "answer": response.strip(),
134
+ "confidence": 0.6,
135
+ "source": "Gemini",
136
+ "original_question": original_question,
137
+ "error": "Failed to parse response properly"
138
+ }
139
+
140
+ def _clean_response(self, response: str) -> str:
141
+ """Clean and format the Gemini response."""
142
+ try:
143
+ # Remove excessive introductory phrases
144
+ response = response.strip()
145
+
146
+ # Remove common verbose openings
147
+ verbose_openings = [
148
+ "Okay, let's",
149
+ "Alright, let's",
150
+ "Sure, let's",
151
+ "Let's solve",
152
+ "I'll solve",
153
+ "Here's how to"
154
+ ]
155
+
156
+ for opening in verbose_openings:
157
+ if response.lower().startswith(opening.lower()):
158
+ # Find the first period or newline and start from there
159
+ first_break = min(
160
+ response.find('.') + 1 if response.find('.') != -1 else len(response),
161
+ response.find('\n') if response.find('\n') != -1 else len(response)
162
+ )
163
+ response = response[first_break:].strip()
164
+ break
165
+
166
+ # Convert LaTeX delimiters to standard format for frontend
167
+ response = response.replace('\\(', '$').replace('\\)', '$')
168
+ response = response.replace('\\[', '$$').replace('\\]', '$$')
169
+
170
+ # Remove markdown formatting
171
+ response = response.replace("**Final Answer:**", "Final Answer:")
172
+ response = response.replace("**Final Answer**", "Final Answer:")
173
+ response = response.replace("## Final Answer", "Final Answer:")
174
+ response = response.replace("## Solution Steps", "Solution Steps:")
175
+ response = response.replace("## Verification", "Verification:")
176
+
177
+ # Clean up excessive asterisks and markdown formatting
178
+ response = re.sub(r'\*{2,}', '', response) # Remove all ** formatting
179
+ response = re.sub(r'#{2,}\s*', '', response) # Remove ## headers
180
+
181
+ # Improve section formatting
182
+ response = re.sub(r'^(\d+\.\s)', r'\n\1', response, flags=re.MULTILINE) # Add newlines before numbered steps
183
+ response = re.sub(r'\n\s*\n\s*\n', '\n\n', response) # Remove excessive line breaks
184
+
185
+ return response.strip()
186
+
187
+ except Exception as e:
188
+ logger.warning("Failed to clean response, returning original", error=str(e))
189
+ return response.strip()
190
+
191
+ def is_available(self) -> bool:
192
+ """Check if Gemini service is available."""
193
+ return self.model is not None
194
+
195
+ async def health_check(self) -> Dict[str, any]:
196
+ """Perform a health check on the Gemini service."""
197
+ if not self.model:
198
+ return {
199
+ "status": "unhealthy",
200
+ "error": "Gemini service not initialized"
201
+ }
202
+
203
+ try:
204
+ # Test with a simple math problem
205
+ test_response = await self.solve_math_problem("What is 2 + 2?")
206
+
207
+ return {
208
+ "status": "healthy",
209
+ "model": "gemini-2.0-flash-exp",
210
+ "test_response_length": len(test_response.get("answer", "")),
211
+ "api_key_configured": bool(self.api_key)
212
+ }
213
+
214
+ except Exception as e:
215
+ return {
216
+ "status": "unhealthy",
217
+ "error": str(e),
218
+ "api_key_configured": bool(self.api_key)
219
+ }
backend/services/guardrails_service.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Guardrails service for input/output validation and safety.
3
+ """
4
+ import re
5
+ import structlog
6
+ from typing import Dict, List, Any, Optional
7
+
8
+ logger = structlog.get_logger()
9
+
10
+ class GuardrailsService:
11
+ """Service for input/output validation using guardrails-ai."""
12
+
13
+ def __init__(self):
14
+ """Initialize Guardrails service."""
15
+ self.initialized = False
16
+ self._setup_validation_rules()
17
+ logger.info("Guardrails service initialized")
18
+
19
+ def _setup_validation_rules(self):
20
+ """Setup validation rules for math content."""
21
+ # TODO: Implement actual guardrails-ai integration
22
+ # For now, implement basic validation rules
23
+
24
+ # Prohibited content patterns
25
+ self.prohibited_patterns = [
26
+ r'(?i)\b(hack|exploit|malicious|virus|attack)\b',
27
+ r'(?i)\b(personal|private|confidential|secret)\b',
28
+ r'(?i)\b(password|credit|social.*security)\b'
29
+ ]
30
+
31
+ # Math-related positive patterns
32
+ self.math_patterns = [
33
+ r'\b\d+\b', # Numbers
34
+ r'[+\-*/=()]', # Math operators
35
+ r'(?i)\b(solve|equation|function|derivative|integral|limit|sum|product)\b',
36
+ r'(?i)\b(algebra|geometry|calculus|trigonometry|statistics|probability)\b',
37
+ r'(?i)\b(theorem|proof|formula|solution|answer)\b'
38
+ ]
39
+
40
+ self.initialized = True
41
+
42
+ def validate_input(self, question: str) -> str:
43
+ """
44
+ Validate and sanitize input question.
45
+
46
+ Args:
47
+ question: Input question to validate
48
+
49
+ Returns:
50
+ Validated and sanitized question
51
+
52
+ Raises:
53
+ ValueError: If input is invalid or unsafe
54
+ """
55
+ try:
56
+ if not question or not question.strip():
57
+ raise ValueError("Question cannot be empty")
58
+
59
+ # Check length limits
60
+ if len(question) > 2000:
61
+ raise ValueError("Question too long (max 2000 characters)")
62
+
63
+ if len(question) < 5:
64
+ raise ValueError("Question too short (min 5 characters)")
65
+
66
+ # Check for prohibited content
67
+ for pattern in self.prohibited_patterns:
68
+ if re.search(pattern, question):
69
+ logger.warning("Prohibited content detected in input",
70
+ pattern=pattern)
71
+ raise ValueError("Input contains prohibited content")
72
+
73
+ # Basic sanitization
74
+ sanitized = question.strip()
75
+
76
+ # Remove potential script injections
77
+ sanitized = re.sub(r'<script.*?</script>', '', sanitized, flags=re.IGNORECASE | re.DOTALL)
78
+ sanitized = re.sub(r'javascript:', '', sanitized, flags=re.IGNORECASE)
79
+
80
+ # Check if it looks like a math question
81
+ has_math_content = any(re.search(pattern, sanitized) for pattern in self.math_patterns)
82
+
83
+ if not has_math_content:
84
+ logger.info("Non-math content detected, proceeding with caution")
85
+
86
+ logger.info("Input validation successful",
87
+ original_length=len(question),
88
+ sanitized_length=len(sanitized),
89
+ has_math_content=has_math_content)
90
+
91
+ return sanitized
92
+
93
+ except ValueError:
94
+ raise
95
+ except Exception as e:
96
+ logger.error("Input validation failed", error=str(e))
97
+ raise ValueError(f"Input validation error: {str(e)}")
98
+
99
+ def validate_output(self, response: str) -> str:
100
+ """
101
+ Validate and sanitize output response.
102
+
103
+ Args:
104
+ response: Output response to validate
105
+
106
+ Returns:
107
+ Validated and sanitized response
108
+
109
+ Raises:
110
+ ValueError: If output is invalid or unsafe
111
+ """
112
+ try:
113
+ if not response or not response.strip():
114
+ return "No response generated"
115
+
116
+ # Check length limits
117
+ if len(response) > 10000:
118
+ logger.warning("Response too long, truncating")
119
+ response = response[:10000] + "... [truncated]"
120
+
121
+ # Basic sanitization
122
+ sanitized = response.strip()
123
+
124
+ # Remove potential harmful content
125
+ sanitized = re.sub(r'<script.*?</script>', '', sanitized, flags=re.IGNORECASE | re.DOTALL)
126
+ sanitized = re.sub(r'javascript:', '', sanitized, flags=re.IGNORECASE)
127
+
128
+ # Check for prohibited content in output
129
+ for pattern in self.prohibited_patterns:
130
+ if re.search(pattern, sanitized):
131
+ logger.warning("Prohibited content detected in output",
132
+ pattern=pattern)
133
+ sanitized = re.sub(pattern, '[FILTERED]', sanitized, flags=re.IGNORECASE)
134
+
135
+ logger.info("Output validation successful",
136
+ original_length=len(response),
137
+ sanitized_length=len(sanitized))
138
+
139
+ return sanitized
140
+
141
+ except Exception as e:
142
+ logger.error("Output validation failed", error=str(e))
143
+ return "Response validation failed - please try again"
144
+
145
+ def is_math_related(self, text: str) -> bool:
146
+ """
147
+ Check if text is math-related.
148
+
149
+ Args:
150
+ text: Text to analyze
151
+
152
+ Returns:
153
+ True if text appears to be math-related
154
+ """
155
+ return any(re.search(pattern, text) for pattern in self.math_patterns)
backend/services/mcp_service.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MCP (Model Context Protocol) service for web search fallback.
3
+ """
4
+ import asyncio
5
+ import structlog
6
+ from typing import Dict, Any, Optional
7
+ import json
8
+
9
+ logger = structlog.get_logger()
10
+
11
+ class MCPService:
12
+ """Service for MCP web search integration."""
13
+
14
+ def __init__(self):
15
+ """Initialize MCP service."""
16
+ self.mcp_server_path = "pranavms13/web-search-mcp"
17
+ self.initialized = False
18
+ logger.info("MCP service initialized")
19
+
20
+ async def search_web(self, question: str) -> Dict[str, Any]:
21
+ """
22
+ Search the web for math-related information using MCP.
23
+
24
+ Args:
25
+ question: The math question to search for
26
+
27
+ Returns:
28
+ Dictionary containing web search results
29
+ """
30
+ try:
31
+ logger.info("Starting web search via MCP", question_length=len(question))
32
+
33
+ # TODO: Implement actual MCP integration
34
+ # For now, return a placeholder response
35
+
36
+ # Simulate web search delay
37
+ await asyncio.sleep(0.5)
38
+
39
+ # Mock response based on question type with realistic confidence scoring
40
+ confidence_score = 0.6 # Default confidence
41
+
42
+ if any(keyword in question.lower() for keyword in ['derivative', 'integral', 'calculus']):
43
+ answer = f"Based on web search: This appears to be a calculus problem. {question} involves applying standard calculus techniques. Consider using the fundamental theorem of calculus or integration by parts."
44
+ confidence_score = 0.75 # Higher confidence for calculus
45
+ elif any(keyword in question.lower() for keyword in ['algebra', 'equation', 'solve']):
46
+ answer = f"Based on web search: This is an algebraic problem. {question} can be solved using algebraic manipulation and equation solving techniques."
47
+ confidence_score = 0.7 # Good confidence for algebra
48
+ elif any(keyword in question.lower() for keyword in ['geometry', 'triangle', 'circle']):
49
+ answer = f"Based on web search: This is a geometry problem. {question} involves geometric principles and may require knowledge of shapes, areas, or angles."
50
+ confidence_score = 0.65 # Moderate confidence for geometry
51
+ elif any(keyword in question.lower() for keyword in ['statistics', 'probability', 'mean', 'standard deviation']):
52
+ answer = f"Based on web search: This is a statistics/probability problem. {question} requires understanding of statistical concepts and may involve data analysis."
53
+ confidence_score = 0.72 # Good confidence for stats
54
+ else:
55
+ answer = f"Based on web search: {question} is a mathematical problem that may require breaking down into smaller steps and applying relevant mathematical concepts."
56
+ confidence_score = 0.55 # Lower confidence for unknown types
57
+
58
+ # Adjust confidence based on question length and complexity
59
+ if len(question) > 100:
60
+ confidence_score += 0.05 # Slightly higher for detailed questions
61
+ if '=' in question and any(op in question for op in ['+', '-', '*', '/', '^']):
62
+ confidence_score += 0.1 # Higher for equations with operators
63
+
64
+ # Cap confidence to ensure it's below KB threshold for testing fallback
65
+ confidence_score = min(confidence_score, 0.79) # Always below 0.8 threshold
66
+
67
+ result = {
68
+ "answer": answer,
69
+ "source": "web_search",
70
+ "confidence": confidence_score,
71
+ "search_query": question,
72
+ "results_count": 1
73
+ }
74
+
75
+ logger.info("Web search completed via MCP",
76
+ answer_length=len(answer),
77
+ confidence=result["confidence"])
78
+
79
+ return result
80
+
81
+ except Exception as e:
82
+ logger.error("Web search via MCP failed", error=str(e))
83
+ raise Exception(f"MCP web search failed: {str(e)}")
84
+
85
+ async def initialize_mcp_connection(self):
86
+ """Initialize connection to MCP server."""
87
+ try:
88
+ # TODO: Implement actual MCP server connection
89
+ # This would involve:
90
+ # 1. Spawning the MCP server process
91
+ # 2. Establishing JSON-RPC communication
92
+ # 3. Calling available tools like web_search
93
+
94
+ self.initialized = True
95
+ logger.info("MCP connection initialized successfully")
96
+
97
+ except Exception as e:
98
+ logger.error("Failed to initialize MCP connection", error=str(e))
99
+ raise
100
+
101
+ def is_available(self) -> bool:
102
+ """Check if MCP service is available."""
103
+ return self.initialized
backend/services/qdrant_service.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Qdrant service for vector database operations.
3
+ """
4
+ import sys
5
+ from pathlib import Path
6
+ import structlog
7
+ from typing import List, Dict, Any, Optional
8
+
9
+ # Add parent directory to import database module
10
+ parent_dir = Path(__file__).parent.parent.parent
11
+ sys.path.append(str(parent_dir))
12
+
13
+ try:
14
+ from database.qdrant_manager import QdrantManager
15
+ from database.utils import EmbeddingGenerator
16
+ from models.schemas import SearchResult, APILogEntry
17
+ except ImportError as e:
18
+ # Services will be initialized when packages are available
19
+ pass
20
+
21
+ logger = structlog.get_logger()
22
+
23
+ class QdrantService:
24
+ """Service layer for Qdrant vector database operations."""
25
+
26
+ def __init__(self):
27
+ """Initialize Qdrant service."""
28
+ self.qdrant_manager = None
29
+ self.embedding_generator = None
30
+ self._initialize()
31
+
32
+ def _initialize(self):
33
+ """Initialize Qdrant manager and embedding generator."""
34
+ try:
35
+ import os
36
+ from dotenv import load_dotenv
37
+
38
+ # Load .env from project root (3 levels up from services)
39
+ env_path = Path(__file__).parent.parent.parent / '.env'
40
+ load_dotenv(env_path)
41
+
42
+ # Qdrant configuration from environment variables
43
+ qdrant_config = {
44
+ 'url': os.getenv('QDRANT_URL'),
45
+ 'api_key': os.getenv('QDRANT_API_KEY'),
46
+ 'collection_name': os.getenv('QDRANT_COLLECTION', 'nuinamath')
47
+ }
48
+
49
+ if not qdrant_config['url'] or not qdrant_config['api_key']:
50
+ raise ValueError("QDRANT_URL and QDRANT_API_KEY must be set in environment variables")
51
+
52
+ self.qdrant_manager = QdrantManager(
53
+ url=qdrant_config['url'],
54
+ api_key=qdrant_config['api_key']
55
+ )
56
+
57
+ self.embedding_generator = EmbeddingGenerator()
58
+
59
+ logger.info("Qdrant service initialized successfully")
60
+
61
+ except Exception as e:
62
+ logger.error("Failed to initialize Qdrant service", error=str(e))
63
+ # Service will work in degraded mode
64
+
65
+ async def search_similar(self, question: str, limit: int = 5) -> List[SearchResult]:
66
+ """
67
+ Search for similar math problems in the knowledge base.
68
+
69
+ Args:
70
+ question: The math question to search for
71
+ limit: Maximum number of results to return
72
+
73
+ Returns:
74
+ List of SearchResult objects
75
+ """
76
+ if not self.qdrant_manager or not self.embedding_generator:
77
+ logger.warning("Qdrant service not properly initialized")
78
+ return []
79
+
80
+ try:
81
+ import os
82
+ # Generate embedding for the question
83
+ query_embedding = self.embedding_generator.embed_text(question)
84
+
85
+ # Search in Qdrant
86
+ collection_name = os.getenv('QDRANT_COLLECTION', 'nuinamath')
87
+ results = self.qdrant_manager.search_similar(
88
+ collection_name=collection_name,
89
+ query_vector=query_embedding,
90
+ limit=limit
91
+ )
92
+
93
+ # Convert to SearchResult objects
94
+ search_results = []
95
+ for result in results:
96
+ payload = result.payload
97
+ search_result = SearchResult(
98
+ problem=payload.get('problem', ''),
99
+ solution=payload.get('solution', ''),
100
+ score=result.score
101
+ )
102
+ search_results.append(search_result)
103
+
104
+ logger.info("Knowledge base search completed",
105
+ question_length=len(question),
106
+ results_count=len(search_results),
107
+ best_score=search_results[0].score if search_results else 0)
108
+
109
+ return search_results
110
+
111
+ except Exception as e:
112
+ logger.error("Knowledge base search failed", error=str(e))
113
+ return []
114
+
115
+ async def log_api_call(
116
+ self,
117
+ endpoint: str,
118
+ method: str,
119
+ request_data: Dict[str, Any],
120
+ response_data: Dict[str, Any],
121
+ response_time_ms: float,
122
+ source: str
123
+ ):
124
+ """
125
+ Log API call to Qdrant for analytics.
126
+
127
+ Args:
128
+ endpoint: API endpoint called
129
+ method: HTTP method
130
+ request_data: Request payload
131
+ response_data: Response payload
132
+ response_time_ms: Response time in milliseconds
133
+ source: Source of the response (KB/MCP)
134
+ """
135
+ if not self.qdrant_manager or not self.embedding_generator:
136
+ logger.warning("Cannot log API call - Qdrant service not initialized")
137
+ return
138
+
139
+ try:
140
+ # Create log entry
141
+ log_entry = APILogEntry(
142
+ endpoint=endpoint,
143
+ method=method,
144
+ request_data=request_data,
145
+ response_data=response_data,
146
+ response_time_ms=response_time_ms,
147
+ source=source,
148
+ status_code=200 # Default to 200 for successful responses
149
+ )
150
+
151
+ # TODO: Store log entry in Qdrant analytics collection
152
+ # For now, just log to stdout
153
+ logger.info("API call logged",
154
+ endpoint=endpoint,
155
+ method=method,
156
+ response_time_ms=response_time_ms,
157
+ source=source)
158
+
159
+ except Exception as e:
160
+ logger.warning("Failed to log API call", error=str(e))