philjosephcohen commited on
Commit
d8f2a0d
·
1 Parent(s): 9b722b4

Nemo fact checker is working ok but still need to improve it

Browse files
multi_agent_demo/guards_demo_ui.py CHANGED
@@ -314,10 +314,67 @@ class SelfContradictionScanner(NemoGuardRailsScanner):
314
  }
315
 
316
  class FactCheckerScanner(NemoGuardRailsScanner):
317
- """Scanner for fact-checking assistant responses"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
 
319
  def scan(self, messages: List[Dict], context: str = "") -> Dict:
320
- # Use enhanced heuristic analysis directly to avoid NeMo GuardRails errors
321
  try:
322
  # Extract assistant messages for fact-checking
323
  assistant_messages = [msg for msg in messages if msg.get("type") == "assistant"]
@@ -326,140 +383,105 @@ class FactCheckerScanner(NemoGuardRailsScanner):
326
 
327
  last_message = assistant_messages[-1]["content"]
328
 
329
- # Advanced heuristic for comprehensive factual claim detection
330
- message_lower = last_message.lower()
331
- words = last_message.split()
332
- sentences = last_message.split('.')
333
-
334
- # 1. Research and authority claims
335
- research_claims = [
336
- any(phrase in message_lower for phrase in ["according to", "studies show", "research indicates", "data shows", "statistics reveal"]),
337
- any(phrase in message_lower for phrase in ["scientists say", "experts believe", "researchers found", "survey shows"]),
338
- any(phrase in message_lower for phrase in ["published in", "peer reviewed", "clinical trial", "meta-analysis"]),
339
- any(phrase in message_lower for phrase in ["university study", "government report", "official data"])
340
- ]
341
-
342
- # 2. Quantitative claims (numbers and statistics)
343
- quantitative_claims = [
344
- any(phrase in message_lower for phrase in ["percent", "%", "percentage", "ratio"]),
345
- any(phrase in message_lower for phrase in ["million", "billion", "trillion", "thousand"]),
346
- any(phrase in message_lower for phrase in ["exactly", "precisely", "specifically", "definitively"]),
347
- len([word for word in last_message.split() if word.replace(',', '').replace('.', '').replace('%', '').isdigit()]) > 2,
348
- any(phrase in last_message for phrase in [" 1.", " 2.", " 3.", " 4.", " 5."]), # Listed facts
349
- any(phrase in message_lower for phrase in ["times more", "times less", "increased by", "decreased by"])
350
- ]
351
-
352
- # 3. Absolute statements (universal claims)
353
- absolute_claims = [
354
- any(phrase in message_lower for phrase in ["always", "never", "all", "every", "no one", "everyone"]),
355
- any(phrase in message_lower for phrase in ["impossible", "guaranteed", "certain", "definitely"]),
356
- any(phrase in message_lower for phrase in ["without exception", "in all cases", "universally true"]),
357
- any(phrase in message_lower for phrase in ["proven fact", "scientific fact", "established truth"])
358
- ]
359
-
360
- # 4. Historical and temporal claims
361
- historical_claims = [
362
- any(phrase in message_lower for phrase in ["in 19", "in 20", "since 19", "since 20"]), # Specific years
363
- any(phrase in message_lower for phrase in ["historically", "throughout history", "has always been"]),
364
- any(phrase in message_lower for phrase in ["first time", "invented in", "discovered in", "founded in"]),
365
- any(phrase in message_lower for phrase in ["world record", "fastest ever", "largest ever", "oldest known"])
366
- ]
367
-
368
- # 5. Medical and scientific claims
369
- medical_scientific_claims = [
370
- any(phrase in message_lower for phrase in ["causes cancer", "prevents disease", "cures", "treatment for"]),
371
- any(phrase in message_lower for phrase in ["scientifically proven", "medically proven", "clinically tested"]),
372
- any(phrase in message_lower for phrase in ["chemical formula", "molecular structure", "genetic"]),
373
- any(phrase in message_lower for phrase in ["side effects", "contraindications", "dosage"])
374
- ]
375
-
376
- # 6. Geographic and demographic claims
377
- geographic_claims = [
378
- any(phrase in message_lower for phrase in ["largest country", "smallest city", "population of", "capital of"]),
379
- any(phrase in message_lower for phrase in ["located at", "coordinates", "altitude", "climate"]),
380
- any(phrase in message_lower for phrase in ["border with", "distance from", "area of", "density"])
381
- ]
382
-
383
- # 7. Economic and financial claims
384
- economic_claims = [
385
- any(phrase in message_lower for phrase in ["gdp", "inflation rate", "unemployment", "stock price"]),
386
- any(phrase in message_lower for phrase in ["market cap", "revenue", "profit margin", "debt ratio"]),
387
- any(phrase in message_lower for phrase in ["exchange rate", "interest rate", "tax rate"])
388
- ]
389
-
390
- # Calculate claim severity
391
- research_count = sum(research_claims)
392
- quantitative_count = sum(quantitative_claims)
393
- absolute_count = sum(absolute_claims)
394
- historical_count = sum(historical_claims)
395
- medical_count = sum(medical_scientific_claims)
396
- geographic_count = sum(geographic_claims)
397
- economic_count = sum(economic_claims)
398
 
399
- total_claims = research_count + quantitative_count + absolute_count + historical_count + medical_count + geographic_count + economic_count
 
 
400
 
401
- # Determine risk level and confidence
402
- if total_claims >= 5:
403
- risk_level = "high"
404
- confidence = 0.3 # Low confidence due to many unverified claims
405
- elif total_claims >= 3:
406
- risk_level = "moderate"
407
- confidence = 0.5
408
- elif total_claims >= 1:
409
- risk_level = "low"
410
- confidence = 0.7
411
- else:
412
- risk_level = "minimal"
413
- confidence = 0.95
414
 
415
- has_factual_claims = total_claims > 0
 
 
 
416
 
417
- # Create detailed reason
418
- if has_factual_claims:
419
- claim_details = []
420
- if research_count > 0:
421
- claim_details.append(f"{research_count} research/authority claim(s)")
422
- if quantitative_count > 0:
423
- claim_details.append(f"{quantitative_count} quantitative claim(s)")
424
- if absolute_count > 0:
425
- claim_details.append(f"{absolute_count} absolute statement(s)")
426
- if historical_count > 0:
427
- claim_details.append(f"{historical_count} historical claim(s)")
428
- if medical_count > 0:
429
- claim_details.append(f"{medical_count} medical/scientific claim(s)")
430
- if geographic_count > 0:
431
- claim_details.append(f"{geographic_count} geographic claim(s)")
432
- if economic_count > 0:
433
- claim_details.append(f"{economic_count} economic claim(s)")
434
-
435
- reason = f"Advanced analysis: {risk_level.capitalize()} risk factual content - {', '.join(claim_details)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
436
  else:
437
- reason = "Advanced analysis: No significant factual claims detected across research, quantitative, absolute, historical, medical, geographic, or economic categories"
 
 
438
 
439
  return {
440
  "scanner": "FactChecker",
441
- "decision": "HUMAN_IN_THE_LOOP" if has_factual_claims else "ALLOW",
442
- "score": confidence,
443
  "reason": reason,
444
- "is_safe": True # Don't block, just flag for review
 
 
 
445
  }
446
 
447
  except Exception as e:
448
- # Fallback to heuristic fact-checking
449
- message_lower = messages[-1]["content"].lower() if messages else ""
450
- factual_indicators = [
451
- any(phrase in message_lower for phrase in ["according to", "studies show", "research indicates"]),
452
- any(phrase in message_lower for phrase in ["in 20", "million", "billion", "percent"])
453
- ]
454
- has_factual_claims = any(factual_indicators)
455
-
456
- return {
457
- "scanner": "FactChecker",
458
- "decision": "HUMAN_IN_THE_LOOP" if has_factual_claims else "ALLOW",
459
- "score": 0.6 if has_factual_claims else 0.9,
460
- "reason": "Heuristic analysis: " + ("Contains factual claims requiring verification" if has_factual_claims else "No strong factual claims detected") + " (NeMo GuardRails unavailable)",
461
- "is_safe": True
462
- }
463
 
464
  class HallucinationDetectorScanner(NemoGuardRailsScanner):
465
  """Scanner for detecting hallucinations in assistant responses"""
@@ -1375,7 +1397,13 @@ def main():
1375
  fig_gauge.update_layout(height=188, showlegend=False, margin={"l": 20, "r": 20, "t": 20, "b": 20})
1376
  st.plotly_chart(fig_gauge, use_container_width=True, key=f"{scanner_name.lower()}_gauge")
1377
 
 
1378
  st.info(f"**Analysis:** {result['reason']}")
 
 
 
 
 
1379
  else:
1380
  st.error(f"Error: {result['error']}")
1381
 
 
314
  }
315
 
316
  class FactCheckerScanner(NemoGuardRailsScanner):
317
+ """Scanner for fact-checking assistant responses using NeMo GuardRails"""
318
+
319
+ def __init__(self):
320
+ """Initialize with proper NeMo GuardRails configuration"""
321
+ if NEMO_GUARDRAILS_AVAILABLE:
322
+ try:
323
+ print("🔧 FactChecker: Attempting to load NeMo GuardRails config...")
324
+
325
+ # Check if config directory exists
326
+ import os
327
+ config_path = "nemo_config/"
328
+ if not os.path.exists(config_path):
329
+ raise FileNotFoundError(f"Config directory '{config_path}' not found")
330
+
331
+ print(f"📁 Config directory found: {config_path}")
332
+ print(f"📄 Config files: {os.listdir(config_path)}")
333
+
334
+ # Check if OPENAI_API_KEY is set
335
+ openai_key = os.getenv('OPENAI_API_KEY')
336
+ if not openai_key:
337
+ raise ValueError("OPENAI_API_KEY environment variable is not set")
338
+ print(f"🔑 OPENAI_API_KEY found: {openai_key[:15]}...{openai_key[-15:]} (length: {len(openai_key)})")
339
+
340
+ # Test OpenAI API access to avoid model access issues
341
+ try:
342
+ import openai
343
+ client = openai.OpenAI(api_key=openai_key)
344
+ # Try to list available models
345
+ models = client.models.list()
346
+ available_models = [model.id for model in models.data]
347
+ print(f"🤖 Available OpenAI models: {available_models[:5]}...") # Show first 5
348
+
349
+ # Check if our preferred models are available
350
+ preferred_models = ["gpt-4o-mini", "gpt-3.5-turbo-instruct", "gpt-3.5-turbo"]
351
+ for model in preferred_models:
352
+ if model in available_models:
353
+ print(f"✅ Model {model} is available")
354
+ else:
355
+ print(f"❌ Model {model} is NOT available")
356
+ except Exception as e:
357
+ print(f"⚠️ Warning: Could not verify OpenAI model access: {e}")
358
+ print("⚠️ Proceeding with configuration, but you may encounter model access errors")
359
+
360
+ # Initialize NeMo GuardRails with the config
361
+ config = RailsConfig.from_path(config_path)
362
+ print("✅ RailsConfig loaded successfully")
363
+
364
+ self.rails = LLMRails(config)
365
+ print("✅ FactChecker: NeMo GuardRails initialized successfully")
366
+ except Exception as e:
367
+ print(f"⚠️ FactChecker: Failed to initialize NeMo GuardRails: {e}")
368
+ print(f"⚠️ Error type: {type(e).__name__}")
369
+ import traceback
370
+ print(f"⚠️ Full traceback: {traceback.format_exc()}")
371
+ self.rails = None
372
+ else:
373
+ print("❌ NeMo GuardRails not available - install with: pip install nemoguardrails")
374
+ self.rails = None
375
 
376
  def scan(self, messages: List[Dict], context: str = "") -> Dict:
377
+ """Scan messages for factual accuracy using NeMo GuardRails"""
378
  try:
379
  # Extract assistant messages for fact-checking
380
  assistant_messages = [msg for msg in messages if msg.get("type") == "assistant"]
 
383
 
384
  last_message = assistant_messages[-1]["content"]
385
 
386
+ # Only use NeMo GuardRails - no heuristic fallback
387
+ if self.rails is not None:
388
+ return self._nemo_fact_check(last_message, messages)
389
+ else:
390
+ return {"error": "NeMo GuardRails not properly initialized", "scanner": "FactChecker"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
 
392
+ except Exception as e:
393
+ print(f"❌ FactChecker error: {e}")
394
+ return {"error": f"Error during fact-checking: {str(e)}", "scanner": "FactChecker"}
395
 
396
+ def _nemo_fact_check(self, message: str, messages: List[Dict]) -> Dict:
397
+ """Use NeMo GuardRails basic fact-checking - no customization"""
398
+ try:
399
+ print(f"🔍 FactChecker: Using NeMo's built-in fact-checking on: {message[:100]}...")
 
 
 
 
 
 
 
 
 
400
 
401
+ # Use NeMo in the simplest way possible - just generate with the message
402
+ response = self.rails.generate(
403
+ prompt=message
404
+ )
405
 
406
+ print(f"🔍 NeMo response: {response}")
407
+ print(f"🔍 Response type: {type(response)}")
408
+ print(f"🔍 Response attributes: {[attr for attr in dir(response) if not attr.startswith('_')]}")
409
+
410
+ # Check actual response content
411
+ if hasattr(response, 'response'):
412
+ print(f"🔍 response.response: {response.response}")
413
+ if hasattr(response, 'llm_output'):
414
+ print(f"🔍 response.llm_output: {response.llm_output}")
415
+ if hasattr(response, 'state'):
416
+ print(f"🔍 response.state: {response.state}")
417
+ if hasattr(response, 'log'):
418
+ print(f"🔍 response.log: {response.log}")
419
+
420
+ # Analyze NeMo's response for fact-checking results
421
+ nemo_response = str(response)
422
+ has_false_claims = False
423
+ claims_detected = []
424
+
425
+ # NeMo provided detailed fact-checking analysis - parse it
426
+ if nemo_response and len(nemo_response) > 50: # Substantial response
427
+ response_lower = nemo_response.lower()
428
+
429
+ # Look for indicators that NeMo found issues
430
+ false_claim_indicators = [
431
+ "inaccuracies" in response_lower,
432
+ "exaggerations" in response_lower,
433
+ "misleading" in response_lower,
434
+ "incorrect" in response_lower,
435
+ "not supported" in response_lower,
436
+ "not feasible" in response_lower,
437
+ "rare for" in response_lower and "uniform" in response_lower,
438
+ "the assertion" in response_lower and "not" in response_lower,
439
+ "the claim" in response_lower and ("incorrect" in response_lower or "wrong" in response_lower)
440
+ ]
441
+
442
+ if any(false_claim_indicators):
443
+ has_false_claims = True
444
+
445
+ # Extract specific claims mentioned by NeMo
446
+ if "gdp growth" in response_lower:
447
+ claims_detected.append("GDP growth uniformity claim")
448
+ if "100% cure rate" in response_lower or "cancer" in response_lower:
449
+ claims_detected.append("Cancer cure rate claim")
450
+ if "unemployment" in response_lower:
451
+ claims_detected.append("Global unemployment rate claim")
452
+ if "water" in response_lower or "2.3 liters" in response_lower:
453
+ claims_detected.append("Daily consumption claim")
454
+ if "coordinates" in response_lower or "population" in response_lower:
455
+ claims_detected.append("Geographic/population claims")
456
+ if "1847" in response_lower or "civilization" in response_lower:
457
+ claims_detected.append("Historical civilization claim")
458
+
459
+ # Set decision based on analysis
460
+ if has_false_claims:
461
+ decision = "BLOCK"
462
+ score = 0.1 # Very low confidence in false content
463
+ reason = f"NeMo Fact-Check: Multiple false/fabricated claims detected. NeMo identified inaccuracies, exaggerations, and misleading statements. Analysis: {nemo_response[:300]}..."
464
  else:
465
+ decision = "ALLOW"
466
+ score = 0.9 # High confidence in accurate content
467
+ reason = f"NeMo Fact-Check: No false claims detected. Response: {nemo_response[:200]}..."
468
 
469
  return {
470
  "scanner": "FactChecker",
471
+ "decision": decision,
472
+ "score": score,
473
  "reason": reason,
474
+ "is_safe": not has_false_claims,
475
+ "claims_detected": claims_detected,
476
+ "analysis_method": "NeMo GuardRails AI Analysis",
477
+ "ai_response": nemo_response
478
  }
479
 
480
  except Exception as e:
481
+ print(f"❌ NeMo fact-checking failed: {e}")
482
+ import traceback
483
+ print(f"❌ Full traceback: {traceback.format_exc()}")
484
+ return {"error": f"NeMo fact-checking failed: {str(e)}", "scanner": "FactChecker"}
 
 
 
 
 
 
 
 
 
 
 
485
 
486
  class HallucinationDetectorScanner(NemoGuardRailsScanner):
487
  """Scanner for detecting hallucinations in assistant responses"""
 
1397
  fig_gauge.update_layout(height=188, showlegend=False, margin={"l": 20, "r": 20, "t": 20, "b": 20})
1398
  st.plotly_chart(fig_gauge, use_container_width=True, key=f"{scanner_name.lower()}_gauge")
1399
 
1400
+ # Show analysis with expandable full response
1401
  st.info(f"**Analysis:** {result['reason']}")
1402
+
1403
+ # Add expandable section for full AI response
1404
+ if "ai_response" in result and result["ai_response"]:
1405
+ with st.expander("🔍 View Full NeMo Analysis"):
1406
+ st.text(result['ai_response'])
1407
  else:
1408
  st.error(f"Error: {result['error']}")
1409
 
nemo_config/config.yml CHANGED
@@ -2,23 +2,34 @@
2
  models:
3
  - type: main
4
  engine: openai
5
- model: gpt-3.5-turbo
 
 
 
6
 
7
- rails:
8
- input:
9
- flows:
10
- # Add input filtering if needed
 
 
11
 
 
 
 
12
  output:
13
  flows:
14
  - self check facts
15
- - self check hallucination
16
 
17
- # Fact-checking configuration
18
  fact_checking:
19
  parameters:
20
- threshold: 0.5
21
  evidence_required: true
 
 
 
 
22
 
23
  # Hallucination detection configuration
24
  hallucination_detection:
 
2
  models:
3
  - type: main
4
  engine: openai
5
+ model: gpt-4o-mini
6
+ - type: fallback
7
+ engine: openai
8
+ model: gpt-3.5-turbo-instruct
9
 
10
+ prompts:
11
+ - task: self_check_facts
12
+ content: |
13
+ Check if the following response contains false or fabricated information:
14
+
15
+ {{ bot_response }}
16
 
17
+ Answer with "yes" if it contains false/fabricated claims, "no" if it's accurate.
18
+
19
+ rails:
20
  output:
21
  flows:
22
  - self check facts
 
23
 
24
+ # Enhanced fact-checking configuration
25
  fact_checking:
26
  parameters:
27
+ threshold: 0.6
28
  evidence_required: true
29
+ confidence_threshold: 0.7
30
+ max_claims_per_response: 5
31
+ verification_sources: ["academic", "government", "verified_media"]
32
+ claim_types: ["quantitative", "historical", "scientific", "medical", "geographic"]
33
 
34
  # Hallucination detection configuration
35
  hallucination_detection:
nemo_config/rails.co CHANGED
@@ -1,44 +1 @@
1
- # Colang rules for AI Agent Guards Testing
2
-
3
- define user express greeting
4
- "hello"
5
- "hi"
6
- "hey"
7
-
8
- define bot express greeting
9
- "Hello! I'm here to help test AI Agent Guards."
10
-
11
- define user ask question
12
- "what is"
13
- "can you tell me"
14
- "explain"
15
-
16
- # Self-contradiction detection flow
17
- define flow self check contradiction
18
- """Check for self-contradictions in the response"""
19
- $result = execute check_self_contradiction
20
- if $result.contradictory
21
- bot refuse contradictory response
22
-
23
- define bot refuse contradictory response
24
- "I notice my response may contain contradictions. Let me clarify..."
25
-
26
- # Fact checking flow
27
- define flow self check facts
28
- """Check factual accuracy of the response"""
29
- $result = execute check_facts
30
- if $result.needs_verification
31
- bot refuse unverified facts
32
-
33
- define bot refuse unverified facts
34
- "I cannot verify the accuracy of this information. Please double-check with reliable sources."
35
-
36
- # Hallucination detection flow
37
- define flow self check hallucination
38
- """Check for hallucinated content"""
39
- $result = execute check_hallucination
40
- if $result.hallucination_detected
41
- bot refuse hallucinated response
42
-
43
- define bot refuse hallucinated response
44
- "I may have generated unverified information. Please treat this response with caution."
 
1
+ # Empty rails file - let NeMo use its built-in capabilities