barathvasan-dev commited on
Commit
719ba4c
Β·
1 Parent(s): ec39753

Major redesign: AI Investigation agent with conversational multi-turn flow, follow-up questions, and better error handling

Browse files
Files changed (2) hide show
  1. ai_investigation.py +359 -814
  2. app.py +46 -31
ai_investigation.py CHANGED
@@ -1,956 +1,501 @@
1
  """
2
- AI INVESTIGATION ASSISTANT
3
-
4
- Analyzes vehicle behavior patterns and generates intelligence summaries.
5
- Works like a surveillance analyst - not SQL generation, but pattern analysis.
6
-
7
- RAG System:
8
- 1. Collect detection data from database
9
- 2. Analyze patterns and behavior
10
- 3. Generate intelligence narratives using Mistral
11
- 4. Provide actionable recommendations
 
 
12
  """
13
 
14
  import pandas as pd
15
- from sqlalchemy import text
16
- from datetime import datetime, timedelta
17
- from database import engine
18
  import json
19
- import os
 
 
20
 
21
  # =====================================================
22
- # MISTRAL AI SETUP (OPTIONAL)
23
  # =====================================================
24
 
 
25
  try:
26
  from mistralai.client import MistralClient
27
  from mistralai.models.chat_message import ChatMessage
28
  MISTRAL_AVAILABLE = True
29
  except ImportError:
30
  MISTRAL_AVAILABLE = False
31
- print("⚠️ Mistral AI not installed - using fallback narrative generation")
32
- except Exception as e:
33
- MISTRAL_AVAILABLE = False
34
- print(f"⚠️ Mistral AI unavailable: {e} - using fallback mode")
 
 
 
 
 
 
35
 
36
 
37
  # =====================================================
38
- # DATA COLLECTION & ANALYSIS
39
  # =====================================================
40
 
41
- def get_vehicle_detections(plate, days=30):
42
- """Get vehicle detection history from database"""
43
  try:
44
- if not plate:
45
- return pd.DataFrame()
46
-
47
- query = """
48
- SELECT
49
- plate,
50
- state,
51
- vehicle_type,
52
- vehicle_conf,
53
- location,
54
- date,
55
- timestamp,
56
- camera_id,
57
- hour,
58
- day
59
- FROM vehicle_logs
60
- WHERE REPLACE(REPLACE(UPPER(plate), ' ', ''), '-', '') = :plate
61
- AND date >= CURRENT_DATE - INTERVAL '1 day' * :days
62
- ORDER BY timestamp DESC
63
- """
64
-
65
- clean_plate = plate.replace(" ", "").replace("-", "").upper()
66
-
67
- with engine.connect() as conn:
68
- result = conn.execute(
69
- text(query),
70
- {"plate": clean_plate, "days": days}
71
- )
72
- rows = result.fetchall()
73
- df = pd.DataFrame(rows, columns=result.keys())
74
-
75
- return df
76
  except Exception as e:
77
- print(f"Error fetching detections: {e}")
78
- return pd.DataFrame()
79
 
80
 
81
- def get_area_activity(location, days=7):
82
- """Get all vehicle activity in a specific area"""
83
- try:
84
- query = """
85
- SELECT
86
- plate,
87
- state,
88
- vehicle_type,
89
- vehicle_conf,
90
- location,
91
- date,
92
- timestamp,
93
- hour
94
- FROM vehicle_logs
95
- WHERE location ILIKE :location
96
- AND date >= CURRENT_DATE - INTERVAL '1 day' * :days
97
- ORDER BY timestamp DESC
98
- """
99
-
100
- with engine.connect() as conn:
101
- result = conn.execute(
102
- text(query),
103
- {"location": f"%{location}%", "days": days}
104
- )
105
- rows = result.fetchall()
106
- df = pd.DataFrame(rows, columns=result.keys())
107
-
108
- return df
109
- except Exception as e:
110
- print(f"Error fetching area activity: {e}")
111
- return pd.DataFrame()
112
 
 
 
 
113
 
114
- def get_midnight_activity(hours_start=22, hours_end=4, days=7):
115
- """Get suspicious midnight activity"""
 
 
 
116
  try:
117
- query = """
118
- SELECT
119
- plate,
120
- state,
121
- vehicle_type,
122
- vehicle_conf,
123
- location,
124
- date,
125
- timestamp,
126
- hour
127
- FROM vehicle_logs
128
- WHERE (hour >= :hour_start OR hour <= :hour_end)
129
- AND date >= CURRENT_DATE - INTERVAL '1 day' * :days
130
- ORDER BY timestamp DESC
131
- """
132
 
 
 
 
 
 
 
133
  with engine.connect() as conn:
134
- result = conn.execute(
135
- text(query),
136
- {"hour_start": hours_start, "hour_end": hours_end, "days": days}
137
- )
138
  rows = result.fetchall()
139
- df = pd.DataFrame(rows, columns=result.keys())
140
 
141
- return df
 
 
 
 
 
142
  except Exception as e:
143
- print(f"Error fetching midnight activity: {e}")
144
- return pd.DataFrame()
 
 
 
 
 
 
 
145
 
146
 
147
  # =====================================================
148
- # PATTERN ANALYSIS
149
  # =====================================================
150
 
151
- def analyze_vehicle_behavior(df):
152
- """Analyze vehicle behavior patterns"""
153
- if df.empty:
154
- return {}
155
-
156
- df['timestamp'] = pd.to_datetime(df['timestamp'])
157
- df['date'] = pd.to_datetime(df['date'])
158
-
159
- analysis = {
160
- "total_detections": len(df),
161
- "unique_locations": df["location"].nunique(),
162
- "unique_dates": df["date"].nunique(),
163
- "date_range": f"{df['date'].min().date()} to {df['date'].max().date()}",
164
- "vehicle_type": df["vehicle_type"].iloc[0] if len(df) > 0 else "Unknown",
165
- "state": df["state"].iloc[0] if len(df) > 0 else "Unknown",
166
- "avg_confidence": float(df["vehicle_conf"].mean()),
167
- "detection_frequency": len(df) / max(df["date"].nunique(), 1),
168
- }
169
-
170
- # Time pattern analysis
171
- df['hour'] = pd.to_datetime(df['timestamp']).dt.hour
172
- midnight_activity = len(df[(df['hour'] >= 22) | (df['hour'] <= 4)])
173
- analysis["midnight_detections"] = midnight_activity
174
- analysis["midnight_percentage"] = round((midnight_activity / len(df) * 100), 2)
175
-
176
- # Location clustering
177
- location_counts = df["location"].value_counts()
178
- analysis["top_locations"] = location_counts.head(5).to_dict()
179
- analysis["location_clustering"] = len(df[df["location"] == location_counts.index[0]]) / len(df) * 100
180
-
181
- # Time gaps between detections
182
- time_diffs = df['timestamp'].diff().dt.total_seconds() / 3600
183
- analysis["avg_time_gap_hours"] = float(time_diffs.mean())
184
- analysis["min_time_gap_hours"] = float(time_diffs.min())
185
-
186
- return analysis
187
-
188
-
189
- def analyze_area_patterns(df):
190
- """Analyze patterns in a specific area"""
191
- if df.empty:
192
  return {}
193
 
194
- df['timestamp'] = pd.to_datetime(df['timestamp'])
195
- df['hour'] = df['timestamp'].dt.hour
196
-
197
  analysis = {
198
- "total_vehicles": df["plate"].nunique(),
199
- "total_detections": len(df),
200
- "unique_states": df["state"].nunique(),
201
- "peak_hour": int(df["hour"].mode()[0]) if len(df["hour"].mode()) > 0 else 0,
 
 
 
 
 
202
  }
203
 
