← back to Bankrupt Leads

src/classify.py

85 lines

"""
Doc classifier — local Ollama (default gemma3:4b).

Runs before extract.py so the expensive model only sees filings that actually
contain assets (Schedule A/B, 363 motion, auctioneer retention, APA, abandonment).
"""
from __future__ import annotations

import argparse
import os
from pathlib import Path

import psycopg

from llm import generate_text

PG_DSN = os.environ.get("BANKRUPT_LEADS_PG", "postgresql:///bankrupt_leads")
MODEL = os.environ.get("CLASSIFY_MODEL", "gemma3:4b")

CATEGORIES = {
    "schedule_ab",
    "363_motion",
    "auctioneer_retention",
    "apa",
    "abandonment",
    "other",
}

PROMPT_TEMPLATE = """Classify this US bankruptcy court filing into exactly ONE category:

- schedule_ab           Schedule A/B (real & personal property inventory)
- 363_motion            Motion to sell assets under 11 U.S.C. section 363
- auctioneer_retention  Application to employ auctioneer or liquidator
- apa                   Asset Purchase Agreement (or notice attaching one)
- abandonment           Notice of abandonment of property under section 554
- other                 everything else (procedural, claims, plan, etc.)

Respond with ONLY the category string. No prose, no punctuation.

FILING TEXT:
{text}
"""


def classify(text: str) -> str:
    prompt = PROMPT_TEMPLATE.format(text=text[:20_000])
    raw = generate_text(MODEL, prompt, num_ctx=8192).strip().lower()
    # Model sometimes wraps in quotes or adds a period — strip.
    raw = raw.strip("`\"' .\n")
    return raw if raw in CATEGORIES else "other"


def main() -> None:
    ap = argparse.ArgumentParser()
    ap.add_argument("--limit", type=int, default=50)
    args = ap.parse_args()

    with psycopg.connect(PG_DSN) as conn:
        with conn.cursor() as cur:
            cur.execute(
                """
                SELECT filing_id, text_path FROM bankrupt_leads.filings
                WHERE classified_at IS NULL AND text_path IS NOT NULL
                LIMIT %s
                """,
                (args.limit,),
            )
            rows = cur.fetchall()

        for filing_id, text_path in rows:
            text = Path(text_path).read_text(encoding="utf-8", errors="replace")
            doc_type = classify(text)
            with conn.cursor() as cur:
                cur.execute(
                    "UPDATE bankrupt_leads.filings SET doc_type = %s, classified_at = now() "
                    "WHERE filing_id = %s",
                    (doc_type, filing_id),
                )
            conn.commit()
            print(f"{filing_id}: {doc_type}")


if __name__ == "__main__":
    main()