correct pr for the added MongoDB support

#2
Files changed (3) hide show
  1. README.md +24 -9
  2. app.py +190 -116
  3. requirements.txt +3 -1
README.md CHANGED
@@ -20,7 +20,7 @@ A sophisticated system for detecting hallucinations in AI responses using a para
20
  - **Paraphrase Generation**: Automatically generates semantically equivalent variations of user queries
21
  - **Multi-Model Architecture**: Uses Mistral Large for responses and OpenAI's o3-mini as a judge
22
  - **Real-time Progress Tracking**: Visual feedback during the analysis process
23
- - **Persistent Feedback Storage**: User feedback and results are stored in a persistent SQLite database
24
  - **Interactive Web Interface**: Clean, responsive Gradio interface with example queries
25
  - **Detailed Analysis**: Provides confidence scores, reasoning, and specific conflicting facts
26
  - **Statistics Dashboard**: Real-time tracking of hallucination detection statistics
@@ -41,11 +41,23 @@ A sophisticated system for detecting hallucinations in AI responses using a para
41
  1. Create a new Space on Hugging Face
42
  2. Select "Gradio" as the SDK
43
  3. Add your repository
44
- 4. Set the following secrets in your Space's settings:
 
45
  - `HF_MISTRAL_API_KEY`
46
  - `HF_OPENAI_API_KEY`
 
47
 
48
- The application uses Hugging Face Spaces' persistent storage (`/data` directory) to maintain feedback data between restarts.
 
 
 
 
 
 
 
 
 
 
49
 
50
  ## Usage
51
 
@@ -71,16 +83,19 @@ The application uses Hugging Face Spaces' persistent storage (`/data` directory)
71
  - Provides confidence scores and reasoning
72
 
73
  3. **Feedback Collection**:
74
- - User feedback is stored in SQLite database
75
- - Persistent storage ensures data survival
76
  - Statistics are updated in real-time
 
77
 
78
  ## Data Persistence
79
 
80
- The application uses SQLite for data storage in Hugging Face Spaces' persistent `/data` directory. This ensures:
81
- - Feedback data survives Space restarts
82
- - Statistics are preserved long-term
83
- - No data loss during inactivity periods
 
 
84
 
85
  ## Contributing
86
 
 
20
  - **Paraphrase Generation**: Automatically generates semantically equivalent variations of user queries
21
  - **Multi-Model Architecture**: Uses Mistral Large for responses and OpenAI's o3-mini as a judge
22
  - **Real-time Progress Tracking**: Visual feedback during the analysis process
23
+ - **Permanent Cloud Storage**: User feedback and results are stored in MongoDB Atlas for persistent storage across restarts
24
  - **Interactive Web Interface**: Clean, responsive Gradio interface with example queries
25
  - **Detailed Analysis**: Provides confidence scores, reasoning, and specific conflicting facts
26
  - **Statistics Dashboard**: Real-time tracking of hallucination detection statistics
 
41
  1. Create a new Space on Hugging Face
42
  2. Select "Gradio" as the SDK
43
  3. Add your repository
44
+ 4. Set up a MongoDB Atlas database (see below)
45
+ 5. Set the following secrets in your Space's settings:
46
  - `HF_MISTRAL_API_KEY`
47
  - `HF_OPENAI_API_KEY`
48
+ - `MONGODB_URI`
49
 