204
- # Repeated vehicles
205
- plate_counts = df["plate"].value_counts()
206
- analysis["most_repeated_vehicle"] = plate_counts.index[0] if len(plate_counts) > 0 else None
207
- analysis["most_repeated_count"] = int(plate_counts.iloc[0]) if len(plate_counts) > 0 else 0
208
- analysis["repeated_vehicles"] = int((plate_counts >= 5).sum())
209
-
210
- # Time analysis
211
- night_detections = len(df[(df["hour"] >= 22) | (df["hour"] <= 4)])
212
- analysis["night_activity_percentage"] = round((night_detections / len(df) * 100), 2)
213
-
214
- return analysis
215
-
216
-
217
- def analyze_midnight_patterns(df):
218
- """Analyze midnight/suspicious timing patterns"""
219
- if df.empty:
220
- return {}
221
-
222
- df['timestamp'] = pd.to_datetime(df['timestamp'])
223
 
224
- analysis = {
225
- "total_midnight_detections": len(df),
226
- "unique_vehicles": df["plate"].nunique(),
227
- "unique_locations": df["location"].nunique(),
228
- "vehicles_with_multiple_visits": int((df["plate"].value_counts() >= 3).sum()),
229
- }
230
 
231
- # Most suspicious vehicles at midnight
232
- plate_counts = df["plate"].value_counts()
233
- analysis["top_suspicious_vehicles"] = plate_counts.head(5).to_dict()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
 
235
- # Industrial zone activity
236
- analysis["industrial_zones_active"] = df["location"].nunique()
 
 
 
 
 
 
 
237
 
238
  return analysis
239
 
240
 
241
  # =====================================================
242
- # MISTRAL AI INVESTIGATION NARRATIVES
243
  # =====================================================
244
 
245
- def get_mistral_client():
246
- """Initialize Mistral client"""
247
- if not MISTRAL_AVAILABLE:
 
 
 
248
  return None
249
 
250
- try:
251
- api_key = os.environ.get("MISTRAL_API_KEY", "")
252
- if not api_key:
253
- return None
254
- client = MistralClient(api_key=api_key)
255
- return client
256
- except Exception as e:
257
- print(f"Error initializing Mistral: {e}")
258
- return None
259
 
 
260
 
261
- def generate_investigation_narrative(analysis, investigation_type="vehicle"):
262
- """Generate AI investigation narrative using Mistral"""
 
 
 
 
 
 
 
263
 
264
- client = get_mistral_client()
265
- if not client:
266
- return _generate_fallback_narrative(analysis, investigation_type)
267
 
268
- # Build context from analysis
269
- context = json.dumps(analysis, indent=2, default=str)
 
 
 
 
 
 
270
 
271
- prompts = {
272
- "vehicle": f"""You are a professional surveillance analyst. Analyze this vehicle behavior data and provide an intelligence summary:
273
-
274
- DATA:
275
- {context}
276
-
277
- Generate a professional investigation narrative that:
278
- 1. Summarizes the vehicle's movement patterns
279
- 2. Identifies suspicious behavior (if any)
280
- 3. Notes timing patterns and location clustering
281
- 4. Provides recommendations for further investigation
282
-
283
- Keep it concise, professional, and actionable. Focus on behavioral patterns, not raw numbers.""",
284
-
285
- "area": f"""You are a professional surveillance analyst. Analyze this area activity data:
286
-
287
- DATA:
288
- {context}
289
-
290
- Generate an area intelligence report that:
291
- 1. Summarizes activity patterns in this zone
292
- 2. Identifies unusual vehicle concentrations
293
- 3. Notes peak activity times
294
- 4. Highlights most suspicious vehicles
295
- 5. Provides zone security recommendations
296
-
297
- Keep it concise and actionable.""",
298
 
299
- "midnight": f"""You are a surveillance intelligence specialist. Analyze this midnight activity data:
300
 
301
- DATA:
302
- {context}
303
-
304
- Generate a night activity intelligence report that:
305
- 1. Analyzes suspicious midnight vehicle movements
306
- 2. Identifies patterns suggesting reconnaissance or illegal activity
307
- 3. Notes industrial zone activity
308
- 4. Highlights repeat offenders
309
- 5. Provides tactical recommendations
310
 
311
- Be professional and factual."""
312
- }
313
-
314
- prompt = prompts.get(investigation_type, prompts["vehicle"])
315
 
316
  try:
317
- message = ChatMessage(role="user", content=prompt)
318
- response = client.chat(
319
- model="mistral-small",
320
- messages=[message],
321
- temperature=0.3,
322
- max_tokens=500
323
- )
324
-
325
- return response.choices[0].message.content
326
  except Exception as e:
327
- print(f"Error generating narrative: {e}")
328
- return _generate_fallback_narrative(analysis, investigation_type)
329
-
330
-
331
- def _generate_fallback_narrative(analysis, investigation_type="vehicle"):
332
- """Fallback narrative generation (no Mistral)"""
333
 
334
- if investigation_type == "vehicle":
335
- narrative = f"""
336
- VEHICLE INVESTIGATION SUMMARY
337
 
338
- Detection Activity:
339
- β€’ Total Detections: {analysis.get('total_detections', 0)}
340
- β€’ Unique Locations: {analysis.get('unique_locations', 0)}
341
- β€’ Detection Span: {analysis.get('date_range', 'Unknown')}
342
- β€’ Average Detection Confidence: {analysis.get('avg_confidence', 0):.2f}
343
 
344
- Behavior Patterns:
345
- β€’ Detection Frequency: {analysis.get('detection_frequency', 0):.2f} per day
346
- β€’ Midnight Activity: {analysis.get('midnight_percentage', 0)}% of detections
347
- β€’ Average Time Gap: {analysis.get('avg_time_gap_hours', 0):.1f} hours
348
- β€’ Location Clustering: {analysis.get('location_clustering', 0):.1f}% concentrated in top location
349
-
350
- Top Locations:
351
- """
352
- for loc, count in list(analysis.get('top_locations', {}).items())[:5]:
353
- narrative += f"β€’ {loc}: {count} detections\n"
354
-
355
- narrative += "\nInvestigation Recommendations:\n"
356
- if analysis.get('midnight_percentage', 0) > 30:
357
- narrative += "🚨 High midnight activity - Consider night surveillance verification\n"
358
- if analysis.get('unique_locations', 0) > 8:
359
- narrative += "🚨 Multi-area movement - Could indicate surveillance-style patterns\n"
360
- if analysis.get('detection_frequency', 0) > 3:
361
- narrative += "🚨 High detection frequency - Recommend blacklist verification\n"
362
- if analysis.get('avg_confidence', 0) < 0.7:
363
- narrative += "⚠️ Low confidence readings - Verify plate clarity\n"
364
-
365
- return narrative
366
-
367
- elif investigation_type == "area":
368
- narrative = f"""
369
- AREA INTELLIGENCE REPORT
370
-
371
- Activity Overview:
372
- β€’ Total Vehicles: {analysis.get('total_vehicles', 0)} unique plates
373
- β€’ Total Detections: {analysis.get('total_detections', 0)}
374
- β€’ Origin States: {analysis.get('unique_states', 0)}
375
- β€’ Peak Activity Hour: {analysis.get('peak_hour', 0)}:00
376
-
377
- Suspicious Vehicles:
378
- β€’ Most Repeated: {analysis.get('most_repeated_vehicle', 'N/A')} ({analysis.get('most_repeated_count', 0)} times)
379
- β€’ Vehicles with 5+ visits: {analysis.get('repeated_vehicles', 0)}
380
- β€’ Night Activity: {analysis.get('night_activity_percentage', 0)}% of detections
381
-
382
- Recommendations:
383
- β€’ Increase monitoring during peak hours
384
- β€’ Flag vehicles with 5+ repeated visits
385
- β€’ Cross-reference interstate plates with blacklist
386
- β€’ Verify CCTV coverage during night hours
387
- """
388
- return narrative
389
 
