bhoomika19 commited on
Commit
1af7ffc
·
1 Parent(s): d0b4013

added frontend and gemini fallback

Browse files
backend/models/schemas.py CHANGED
@@ -29,7 +29,7 @@ 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"] = 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")
@@ -50,7 +50,7 @@ class APILogEntry(BaseModel):
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"] = 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
 
 
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")
 
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
 
backend/requirements.txt CHANGED
@@ -11,6 +11,9 @@ qdrant-client==1.8.0
11
  # AI Guardrails
12
  guardrails-ai==0.4.5
13
 
 
 
 
14
  # Environment management
15
  python-dotenv==1.0.0
16
 
 
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
 
backend/routes/search.py CHANGED
@@ -16,6 +16,7 @@ from models.schemas import SearchRequest, SearchResponse, ErrorResponse, SearchR
16
  from services.qdrant_service import QdrantService
17
  from services.mcp_service import MCPService
18
  from services.guardrails_service import GuardrailsService
 
19
 
20
  router = APIRouter()
21
  logger = structlog.get_logger()
@@ -24,15 +25,17 @@ logger = structlog.get_logger()
24
  qdrant_service = None
25
  mcp_service = None
26
  guardrails_service = None
 
27
 
28
  def initialize_services():
29
  """Initialize services on first request."""
30
- global qdrant_service, mcp_service, guardrails_service
31
 
32
  if qdrant_service is None:
33
  qdrant_service = QdrantService()
34
  mcp_service = MCPService()
35
  guardrails_service = GuardrailsService()
 
36
 
37
  @router.post("/search", response_model=SearchResponse)
38
  async def search_math_problems(
@@ -66,49 +69,165 @@ async def search_math_problems(
66
  # Step 2: Search knowledge base (Qdrant)
67
  kb_results = await qdrant_service.search_similar(validated_question)
68
 
69
- # Step 3: Determine if we need web search fallback
70
  confidence_threshold = 0.8 # Increased from 0.5 to 0.8 for higher confidence requirement
71
  best_score = kb_results[0].score if kb_results else 0.0
72
 
 
 
 
 
 
73
  if best_score >= confidence_threshold:
74
- # Use knowledge base results
75
  source = "KB"
76
  final_answer = kb_results[0].solution if kb_results else "No solution found"
77
- explanation = f"Found similar problem with confidence score: {best_score:.3f}"
78
  results = kb_results[:3] # Return top 3 results
79
 
 
 
 
 
80
  else:
81
- # Fallback to web search via MCP
82
- logger.info("Low confidence KB results, using web search fallback",
83
- best_score=best_score, threshold=confidence_threshold)
 
84
 
85
  try:
86
  web_results = await mcp_service.search_web(validated_question)
87
- source = "MCP"
88
- final_answer = web_results.get("answer", "No web results found")
89
- explanation = f"Knowledge base confidence too low ({best_score:.3f}), used web search"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
- # Convert web results to SearchResult format
92
- results = [SearchResult(
93
- problem=validated_question,
94
- solution=final_answer,
95
- score=0.8 # Default score for web results
96
- )]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
- except Exception as e:
99
- logger.error("Web search failed, falling back to KB results", error=str(e))
100
- source = "KB"
101
- final_answer = kb_results[0].solution if kb_results else "No solution available"
102
- explanation = f"Web search failed, using best KB result (score: {best_score:.3f})"
103
- results = kb_results[:1] if kb_results else []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
105
- # Step 4: Validate output with guardrails
106
- validated_response = guardrails_service.validate_output(final_answer)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
  # Calculate response time
109
  response_time_ms = (time.time() - start_time) * 1000
110
 
111
- # Create response
112
  response = SearchResponse(
113
  response_id=response_id,
114
  final_answer=validated_response,
@@ -118,12 +237,74 @@ async def search_math_problems(
118
  metadata={
119
  "confidence_score": best_score,
120
  "threshold_used": confidence_threshold,
121
- "kb_results_count": len(kb_results) if kb_results else 0
 
 
 
122
  },
123
  response_time_ms=response_time_ms
124
  )
125
 
126
- # Log API call in background
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  background_tasks.add_task(
128
  log_api_call,
129
  request=request.dict(),
@@ -132,10 +313,16 @@ async def search_math_problems(
132
  source=source
133
  )
134
 
 
135
  logger.info("Search request completed successfully",
136
  request_id=response_id,
137
  source=source,
138
- response_time_ms=response_time_ms)
 
 
 
 
 
139
 
140
  return response
141
 
@@ -168,3 +355,98 @@ async def log_api_call(
168
  )
169
  except Exception as e:
170
  logger.warning("Failed to log API call", error=str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()
 
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(
 
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,
 
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(),
 
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
 
 
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/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/mcp_service.py CHANGED
@@ -36,20 +36,38 @@ class MCPService:
36
  # Simulate web search delay
37
  await asyncio.sleep(0.5)
38
 
39
- # Mock response based on question type
 
 
40
  if any(keyword in question.lower() for keyword in ['derivative', 'integral', 'calculus']):
41
  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."
 
42
  elif any(keyword in question.lower() for keyword in ['algebra', 'equation', 'solve']):
43
  answer = f"Based on web search: This is an algebraic problem. {question} can be solved using algebraic manipulation and equation solving techniques."
 
44
  elif any(keyword in question.lower() for keyword in ['geometry', 'triangle', 'circle']):
45
  answer = f"Based on web search: This is a geometry problem. {question} involves geometric principles and may require knowledge of shapes, areas, or angles."
 
 
 
 
46
  else:
47
  answer = f"Based on web search: {question} is a mathematical problem that may require breaking down into smaller steps and applying relevant mathematical concepts."
 
 
 
 
 
 
 
 
 
 
48
 
49
  result = {
50
  "answer": answer,
51
  "source": "web_search",
52
- "confidence": 0.7,
53
  "search_query": question,
54
  "results_count": 1
55
  }
 
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
  }
backend/services/qdrant_service.py CHANGED
@@ -34,7 +34,10 @@ class QdrantService:
34
  try:
35
  import os
36
  from dotenv import load_dotenv
37
- load_dotenv()
 
 
 
38
 
39
  # Qdrant configuration from environment variables
40
  qdrant_config = {
 
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 = {