add post-discovery website/domain/contact enrichment
CI / compose (push) Failing after 5m52s

This commit is contained in:
Marco0300
2026-09-04 21:36:16 +02:00
parent 4ec9dc608b
commit 73ababe90c
4 changed files with 419 additions and 0 deletions
+36
View File
@@ -16,12 +16,14 @@ if __package__ in (None, ""):
from app.scoring import DEFAULT_RULES, signals_for_business, evaluate_score, score_business_opportunity, SCORE_VERSION
from app.ai_assistance import generate as generate_ai, input_fingerprint, evidence_hashes, provider_status, MAX_INPUT_ITEMS, MAX_OUTPUT_CHARS
from app.ai_opportunity import assess_opportunity, DETERMINISTIC_ASSESSMENT_THRESHOLD, ASSESSMENT_SCHEMA_VERSION
from app.enrichment import enrich_website, enrich_domain, enrich_contacts
from app.discovery import discover as scoped_discover
from app.ai_research import provider_status as ai_research_provider_status, configure_db as configure_ai_research_db, 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
from app.provider_config import validate_payload as validate_remote_provider, encrypt as encrypt_provider_secret, decrypt as decrypt_provider_secret, safe_status as remote_provider_status, test_connectivity as test_remote_connectivity
else:
from .enrichment import enrich_website, enrich_domain, enrich_contacts
from .domain import deduplication_key, deduplicate_businesses, is_suppressed, normalize_business, normalize_domain, normalize_phone, match_businesses
from .sources import adapter_for, contains_secret, available_adapters, normalize_record, circuit_is_open, DISCOVERY_CRITERIA_FIELDS, GoogleBrowserSearchBlocked
from .domain_intelligence import normalize_registrable_domain, resolve_domain, generate_candidate_domains
@@ -961,6 +963,39 @@ class ApiHandler(BaseHTTPRequestHandler):
self.audit(db,user,"domain.availability.checked",str(bid)); db.commit()
return self.send_json(200,{"business_id":bid,"status":"unknown","reason":"not_configured","provider_configured":False,"items":[{"domain":d,"status":"unknown","reason":"not_configured"} for d in domains]})
def enrich_business(self, bid, payload, db, user):
"""Post-discovery website/domain/contact enrichment for one business."""
org = user["organization_id"]
business_row = self.business(db, bid, org)
if not business_row:
return self.send_json(404, {"error": "not_found"})
business = dict(business_row)
url = str(business.get("website") or payload.get("url", "")).strip()
domain = business.get("website_domain") or normalize_registrable_domain(url)
suppressions=[dict(r) for r in db.execute("SELECT kind,value FROM suppressions WHERE organization_id=? AND active=1",(org,))]
enrichment = {
"business_id": bid,
"website": None,
"domain": enrich_domain(domain) if domain else None,
"contacts": [],
}
if url:
scan = enrich_website(url, fetch=lambda u, timeout=5.0, max_bytes=256*1024: scan_website(u, max_pages=1))
enrichment["website"] = scan
html = scan.get("html") or ""
if not html and scan.get("body"):
body = scan["body"]
html = body.decode("utf-8", "replace") if isinstance(body, bytes) else str(body)
if html and isinstance(html, str):
contacts = enrich_contacts(html, scan.get("final_url") or url, suppressions=suppressions, max_results=25)
enrichment["contacts"] = contacts
for contact in contacts:
key = hashlib.sha256((str(bid) + contact["source_url"] + contact["kind"] + contact["value"]).encode()).hexdigest()
db.execute("INSERT OR IGNORE INTO contact_extractions(organization_id,business_id,website_scan_id,extraction_key,kind,value,label,classification,confidence,source_url,public_business,mx_status,suppressed,do_not_contact,provenance) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",(org,bid,None,key,contact["kind"],contact["value"],contact["label"],contact["classification"],contact["confidence"],contact["source_url"],1,contact["mx_status"],int(contact["suppressed"]),int(contact["do_not_contact"]),contact["provenance"]))
db.commit()
self.audit(db, user, "business.enriched", str(bid)); db.commit()
return self.send_json(200, enrichment)
def list_jobs(self, db, org, query):
try:
limit=max(1,min(int(query.get("page_size",[50])[0]),JOB_PAGE_SIZE)); page=max(1,int(query.get("page",[1])[0])); offset=max(0,int(query.get("offset",[0])[0]))+(page-1)*limit
@@ -1220,6 +1255,7 @@ class ApiHandler(BaseHTTPRequestHandler):
if path=="/api/v1/businesses/bulk-review": return self.bulk_review(payload,db,user)
bits_ai=path.split("/")
if len(bits_ai)==7 and bits_ai[:4]==["","api","v1","businesses"] and bits_ai[5]=="ai" and bits_ai[6]=="opportunity-assessment": return self.assess_ai_opportunity(int(bits_ai[4]) if bits_ai[4].isdigit() else -1,payload,db,user)
if len(bits_ai)==6 and bits_ai[:4]==["","api","v1","businesses"] and bits_ai[5]=="enrichment": return self.enrich_business(int(bits_ai[4]) if bits_ai[4].isdigit() else -1,payload,db,user)
if len(bits_ai)==7 and bits_ai[:4]==["","api","v1","businesses"] and bits_ai[5]=="ai" and bits_ai[6]=="suggest": return self.suggest_ai(int(bits_ai[4]) if bits_ai[4].isdigit() else -1,payload,db,user)
if len(bits_ai)==6 and bits_ai[:4]==["","api","v1","ai-runs"] and bits_ai[4].isdigit() and bits_ai[5] in {"approve","reject"}: return self.decide_ai(int(bits_ai[4]),bits_ai[5],db,user)
if path=="/api/v1/score-rules": return self.create_score_rule(payload,db,user)