from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel import requests import os import logging # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) app = FastAPI() # Enable CORS app.add_middleware( CORSMiddleware, allow_origins=["*"], # Allow all origins, or replace with your frontend URL allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Read OpenRouter API key from environment variable (HF Secret) OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY") if not OPENROUTER_API_KEY: raise RuntimeError("OPENROUTER_API_KEY environment variable not set!") # Request schema class ExplainRequest(BaseModel): message: str label: str = None # Optional, for explanations model_id: str = "arcee-ai/trinity-large-preview:free" # Default model, can be overridden @app.get("/") def root(): return { "status": "healthy", "service": "Anti-Phishing Explainer", "endpoints": { "/explain": "POST - Generate explanation for classification", "/classify": "POST - Classify text as Phishing or Safe", "/health": "GET - Health check" } } @app.get("/health") def health(): return { "status": "healthy", "service": "explainer", "openrouter_configured": bool(OPENROUTER_API_KEY) } @app.post("/explain") def explain(req: ExplainRequest): """Generate a human-readable explanation for why a message was classified as Phishing or Safe""" user_message = req.message.strip() label = req.label.strip() if req.label else None if not user_message or not label: raise HTTPException(status_code=400, detail="Missing message or label") # Updated system prompt with bullet-point format and language adaptation system_prompt = ( f"You are a robot that identifies phishing and safe messages. " f"The message was classified as '{label}'. " "Explain why this decision was made and point out any words or patterns that led to it. " "No greetings, introductions, or closing remarks. " "Don't restate the message or its classification. " "Output only the explanation as bullet points. " "Limit each bullet to 1–2 sentences. " "Limit the number of bullets to 3-4. " f"Message:\n\n{user_message}\n\n" "Respond using the same language as the message." ) url = "https://openrouter.ai/api/v1/chat/completions" headers = { "Authorization": f"Bearer {OPENROUTER_API_KEY}", "Content-Type": "application/json" } payload = { "model": "arcee-ai/trinity-large-preview:free", "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_message} ] } try: logger.info(f"Calling OpenRouter /explain with model: arcee-ai/trinity-large-preview:free") response = requests.post(url, headers=headers, json=payload, timeout=20) response.raise_for_status() result = response.json() # Check for OpenRouter error in response body (even with 200 status) if "error" in result: error_detail = result.get("error", {}).get("message", str(result.get("error"))) logger.error(f"OpenRouter returned error in /explain: {error_detail}") logger.error(f"Full response: {result}") raise HTTPException(status_code=500, detail=f"OpenRouter error: {error_detail}") reply = result.get("choices", [{}])[0].get("message", {}).get("content", "").strip() if not reply: logger.error(f"OpenRouter returned empty response in /explain. Full result: {result}") reply = "[No explanation returned]" logger.info("Explanation generated successfully") return {"reply": reply} except requests.RequestException as e: logger.error(f"OpenRouter network error in /explain: {e}") raise HTTPException(status_code=500, detail=f"Error contacting OpenRouter: {e}") except Exception as e: logger.error(f"Unexpected error in /explain: {e}") raise HTTPException(status_code=500, detail=f"Unexpected error: {e}") @app.post("/classify") def classify(req: ExplainRequest): """Classify text as Phishing or Safe via OpenRouter""" user_message = req.message.strip() if not user_message: raise HTTPException(status_code=400, detail="Missing message") # Use provided model_id or default model_id = req.model_id or "arcee-ai/trinity-large-preview:free" system_prompt = ( 'You are a phishing detector. Classify the text as "Phishing" or "Safe". ' 'Respond ONLY with valid JSON: {"label": "Phishing"|"Safe", "confidence": <0-100 float>}. ' 'No other text.' ) url = "https://openrouter.ai/api/v1/chat/completions" headers = { "Authorization": f"Bearer {OPENROUTER_API_KEY}", "Content-Type": "application/json" } payload = { "model": model_id, # Use the passed model_id "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_message} ], "temperature": 0.0, "max_tokens": 2000 # Increased for models with extended reasoning/thinking } try: logger.info(f"Calling OpenRouter /classify with model: {model_id}") response = requests.post(url, headers=headers, json=payload, timeout=20) response.raise_for_status() result = response.json() # Check for OpenRouter error in response body (even with 200 status) if "error" in result: error_detail = result.get("error", {}).get("message", str(result.get("error"))) logger.error(f"OpenRouter returned error in /classify: {error_detail}") logger.error(f"Full response: {result}") raise HTTPException(status_code=500, detail=f"OpenRouter error: {error_detail}") reply = result.get("choices", [{}])[0].get("message", {}).get("content", "").strip() if not reply: logger.warning(f"Empty content in response. Finish reason: {result.get('choices', [{}])[0].get('finish_reason')}") # If content is empty, try to extract from reasoning or log for debugging reasoning = result.get("choices", [{}])[0].get("message", {}).get("reasoning", "") if reasoning: logger.warning(f"Model has reasoning but no content. This may indicate truncation.") logger.error(f"OpenRouter returned empty response in /classify. Full result: {result}") raise HTTPException(status_code=500, detail="No response from OpenRouter") logger.info(f"Classification successful with model {model_id}: {reply}") return {"reply": reply, "status": response.status_code, "model": model_id} except requests.RequestException as e: logger.error(f"OpenRouter network error in /classify: {e}") raise HTTPException(status_code=500, detail=f"Error contacting OpenRouter: {e}") except Exception as e: logger.error(f"Unexpected error in /classify: {e}") raise HTTPException(status_code=500, detail=f"Unexpected error: {e}") # Optional: Run with uvicorn if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=7860)