50
+ ### MongoDB Atlas Setup
51
+
52
+ For permanent data storage that persists across HuggingFace Space restarts:
53
+
54
+ 1. Create a free [MongoDB Atlas account](https://www.mongodb.com/cloud/atlas/register)
55
+ 2. Create a new cluster (the free tier is sufficient)
56
+ 3. In the "Database Access" menu, create a database user with read/write permissions
57
+ 4. In the "Network Access" menu, add IP `0.0.0.0/0` to allow access from anywhere (required for HuggingFace Spaces)
58
+ 5. In the "Databases" section, click "Connect" and choose "Connect your application"
59
+ 6. Copy the connection string and replace `<password>` with your database user's password
60
+ 7. Set this as your `MONGODB_URI` secret in HuggingFace Spaces settings
61
 
62
  ## Usage
63
 
 
83
  - Provides confidence scores and reasoning
84
 
85
  3. **Feedback Collection**:
86
+ - User feedback is stored in MongoDB Atlas
87
+ - Cloud-based persistent storage ensures data survival
88
  - Statistics are updated in real-time
89
+ - Data can be exported for further analysis
90
 
91
  ## Data Persistence
92
 
93
+ The application uses MongoDB Atlas for data storage, providing several benefits:
94
+ - **Permanent Storage**: Data persists even when Hugging Face Spaces restart
95
+ - **Scalability**: MongoDB scales as your data grows
96
+ - **Cloud-based**: No reliance on Space-specific storage that can be lost
97
+ - **Query Capabilities**: Powerful query functionality for data analysis
98
+ - **Export Options**: Built-in methods to export data to CSV
99
 
100
  ## Contributing
101
 
app.py CHANGED
@@ -14,7 +14,13 @@ import time
14
  import concurrent.futures
15
  from concurrent.futures import ThreadPoolExecutor
16
  import threading
17
- import sqlite3
 
 
 
 
 
 
18
 
19
  # Configure logging
20
  logging.basicConfig(
@@ -380,78 +386,44 @@ Your response should be a JSON with the following fields:
380
  class HallucinationDetectorApp:
381
  def __init__(self):
382
  self.pas2 = None
383
- # Use the default HF Spaces persistent storage location
384
- self.data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
385
- self.db_path = os.path.join(self.data_dir, "feedback.db")
386
  logger.info("Initializing HallucinationDetectorApp")
387
  self._initialize_database()
388
  self.progress_callback = None
389
 
390
  def _initialize_database(self):
391
- """Initialize SQLite database for feedback storage in persistent directory"""
392
  try:
393
- # Create data directory if it doesn't exist
394
- os.makedirs(self.data_dir, exist_ok=True)
395
- logger.info(f"Ensuring data directory exists at {self.data_dir}")
396
-
397
- conn = sqlite3.connect(self.db_path)
398
- cursor = conn.cursor()
399
-
400
- # Create table if it doesn't exist
401
- cursor.execute('''
402
- CREATE TABLE IF NOT EXISTS feedback (
403
- id INTEGER PRIMARY KEY AUTOINCREMENT,
404
- timestamp TEXT,
405
- original_query TEXT,
406
- original_response TEXT,
407
- paraphrased_queries TEXT,
408
- paraphrased_responses TEXT,
409
- hallucination_detected INTEGER,
410
- confidence_score REAL,
411
- conflicting_facts TEXT,
412
- reasoning TEXT,
413
- summary TEXT,
414
- user_feedback TEXT
415
- )
416
- ''')
417
 
418
- conn.commit()
419
- conn.close()
420
- logger.info(f"Database initialized successfully at {self.db_path}")
421
  except Exception as e:
422
- logger.error(f"Error initializing database: {str(e)}", exc_info=True)
423
- # Fallback to temporary directory if /data is not accessible
424
- temp_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "temp_data")
425
- os.makedirs(temp_dir, exist_ok=True)
426
- self.db_path = os.path.join(temp_dir, "feedback.db")
427
- logger.warning(f"Using fallback database location: {self.db_path}")
428
-
429
- # Try creating database in fallback location
430
- try:
431
- conn = sqlite3.connect(self.db_path)
432
- cursor = conn.cursor()
433
- cursor.execute('''
434
- CREATE TABLE IF NOT EXISTS feedback (
435
- id INTEGER PRIMARY KEY AUTOINCREMENT,
436
- timestamp TEXT,
437
- original_query TEXT,
438
- original_response TEXT,
439
- paraphrased_queries TEXT,
440
- paraphrased_responses TEXT,
441
- hallucination_detected INTEGER,
442
- confidence_score REAL,
443
- conflicting_facts TEXT,
444
- reasoning TEXT,
445
- summary TEXT,
446
- user_feedback TEXT
447
- )
448
- ''')
449
- conn.commit()
450
- conn.close()
451
- logger.info(f"Database initialized in fallback location")
452
- except Exception as fallback_error:
453
- logger.error(f"Critical error: Could not initialize database in fallback location: {str(fallback_error)}", exc_info=True)
454
- raise
455
 
456
  def set_progress_callback(self, callback):
457
  """Set the progress callback function"""
@@ -503,80 +475,182 @@ class HallucinationDetectorApp:
503
  }
504
 
505
  def save_feedback(self, results, feedback):
506
- """Save results and user feedback to SQLite database"""
507
  try:
508
  logger.info("Saving user feedback: %s", feedback)
509
 