390
- elif investigation_type == "midnight":
391
- narrative = f"""
392
- MIDNIGHT ACTIVITY INTELLIGENCE REPORT
393
-
394
- Night Activity Summary:
395
- β€’ Total Midnight Detections: {analysis.get('total_midnight_detections', 0)}
396
- β€’ Unique Vehicles: {analysis.get('unique_vehicles', 0)}
397
- β€’ Unique Locations: {analysis.get('unique_locations', 0)}
398
- β€’ Vehicles with Multiple Visits: {analysis.get('vehicles_with_multiple_visits', 0)}
399
-
400
- Suspicious Vehicles (Multiple Visits):
401
- """
402
- for plate, count in list(analysis.get('top_suspicious_vehicles', {}).items())[:5]:
403
- narrative += f"β€’ {plate}: {count} midnight detections\n"
404
-
405
- narrative += f"""
406
- Activity Pattern:
407
- β€’ Industrial Zone Activity: {analysis.get('industrial_zones_active', 0)} locations
408
- β€’ Concentration Level: High (multiple zones active)
409
-
410
- Tactical Recommendations:
411
- β€’ Deploy night patrol units to monitor top 3 vehicles
412
- β€’ Coordinate with neighboring jurisdictions for interstate tracking
413
- β€’ Verify industrial zone security protocols
414
- β€’ Consider checkpoints during peak midnight hours (10PM-3AM)
415
- """
416
- return narrative
417
 
418
- return "Investigation data incomplete"
419
-
420
-
421
- # =====================================================
422
- # MAIN INVESTIGATION FUNCTIONS
423
- # =====================================================
424
-
425
- def investigate_vehicle(plate):
426
- """Investigate single vehicle"""
427
- df = get_vehicle_detections(plate, days=30)
428
- if df.empty:
429
- return {
430
- "status": "error",
431
- "message": f"No detections found for {plate}"
432
- }
433
 
434
- analysis = analyze_vehicle_behavior(df)
435
- narrative = generate_investigation_narrative(analysis, "vehicle")
 
 
436
 
437
- return {
438
- "status": "success",
439
- "plate": plate,
440
- "analysis": analysis,
441
- "investigation_narrative": narrative,
442
- "data": df.to_dict(orient="records")[:20] # Top 20 records
443
- }
444
 
445
 
446
- def investigate_area(location):
447
- """Investigate area activity"""
448
- df = get_area_activity(location, days=7)
449
- if df.empty:
450
- return {
451
- "status": "error",
452
- "message": f"No activity found in {location}"
453
- }
454
-
455
- analysis = analyze_area_patterns(df)
456
- narrative = generate_investigation_narrative(analysis, "area")
457
-
458
- return {
459
- "status": "success",
460
- "location": location,
461
- "analysis": analysis,
462
- "investigation_narrative": narrative,
463
- "top_vehicles": analysis.get("repeated_vehicles", [])
464
- }
465
-
466
 
467
- def investigate_midnight_activity():
468
- """Investigate suspicious midnight activity"""
469
- df = get_midnight_activity(hours_start=22, hours_end=4, days=7)
470
- if df.empty:
471
- return {
472
- "status": "error",
473
- "message": "No midnight activity found"
474
- }
475
-
476
- analysis = analyze_midnight_patterns(df)
477
- narrative = generate_investigation_narrative(analysis, "midnight")
478
 
479
- return {
480
- "status": "success",
481
- "analysis": analysis,
482
- "investigation_narrative": narrative,
483
- "suspicious_vehicles": analysis.get("top_suspicious_vehicles", {})
484
- }
485
-
486
-
487
- def find_most_suspicious_today():
488
- """Find most suspicious vehicle today"""
489
  try:
490
- query = """
491
- SELECT
492
- plate,
493
- state,
494
- vehicle_type,
495
- vehicle_conf,
496
- location,
497
- date,
498
- timestamp,
499
- hour
500
- FROM vehicle_logs
501
- WHERE date = CURRENT_DATE
502
- ORDER BY timestamp DESC
503
- """
504
 
505
- with engine.connect() as conn:
506
- result = conn.execute(text(query))
507
- rows = result.fetchall()
508
- df = pd.DataFrame(rows, columns=result.keys())
509
 
510
- if df.empty:
511
- return {
512
- "status": "error",
513
- "message": "No detections today"
514
- }
515
 
516
- # Find vehicle with most detections
517
- plate_counts = df["plate"].value_counts()
518
- if plate_counts.empty:
519
- return {
520
- "status": "error",
521
- "message": "No data available"
522
- }
523
 
524
- top_plate = plate_counts.index[0]
525
- return investigate_vehicle(top_plate)
526
-
527
- except Exception as e:
528
- print(f"Error finding suspicious vehicle: {e}")
529
- return {
530
- "status": "error",
531
- "message": str(e)
532
- }
533
-
534
-
535
- # =====================================================
536
- # FORMAT OUTPUT
537
- # =====================================================
538
-
539
- # =====================================================
540
- # NLP TO SQL CONVERSION WITH MISTRAL
541
- # =====================================================
542
-
543
- def convert_nlp_to_sql(question):
544
- """Convert natural language question to SQL query using Mistral"""
545
-
546
- client = get_mistral_client()
547
- if not client:
548
- return None
549
-
550
- sql_prompt = f"""You are a PostgreSQL expert for vehicle surveillance database.
551
-
552
- Database schema:
553
- - vehicle_logs table has: plate, state, vehicle_type, location, timestamp, date, hour, camera_id, vehicle_conf, day
554
- - All text fields are case-insensitive, use LOWER() for matching
555
- - timestamp and date store when vehicle was detected
556
-
557
- User Question: {question}
558
-
559
- Generate ONLY a valid PostgreSQL SELECT query with these rules:
560
- 1. MUST use LOWER() for case-insensitive matching on text fields
561
- 2. MUST use LIKE '%pattern%' for partial/flexible matching
562
- 3. MUST handle multiple conditions with AND/OR appropriately
563
- 4. Multiple items (vehicles/locations) β†’ use OR inside parentheses
564
- 5. Single item with multiple types (e.g., "bike and mini truck") β†’ use OR for vehicle_type
565
- 6. Always ORDER BY timestamp DESC for latest records
566
- 7. Always LIMIT 100 for safety
567
- 8. SELECT plate, location, vehicle_type, timestamp, state
568
- 9. No explanations, only the SQL query
569
- 10. Handle these patterns:
570
- - "show bike in location" β†’ WHERE LOWER(vehicle_type) LIKE '%bike%' AND LOWER(location) LIKE '%location%'
571
- - "show bike and car in location" β†’ WHERE (LOWER(vehicle_type) LIKE '%bike%' OR LOWER(vehicle_type) LIKE '%car%') AND LOWER(location) LIKE '%location%'
572
- - "show bikes in location1 and location2" β†’ WHERE LOWER(vehicle_type) LIKE '%bike%' AND (LOWER(location) LIKE '%location1%' OR LOWER(location) LIKE '%location2%')
573
- - "vehicle_type in multiple areas" β†’ build OR condition for locations
574
-
575
- Output ONLY the SQL query, no markdown or explanation:"""
576
-
577
- try:
578
- response = client.chat(
579
- model="mistral-small",
580
- messages=[{"role": "user", "content": sql_prompt}],
581
- temperature=0.1,
582
- max_tokens=400
583
- )
584
 
585
- sql_query = response.choices[0].message.content.strip()
586
- # Clean up the query
587
- sql_query = sql_query.replace("```sql", "").replace("```", "").replace("```postgresql", "").strip()
 
 
 
 
 
 
 
 
588
 
589
- print(f"Generated SQL: {sql_query}")
590
- return sql_query
591
-
592
  except Exception as e:
