249 lines
8.0 KiB
Python
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)
|