from __future__ import annotations
import json
import re
import sqlite3
from pathlib import Path
from typing import Iterable
from project_catalog import load_all_projects
from v2_schema import DB_PATH, init_schema, sync_projects
WEIGHTS = {
'industry_fit': 0.25,
'problem_pressure': 0.20,
'company_fit': 0.10,
'technical_fit': 0.10,
'economic_value': 0.10,
'reachability': 0.10,
'sale_probability': 0.10,
'evidence_quality': 0.05,
}
STOP = {'und','oder','mit','die','der','das','ein','eine','von','für','fuer','bei','auf','im','in','zu','als'}
def tokens(value: str) -> set[str]:
words = re.findall(r'[a-z0-9äöüß-]{3,}', (value or '').lower())
return {w for w in words if w not in STOP}
def overlap_score(a: Iterable[str], b: Iterable[str]) -> float:
sa, sb = set(a), set(b)
if not sa or not sb:
return 0.0
overlap = len(sa & sb)
return min(10.0, overlap / max(1, min(len(sa), len(sb))) * 10.0)
def score_contact(contact: dict, project) -> dict:
industry_words = tokens(contact.get('industry', ''))
target_words = tokens(' '.join(project.target_industries))
industry_fit = overlap_score(industry_words, target_words)
evidence_text = ' '.join([
contact.get('company_description') or '',
contact.get('pain_point') or '',
contact.get('notes') or '',
contact.get('key_products') or '',
])
problem_pressure = overlap_score(tokens(evidence_text), tokens(project.customer_problem))
technical_fit = overlap_score(tokens(evidence_text), tokens(project.description))
company_fit = 2.0
if contact.get('company_description'):
company_fit += 3.0
if contact.get('company_size'):
company_fit += 3.0
if contact.get('industry'):
company_fit += 2.0
economic_value = project.potential_score or 5.0
reachability = 0.0
if contact.get('email'):
reachability += 5.0
if contact.get('website'):
reachability += 3.0
if contact.get('phone'):
reachability += 2.0
evidence_quality = 0.0
if contact.get('company_description'):
evidence_quality += 3.0
if contact.get('pain_point'):
evidence_quality += 2.0
if contact.get('website'):
evidence_quality += 2.0
if contact.get('enrichment_source'):
evidence_quality += 2.0
if contact.get('linkedin_url'):
evidence_quality += 1.0
sale_probability = min(10.0, (industry_fit * 0.4 + problem_pressure * 0.3 + reachability * 0.2 + evidence_quality * 0.1))
parts = {
'industry_fit': industry_fit,
'problem_pressure': problem_pressure,
'company_fit': min(10.0, company_fit),
'technical_fit': technical_fit,
'economic_value': min(10.0, economic_value),
'reachability': min(10.0, reachability),
'sale_probability': sale_probability,
'evidence_quality': min(10.0, evidence_quality),
}
lead_score = sum(parts[k] * WEIGHTS[k] for k in WEIGHTS)
rationale = (
f"Branchen-Fit {industry_fit:.1f}/10; Problemdruck {problem_pressure:.1f}/10; "
f"technischer Fit {technical_fit:.1f}/10; Erreichbarkeit {reachability:.1f}/10; "
f"Evidenz {evidence_quality:.1f}/10."
)
return {**parts, 'lead_score': round(lead_score, 2), 'rationale': rationale}
def score_campaign(campaign_id: int, db_path: Path = DB_PATH) -> int:
init_schema(db_path)
sync_projects(db_path)
projects = load_all_projects()
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
contacts = [dict(r) for r in conn.execute(
'SELECT * FROM cold_contacts WHERE campaign_id=?', (campaign_id,)
).fetchall()]
written = 0
for c in contacts:
for p in projects:
s = score_contact(c, p)
conn.execute('''
INSERT OR REPLACE INTO lead_project_matches (
contact_id, project_id, industry_fit, problem_pressure,
company_fit, technical_fit, economic_value, reachability,
evidence_quality, sale_probability, lead_score, rationale,
evidence_json, status, human_gate, updated_at
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,'researched','pending',CURRENT_TIMESTAMP)
''', (
c['id'], p.project_id, s['industry_fit'], s['problem_pressure'],
s['company_fit'], s['technical_fit'], s['economic_value'],
s['reachability'], s['evidence_quality'], s['sale_probability'],
s['lead_score'], s['rationale'], json.dumps({'source': c.get('enrichment_source')}, ensure_ascii=False),
))
written += 1
conn.commit()
conn.close()
return written
def main():
import argparse
parser = argparse.ArgumentParser(description='Lead Engine 2.0 scoring')
parser.add_argument('--campaign', type=int, required=True)
args = parser.parse_args()
count = score_campaign(args.campaign)
print(f'scored_matches={count}')
if __name__ == '__main__':
main()