| from fastapi import FastAPI, HTTPException |
| from fastapi.middleware.cors import CORSMiddleware |
| from pydantic import BaseModel |
| import requests |
| import os |
| import logging |
|
|
| |
| logging.basicConfig(level=logging.INFO) |
| logger = logging.getLogger(__name__) |
|
|
| app = FastAPI() |
|
|
| |
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=["*"], |
| allow_credentials=True, |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
|
|
| |
| OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY") |
| if not OPENROUTER_API_KEY: |
| raise RuntimeError("OPENROUTER_API_KEY environment variable not set!") |
|
|
| |
| class ExplainRequest(BaseModel): |
| message: str |
| label: str = None |
| model_id: str = "openai/gpt-oss-120b:free" |
|
|
| @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") |
|
|
| |
| 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": req.model_id, |
| "messages": [ |
| {"role": "system", "content": system_prompt}, |
| {"role": "user", "content": user_message} |
| ] |
| } |
|
|
| try: |
| logger.info(f"Calling OpenRouter /explain with model: {req.model_id}") |
| response = requests.post(url, headers=headers, json=payload, timeout=20) |
| response.raise_for_status() |
| result = response.json() |
| |
| |
| 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") |
|
|
| |
| 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, |
| "messages": [ |
| {"role": "system", "content": system_prompt}, |
| {"role": "user", "content": user_message} |
| ], |
| "temperature": 0.0, |
| "max_tokens": 2000 |
| } |
|
|
| 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() |
| |
| |
| 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')}") |
| |
| 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}") |
|
|
| |
| if __name__ == "__main__": |
| import uvicorn |
| uvicorn.run(app, host="0.0.0.0", port=7860) |