Add fail-closed AI web research discovery provider
CI / compose (push) Successful in 10m53s

This commit is contained in:
Marco0300
2026-09-03 20:32:33 +02:00
parent bf5b54679a
commit da91c188f3
7 changed files with 235 additions and 10 deletions
+13 -4
View File
@@ -15,6 +15,7 @@ if __package__ in (None, ""):
from app.scoring import DEFAULT_RULES, signals_for_business, evaluate_score, SCORE_VERSION
from app.ai_assistance import generate as generate_ai, input_fingerprint, provider_status, MAX_INPUT_ITEMS, MAX_OUTPUT_CHARS
from app.discovery import discover as scoped_discover
from app.ai_research import provider_status as ai_research_provider_status, validate_criteria as validate_ai_research_criteria, AIResearchConfigError
from app.search_provider import provider_status as search_provider_status
from app.config import load_config
else:
@@ -26,6 +27,7 @@ else:
from .scoring import DEFAULT_RULES, signals_for_business, evaluate_score, SCORE_VERSION
from .ai_assistance import generate as generate_ai, input_fingerprint, provider_status, MAX_INPUT_ITEMS, MAX_OUTPUT_CHARS
from .discovery import discover as scoped_discover
from .ai_research import provider_status as ai_research_provider_status, validate_criteria as validate_ai_research_criteria, AIResearchConfigError
from .search_provider import provider_status as search_provider_status
from .config import load_config
ORGANIZATION_ID = "demo-tenant"
@@ -597,7 +599,8 @@ class ApiHandler(BaseHTTPRequestHandler):
if path=="/api/v1/sources": return self.list_sources(db,org)
if path=="/api/v1/discovery-queries": return self.list_queries(db,org)
if path=="/api/v1/discovery-runs": return self.list_discovery_runs(db,org,parse_qs(parsed.query))
if path=="/api/v1/discovery/provider-status": return self.send_json(200, search_provider_status())
if path=="/api/v1/discovery/provider-status": return self.send_json(200, ai_research_provider_status())
if path=="/api/v1/discovery/ai-provider-status": return self.send_json(200, ai_research_provider_status())
if path=="/api/v1/source-records": return self.list_source_records(db,org,parse_qs(parsed.query))
if path=="/api/v1/jobs": return self.list_jobs(db,org,parse_qs(parsed.query))
if path=="/api/v1/domain-checks": return self.list_domain_checks(db,org,parse_qs(parsed.query))
@@ -812,8 +815,14 @@ class ApiHandler(BaseHTTPRequestHandler):
if not isinstance(criteria, dict): return self.send_json(400, {"error": "invalid_criteria"})
if not criteria_only and (not isinstance(seeds, list) or not seeds): return self.send_json(400, {"error": "seed_urls_required"})
if criteria_only:
status = search_provider_status()
if status["status"] != "ready": return self.send_json(503, {"error": status["status"], "provider": status["provider"]})
try: validate_ai_research_criteria(criteria)
except AIResearchConfigError as exc:
if str(exc) in {"prompt_injection_rejected", "criteria_too_large"}: return self.send_json(400, {"error": str(exc)})
status = ai_research_provider_status()
# Legacy SEARCH_PROVIDER_* may pass only during migration; the
# primary status and endpoint remain AI research.
legacy = search_provider_status()
if status["status"] != "ready" and legacy["status"] != "ready": return self.send_json(503, {"error": status["status"], "provider": status["provider"]})
try:
if not criteria_only and len(seeds) > 5: raise ValueError("invalid_criteria")
if len(json.dumps(criteria).encode()) > 8192: raise ValueError("invalid_criteria")
@@ -1360,7 +1369,7 @@ class ApiHandler(BaseHTTPRequestHandler):
def _run_scoped_discovery(db, job, handler):
payload = json.loads(job["payload"] or "{}")
result = scoped_discover(payload.get("criteria", {}), payload.get("seed_urls", []), max_pages=payload.get("max_pages", 20), max_candidates=payload.get("max_candidates", 50))
result = scoped_discover(payload.get("criteria", {}), payload.get("seed_urls"), max_pages=payload.get("max_pages", 20), max_candidates=payload.get("max_candidates", 50))
org = job["organization_id"]; persisted = []
for candidate in result["candidates"]:
b = normalize_business(candidate)