510
- conn = sqlite3.connect(self.db_path)
511
- cursor = conn.cursor()
512
-
513
- # Prepare data
514
- data = (
515
- datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
516
- results.get('original_query', ''),
517
- results.get('original_response', ''),
518
- str(results.get('paraphrased_queries', [])),
519
- str(results.get('paraphrased_responses', [])),
520
- 1 if results.get('hallucination_detected', False) else 0,
521
- results.get('confidence_score', 0.0),
522
- str(results.get('conflicting_facts', [])),
523
- results.get('reasoning', ''),
524
- results.get('summary', ''),
525
- feedback
526
- )
527
-
528
- # Insert data
529
- cursor.execute('''
530
- INSERT INTO feedback (
531
- timestamp, original_query, original_response,
532
- paraphrased_queries, paraphrased_responses,
533
- hallucination_detected, confidence_score,
534
- conflicting_facts, reasoning, summary, user_feedback
535
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
536
- ''', data)
537
 
538
- conn.commit()
539
- conn.close()
540
 
541
- logger.info("Feedback saved successfully to database")
542
  return "Feedback saved successfully!"
543
  except Exception as e:
544
  logger.error("Error saving feedback: %s", str(e), exc_info=True)
545
  return f"Error saving feedback: {str(e)}"
546
 
547
  def get_feedback_stats(self):
548
- """Get statistics about collected feedback"""
549
  try:
550
- conn = sqlite3.connect(self.db_path)
551
- cursor = conn.cursor()
 
552
 
553
  # Get total feedback count
554
- cursor.execute("SELECT COUNT(*) FROM feedback")
555
- total_count = cursor.fetchone()[0]
556
-
557
- # Get hallucination detection stats
558
- cursor.execute("""
559
- SELECT hallucination_detected, COUNT(*)
560
- FROM feedback
561
- GROUP BY hallucination_detected
562
- """)
563
- detection_stats = dict(cursor.fetchall())
 
564
 
565
  # Get average confidence score
566
- cursor.execute("SELECT AVG(confidence_score) FROM feedback")
567
- avg_confidence = cursor.fetchone()[0] or 0
568
-
569
- conn.close()
 
 
 
 
570
 
571
  return {
572
  "total_feedback": total_count,
573
- "hallucinations_detected": detection_stats.get(1, 0),
574
- "no_hallucinations": detection_stats.get(0, 0),
575
  "average_confidence": round(avg_confidence, 2)
576
  }
577
  except Exception as e:
578
  logger.error("Error getting feedback stats: %s", str(e), exc_info=True)
579
  return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
580
 
581
 
582
  # Progress tracking for UI updates
@@ -1480,4 +1554,4 @@ if __name__ == "__main__":
1480
 
1481
  # Uncomment this line to run the test function instead of the main interface
1482
  # if __name__ == "__main__":
1483
- # test_progress()
 
14
  import concurrent.futures
15
  from concurrent.futures import ThreadPoolExecutor
16
  import threading
17
+ import pymongo
18
+ from pymongo import MongoClient
19
+ from bson.objectid import ObjectId
20
+ from dotenv import load_dotenv
21
+
22
+ # Load environment variables
23
+ load_dotenv()
24
 
25
  # Configure logging
26
  logging.basicConfig(
 
386
  class HallucinationDetectorApp:
387
  def __init__(self):
388
  self.pas2 = None
 
 
 
389
  logger.info("Initializing HallucinationDetectorApp")
390
  self._initialize_database()
391
  self.progress_callback = None
392
 
393
  def _initialize_database(self):
394
+ """Initialize MongoDB connection for persistent feedback storage"""
395
  try:
396
+ # Get MongoDB connection string from environment variable
397
+ mongo_uri = os.environ.get("MONGODB_URI")
398
+
399
+ if not mongo_uri:
400
+ logger.warning("MONGODB_URI not found in environment variables. Please set it in HuggingFace Spaces secrets.")
401
+ logger.warning("Using a placeholder URI for now - connection will fail until proper URI is provided.")
402
+ # Use a placeholder - this will fail but allows the app to initialize
403
+ mongo_uri = "mongodb+srv://username:[email protected]/?retryWrites=true&w=majority"
404
+
405
+ # Connect to MongoDB
406
+ self.mongo_client = MongoClient(mongo_uri)
407
+
408
+ # Access or create database
409
+ self.db = self.mongo_client["hallucination_detector"]
410
+
411
+ # Access or create collection
412
+ self.feedback_collection = self.db["feedback"]
413
+
414
+ # Create index on timestamp for faster querying
415
+ self.feedback_collection.create_index("timestamp")
416
+
417
+ # Test connection
418
+ self.mongo_client.admin.command('ping')
419
+ logger.info("MongoDB connection successful")
420
 
 
 
 
421
  except Exception as e:
422
+ logger.error(f"Error initializing MongoDB: {str(e)}", exc_info=True)
423
+ logger.warning("Proceeding without database connection. Data will not be saved persistently.")
424
+ self.mongo_client = None
425
+ self.db = None
426
+ self.feedback_collection = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
427
 
428
  def set_progress_callback(self, callback):
429
  """Set the progress callback function"""
 
475
  }
