Skip to content

Perf: donor classification in card.py calls Claude Haiku once per unknown donor, sequentially #54

Description

@mahsa7haft

Problem

When a card is generated for an MP, each donor name that isn't already in the donor_company_links database triggers an individual Claude Haiku call to resolve it:

# _classify_donor() in card.py (simplified)
link = db.get_donor_company_link(donor_name)   # DB lookup first
if not link:
    link = resolve_person_to_company(donor_name)  # → Claude Haiku call

These calls are made sequentially inside a loop over all donors. An MP with 20 unknown donors makes 20 sequential Haiku calls before the card can render, adding 1–3 s per unknown donor to first-generation latency.

Once resolved, results are saved to donor_company_links permanently — so each donor is only resolved once ever. But for new MPs or newly declared donors, the first card generation can take 20–60 s.

Proposed fix

Option A — Parallelize with ThreadPoolExecutor (quick win)

Run all resolve_person_to_company calls concurrently within generate_card:

with ThreadPoolExecutor(max_workers=10) as pool:
    futures = {pool.submit(resolve_person_to_company, name): name for name in unknown_donors}
    for fut in as_completed(futures):
        links[futures[fut]] = fut.result()

This reduces latency from N × 2s to approximately max(individual calls) ≈ 2–5 s regardless of donor count.

Option B — Pre-resolve donors in a background job (thorough)

Add a resolve_donors management command that iterates all known donors in donor_company_links and fills gaps in bulk. Run it after each ingestion pass in paidup-intelligence. Card generation then always hits the DB cache.

Option C — Batch Haiku call (most efficient)

Send all unknown donor names in a single Haiku prompt and parse a structured response. Cuts API calls from N to 1, at the cost of some prompt engineering.

Recommended approach

Start with Option A (parallelization) as a one-line change with immediate impact. Follow with Option B (pre-resolution job) to eliminate the cold path entirely for known MPs.

Notes

  • resolve_person_to_company lives in card.py
  • Results are stored in donor_company_links via database.py — the DB write happens inside the function
  • Parallel writes to donor_company_links should be safe as the table has a unique constraint on donor name and upserts are idempotent
  • See also: issue Bug: companies frequently misclassified as persons in donor table #46 (companies misclassified as persons) — fixing classification accuracy first will reduce the number of unnecessary Haiku calls

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions