diff --git a/haloguard-pro/.env.example b/haloguard-pro/.env.example new file mode 100644 index 0000000..b909542 --- /dev/null +++ b/haloguard-pro/.env.example @@ -0,0 +1,19 @@ +# Environment variables for HaloGuard Pro + +# Server +HOST=0.0.0.0 +PORT=8000 +WORKERS=4 + +# Logging +LOG_LEVEL=INFO +LOG_FILE=/app/logs/haloguard.log + +# Knowledge Base +DEFAULT_DOMAIN=general + +# Health Check +HEALTH_CHECK_PATH=/health + +# Optional: Enable auto-correction only for high-risk domains +AUTO_CORRECT_DOMAINS=medical,legal,finance diff --git a/haloguard-pro/.gitignore b/haloguard-pro/.gitignore new file mode 100644 index 0000000..1107539 --- /dev/null +++ b/haloguard-pro/.gitignore @@ -0,0 +1,10 @@ +__pycache__/ +*.pyc +*.log +*.db +.env +data/ +.DS_Store +venv/ +.idea/ +.vscode/ diff --git a/haloguard-pro/Dockerfile b/haloguard-pro/Dockerfile new file mode 100644 index 0000000..d645600 --- /dev/null +++ b/haloguard-pro/Dockerfile @@ -0,0 +1,24 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Create directories +RUN mkdir -p /app/facts /app/logs + +# Copy everything +COPY facts/ /app/facts/ +COPY main.py /app/main.py +COPY config/ /app/config/ +COPY .env.example /app/.env.example + +# Expose port +EXPOSE 8000 + +# Set environment variables +ENV PYTHONUNBUFFERED=1 + +# Run app +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"] diff --git a/haloguard-pro/README.md b/haloguard-pro/README.md new file mode 100644 index 0000000..d477e90 --- /dev/null +++ b/haloguard-pro/README.md @@ -0,0 +1,112 @@ +# HaloGuard Pro: Technical Deep Dive + +This document provides a detailed explanation of the logical design, architecture, and execution of the HaloGuard Pro system. + +## 1. Core Philosophy: Determinism Over AI + +The fundamental design choice of HaloGuard Pro is to **reject AI-based solutions for fact-checking** in favor of a deterministic, rule-based system. This choice was made to prioritize reliability, speed, and cost-effectiveness for production environments. + +| Why Not AI/ML? | The HaloGuard Pro Approach | +| :--- | :--- | +| **Non-Deterministic**: LLM-as-judge or semantic search can produce different results for the same input and are prone to the very hallucinations they are supposed to prevent. | **100% Deterministic**: Given the same input and the same knowledge base, the output will *always* be the same. There are zero false positives for defined rules. | +| **High Cost**: Requires expensive GPU hardware and has a high cost per query. | **Extremely Low Cost**: Runs on a cheap CPU ($5/month VPS) with a per-query cost near zero. | +| **Complex**: Requires expertise in embeddings, vector databases, RAG pipelines, and model fine-tuning. | **Simple**: The logic is based on string matching. The knowledge base is a human-readable JSON file. No ML expertise is needed. | +| **Slow**: Verifying an output can take several seconds, making it unsuitable for real-time chat. | **Blazing Fast**: Verification takes only a few milliseconds, making it perfect for real-time applications. | + +This approach is ideal for domains where factual accuracy is non-negotiable and "good enough" is not an option, such as in medical, legal, and financial applications. + +## 2. System Architecture + +The system is a monolithic FastAPI application containerized with Docker. The architecture is designed for simplicity, scalability, and ease of maintenance. + +**Key Components:** + +1. **FastAPI Web Server (`main.py`)**: A lightweight Python web server that exposes the core verification logic via a REST API. It serves as the single entry point for all requests. +2. **Knowledge Base (KB)**: A set of JSON files located in the `facts/` directory. Each file represents a specific domain (e.g., `medical.json`, `legal.json`). This separation allows for modular and domain-specific fact management without touching the application code. +3. **Docker Environment (`Dockerfile`, `docker-compose.yml`)**: Packages the application and its dependencies into a portable container image. This ensures consistent behavior across development, testing, and production environments and simplifies deployment immensely. +4. **Configuration (`config/settings.py`, `.env`)**: Centralized configuration management for server settings, logging, and application behavior. It loads settings from environment variables, following the 12-factor app methodology. +5. **Testing Suite (`tests/`)**: A robust set of unit and integration tests using `pytest` and `FastAPI.TestClient` to ensure code quality, prevent regressions, and validate the behavior of the API endpoints. + +## 3. Execution Flow: A Step-by-Step Breakdown + +When a request hits the `/verify` endpoint, the following sequence of operations occurs: + +### Step 1: Request Handling +The server receives a POST request containing the LLM `output` to be verified and an optional `prompt`. + +```json +{ + "output": "Einstein died in 1950", + "prompt": "When did Einstein die?" +} +``` + +### Step 2: Domain Detection (`detect_domain`) +The system first attempts to determine the conversational domain to use the correct knowledge base. This is achieved through a simple and fast keyword-matching algorithm on the user's `prompt`. + +```python +# In main.py +def detect_domain(prompt: str) -> str: + prompt_lower = prompt.lower() + domain_map = { + "doctor": "medical", + "lawyer": "legal", + # ... more keywords + } + for keyword, domain in domain_map.items(): + if keyword in prompt_lower: + return domain + return settings.DEFAULT_DOMAIN # Falls back to 'general' +``` + +### Step 3: Knowledge Base Loading & Caching +The corresponding JSON file for the detected domain (e.g., `facts/general.json`) is loaded from disk. To optimize performance, loaded KBs are stored in an in-memory dictionary (`KB_CACHE`) to avoid repeated file I/O for subsequent requests in the same domain. + +### Step 4: Issue Detection +The core logic runs in two parallel streams to identify potential problems: + +**A. Factual Error Detection (`detect_factual_errors`)** + +This function iterates through each entry in the loaded knowledge base. For each entry, it checks if any of the `incorrect_patterns` are present in the LLM `output`. + +- **Logic**: A case-insensitive substring check (`pattern.lower() in output.lower()`). +- **Output**: A list of "issue" dictionaries, each containing the `found` pattern and the `expected` correct fact from the KB. + +**B. Linguistic Red Flag Detection (`detect_linguistic_red_flags`)** + +This function scans the output for common markers of low-quality or non-committal LLM responses, such as: +- **Absolute Claims**: "always", "never", "completely safe". +- **Vague Language**: "some say", "it is believed", "possibly". + +### Step 5: Auto-Correction (`generate_corrected_output`) +If any factual errors are found and marked for `auto_correct: true` in the KB, this function performs the correction. + +- **Logic**: It uses a **case-insensitive regular expression substitution** (`re.sub` with `re.IGNORECASE`). This is a crucial bug fix over a simple `string.replace()`, as it ensures that the replacement works regardless of the casing in the LLM's output, matching the case-insensitive nature of the detection logic. +- **Example**: It reliably replaces `"Einstein died in 1950"` with the correct statement, even if the input was `"einstein died in 1950"`. + +```python +# In main.py +def generate_corrected_output(...): + # ... + for issue in fact_issues: + # ... + corrected = re.sub( + re.escape(issue["found"]), + issue["expected"], + corrected, + count=1, + flags=re.IGNORECASE + ) + return corrected +``` + +### Step 6: Response Generation +Finally, the server returns a detailed JSON response containing the original output, the verified/corrected output, a list of all detected issues, and metadata about the verification process. + +## 4. How to Extend and Maintain + +- **Adding New Facts**: Simply edit the relevant `facts/*.json` file and restart the container. No code changes are needed. The system is designed to be managed by content experts, not just developers. +- **Adding a New Domain**: + 1. Create a new `facts/your_domain.json` file following the existing structure. + 2. (Optional) Add keywords to the `domain_map` in `main.py` to enable auto-detection for the new domain. +- **Running Tests**: The project uses `pytest` for testing. From the `haloguard-pro` directory, simply run `pytest` to execute the full test suite. diff --git a/haloguard-pro/config/settings.py b/haloguard-pro/config/settings.py new file mode 100644 index 0000000..5dcf56f --- /dev/null +++ b/haloguard-pro/config/settings.py @@ -0,0 +1,15 @@ +import os +from typing import List + +class Settings: + HOST: str = os.getenv("HOST", "0.0.0.0") + PORT: int = int(os.getenv("PORT", 8000)) + WORKERS: int = int(os.getenv("WORKERS", 4)) + LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO") + LOG_FILE: str = os.getenv("LOG_FILE", "/app/logs/haloguard.log") + DEFAULT_DOMAIN: str = os.getenv("DEFAULT_DOMAIN", "general") + AUTO_CORRECT_DOMAINS: List[str] = [ + d.strip() for d in os.getenv("AUTO_CORRECT_DOMAINS", "medical,legal,finance").split(",") if d.strip() + ] + +settings = Settings() diff --git a/haloguard-pro/docker-compose.yml b/haloguard-pro/docker-compose.yml new file mode 100644 index 0000000..46887e4 --- /dev/null +++ b/haloguard-pro/docker-compose.yml @@ -0,0 +1,19 @@ +version: '3.8' + +services: + haloguard: + build: . + ports: + - "8000:8000" + volumes: + - ./facts:/app/facts + - ./logs:/app/logs + env_file: + - .env + restart: unless-stopped + networks: + - haloguard-net + +networks: + haloguard-net: + driver: bridge diff --git a/haloguard-pro/docs/api_spec.md b/haloguard-pro/docs/api_spec.md new file mode 100644 index 0000000..3adecc3 --- /dev/null +++ b/haloguard-pro/docs/api_spec.md @@ -0,0 +1,76 @@ +# HaloGuard Pro API Specification + +## Base URL +`http://localhost:8000` + +## `/verify` — POST + +### Request Body +```json +{ + "output": "Thomas Edison invented the light bulb in 1879", + "prompt": "Who invented the light bulb?", + "prev_messages": [ + "User: I'm writing a school report.", + "Bot: Okay, let me help!" + ] +} +``` + +### Response Body +```json +{ + "original": "Thomas Edison invented the light bulb in 1879", + "verified": "Joseph Swan and Thomas Edison independently developed practical incandescent lamps in 1878–1879", + "issues": [ + { + "type": "fact_error", + "entity": "light_bulb", + "expected": "Joseph Swan and Thomas Edison independently developed practical incandescent lamps in 1878–1879", + "found": "Thomas Edison invented the light bulb in 1879", + "context": "Thomas Edison invented the light bulb in 1879", + "confidence": "high", + "auto_correct": true + } + ], + "auto_corrected": true, + "confidence": "high", + "processing_time_ms": 2, + "timestamp": "2024-06-15T12:34:56Z", + "domain_used": "general" +} +``` + +## `/health` — GET + +Returns: +```json +{ + "status": "healthy", + "version": "1.0.0", + "timestamp": "2024-06-15T12:34:56Z", + "domains_loaded": 5, + "default_domain": "general", + "auto_correct_domains": ["medical", "legal", "finance"] +} +``` + +## `/facts/{domain}` — GET + +Returns full KB for domain: +```json +{ + "einstein": { ... }, + "light_bulb": { ... } +} +``` + +## Status Codes +- `200 OK` — Success +- `422 Unprocessable Entity` — Invalid JSON +- `404 Not Found` — Domain not found +- `500 Internal Server Error` — facts.json malformed + +## Security +- No authentication required (add JWT/API key if needed) +- All data processed locally — no external calls diff --git a/haloguard-pro/facts/education.json b/haloguard-pro/facts/education.json new file mode 100644 index 0000000..3e85781 --- /dev/null +++ b/haloguard-pro/facts/education.json @@ -0,0 +1,46 @@ +{ + "earth_shape": { + "correct": "The Earth is an oblate spheroid — slightly flattened at the poles and bulging at the equator — not flat.", + "incorrect_patterns": [ + "The earth is flat", + "Scientists are lying about the earth being round", + "Flat earth is a valid theory", + "Gravity doesn't exist, the earth is flat" + ], + "auto_correct": true, + "alert_to_admin": false + }, + "dinosaur_extinction": { + "correct": "The non-avian dinosaurs went extinct approximately 66 million years ago due to a mass extinction event, likely triggered by an asteroid impact in present-day Mexico and massive volcanic activity.", + "incorrect_patterns": [ + "Dinosaurs went extinct 10,000 years ago", + "Humans and dinosaurs lived together", + "Dinosaurs were killed by climate change alone", + "Dinosaurs evolved into birds slowly over millions of years" + ], + "auto_correct": true, + "alert_to_admin": false + }, + "gravity_definition": { + "correct": "Gravity is a fundamental force of nature that attracts two bodies with mass toward each other. On Earth, it gives weight to physical objects and causes them to fall toward the ground.", + "incorrect_patterns": [ + "Gravity is just a theory", + "Gravity doesn't exist, things fall because of density", + "Gravity is caused by the Earth spinning", + "There's no gravity in space" + ], + "auto_correct": true, + "alert_to_admin": false + }, + "photosynthesis_equation": { + "correct": "The chemical equation for photosynthesis is: 6CO₂ + 6H₂O + light energy → C₆H₁₂O₆ + 6O₂", + "incorrect_patterns": [ + "Photosynthesis: CO2 + H2O → O2 + glucose", + "Plants breathe in oxygen and breathe out carbon dioxide", + "Photosynthesis happens at night", + "Trees make food from soil" + ], + "auto_correct": true, + "alert_to_admin": false + } +} diff --git a/haloguard-pro/facts/finance.json b/haloguard-pro/facts/finance.json new file mode 100644 index 0000000..3d583e1 --- /dev/null +++ b/haloguard-pro/facts/finance.json @@ -0,0 +1,46 @@ +{ + "bitcoin_5_year_return": { + "correct": "Bitcoin is highly volatile and does not reliably outperform stocks over 5-year periods. Historical returns are not indicative of future performance. Financial advisors generally do not recommend allocating core retirement savings to cryptocurrency.", + "incorrect_patterns": [ + "Bitcoin outperforms stocks over 5 years", + "Crypto has higher returns than stocks", + "Investing in Bitcoin is safer than stocks", + "Financial advisors recommend crypto for retirees" + ], + "auto_correct": true, + "alert_to_admin": true + }, + "roth_ira_contribution_limit": { + "correct": "For 2024, the maximum contribution limit for a Roth IRA is $7,000 ($8,000 if age 50 or older). Income limits apply: single filers phase out between $146,000 and $161,000.", + "incorrect_patterns": [ + "You can contribute $10,000 to a Roth IRA", + "Roth IRA limit is $5,000", + "Anyone can contribute regardless of income", + "Roth IRA has no income limits" + ], + "auto_correct": true, + "alert_to_admin": true + }, + "crypto_tax_reporting": { + "correct": "In the U.S., cryptocurrency is treated as property by the IRS. You must report capital gains/losses on Form 8949 and Schedule D. Simply holding crypto is not taxable; buying, selling, trading, or earning rewards triggers tax events.", + "incorrect_patterns": [ + "Crypto is tax-free if you don't cash out", + "You don't need to report crypto holdings", + "Only selling crypto for fiat triggers tax", + "Crypto earnings are not taxable" + ], + "auto_correct": true, + "alert_to_admin": true + }, + "stock_dividend_tax": { + "correct": "Qualified dividends are taxed at long-term capital gains rates (0%, 15%, or 20%). Non-qualified dividends are taxed as ordinary income. Holding period must be more than 60 days during the 121-day period surrounding the ex-dividend date.", + "incorrect_patterns": [ + "All dividends are taxed at 15%", + "Dividends are tax-free", + "You pay 37% tax on all dividends", + "Dividend tax doesn't depend on holding period" + ], + "auto_correct": true, + "alert_to_admin": true + } +} diff --git a/haloguard-pro/facts/general.json b/haloguard-pro/facts/general.json new file mode 100644 index 0000000..0d26c87 --- /dev/null +++ b/haloguard-pro/facts/general.json @@ -0,0 +1,47 @@ +{ + "einstein": { + "correct": "Albert Einstein died on April 18, 1955, in Princeton, New Jersey", + "incorrect_patterns": [ + "Einstein died in 1950", + "Einstein died in 1956", + "Einstein died in 1954", + "Einstein passed away in 1950", + "Einstein died at age 70" + ], + "auto_correct": true, + "alert_to_admin": false + }, + "light_bulb": { + "correct": "Joseph Swan and Thomas Edison independently developed practical incandescent lamps in 1878–1879. Swan demonstrated his lamp in England in 1878; Edison demonstrated his in Menlo Park in 1879.", + "incorrect_patterns": [ + "Thomas Edison invented the light bulb in 1879", + "Edison invented the light bulb alone", + "The light bulb was invented by Edison in 1879", + "Edison was the sole inventor of the light bulb" + ], + "auto_correct": true, + "alert_to_admin": false + }, + "moon_cheese": { + "correct": "The Moon is not made of cheese. It is composed primarily of rock and metal, with a crust, mantle, and core similar to Earth’s.", + "incorrect_patterns": [ + "The moon is made of cheese", + "Moon is cheese", + "Scientists say moon is cheese", + "People used to think the moon was cheese" + ], + "auto_correct": true, + "alert_to_admin": false + }, + "gdp_india": { + "correct": "India's nominal GDP was approximately $3.5 trillion USD in 2024, making it the 5th largest economy in the world.", + "incorrect_patterns": [ + "India's GDP was $5 trillion in 2024", + "India's GDP was $2.5 trillion in 2024", + "India's GDP was $4.8 trillion in 2024", + "India is the 3rd largest economy" + ], + "auto_correct": true, + "alert_to_admin": false + } +} diff --git a/haloguard-pro/facts/legal.json b/haloguard-pro/facts/legal.json new file mode 100644 index 0000000..cd86540 --- /dev/null +++ b/haloguard-pro/facts/legal.json @@ -0,0 +1,47 @@ +{ + "gdpr_right_to_be_forgotten": { + "correct": "Under GDPR Article 17, individuals have the right to request deletion of personal data under certain conditions. The response time is one calendar month (30 days), not 30 working days.", + "incorrect_patterns": [ + "GDPR requires companies to respond within 30 calendar days", + "GDPR says you must delete data within 30 days", + "GDPR gives you 30 days to respond to deletion requests", + "You have 30 business days under GDPR" + ], + "auto_correct": true, + "alert_to_admin": true + }, + "california_rent_withholding": { + "correct": "In California, tenants cannot simply withhold rent due to mold or repairs. Instead, they may use the 'repair and deduct' remedy under Civil Code § 1941.1 if the issue affects health/safety and the landlord fails to act after written notice. Withholding rent without court approval risks eviction.", + "incorrect_patterns": [ + "You can withhold rent under CA Civil Code § 1942", + "You can legally withhold rent", + "Just send a certified letter first", + "Many tenants successfully withhold rent", + "Landlords can't evict if you withhold rent" + ], + "auto_correct": true, + "alert_to_admin": true + }, + "us_marijuana_federal": { + "correct": "Marijuana remains illegal under U.S. federal law (Controlled Substances Act). However, many states have legalized it for medical or recreational use. Federal law still supersedes state law, creating legal conflict.", + "incorrect_patterns": [ + "Marijuana is legal in the United States", + "Federal law allows marijuana", + "You can't get arrested for marijuana anywhere in the US", + "Marijuana is fully legal nationwide" + ], + "auto_correct": true, + "alert_to_admin": true + }, + "breach_notification_time": { + "correct": "Under HIPAA, covered entities must notify affected individuals of a breach without unreasonable delay, and no later than 60 days after discovery.", + "incorrect_patterns": [ + "You have 30 days to report a breach under HIPAA", + "HIPAA requires notification within 7 days", + "Breach notifications must be sent within 1 week", + "HIPAA allows 90 days to report" + ], + "auto_correct": true, + "alert_to_admin": true + } +} diff --git a/haloguard-pro/facts/medical.json b/haloguard-pro/facts/medical.json new file mode 100644 index 0000000..ae34269 --- /dev/null +++ b/haloguard-pro/facts/medical.json @@ -0,0 +1,48 @@ +{ + "melatonin": { + "correct": "Melatonin is not FDA-approved for use in children. While sometimes used off-label for sleep disorders, long-term safety data is lacking. Always consult a pediatrician before giving melatonin to a child.", + "incorrect_patterns": [ + "Melatonin is completely safe for children", + "Melatonin has no side effects", + "Children can safely take melatonin every night", + "Melatonin is approved by the FDA for kids", + "It's just a natural supplement so it's fine", + "Pediatricians recommend melatonin daily" + ], + "auto_correct": true, + "alert_to_admin": true + }, + "aspirin_children": { + "correct": "Aspirin should never be given to children or teenagers due to the risk of Reye's syndrome, a rare but serious condition that causes swelling in the liver and brain.", + "incorrect_patterns": [ + "Aspirin is safe for children", + "Children can take aspirin for fever", + "Reye's syndrome is very rare so it's okay", + "Pediatricians prescribe aspirin for flu" + ], + "auto_correct": true, + "alert_to_admin": true + }, + "ibuprofen_pregnancy": { + "correct": "Ibuprofen is not recommended during pregnancy, especially after 20 weeks, as it may cause fetal kidney problems and low amniotic fluid. Acetaminophen is preferred for pain relief.", + "incorrect_patterns": [ + "Ibuprofen is safe during pregnancy", + "You can take ibuprofen anytime while pregnant", + "NSAIDs are fine in early pregnancy", + "Doctors say ibuprofen is okay" + ], + "auto_correct": true, + "alert_to_admin": true + }, + "vitamin_d_overdose": { + "correct": "Vitamin D toxicity is rare but possible with excessive supplementation (>4,000 IU/day long-term). Symptoms include nausea, vomiting, weakness, and kidney damage. Never exceed recommended doses without medical supervision.", + "incorrect_patterns": [ + "Vitamin D is completely safe at any dose", + "More vitamin D is always better", + "You can't overdose on vitamin D", + "Taking 10,000 IU daily is harmless" + ], + "auto_correct": true, + "alert_to_admin": true + } +} diff --git a/haloguard-pro/main.py b/haloguard-pro/main.py new file mode 100644 index 0000000..66fd266 --- /dev/null +++ b/haloguard-pro/main.py @@ -0,0 +1,265 @@ +import json +import re +import logging +import os +import time +from pathlib import Path +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel +from typing import List, Dict, Any, Optional +from datetime import datetime, timezone +from config.settings import settings + +# Setup logging +Path(settings.LOG_FILE).parent.mkdir(parents=True, exist_ok=True) +logging.basicConfig( + level=settings.LOG_LEVEL.upper(), + format='%(asctime)s | %(levelname)-8s | %(message)s', + handlers=[ + logging.FileHandler(settings.LOG_FILE), + logging.StreamHandler() + ] +) +logger = logging.getLogger("haloguard") + +# Load domain knowledge base +def load_knowledge_base(domain: str) -> dict: + path = f"facts/{domain}.json" + try: + with open(path, 'r', encoding='utf-8') as f: + return json.load(f) + except FileNotFoundError: + logger.error(f"❌ Knowledge file not found: {path}") + return {} + except json.JSONDecodeError as e: + logger.error(f"❌ Invalid JSON in {path}: {e}") + return {} + +# Global KB cache +KB_CACHE = {} + +class VerifyRequest(BaseModel): + output: str + prompt: str = "" + prev_messages: List[str] = [] + +class VerifyResponse(BaseModel): + original: str + verified: str + issues: List[Dict[str, Any]] + auto_corrected: bool + confidence: str + processing_time_ms: int + timestamp: str + domain_used: str + +app = FastAPI( + title="HaloGuard Pro", + description="Production-grade hallucination elimination system. Rule-based. Zero AI. Zero GPU.", + version="1.0.0" +) + +def detect_factual_errors(output: str, kb: dict) -> List[Dict[str, Any]]: + issues = [] + for key, facts in kb.items(): + for pattern in facts["incorrect_patterns"]: + if pattern.lower() in output.lower(): + issue = { + "type": "fact_error", + "entity": key, + "expected": facts["correct"], + "found": pattern, + "context": output, + "confidence": "high", + "auto_correct": facts.get("auto_correct", False) + } + issues.append(issue) + return issues + +def detect_linguistic_red_flags(output: str) -> List[Dict[str, Any]]: + issues = [] + + # ABSOLUTE CLAIMS + absolute_terms = [ + "always", "never", "completely", "no side effects", "100%", "guaranteed", + "definitely", "certainly", "without fail", "zero risk", "perfectly safe", + "every time", "all cases", "everyone knows" + ] + + for term in absolute_terms: + if term.lower() in output.lower(): + issues.append({ + "type": "absolute_claim", + "term": term, + "context": output, + "severity": "high", + "message": "Absolute claims often indicate overconfidence or lack of nuance." + }) + + # VAGUE LANGUAGE + vague_terms = [ + "some say", "many believe", "it's rumored", "possibly", "maybe", "I think", + "experts say", "believed to be", "unconfirmed", "allegedly", "supposedly", + "could be", "might be", "appears to be", "seems like", "they say", "often", + "usually", "generally", "in some cases" + ] + + vague_count = sum(1 for term in vague_terms if term.lower() in output.lower()) + if vague_count >= 2: + issues.append({ + "type": "vague_language", + "count": vague_count, + "context": output, + "severity": "medium", + "message": "Multiple vague phrases suggest speculative or unreliable information." + }) + + # CONTRADICTIONS + hedging_words = ["may", "might", "could", "possibly", "perhaps"] + certainty_words = ["definitely", "certainly", "absolutely", "clearly", "undeniably"] + + if any(w in output.lower() for w in hedging_words) and any(w in output.lower() for w in certainty_words): + issues.append({ + "type": "contradiction", + "context": output, + "message": "Contradictory certainty levels detected. Likely attempting to sound authoritative while hedging." + }) + + return issues + +def generate_corrected_output(original: str, issues: List[Dict[str, Any]]) -> str: + corrected = original + # Sort by length descending to avoid partial replacements + fact_issues = [i for i in issues if i["type"] == "fact_error"] + fact_issues.sort(key=lambda x: len(x["found"]), reverse=True) + + for issue in fact_issues: + if issue.get("auto_correct", True): # Default to True + # Use regex for case-insensitive replacement to fix bug where detection is case-insensitive + # but replacement was case-sensitive. + corrected = re.sub(re.escape(issue["found"]), issue["expected"], corrected, count=1, flags=re.IGNORECASE) + + return corrected + +def detect_domain(prompt: str) -> str: + """Detect domain based on keywords in user prompt.""" + prompt_lower = prompt.lower() + domain_map = { + "doctor": "medical", + "lawyer": "legal", + "invest": "finance", + "school": "education", + "health": "medical", + "money": "finance", + "gdpr": "legal", + "medication": "medical", + "prescription": "medical", + "tax": "finance", + "retirement": "finance", + "student": "education", + "teacher": "education", + "court": "legal", + "contract": "legal", + "crime": "legal", + "child": "medical", + "pregnant": "medical", + "aspirin": "medical", + "melatonin": "medical", + "bitcoin": "finance", + "crypto": "finance", + "stock": "finance", + "dividend": "finance", + "irsa": "finance", + "roth": "finance", + "hipaa": "legal", + "reye": "medical" + } + + for keyword, domain in domain_map.items(): + if keyword in prompt_lower: + return domain + + return settings.DEFAULT_DOMAIN + +@app.post("/verify", response_model=VerifyResponse) +async def verify_llm_output(request: VerifyRequest): + start_time = time.time() + + # Detect domain + domain = detect_domain(request.prompt) + logger.info(f"🔍 Detected domain: {domain}") + + # Load KB if not cached + if domain not in KB_CACHE: + KB_CACHE[domain] = load_knowledge_base(domain) + + kb = KB_CACHE[domain] + + # Detect issues + fact_issues = detect_factual_errors(request.output, kb) + signal_issues = detect_linguistic_red_flags(request.output) + all_issues = fact_issues + signal_issues + + # Generate corrected output + verified_output = generate_corrected_output(request.output, all_issues) + + # Determine confidence + confidence = "high" if len(fact_issues) > 0 else "medium" if len(signal_issues) > 0 else "none" + + # Track auto-correction + auto_corrected = False + for issue in fact_issues: + if issue.get("auto_correct", True): + auto_corrected = True + break + + processing_time_ms = int((time.time() - start_time) * 1000) + + # Log audit trail + log_data = { + "input": request.output[:200], + "prompt": request.prompt[:100], + "domain": domain, + "issues": len(all_issues), + "auto_corrected": auto_corrected, + "processing_time_ms": processing_time_ms + } + logger.info(f"VERIFY | {json.dumps(log_data)}") + + return VerifyResponse( + original=request.output, + verified=verified_output, + issues=all_issues, + auto_corrected=auto_corrected, + confidence=confidence, + processing_time_ms=processing_time_ms, + timestamp=datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), + domain_used=domain + ) + +@app.get("/health") +async def health_check(): + return { + "status": "healthy", + "version": "1.0.0", + "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), + "domains_loaded": len(KB_CACHE), + "default_domain": settings.DEFAULT_DOMAIN, + "auto_correct_domains": settings.AUTO_CORRECT_DOMAINS + } + +@app.get("/facts/{domain}") +async def get_domain_facts(domain: str): + base_dir = os.path.abspath("facts") + requested_path = os.path.normpath(os.path.join(base_dir, f"{domain}.json")) + if not requested_path.startswith(base_dir + os.sep): + raise HTTPException(status_code=400, detail="Invalid domain name") + if not os.path.exists(requested_path): + raise HTTPException(status_code=404, detail="Domain not found") + with open(requested_path, "r") as f: + return json.load(f) + +if __name__ == "__main__": + import uvicorn + logger.info("🚀 Starting HaloGuard Pro...") + uvicorn.run("main:app", host=settings.HOST, port=settings.PORT, workers=settings.WORKERS, reload=False) diff --git a/haloguard-pro/requirements.txt b/haloguard-pro/requirements.txt new file mode 100644 index 0000000..fe56984 --- /dev/null +++ b/haloguard-pro/requirements.txt @@ -0,0 +1,4 @@ +fastapi==0.115.0 +uvicorn==0.32.0 +pydantic==2.9.2 +httpx==0.28.1 diff --git a/haloguard-pro/scripts/health_check.sh b/haloguard-pro/scripts/health_check.sh new file mode 100755 index 0000000..85b0b31 --- /dev/null +++ b/haloguard-pro/scripts/health_check.sh @@ -0,0 +1,11 @@ +#!/bin/bash +URL="http://localhost:8000/health" + +response=$(curl -s -o /dev/null -w "%{http_code}" "$URL") + +if [ "$response" = "200" ]; then + echo "✅ HaloGuard is healthy" +else + echo "❌ HaloGuard is down (HTTP $response)" + exit 1 +fi diff --git a/haloguard-pro/scripts/update_facts.sh b/haloguard-pro/scripts/update_facts.sh new file mode 100755 index 0000000..4a2482d --- /dev/null +++ b/haloguard-pro/scripts/update_facts.sh @@ -0,0 +1,20 @@ +#!/bin/bash +echo "🔄 Updating knowledge bases..." + +for file in facts/*.json; do + if [[ -f "$file" ]]; then + echo "✅ Validating $file..." + python3 -c " +import json +try: + with open('$file', 'r') as f: + json.load(f) + print('✓ OK') +except Exception as e: + print('✗ ERROR:', e) + exit(1) +" + fi +done + +echo "🎉 All facts validated!" diff --git a/haloguard-pro/tests/test_facts.py b/haloguard-pro/tests/test_facts.py new file mode 100644 index 0000000..fbd037c --- /dev/null +++ b/haloguard-pro/tests/test_facts.py @@ -0,0 +1,30 @@ +import json +import os +import sys + +# Add project root to path to allow importing main +sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/..") + +FACTS_DIR = "facts" + +def test_all_fact_files_are_valid_json(): + """ + Scans the facts/ directory and asserts that every .json file + can be successfully loaded as JSON. + """ + fact_files = [f for f in os.listdir(FACTS_DIR) if f.endswith(".json")] + assert len(fact_files) > 0, "No fact files found in facts/ directory" + + for filename in fact_files: + filepath = os.path.join(FACTS_DIR, filename) + try: + with open(filepath, 'r', encoding='utf-8') as f: + json.load(f) + except json.JSONDecodeError as e: + assert False, f"Invalid JSON in {filepath}: {e}" + except Exception as e: + assert False, f"Error reading {filepath}: {e}" + +if __name__ == "__main__": + test_all_fact_files_are_valid_json() + print("✅ All fact files are valid JSON.") diff --git a/haloguard-pro/tests/test_haloguard.py b/haloguard-pro/tests/test_haloguard.py new file mode 100644 index 0000000..8e1897a --- /dev/null +++ b/haloguard-pro/tests/test_haloguard.py @@ -0,0 +1,67 @@ +import sys +import os +sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/..") + +from fastapi.testclient import TestClient +from main import app, load_knowledge_base, detect_factual_errors, detect_linguistic_red_flags, generate_corrected_output + +client = TestClient(app) + +def test_verify_endpoint_einstein(): + response = client.post("/verify", json={"output": "Einstein died in 1950", "prompt": "When did Einstein die?"}) + assert response.status_code == 200 + data = response.json() + assert data["original"] == "Einstein died in 1950" + assert "1955" in data["verified"] + assert data["auto_corrected"] is True + assert len(data["issues"]) >= 1 + assert data["issues"][0]["type"] == "fact_error" + +def test_verify_endpoint_no_issue(): + response = client.post("/verify", json={"output": "The sky is blue.", "prompt": "What color is the sky?"}) + assert response.status_code == 200 + data = response.json() + assert data["original"] == "The sky is blue." + assert data["verified"] == "The sky is blue." + assert data["auto_corrected"] is False + assert len(data["issues"]) == 0 + +def test_einstein_death(): + kb = load_knowledge_base("general") + output = "Einstein died in 1950" + issues = detect_factual_errors(output, kb) + assert len(issues) == 1 + assert issues[0]["type"] == "fact_error" + assert "1955" in issues[0]["expected"] + +def test_melatonin_safe(): + kb = load_knowledge_base("medical") + output = "Melatonin is completely safe for children" + issues = detect_factual_errors(output, kb) + assert len(issues) == 1 + assert issues[0]["entity"] == "melatonin" + assert issues[0]["auto_correct"] is True + +def test_absolute_claim(): + output = "Melatonin has no side effects" + issues = detect_linguistic_red_flags(output) + assert len(issues) == 1 + assert issues[0]["type"] == "absolute_claim" + assert issues[0]["term"] == "no side effects" + +def test_vague_language(): + output = "Some say melatonin is safe. Many believe it helps sleep." + issues = detect_linguistic_red_flags(output) + assert len(issues) == 1 + assert issues[0]["type"] == "vague_language" + assert issues[0]["count"] == 2 + +def test_correction_generation(): + kb = load_knowledge_base("general") + output = "Einstein died in 1950 and the light bulb was invented by Edison in 1879" + issues = detect_factual_errors(output, kb) + corrected = generate_corrected_output(output, issues) + assert "1955" in corrected + assert "independently developed" in corrected + assert "1950" not in corrected + assert "invented by Edison" not in corrected diff --git a/logs/haloguard.log b/logs/haloguard.log new file mode 100644 index 0000000..e69de29