593
- print(f"Error converting NLP to SQL: {e}")
594
- return None
595
-
596
-
597
- def execute_custom_sql(sql_query):
598
- """Execute custom SQL query with error handling"""
599
-
600
- try:
601
- with engine.connect() as conn:
602
- # Add safety checks
603
- query_upper = sql_query.upper().strip()
604
-
605
- # Only allow SELECT queries
606
- if not query_upper.startswith("SELECT"):
607
- return None, "Query must be a SELECT statement"
608
-
609
- # Prevent dangerous operations
610
- dangerous = ["DROP", "DELETE", "UPDATE", "INSERT", "TRUNCATE", "ALTER", "CREATE"]
611
- if any(d in query_upper for d in dangerous):
612
- return None, "Query contains restricted operations"
613
-
614
- # Check for SQL injection patterns
615
- if ";" in sql_query.rstrip(";")[len("SELECT"):]: # semicolons after SELECT are dangerous
616
- return None, "Invalid query format"
617
-
618
- result = conn.execute(text(sql_query))
619
- rows = result.fetchall()
620
-
621
- if not rows:
622
- return None, "No records found"
623
-
624
- df = pd.DataFrame(rows, columns=result.keys())
625
- return df, None
626
 
627
- except Exception as e:
628
- error_msg = str(e)
629
- print(f"SQL Execution Error: {error_msg}")
630
- # Return user-friendly error message
631
- if "syntax" in error_msg.lower():
632
- return None, "SQL syntax error in generated query"
633
- elif "column" in error_msg.lower():
634
- return None, "Invalid column reference in query"
635
- else:
636
- return None, f"Query error: {error_msg[:100]}"
637
 
638
 
639
  # =====================================================
640
- # UNIFIED AI AGENT - NATURAL LANGUAGE QUESTIONS
641
  # =====================================================
642
 
643
- def ask_investigation_question(question):
644
  """
645
- Advanced AI Investigation Agent with NLP-to-SQL
 
646
 
647
- Flow:
648
- 1. Uses Mistral to convert NLP to SQL (handles complex queries)
649
- 2. Executes query safely with validation
650
- 3. Analyzes retrieved data
651
- 4. Uses Mistral for deep analysis with RAG to answer user question
652
  """
653
 
654
  if not question or len(question.strip()) < 2:
655
  return {
656
  "status": "error",
657
- "message": "Please enter a valid question"
 
 
 
658
  }
659
 
660
- client = get_mistral_client()
 
 
661
 
662
  try:
663
- print(f"\n=== Processing Question ===")
664
- print(f"Question: {question}")
 
 
665
 
666
- # Step 1: Convert NLP to SQL
667
- print("Step 1: Converting natural language to SQL...")
668
- sql_query = convert_nlp_to_sql(question)
669
 
670
- if not sql_query:
 
671
  return {
672
  "status": "error",
673
- "message": "Could not generate SQL query from your question. Please try rephrasing."
 
 
 
 
 
 
 
 
674
  }
675
 
676
- print(f"Generated SQL: {sql_query}")
677
 
678
- # Step 2: Execute SQL query
679
- print("Step 2: Executing query...")
680
- df, error = execute_custom_sql(sql_query)
681
-
682
- data_context = {"summary": "", "analysis": {}, "findings": [], "raw_data": None}
683
-
684
- if error:
685
- print(f"Execution error: {error}")
686
- return {
687
- "status": "error",
688
- "message": f"Query execution failed: {error}"
689
- }
690
 
691
- if df is None or df.empty:
692
- return {
693
- "status": "error",
694
- "message": "No records found matching your query criteria"
695
- }
696
 
697
- print(f"Retrieved {len(df)} records")
 
698
 
699
- # Step 3: Analyze the data
700
- print("Step 3: Analyzing data...")
701
- data_context = _analyze_custom_query_results(df, question)
702
- print(f"Analysis: {data_context['summary']}")
703
 
704
- # Step 4: Generate intelligent answer with RAG
705
- print("Step 4: Generating analysis with AI...")
706
- answer = _generate_advanced_rag_answer(question, data_context, client, df)
 
707
 
708
- if not answer:
709
- answer = _generate_fallback_analysis(question, data_context, df)
710
 
711
- print(f"=== Analysis Complete ===\n")
712
 
713
  return {
714
  "status": "success",
715
  "question": question,
 
716
  "answer": answer,
717
- "data_summary": data_context.get("summary", ""),
718
- "analysis": data_context.get("analysis", {}),
719
- "findings": data_context.get("findings", []),
720
- "raw_data": df.to_dict(orient="records")[:20] if df is not None else None
 
 
721
  }
722
 
723
  except Exception as e:
724
- print(f"Error in investigation: {e}")
725
- import traceback
726
  traceback.print_exc()
727
  return {
728
  "status": "error",
729
- "message": f"Investigation failed: {str(e)[:100]}"
 
 
 
730
  }
731
 
732
 
733
- def _analyze_custom_query_results(df, question):
734
- """Analyze results from custom SQL queries with robust handling"""
735
-
736
- if df.empty:
737
- return {"summary": "No data found matching your query", "analysis": {}, "findings": [], "raw_data": None}
738
-
739
- # Build analysis with safe column access
740
- analysis = {
741
- "total_records": len(df),
742
- "unique_vehicles": df["plate"].nunique() if "plate" in df.columns else 0,
743
- "unique_locations": df["location"].nunique() if "location" in df.columns else 0,
744
- "vehicle_types": [],
745
- "date_range": "Unknown"
746
- }
747
-
748
- # Safe extraction of vehicle types
749
- if "vehicle_type" in df.columns:
750
- types = df["vehicle_type"].unique()
751
- analysis["vehicle_types"] = [str(t) for t in types if pd.notna(t)]
752
-
753
- # Safe date range calculation
754
- if "timestamp" in df.columns:
755
- valid_times = pd.to_datetime(df["timestamp"], errors='coerce')
756
- valid_times = valid_times[valid_times.notna()]
757
- if len(valid_times) > 0:
758
- analysis["date_range"] = f"{valid_times.min()} to {valid_times.max()}"
759
-
760
- # Extract findings with robust error handling
761
- findings = []
762
-
763
- try:
764
- if "plate" in df.columns:
765
- plate_counts = df["plate"].value_counts()
766
- if len(plate_counts) > 0:
767
- findings.append(f"Most frequently detected: {plate_counts.index[0]} ({plate_counts.iloc[0]} detections)")
768
- if len(plate_counts) > 1:
769
- findings.append(f"Total unique vehicles: {len(plate_counts)}")
770
- except:
771
- pass
772
-
773
- try:
774
- if "location" in df.columns:
775
- loc_counts = df["location"].value_counts()
776
- if len(loc_counts) > 0:
777
- findings.append(f"Most active location: {loc_counts.index[0]} ({loc_counts.iloc[0]} detections)")
778
- if len(loc_counts) > 1:
779
- findings.append(f"Covered {len(loc_counts)} different locations")
780
- except:
781
- pass
782
-
783
- try:
784
- if "vehicle_type" in df.columns:
785
- vehicles = df["vehicle_type"].value_counts()
786
- if len(vehicles) > 0:
787
- types_str = ", ".join([f"{v} ({c})" for v, c in vehicles.head(3).items()])
788
- findings.append(f"Vehicle breakdown: {types_str}")
789
- except:
790
- pass
791
-
792
- try:
793
- if "timestamp" in df.columns and "hour" in df.columns:
794
- df["hour_numeric"] = pd.to_datetime(df["timestamp"], errors='coerce').dt.hour
795
- peak_hour = df["hour_numeric"].mode()
796
- if len(peak_hour) > 0:
797
- findings.append(f"Peak activity hour: {int(peak_hour[0])}:00")
798
- except:
799
- pass
800
-
801
- # Build summary
802
- summary_parts = [f"Retrieved {len(df)} records"]
803
- if analysis["unique_vehicles"] > 0:
804
- summary_parts.append(f"from {analysis['unique_vehicles']} unique vehicles")
805
- if analysis["unique_locations"] > 0:
806
- summary_parts.append(f"across {analysis['unique_locations']} locations")
807
- if analysis["vehicle_types"]:
808
- summary_parts.append(f"(Types: {', '.join(analysis['vehicle_types'][:3])})")
809
-
810
- summary = " ".join(summary_parts)
811
-
812
- return {
813
- "summary": summary,
814
- "analysis": analysis,
815
- "findings": findings[:5], # Top 5 findings
816
- "raw_data": df.to_dict(orient="records")
817
- }
818
-
819
 
