Files

249 lines
8.0 KiB
Python

"""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)