476
 
477
  def save_feedback(self, results, feedback):
478
+ """Save results and user feedback to MongoDB"""
479
  try:
480
  logger.info("Saving user feedback: %s", feedback)
481
 
482
+ if self.feedback_collection is None:
483
+ logger.error("MongoDB connection not available. Cannot save feedback.")
484
+ return "Database connection not available. Feedback not saved."
485
+
486
+ # Prepare document for MongoDB
487
+ document = {
488
+ "timestamp": datetime.now(),
489
+ "original_query": results.get('original_query', ''),
490
+ "original_response": results.get('original_response', ''),
491
+ "paraphrased_queries": results.get('paraphrased_queries', []),
492
+ "paraphrased_responses": results.get('paraphrased_responses', []),
493
+ "hallucination_detected": results.get('hallucination_detected', False),
494
+ "confidence_score": results.get('confidence_score', 0.0),
495
+ "conflicting_facts": results.get('conflicting_facts', []),
496
+ "reasoning": results.get('reasoning', ''),
497
+ "summary": results.get('summary', ''),
498
+ "user_feedback": feedback
499
+ }
 
 
 
 
 
 
 
 
 
500
 
501
+ # Insert document into collection
502
+ self.feedback_collection.insert_one(document)
503
 
504
+ logger.info("Feedback saved successfully to MongoDB")
505
  return "Feedback saved successfully!"
506
  except Exception as e:
507
  logger.error("Error saving feedback: %s", str(e), exc_info=True)
508
  return f"Error saving feedback: {str(e)}"
509
 
510
  def get_feedback_stats(self):
511
+ """Get statistics about collected feedback from MongoDB"""
512
  try:
513
+ if self.feedback_collection is None:
514
+ logger.error("MongoDB connection not available. Cannot get feedback stats.")
515
+ return None
516
 
517
  # Get total feedback count
518
+ total_count = self.feedback_collection.count_documents({})
519
+
520
+ # Get hallucination detection stats using aggregation
521
+ hallucination_pipeline = [
522
+ {"$group": {
523
+ "_id": "$hallucination_detected",
524
+ "count": {"$sum": 1}
525
+ }}
526
+ ]
527
+ detection_stats = {doc["_id"]: doc["count"]
528
+ for doc in self.feedback_collection.aggregate(hallucination_pipeline)}
529
 
530
  # Get average confidence score
531
+ avg_pipeline = [
532
+ {"$group": {
533
+ "_id": None,
534
+ "average": {"$avg": "$confidence_score"}
535
+ }}
536
+ ]
537
+ avg_result = list(self.feedback_collection.aggregate(avg_pipeline))
538
+ avg_confidence = avg_result[0]["average"] if avg_result else 0
539
 
540
  return {
541
  "total_feedback": total_count,
542
+ "hallucinations_detected": detection_stats.get(True, 0),
543
+ "no_hallucinations": detection_stats.get(False, 0),
544
  "average_confidence": round(avg_confidence, 2)
545
  }
546
  except Exception as e:
547
  logger.error("Error getting feedback stats: %s", str(e), exc_info=True)
548
  return None