820
- def _generate_advanced_rag_answer(question, data_context, client, df):
821
- """Generate advanced RAG answer using Mistral with full data context"""
822
 
823
- analysis = data_context.get("analysis", {})
824
- summary = data_context.get("summary", "")
825
- findings = data_context.get("findings", [])
826
 
827
- # Build detailed context for Mistral
828
- context_details = f"""Question: {question}
829
-
830
- Retrieved Data Summary:
831
- - {summary}
 
 
 
 
 
 
832
 
833
- Data Statistics:
834
  """
835
 
836
- for key, value in analysis.items():
837
- if isinstance(value, list):
838
- context_details += f" - {key}: {', '.join(str(v) for v in value[:5])}\n"
839
- else:
840
- context_details += f" - {key}: {value}\n"
841
-
842
- # Add sample records for context
843
- if df is not None and not df.empty:
844
- context_details += f"\nSample Data (showing {min(3, len(df))} records):\n"
845
- for idx, row in df.head(3).iterrows():
846
- row_dict = dict(row)
847
- record_str = ", ".join([f"{k}: {v}" for k, v in list(row_dict.items())[:6]])
848
- context_details += f" - {record_str}\n"
849
 
850
- if findings:
851
- context_details += f"\nInitial Findings:\n"
852
- for finding in findings[:3]:
853
- context_details += f" - {finding}\n"
854
 
855
- # Better prompt for detailed analysis
856
- prompt = f"""You are a professional traffic surveillance analyst.
857
- The user asked a specific question and we retrieved vehicle detection data from the database.
858
-
859
- {context_details}
860
-
861
- Now analyze this data and answer the user's original question: "{question}"
862
-
863
- Your response should:
864
- 1. Directly answer what the user asked
865
- 2. Provide specific data-backed insights
866
- 3. Highlight patterns, anomalies, or important findings
867
- 4. Be concise but comprehensive
868
- 5. Use natural conversational language (not bullet points)
869
-
870
- Provide a detailed professional analysis:"""
871
-
872
- try:
873
- if client:
874
- response = client.chat(
875
- model="mistral-small",
876
- messages=[{"role": "user", "content": prompt}],
877
- temperature=0.3,
878
- max_tokens=1200
879
- )
880
- answer = response.choices[0].message.content.strip()
881
-
882
- # Ensure answer is not empty
883
- if not answer or answer.lower() == "none":
884
- return _generate_fallback_analysis(question, data_context, df)
885
- return answer
886
- else:
887
- return _generate_fallback_analysis(question, data_context, df)
888
 
889
- except Exception as e:
890
- print(f"Error generating answer: {e}")
891
- return _generate_fallback_analysis(question, data_context, df)
892
-
893
-
894
- def _generate_fallback_analysis(question, data_context, df):
895
- """Fallback analysis without Mistral - provides structured response"""
896
-
897
- summary = data_context.get('summary', 'No data available')
898
- analysis = data_context.get('analysis', {})
899
- findings = data_context.get("findings", [])
900
-
901
- answer = f"Analysis for: {question}\n\n"
902
- answer += f"Summary: {summary}\n\n"
903
-
904
- if df is not None and not df.empty:
905
- answer += f"Retrieved Data Details:\n"
906
- answer += f"- Total Records: {len(df)}\n"
907
-
908
- if analysis:
909
- answer += f"\nKey Metrics:\n"
910
- for key, value in list(analysis.items())[:8]:
911
- if isinstance(value, list):
912
- if value:
913
- answer += f"- {key}: {', '.join(str(v) for v in value[:3])}\n"
914
- elif isinstance(value, dict):
915
- if value:
916
- answer += f"- {key}: {len(value)} items\n"
917
- else:
918
- answer += f"- {key}: {value}\n"
919
-
920
- if findings:
921
- answer += f"\nKey Findings:\n"
922
- for finding in findings:
923
- answer += f"- {finding}\n"
924
-
925
- if df is not None and not df.empty and len(df) > 0:
926
- answer += f"\nSample Records:\n"
927
- for idx, row in df.head(3).iterrows():
928
- record_dict = dict(row)
929
- record_str = ", ".join([f"{k}: {v}" for k, v in list(record_dict.items())[:5]])
930
- answer += f"- {record_str}\n"
931
 
932
- return answer
933
 
934
 
935
- def format_investigation_output(result):
936
- """Format investigation result for display"""
937
- if result.get("status") == "error":
938
- return {
939
- "error": result.get("message", "Investigation failed")
940
- }
941
-
942
- return {
943
- "query": result.get("plate") or result.get("location") or "Investigation",
944
- "answer": result.get("answer", ""),
945
- "analysis": result.get("analysis", {}),
946
- "key_findings": result.get("findings", [])
947
- }
948
 
 
 
 
949
 
950
- if __name__ == "__main__":
951
- # Test the investigation assistant
952
- print("AI Investigation Assistant Ready\n")
953
-
954
- # Example: Ask a question
955
- result = ask_investigation_question("show bikes in Adyar")
956
- print(json.dumps(result, indent=2, default=str))
 
1
  """
2
+ πŸ€– CONVERSATIONAL AI INVESTIGATION AGENT
3
+ =====================================
4
+ Professional multi-turn conversational agent for vehicle surveillance analysis.
5
+ Works like ChatGPT - asks clarifying questions, provides insights, suggests related queries.
6
+
7
+ Architecture:
8
+ 1. Query Understanding - Parse user intent
9
+ 2. Data Collection - Use advanced NLP-to-SQL from database.py
10
+ 3. Data Analysis - Extract insights from retrieved data
11
+ 4. Response Generation - Answer user question with data
12
+ 5. Clarification - Ask follow-up questions or suggest related queries
13
+ 6. Memory - Keep conversation context for multi-turn interactions
14
  """
15
 
16
  import pandas as pd
 
 
 
17
  import json
18
+ import traceback
19
+ from datetime import datetime
20
+ from sqlalchemy import text
21
 
22
  # =====================================================
23
+ # IMPORTS
24
  # =====================================================
25
 
26
+ from database import engine, ask_llm
27
  try:
28
  from mistralai.client import MistralClient
29
  from mistralai.models.chat_message import ChatMessage
30
  MISTRAL_AVAILABLE = True
31
  except ImportError:
32
  MISTRAL_AVAILABLE = False
33
+ print("⚠️ Mistral not available - fallback mode")
34
+
35
+ # =====================================================
36
+ # CONFIGURATION
37
+ # =====================================================
38
+
39
+ MISTRAL_MODEL = "mistral-small"
40
+ MISTRAL_TIMEOUT = 10
41
+ DATA_SAMPLE_SIZE = 5 # Show top 5 records in response
42
+ FOLLOW_UP_QUESTIONS = 3 # Max follow-up questions to suggest
43
 
44
 
45
  # =====================================================
46
+ # MISTRAL CLIENT
47
  # =====================================================
48
 
49
+ def get_mistral_client():
50
+ """Get or create Mistral client"""
51
  try:
52
+ api_key = "any" # Using HF inference
53
+ client = MistralClient(api_key=api_key)
54
+ return client
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  except Exception as e:
56
+ print(f"⚠️ Mistral client error: {e}")
57
+ return None
58
 
59
 
