Explorer
/opt/struktur/lead-engine/lead_scoring_v2.py
← Zurück ↓ Download
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()