549
+
550
+ def export_data_to_csv(self, filepath=None):
551
+ """Export all feedback data to a CSV file for analysis"""
552
+ try:
553
+ if self.feedback_collection is None:
554
+ logger.error("MongoDB connection not available. Cannot export data.")
555
+ return "Database connection not available. Cannot export data."
556
+
557
+ # Query all feedback data
558
+ cursor = self.feedback_collection.find({})
559
+
560
+ # Convert cursor to list of dictionaries
561
+ records = list(cursor)
562
+
563
+ # Convert MongoDB documents to pandas DataFrame
564
+ # Handle nested arrays and complex objects
565
+ for record in records:
566
+ # Convert ObjectId to string
567
+ record['_id'] = str(record['_id'])
568
+
569
+ # Convert datetime objects to string
570
+ if 'timestamp' in record:
571
+ record['timestamp'] = record['timestamp'].strftime("%Y-%m-%d %H:%M:%S")
572
+
573
+ # Convert lists to strings for CSV storage
574
+ if 'paraphrased_queries' in record:
575
+ record['paraphrased_queries'] = json.dumps(record['paraphrased_queries'])
576
+ if 'paraphrased_responses' in record:
577
+ record['paraphrased_responses'] = json.dumps(record['paraphrased_responses'])
578
+ if 'conflicting_facts' in record:
579
+ record['conflicting_facts'] = json.dumps(record['conflicting_facts'])
580
+
581
+ # Create DataFrame
582
+ df = pd.DataFrame(records)
583
+
584
+ # Define default filepath if not provided
585
+ if not filepath:
586
+ filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)),
587
+ f"hallucination_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv")
588
+
589
+ # Export to CSV
590
+ df.to_csv(filepath, index=False)
591
+ logger.info(f"Data successfully exported to {filepath}")
592
+
593
+ return filepath
594
+ except Exception as e:
595
+ logger.error(f"Error exporting data: {str(e)}", exc_info=True)
596
+ return f"Error exporting data: {str(e)}"
597
+
598
+ def get_recent_queries(self, limit=10):
599
+ """Get most recent queries for display in the UI"""
600
+ try:
601
+ if self.feedback_collection is None:
602
+ logger.error("MongoDB connection not available. Cannot get recent queries.")
603
+ return []
604
+
605
+ # Get most recent queries
606
+ cursor = self.feedback_collection.find(
607
+ {},
608
+ {"original_query": 1, "hallucination_detected": 1, "timestamp": 1}
609
+ ).sort("timestamp", pymongo.DESCENDING).limit(limit)
610
+
611
+ # Convert to list of dictionaries
612
+ recent_queries = []
613
+ for doc in cursor:
614
+ recent_queries.append({
615
+ "id": str(doc["_id"]),
616
+ "query": doc["original_query"],
617
+ "hallucination_detected": doc.get("hallucination_detected", False),
618
+ "timestamp": doc["timestamp"].strftime("%Y-%m-%d %H:%M:%S") if isinstance(doc["timestamp"], datetime) else doc["timestamp"]
619
+ })
620
+
621
+ return recent_queries
622
+ except Exception as e:
623
+ logger.error(f"Error getting recent queries: {str(e)}", exc_info=True)
624
+ return []
625
+
626
+ def get_query_details(self, query_id):
627
+ """Get full details for a specific query by ID"""
628
+ try:
629
+ if self.feedback_collection is None:
630
+ logger.error("MongoDB connection not available. Cannot get query details.")
631
+ return None
632
+
633
+ # Convert string ID to ObjectId
634
+ obj_id = ObjectId(query_id)
635
+
636
+ # Find the query by ID
637
+ doc = self.feedback_collection.find_one({"_id": obj_id})
638
+
639
+ if doc is None:
640
+ logger.warning(f"No query found with ID {query_id}")
641
+ return None
642
+
643
+ # Convert ObjectId to string for JSON serialization
644
+ doc["_id"] = str(doc["_id"])
645
+
646
+ # Convert timestamp to string
647
+ if "timestamp" in doc and isinstance(doc["timestamp"], datetime):
648
+ doc["timestamp"] = doc["timestamp"].strftime("%Y-%m-%d %H:%M:%S")
649
+
650
+ return doc
651
+ except Exception as e:
652
+ logger.error(f"Error getting query details: {str(e)}", exc_info=True)
653
+ return None
654
 
655
 
656
  # Progress tracking for UI updates
 
1554
 
1555
  # Uncomment this line to run the test function instead of the main interface
1556
  # if __name__ == "__main__":
1557
+ # test_progress()
requirements.txt CHANGED
@@ -4,4 +4,6 @@ numpy
4
  mistralai
5
  openai
6
  pydantic
7
- python-dotenv
 
 
 
4
  mistralai
5
  openai
6
  pydantic
7
+ python-dotenv
8
+ pymongo
9
+ dnspython