60
+ # =====================================================
61
+ # STEP 1: QUERY UNDERSTANDING
62
+ # =====================================================
63
+
64
+ def understand_query(user_question):
65
+ """
66
+ Understand user intent and extract what they're looking for.
67
+ Returns classification of query type.
68
+ """
69
+ q = user_question.lower()
70
+
71
+ intent_types = {
72
+ "tracking": any(w in q for w in ["where", "location", "found", "been", "detected", "show"]),
73
+ "counting": any(w in q for w in ["how many", "count", "total", "number of"]),
74
+ "pattern": any(w in q for w in ["pattern", "habit", "regular", "often", "frequent"]),
75
+ "comparison": any(w in q for w in ["compare", "vs", "versus", "different", "same"]),
76
+ "timeline": any(w in q for w in ["when", "time", "date", "hour", "period", "last", "first"]),
77
+ "alert": any(w in q for w in ["suspicious", "alert", "warning", "anomaly", "unusual"]),
78
+ "statistics": any(w in q for w in ["statistics", "stats", "analysis", "breakdown", "distribution"])
79
+ }
80
+
81
+ return {
82
+ "question": user_question,
83
+ "primary_intent": max(intent_types.items(), key=lambda x: x[1])[0] if any(intent_types.values()) else "general",
84
+ "all_intents": {k: v for k, v in intent_types.items() if v}
85
+ }
86
+
 
 
 
 
87
 
88
+ # =====================================================
89
+ # STEP 2: DATA COLLECTION (ADVANCED)
90
+ # =====================================================
91
 
92
+ def collect_data(user_question):
93
+ """
94
+ Collect data using the advanced NLP-to-SQL engine from database.py
95
+ Handles complex queries with multiple filters.
96
+ """
97
  try:
98
+ print(f"\nπŸ” COLLECTING DATA FOR: {user_question}")
99
+
100
+ # Use the advanced ask_llm function from database.py
101
+ sql_query = ask_llm(user_question)
 
 
 
 
 
 
 
 
 
 
 
102
 
103
+ if not sql_query:
104
+ return None, None, "Could not parse your question. Try being more specific (e.g., 'show bikes in adyar')"
105
+
106
+ print(f"πŸ“Š Generated SQL:\n{sql_query}")
107
+
108
+ # Execute query
109
  with engine.connect() as conn:
110
+ conn.execute(text("SET statement_timeout = 30000")) # 30 second timeout
111
+ result = conn.execute(text(sql_query))
 
 
112
  rows = result.fetchall()
 
113
 
114
+ if not rows:
115
+ return None, sql_query, "No data found matching your criteria"
116
+
117
+ df = pd.DataFrame(rows, columns=result.keys())
118
+ return df, sql_query, None
119
+
120
  except Exception as e:
121
+ print(f"❌ Data collection error: {e}")
122
+ traceback.print_exc()
123
+ error_msg = str(e)
124
+ if "timeout" in error_msg.lower():
125
+ return None, None, "Query took too long. Try narrowing down your search (add location, date, or vehicle type)"
126
+ elif "syntax" in error_msg.lower():
127
+ return None, None, "Query syntax error. Please rephrase your question"
128
+ else:
129
+ return None, None, f"Data collection failed: {error_msg[:80]}"
130
 
131
 
132
  # =====================================================
133
+ # STEP 3: DATA ANALYSIS
134
  # =====================================================
135
 
136
+ def analyze_data(df, user_question):
137
+ """
138
+ Analyze collected data to extract insights.
139
+ Returns structured analysis with key metrics and findings.
140
+ """
141
+ if df is None or df.empty:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  return {}
143
 
 
 
 
144
  analysis = {
145
+ "total_records": len(df),
146
+ "columns": list(df.columns),
147
+ "unique_vehicles": df["plate"].nunique() if "plate" in df.columns else 0,
148
+ "unique_locations": df["location"].nunique() if "location" in df.columns else 0,
149
+ "vehicle_types": [],
150
+ "states": [],
151
+ "date_range": None,
152
+ "time_range": None,
153
+ "key_findings": []
154
  }
155
 
156
+ # Vehicle types
157
+ if "vehicle_type" in df.columns:
158
+ type_counts = df["vehicle_type"].value_counts()
159
+ analysis["vehicle_types"] = [(v, int(c)) for v, c in type_counts.head(5).items()]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
 
161
+ # States
162
+ if "state" in df.columns:
163
+ state_counts = df["state"].value_counts()
164
+ analysis["states"] = [(s, int(c)) for s, c in state_counts.head(5).items()]
 
 
165
 
166
+ # Date range
167
+ if "timestamp" in df.columns:
168
+ try:
169
+ df["timestamp_dt"] = pd.to_datetime(df["timestamp"], errors='coerce')
170
+ valid_times = df["timestamp_dt"].dropna()
171
+ if len(valid_times) > 0:
172
+ analysis["date_range"] = {
173
+ "start": str(valid_times.min()),
174
+ "end": str(valid_times.max())
175
+ }
176
+ except:
177
+ pass
178
+
179
+ # Key findings
180
+ try:
181
+ if "plate" in df.columns:
182
+ plate_counts = df["plate"].value_counts()
183
+ if len(plate_counts) > 0:
184
+ analysis["key_findings"].append(
185
+ f"Most detected: {plate_counts.index[0]} ({plate_counts.iloc[0]} times)"
186
+ )
187
+ except:
188
+ pass
189
 
190
+ try:
191
+ if "location" in df.columns:
192
+ loc_counts = df["location"].value_counts()
193
+ if len(loc_counts) > 0:
194
+ analysis["key_findings"].append(
195
+ f"Hottest location: {loc_counts.index[0]} ({loc_counts.iloc[0]} detections)"
196
+ )
197
+ except:
198
+ pass
199
 
200
  return analysis
201
 
202
 
203
  # =====================================================
204
+ # STEP 4: RESPONSE GENERATION
205
  # =====================================================
206
 
207
+ def generate_answer(user_question, df, analysis, client):
208
+ """
209
+ Generate intelligent answer using Mistral with data context.
210
+ Answers the user's original question based on retrieved data.
211
+ """
212
+ if df is None or df.empty:
213
  return None
214
 
215
+ # Build data context for Mistral
216
+ context = f"""
217
+ You are a professional vehicle surveillance analyst. A user asked you a question and you retrieved vehicle detection data from a database.
 
 
 
 
 
 
218
 
219
+ USER QUESTION: {user_question}
220
 
221
+ DATA RETRIEVED:
222
+ - Total Records: {analysis.get('total_records', 0)}
223
+ - Unique Vehicles: {analysis.get('unique_vehicles', 0)}
224
+ - Unique Locations: {analysis.get('unique_locations', 0)}
225
+ - Unique States: {len(analysis.get('states', []))}
226
+ - Data Time Range: {analysis.get('date_range', 'Unknown')}
227
+
228
+ KEY FINDINGS:
229
+ """
230
 
231
+ for finding in analysis.get("key_findings", []):
232
+ context += f"- {finding}\n"
 
233
 
234
+ # Add sample records
235
+ context += "\nSAMPLE RECORDS:\n"
236
+ for idx, row in df.head(DATA_SAMPLE_SIZE).iterrows():
237
+ row_dict = dict(row)
238
+ # Show only key columns
239
+ key_cols = ["plate", "location", "vehicle_type", "timestamp", "state"]
240
+ filtered = {k: v for k, v in row_dict.items() if k in key_cols and k in row_dict}
241
+ context += f"- {filtered}\n"
242
 
243
+ prompt = f"""{context}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
 
245
+ Now analyze this data and answer the user's question: "{user_question}"
246
 
247
+ Your response should:
248
+ 1. Directly answer what the user asked
249
+ 2. Cite specific data points from the results
250
+ 3. Provide relevant insights or patterns
251
+ 4. Be concise but comprehensive (2-3 paragraphs)
252
+ 5. Use professional but conversational language
 
 
 
253
 
254
+ Provide your detailed analysis:"""
 
 
 
