anshmittal/fact-checking-api
0
1from fastapi import FastAPI, HTTPException2from pydantic import BaseModel3import os4from dotenv import load_dotenv5import logging6import openai7from typing import Dict, Any8 9# Load .env locally if present (HF Spaces will use Secrets - no .env required there)10load_dotenv()11 12# Basic logging13logging.basicConfig(level=logging.INFO)14logger = logging.getLogger(__name__)15 16# Read API key from env17OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")18if not OPENROUTER_API_KEY:19 raise RuntimeError(20 "OPENROUTER_API_KEY environment variable is required. "21 "On Hugging Face Spaces, add it under Settings -> Secrets."22 )23 24# Configure OpenAI client to use OpenRouter25client = openai.OpenAI(26 base_url="https://openrouter.ai/api/v1",27 api_key=OPENROUTER_API_KEY,28 default_headers={29 "HTTP-Referer": "https://your-domain.com", # Optional: replace with your domain30 "X-Title": "Fact Checker API", # Optional: for OpenRouter leaderboards31 }32)33 34app = FastAPI(title="Fact Checker API (OpenRouter + Web Search)",35 description="Fact-check text using Gemini via OpenRouter with web search")36 37class TextInput(BaseModel):38 text: str39 40class FactCheckResponse(BaseModel):41 is_factual: str # "correct", "incorrect", or "unsure"42 summary: str43 explanation: str44 45@app.post("/fact-check", response_model=FactCheckResponse)46def fact_check_text(input_data: TextInput) -> FactCheckResponse:47 """48 Fact-check the provided text using Gemini via OpenRouter with web search.49 Uses the :online suffix method for web search.50 """51 # Build human-friendly prompt that instructs the model to use web search52 prompt = f"""53Please fact-check the following text and provide your analysis in the exact format below. You have access to current web search results to verify the claims.54 55Text to analyze: "{input_data.text}"56 57Instructions:581. Use the web search results to verify the claims in the text592. Determine if the text is factually CORRECT, INCORRECT, or if there's INSUFFICIENT DATA603. Provide a short SUMMARY line614. Provide a clear EXPLANATION with evidence and sources when available62 63Respond exactly like:64STATUS: [CORRECT/INCORRECT/UNSURE]65SUMMARY: [short summary]66EXPLANATION: [detailed explanation with sources]67"""68 69 try:70 # Use Gemini model through OpenRouter with web search enabled71 # The ":online" suffix enables web search functionality72 model_name = "google/gemini-2.5-flash:online"73 74 response = client.chat.completions.create(75 model=model_name,76 messages=[77 {78 "role": "user", 79 "content": prompt80 }81 ],82 max_tokens=1000,83 temperature=0.3,84 )85 86 model_text = response.choices[0].message.content87 if not model_text:88 raise HTTPException(status_code=500, detail="No text returned from model.")89 90 model_text = model_text.strip()91 logger.info(f"Model response: {model_text[:200]}...") # Log first 200 chars for debugging92 93 # Parse the model's exact-format response94 status = "unsure"95 summary = ""96 explanation = ""97 98 for line in model_text.splitlines():99 line = line.strip()100 if line.upper().startswith("STATUS:"):101 s = line[len("STATUS:"):].strip().lower()102 if "correct" in s and "incorrect" not in s:103 status = "correct"104 elif "incorrect" in s:105 status = "incorrect"106 else:107 status = "unsure"108 elif line.upper().startswith("SUMMARY:"):109 summary = line[len("SUMMARY:"):].strip()110 elif line.upper().startswith("EXPLANATION:"):111 explanation = line[len("EXPLANATION:"):].strip()112 # If multi-line explanation, keep appending following lines (simple heuristic)113 elif explanation and line:114 explanation += "\n" + line115 116 # Fallback if parsing failed117 if not summary and not explanation:118 summary = "Analysis completed (raw model output)"119 explanation = model_text120 status = "unsure"121 122 # Additional safety defaults123 if status == "incorrect" and not explanation:124 explanation = "The model marked the text incorrect but did not provide a reason."125 elif status == "unsure" and not explanation:126 explanation = "There is insufficient reliable data available to make a definitive determination."127 elif status == "correct" and not explanation:128 explanation = "The provided information appears consistent with available data."129 130 return FactCheckResponse(is_factual=status, summary=summary, explanation=explanation)131 132 except Exception as e:133 logger.exception("Fact-check generation failed")134 raise HTTPException(status_code=500, detail=f"Error processing fact-check request: {e}")135 136@app.post("/fact-check-plugin", response_model=FactCheckResponse)137def fact_check_text_plugin(input_data: TextInput) -> FactCheckResponse:138 """139 Alternative endpoint using explicit web plugin configuration.140 This method uses the web plugin directly with customizable parameters.141 """142 prompt = f"""143Please fact-check the following text and provide your analysis in the exact format below. You have access to current web search results to verify the claims.144 145Text to analyze: "{input_data.text}"146 147Instructions:1481. Use the web search results to verify the claims in the text1492. Determine if the text is factually CORRECT, INCORRECT, or if there's INSUFFICIENT DATA1503. Provide a short SUMMARY line1514. Provide a clear EXPLANATION with evidence and sources when available152 153Respond exactly like:154STATUS: [CORRECT/INCORRECT/UNSURE]155SUMMARY: [short summary]156EXPLANATION: [detailed explanation with sources]157"""158 159 try:160 # Use regular Gemini model with explicit web plugin configuration161 response = client.chat.completions.create(162 model="google/gemini-2.5-flash",163 messages=[164 {165 "role": "user", 166 "content": prompt167 }168 ],169 max_tokens=1000,170 temperature=0.3,171 # Correct web plugin configuration according to OpenRouter docs172 plugins=[173 {174 "name": "web",175 "arguments": {176 "max_results": 5,177 "search_prompt": f"Find reliable, current information to fact-check this claim: {input_data.text}"178 }179 }180 ]181 )182 183 # Rest of the processing is identical184 model_text = response.choices[0].message.content185 if not model_text:186 raise HTTPException(status_code=500, detail="No text returned from model.")187 188 model_text = model_text.strip()189 logger.info(f"Model response: {model_text[:200]}...") # Log first 200 chars for debugging190 191 # Parse the model's exact-format response (same parsing logic)192 status = "unsure"193 summary = ""194 explanation = ""195 196 for line in model_text.splitlines():197 line = line.strip()198 if line.upper().startswith("STATUS:"):199 s = line[len("STATUS:"):].strip().lower()200 if "correct" in s and "incorrect" not in s:201 status = "correct"202 elif "incorrect" in s:203 status = "incorrect"204 else:205 status = "unsure"206 elif line.upper().startswith("SUMMARY:"):207 summary = line[len("SUMMARY:"):].strip()208 elif line.upper().startswith("EXPLANATION:"):209 explanation = line[len("EXPLANATION:"):].strip()210 elif explanation and line:211 explanation += "\n" + line212 213 if not summary and not explanation:214 summary = "Analysis completed (raw model output)"215 explanation = model_text216 status = "unsure"217 218 if status == "incorrect" and not explanation:219 explanation = "The model marked the text incorrect but did not provide a reason."220 elif status == "unsure" and not explanation:221 explanation = "There is insufficient reliable data available to make a definitive determination."222 elif status == "correct" and not explanation:223 explanation = "The provided information appears consistent with available data."224 225 return FactCheckResponse(is_factual=status, summary=summary, explanation=explanation)226 227 except Exception as e:228 logger.exception("Fact-check generation failed")229 raise HTTPException(status_code=500, detail=f"Error processing fact-check request: {e}")230 231@app.post("/fact-check-with-context", response_model=FactCheckResponse)232def fact_check_with_search_context(input_data: TextInput) -> FactCheckResponse:233 """234 Enhanced endpoint that first performs web search, then uses those results for fact-checking.235 This approach ensures the model receives search results and can reference them directly.236 """237 # Step 1: Get web search results first238 search_prompt = f"Find reliable, current information to fact-check this claim: {input_data.text}"239 240 try:241 # First call to get search results242 search_response = client.chat.completions.create(243 model="google/gemini-2.5-flash:online",244 messages=[245 {246 "role": "user", 247 "content": search_prompt248 }249 ],250 max_tokens=500,251 temperature=0.1,252 )253 254 search_results = search_response.choices[0].message.content255 if not search_results:256 search_results = "No search results available."257 258 # Step 2: Use search results for fact-checking259 fact_check_prompt = f"""260Based on the web search results below, please fact-check the following text and provide your analysis in the exact format specified.261 262Text to fact-check: "{input_data.text}"263 264Web search results:265{search_results}266 267Instructions:2681. Use the web search results above to verify the claims in the text2692. Determine if the text is factually CORRECT, INCORRECT, or if there's INSUFFICIENT DATA2703. Provide a short SUMMARY line2714. Provide a clear EXPLANATION with evidence and sources when available272 273Respond exactly like:274STATUS: [CORRECT/INCORRECT/UNSURE]275SUMMARY: [short summary]276EXPLANATION: [detailed explanation with sources from the search results]277"""278 279 # Second call for fact-checking with search context280 fact_check_response = client.chat.completions.create(281 model="google/gemini-2.5-flash",282 messages=[283 {284 "role": "user", 285 "content": fact_check_prompt286 }287 ],288 max_tokens=1000,289 temperature=0.3,290 )291 292 model_text = fact_check_response.choices[0].message.content293 if not model_text:294 raise HTTPException(status_code=500, detail="No text returned from model.")295 296 model_text = model_text.strip()297 logger.info(f"Fact-check response: {model_text[:200]}...")298 299 # Parse the response (same parsing logic)300 status = "unsure"301 summary = ""302 explanation = ""303 304 for line in model_text.splitlines():305 line = line.strip()306 if line.upper().startswith("STATUS:"):307 s = line[len("STATUS:"):].strip().lower()308 if "correct" in s and "incorrect" not in s:309 status = "correct"310 elif "incorrect" in s:311 status = "incorrect"312 else:313 status = "unsure"314 elif line.upper().startswith("SUMMARY:"):315 summary = line[len("SUMMARY:"):].strip()316 elif line.upper().startswith("EXPLANATION:"):317 explanation = line[len("EXPLANATION:"):].strip()318 elif explanation and line:319 explanation += "\n" + line320 321 if not summary and not explanation:322 summary = "Analysis completed with search context"323 explanation = model_text324 status = "unsure"325 326 if status == "incorrect" and not explanation:327 explanation = "The model marked the text incorrect but did not provide a reason."328 elif status == "unsure" and not explanation:329 explanation = "There is insufficient reliable data available to make a definitive determination."330 elif status == "correct" and not explanation:331 explanation = "The provided information appears consistent with available data."332 333 return FactCheckResponse(is_factual=status, summary=summary, explanation=explanation)334 335 except Exception as e:336 logger.exception("Fact-check with context generation failed")337 raise HTTPException(status_code=500, detail=f"Error processing fact-check request: {e}")338 339@app.get("/")340def root():341 return {342 "message": "Fact Checker API using OpenRouter (Gemini) + Web Search",343 "endpoints": {344 "/fact-check": "POST - Main fact-checking endpoint with web search (:online suffix)",345 "/fact-check-plugin": "POST - Alternative endpoint with explicit web plugin config",346 "/fact-check-with-context": "POST - Enhanced endpoint with explicit search context"347 },348 "method": "POST",349 "notes": "Set OPENROUTER_API_KEY in environment (Spaces Secrets). Web search enabled via OpenRouter.",350 "pricing": "Web search costs $4 per 1000 results (default max 5 results = ~$0.02 per request)"351 }352 353@app.get("/health")354def health_check():355 return {"status": "healthy"}