"""Flask API server for Lead Generation & Tech Scoping Engine.""" from __future__ import annotations import json import logging import os import sys from pathlib import Path import httpx from flask import Flask, request, jsonify from models import CompanyTechProfile, LeadGenRequest, LeadGenResponse from engine import LeadScopingEngine # --------------------------------------------------------------------------- # Logging # --------------------------------------------------------------------------- logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", stream=sys.stdout, ) logger = logging.getLogger("leadgen") # --------------------------------------------------------------------------- # Config file # --------------------------------------------------------------------------- CONFIG_PATH = Path(os.getenv("LEADGEN_CONFIG", "/app/data/leadgen_config.json")) CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True) DEFAULT_CONFIG = { "api_key": os.getenv("DEEPSEEK_API_KEY", ""), "base_url": "https://openrouter.ai/api/v1", "model": "qwen/qwen3-coder:free", "fallback_models": [ "meta-llama/llama-3.3-70b-instruct:free", "google/gemma-4-26b-a4b-it:free", "nvidia/nemotron-3-super-120b-a12b:free", ], } def load_config() -> dict: if CONFIG_PATH.exists(): try: return json.loads(CONFIG_PATH.read_text()) except Exception: pass cfg = dict(DEFAULT_CONFIG) save_config(cfg) return cfg def save_config(cfg: dict) -> None: CONFIG_PATH.write_text(json.dumps(cfg, indent=2)) config = load_config() # --------------------------------------------------------------------------- # App # --------------------------------------------------------------------------- app = Flask(__name__) def get_engine() -> LeadScopingEngine: cfg = load_config() return LeadScopingEngine( api_key=cfg.get("api_key"), api_base=cfg.get("base_url"), ) @app.route("/python-leadgen/", methods=["GET"]) def index(): return jsonify({ "service": "Mek-Tech Lead Generation & Tech Scoping Engine", "version": "2.0", "endpoints": { "GET /python-leadgen/config": "Get current config (model, base_url, key masked)", "POST /python-leadgen/config": "Update config", "GET /python-leadgen/models": "List available free models from configured API", "POST /python-leadgen/analyze": "Analyze a single URL", "POST /python-leadgen/batch": "Analyze multiple URLs", "GET /python-leadgen/health": "Health check", }, }) @app.route("/python-leadgen/health", methods=["GET"]) def health(): cfg = load_config() return jsonify({ "status": "healthy", "api_configured": bool(cfg.get("api_key")), "model": cfg.get("model"), "base_url": cfg.get("base_url"), }) @app.route("/python-leadgen/config", methods=["GET"]) def get_config(): cfg = load_config() key = cfg.get("api_key", "") return jsonify({ "base_url": cfg.get("base_url"), "model": cfg.get("model"), "fallback_models": cfg.get("fallback_models", []), "api_key_set": bool(key), "api_key_preview": (key[:12] + "..." + key[-4:]) if len(key) > 16 else ("***" if key else ""), }) @app.route("/python-leadgen/config", methods=["POST"]) def set_config(): try: body = request.get_json(silent=True) or {} except Exception: return jsonify({"success": False, "error": "Invalid JSON"}), 400 cfg = load_config() if "api_key" in body and body["api_key"]: cfg["api_key"] = body["api_key"] if "base_url" in body: cfg["base_url"] = body["base_url"] if "model" in body: cfg["model"] = body["model"] if "fallback_models" in body: cfg["fallback_models"] = body["fallback_models"] save_config(cfg) logger.info("Config updated: model=%s base_url=%s", cfg.get("model"), cfg.get("base_url")) return jsonify({"success": True, "model": cfg["model"]}) @app.route("/python-leadgen/models", methods=["GET"]) async def list_models(): """Fetch available free models from the configured API endpoint.""" cfg = load_config() base = cfg.get("base_url", "https://openrouter.ai/api/v1") key = cfg.get("api_key", "") models_url = base.rstrip("/") + "/models" try: async with httpx.AsyncClient(timeout=15) as client: resp = await client.get( models_url, headers={"Authorization": f"Bearer {key}"} if key else {}, ) if resp.status_code != 200: return jsonify({ "success": False, "error": f"API returned {resp.status_code}", "models": _fallback_model_list(), }) data = resp.json() all_models = data.get("data", []) free_models = [ { "id": m["id"], "name": m.get("name", m["id"]), "context_length": m.get("context_length", 0), "pricing": str(m.get("pricing", {})), } for m in all_models if ":free" in m.get("id", "") ] free_models.sort(key=lambda m: -m["context_length"]) return jsonify({"success": True, "total": len(free_models), "models": free_models}) except Exception as e: return jsonify({ "success": False, "error": str(e), "models": _fallback_model_list(), }) def _fallback_model_list(): return [ {"id": "qwen/qwen3-coder:free", "name": "Qwen 3 Coder (Free)", "context_length": 1048576}, {"id": "google/gemma-4-31b-it:free", "name": "Gemma 4 31B (Free)", "context_length": 262144}, {"id": "google/gemma-4-26b-a4b-it:free", "name": "Gemma 4 26B (Free)", "context_length": 262144}, {"id": "meta-llama/llama-3.3-70b-instruct:free", "name": "Llama 3.3 70B (Free)", "context_length": 131072}, {"id": "nvidia/nemotron-3-super-120b-a12b:free", "name": "Nemotron Super 120B (Free)", "context_length": 1000000}, ] @app.route("/python-leadgen/analyze", methods=["POST"]) async def analyze(): try: body = request.get_json(silent=True) or {} except Exception: return jsonify({"success": False, "error": "Invalid JSON body"}), 400 url = body.get("url", "").strip() if not url: return jsonify({"success": False, "error": "Missing required field: url"}), 400 engine = get_engine() try: result: LeadGenResponse = await engine.analyze_url(url) except Exception as exc: logger.exception("Unexpected error analyzing %s", url) return jsonify({"success": False, "error": str(exc)}), 500 if result.success and result.profile: return jsonify(result.model_dump()) else: return jsonify(result.model_dump()), 422 @app.route("/python-leadgen/batch", methods=["POST"]) async def batch(): try: body = request.get_json(silent=True) or {} except Exception: return jsonify({"success": False, "error": "Invalid JSON body"}), 400 urls = body.get("urls", []) if not urls or not isinstance(urls, list): return jsonify({"success": False, "error": "Missing required field: urls (list)"}), 400 if len(urls) > 10: return jsonify({"success": False, "error": "Maximum 10 URLs per batch"}), 400 engine = get_engine() try: results = await engine.batch_analyze(urls) except Exception as exc: logger.exception("Batch analysis failed") return jsonify({"success": False, "error": str(exc)}), 500 return jsonify({ "success": True, "total": len(urls), "results": [r.model_dump() for r in results], }) if __name__ == "__main__": port = int(os.getenv("LEADGEN_PORT", "3003")) debug = os.getenv("LEADGEN_DEBUG", "0") == "1" app.run(host="0.0.0.0", port=port, debug=debug)