255
 
256
  try:
257
+ if client:
258
+ response = client.chat(
259
+ model=MISTRAL_MODEL,
260
+ messages=[{"role": "user", "content": prompt}],
261
+ temperature=0.3,
262
+ max_tokens=1000
263
+ )
264
+ return response.choices[0].message.content.strip()
 
265
  except Exception as e:
266
+ print(f"⚠️ Mistral error: {e}")
 
 
 
 
 
267
 
268
+ # Fallback response
269
+ return generate_fallback_answer(user_question, analysis, df)
 
270
 
 
 
 
 
 
271
 
272
+ def generate_fallback_answer(user_question, analysis, df):
273
+ """Fallback answer generation without Mistral"""
274
+ answer = f"Based on {analysis['total_records']} records:\n\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
 
276
+ if analysis["key_findings"]:
277
+ answer += "Key Findings:\n"
278
+ for finding in analysis["key_findings"][:3]:
279
+ answer += f"β€’ {finding}\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
 
281
+ if analysis["vehicle_types"]:
282
+ answer += "\nVehicle Breakdown:\n"
283
+ for vtype, count in analysis["vehicle_types"][:3]:
284
+ answer += f"β€’ {vtype}: {count} detections\n"
 
 
 
 
 
 
 
 
 
 
 
285
 
286
+ if analysis["states"]:
287
+ answer += "\nState Distribution:\n"
288
+ for state, count in analysis["states"][:3]:
289
+ answer += f"β€’ {state}: {count} detections\n"
290
 
291
+ return answer
 
 
 
 
 
 
292
 
293
 
294
+ # =====================================================
295
+ # STEP 5: FOLLOW-UP QUESTIONS & CLARIFICATIONS
296
+ # =====================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
 
298
+ def generate_follow_ups(user_question, analysis, intent):
299
+ """
300
+ Generate intelligent follow-up questions or clarifications.
301
+ Suggests related queries the user might be interested in.
302
+ """
303
+ follow_ups = []
 
 
 
 
 
304
 
 
 
 
 
 
 
 
 
 
 
305
  try:
306
+ primary_intent = intent.get("primary_intent", "general")
 
 
 
 
 
 
 
 
 
 
 
 
 
307
 
308
+ # Extract key entities from question
309
+ q = user_question.lower()
 
 
310
 
311
+ # Suggest follow-ups based on data and intent
312
+ if analysis.get("unique_vehicles", 0) > 1 and "plate" not in q:
313
+ follow_ups.append("Would you like to focus on a specific vehicle plate?")
 
 
314
 
315
+ if analysis.get("unique_locations", 0) > 1 and "location" not in q:
316
+ follow_ups.append("Would you like details about a specific location?")
 
 
 
 
 
317
 
318
+ if primary_intent == "tracking":
319
+ follow_ups.append("Do you want to see the complete route history of this vehicle?")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
 
321
+ if primary_intent == "counting":
322
+ follow_ups.append("Would you like to see the breakdown by location or vehicle type?")
323
+
324
+ if primary_intent == "pattern":
325
+ follow_ups.append("Should I analyze time patterns (peak hours, days)?")
326
+
327
+ if analysis.get("total_records", 0) > 20:
328
+ follow_ups.append("The results have many records. Would you like me to focus on specific criteria?")
329
+
330
+ if analysis.get("states") and len(analysis["states"]) > 1:
331
+ follow_ups.append("I found vehicles from multiple states. Want to focus on one state?")
332
 
 
 
 
333
  except Exception as e:
334
+ print(f"Follow-up generation error: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
 
336
+ return follow_ups[:FOLLOW_UP_QUESTIONS]
 
 
 
 
 
 
 
 
 
337
 
338
 
339
  # =====================================================
340
+ # STEP 6: MULTI-TURN CONVERSATION
341
  # =====================================================
342
 
343
+ def ask_investigation_question(question, conversation_history=None):
344
  """
345
+ Main conversational agent function.
346
+ Handles single or multi-turn conversations.
347
 
348
+ Returns structured response with answer, data, and follow-ups.
 
 
 
 
349
  """
350
 
351
  if not question or len(question.strip()) < 2:
352
  return {
353
  "status": "error",
354
+ "message": "Please enter a valid question",
355
+ "answer": None,
356
+ "data": None,
357
+ "follow_ups": []
358
  }
359
 
360
+ print(f"\n{'='*60}")
361
+ print(f"πŸš€ PROCESSING: {question}")
362
+ print(f"{'='*60}")
363
 
364
  try:
365
+ # Step 1: Understand query
366
+ print("πŸ“– Step 1: Understanding query...")
367
+ intent = understand_query(question)
368
+ print(f" Intent: {intent['primary_intent']}")
369
 
370
+ # Step 2: Collect data
371
+ print("πŸ“Š Step 2: Collecting data...")
372
+ df, sql, error = collect_data(question)
373
 
374
+ if error:
375
+ print(f" Error: {error}")
376
  return {
377
  "status": "error",
378
+ "message": error,
379
+ "sql_generated": sql,
380
+ "answer": None,
381
+ "data": None,
382
+ "follow_ups": [
383
+ "Try being more specific (e.g., 'show bikes in adyar')",
384
+ "Or specify a date range (e.g., 'bikes from 10-05-2026 to 18-05-2026')",
385
+ "Or add a vehicle type (e.g., 'show vehicles in location X')"
386
+ ]
387
  }
388
 
389
+ print(f" βœ… Retrieved {len(df)} records")
390
 
391
+ # Step 3: Analyze data
392
+ print("πŸ” Step 3: Analyzing data...")
393
+ analysis = analyze_data(df, question)
394
+ print(f" Unique vehicles: {analysis['unique_vehicles']}")
395
+ print(f" Unique locations: {analysis['unique_locations']}")
 
 
 
 
 
 
 
396
 
397
+ # Step 4: Generate answer
398
+ print("πŸ’¬ Step 4: Generating answer...")
399
+ client = get_mistral_client()
400
+ answer = generate_answer(question, df, analysis, client)
 
401
 
402
+ if not answer:
403
+ answer = generate_fallback_answer(question, analysis, df)
404
 
405
+ print(f" βœ… Answer generated")
 
 
 
406
 
407
+ # Step 5: Generate follow-ups
408
+ print("❓ Step 5: Generating follow-up questions...")
409
+ follow_ups = generate_follow_ups(question, analysis, intent)
410
+ print(f" {len(follow_ups)} follow-ups suggested")
411
 
412
+ # Prepare response data
413
+ data_preview = df.head(10).to_dict(orient="records") if df is not None else None
414
 
415
+ print(f"{'='*60}βœ… COMPLETE\n")
416
 
417
  return {
418
  "status": "success",
419
  "question": question,
420
+ "intent": intent,
421
  "answer": answer,
422
+ "analysis": analysis,
423
+ "data_preview": data_preview,
424
+ "total_records": len(df),
425
+ "sql_generated": sql,
426
+ "follow_ups": follow_ups,
427
+ "raw_data": df.to_dict(orient="records") if df is not None else None
428
  }
429
 
430
  except Exception as e:
431
+ print(f"❌ Error: {e}")
 
432
  traceback.print_exc()
433
  return {
434
  "status": "error",
435
+ "message": f"Investigation error: {str(e)[:100]}",
436
+ "answer": None,
437
+ "data": None,
438
+ "follow_ups": ["Try rephrasing your question with more specific details"]
439
  }
440
 
441
 
442
+ # =====================================================
443
+ # UTILITY FUNCTIONS
444
+ # =====================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
445
 
446
+ def format_investigation_output(result):
447
+ """Format investigation result for display (Gradio compatible)"""
448
 
449
+ if result["status"] == "error":
450
+ return result["message"], "", json.dumps(result.get("follow_ups", []), indent=2), "{}"
 
451
 
452
+ # Main answer
453
+ answer_text = result.get("answer", "No answer generated")
454
+
455
+ # Data structure summary
456
+ analysis = result.get("analysis", {})
457
+ data_structure = f"""
458
+ RETRIEVED DATA SUMMARY:
459
+ - Total Records: {result.get('total_records', 0)}
460
+ - Unique Vehicles: {analysis.get('unique_vehicles', 0)}
461
+ - Unique Locations: {analysis.get('unique_locations', 0)}
462
+ - Date Range: {analysis.get('date_range', 'Unknown')}
463
 
464
+ KEY METRICS:
465
  """
466
 
467
+ if analysis.get("vehicle_types"):
468
+ data_structure += "\nVehicle Types:\n"
469
+ for vtype, count in analysis["vehicle_types"]:
470
+ data_structure += f" β€’ {vtype}: {count}\n"
 
 
 
 
 
 
 
 
 
471
 
472
+ if analysis.get("states"):
473
+ data_structure += "\nStates:\n"
474
+ for state, count in analysis["states"]:
475
+ data_structure += f" β€’ {state}: {count}\n"
476
 
477
+ # Follow-up questions
478
+ follow_ups = result.get("follow_ups", [])
479
+ follow_up_text = json.dumps(follow_ups, indent=2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
480
 
481
+ # Data preview
482
+ data_preview = result.get("data_preview", [])
483
+ data_json = json.dumps(data_preview[:5], indent=2, default=str)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
484
 
485
+ return answer_text, data_structure, follow_up_text, data_json
486
 
487
 
488
+ # =====================================================
489
+ # LEGACY FUNCTIONS (kept for compatibility)
490
+ # =====================================================
 
 
 
 
 
 
 
 
 
 
491
 
492
+ def format_investigation_output_tuple(result):
493
+ """Returns tuple of (answer, data_structure, follow_ups_json, data_json)"""
494
+ return format_investigation_output(result)
495
 
496
+
497
+ # For backwards compatibility with app.py
498
+ def investigate_with_npl(question):
499
+ """Wrapper function that matches app.py expectation"""
500
+ result = ask_investigation_question(question)
501
+ return format_investigation_output(result)
 
app.py CHANGED
@@ -648,60 +648,75 @@ with gr.Blocks(
648
  label="Records Retrieved",
649
  interactive=False
650
  )
 
 
 
 
 
 
 
 
651
 
652
  def investigate_with_npl(question):
653
  """Investigate with NLP-to-SQL and advanced analysis"""
654
  if not question or len(question.strip()) < 2:
655
- return ("Please ask a question", "", "", None)
656
 
657
  result = ask_investigation_question(question)
658
 
659
  if result.get("status") == "error":
660
  error_msg = result.get("message", "Investigation failed")
661
- return (f"Error: {error_msg}", "", f"Error: {error_msg}", None)
 
 
662
 
663
- # Format data structure info
664
- data_info = result.get("data_summary", "")
665
- analysis = result.get("analysis", {})
666
 
667
- # Build data structure with execution flow
668
- structure = f"""Data Execution Flow
669
- ━━━━━━━━━━━━━━━━━━━━━━
670
- Summary: {data_info}
671
-
672
- Data Structure Retrieved:
673
- """
 
 
 
674
 
675
- if analysis:
676
- for key, value in list(analysis.items())[:10]:
677
- if isinstance(value, list):
678
- structure += f"β†’ {key}: {', '.join(str(v) for v in value[:5])}\n"
679
- elif isinstance(value, dict):
680
- structure += f"β†’ {key}: {len(value)} items\n"
681
- else:
682
- structure += f"β†’ {key}: {value}\n"
683
 
684
- # Format response (remove markdown)
685
- response = result.get("answer", "").strip()
686
- if not response:
687
- response = f"Analysis: {data_info}"
688
 
689
- # Format findings
690
- findings_list = result.get("findings", [])
691
- findings_text = "\n".join([f"{f}" for f in findings_list]) if findings_list else "No specific findings"
 
692
 
693
  # Get raw data for table
694
  df_data = None
695
- if result.get("raw_data"):
696
  import pandas as pd
697
- df_data = pd.DataFrame(result.get("raw_data", []))
 
 
 
 
 
 
 
 
698
 
699
- return (response, findings_text, structure, df_data)
700
 
701
  investigate_btn.click(
702
  fn=investigate_with_npl,
703
  inputs=[question_input],
704
- outputs=[ai_response, findings, data_structure, data_table]
705
  )
706
 
707
  # =====================================================
 
648
  label="Records Retrieved",
649
  interactive=False
650
  )
651
+
652
+ # Follow-up Questions
653
+ with gr.Tab("❓ Next Questions"):
654
+ follow_up_questions = gr.Textbox(
655
+ label="Suggested Follow-ups & Clarifications",
656
+ lines=6,
657
+ interactive=False
658
+ )
659
 
660
  def investigate_with_npl(question):
661
  """Investigate with NLP-to-SQL and advanced analysis"""
662
  if not question or len(question.strip()) < 2:
663
+ return ("Please ask a question", "", "", None, "")
664
 
665
  result = ask_investigation_question(question)
666
 
667
  if result.get("status") == "error":
668
  error_msg = result.get("message", "Investigation failed")
669
+ follow_ups = result.get("follow_ups", [])
670
+ suggestions = "\n".join([f"β€’ {f}" for f in follow_ups]) if follow_ups else "Try with more specific details"
671
+ return (f"❌ {error_msg}", "No data collected", "", None, f"πŸ’‘ Try this:\n{suggestions}")
672
 
673
+ # Main AI answer
674
+ answer_text = result.get("answer", "")
 
675
 
676
+ # Data structure info (enhanced)
677
+ analysis = result.get("analysis", {})
678
+ data_info = f"""πŸ“Š DATA COLLECTED:
679
+ ────────────────────────
680
+ β€’ Total Records: {result.get('total_records', 0)}
681
+ β€’ Unique Vehicles: {analysis.get('unique_vehicles', 0)}
682
+ β€’ Unique Locations: {analysis.get('unique_locations', 0)}
683
+ β€’ Time Range: {analysis.get('date_range', 'Unknown')}
684
+
685
+ πŸ” KEY FINDINGS:"""
686
 
687
+ for finding in analysis.get("key_findings", []):
688
+ data_info += f"\n β€’ {finding}"
 
 
 
 
 
 
689
 
690
+ if analysis.get("vehicle_types"):
691
+ data_info += "\n\nπŸš— VEHICLE BREAKDOWN:"
692
+ for vtype, count in analysis.get("vehicle_types", []):
693
+ data_info += f"\n β€’ {vtype}: {count} detections"
694
 
695
+ # Insights (findings reformatted)
696
+ insights = "\n".join(analysis.get("key_findings", []))
697
+ if not insights:
698
+ insights = "Data analysis complete - see data structure for details"
699
 
700
  # Get raw data for table
701
  df_data = None
702
+ if result.get("data_preview"):
703
  import pandas as pd
704
+ df_data = pd.DataFrame(result.get("data_preview", []))
705
+
706
+ # Follow-up questions (new feature!)
707
+ follow_ups = result.get("follow_ups", [])
708
+ follow_up_text = ""
709
+ if follow_ups:
710
+ follow_up_text = "πŸ’¬ FOLLOW-UP QUESTIONS:\n────────────────────\n"
711
+ for idx, q in enumerate(follow_ups, 1):
712
+ follow_up_text += f"{idx}. {q}\n"
713
 
714
+ return (answer_text, data_info, insights, df_data, follow_up_text)
715
 
716
  investigate_btn.click(
717
  fn=investigate_with_npl,
718
  inputs=[question_input],
719
+ outputs=[ai_response, data_structure, findings, data_table, follow_up_questions]
720
  )
721
 
722
  # =====================================================