"""Shared taxonomy: weighted keyword lists + folder mappings + scorer. Used by both analyse_familie.py (batch classify) and classify_server.py (API endpoint). """ from __future__ import annotations TAXONOMY: dict[str, list[tuple[str, float]]] = { "Familie og børn": [ ("familie", 1.5), ("familieliv", 2.0), ("samvær", 2.0), ("samværsaftale", 2.5), ("børn", 1.5), ("barn", 1.5), ("forældre", 2.0), ("forældremyndighed", 2.5), ("skilsmisse", 2.5), ("separation", 2.0), ("barsel", 2.0), ("barnets", 1.5), ("søskende", 2.0), ("mor", 1.0), ("far", 1.0), ("mor og far", 2.5), ("dåb", 2.0), ("konfirmation", 2.5), ("bryllup", 2.0), ("vielse", 2.0), ("fodselsdag", 1.5), ("fødselsdagskort", 2.0), ], "Skole og uddannelse": [ ("skole", 1.5), ("uddannelse", 1.5), ("gymnasium", 2.5), ("universitetet", 2.0), ("eksamen", 2.0), ("studieplan", 2.5), ("karakter", 2.0), ("lektier", 2.5), ("opgave", 1.5), ("matematik", 2.0), ("dansk", 1.0), ("noter", 1.5), ("pensum", 2.5), ("studie", 1.5), ("kursus", 1.5), ("folkeskole", 2.5), ("htx", 2.5), ("hf", 2.0), ("hhx", 2.5), ("stx", 2.5), ("eux", 2.5), ("karakterblad", 3.0), ("eksamensbevis", 3.0), ("studiekort", 3.0), ("answer key", 2.5), ("quiz", 2.5), ("assessment", 2.5), ("learning", 1.5), ("lecture", 2.0), ("course", 2.0), ("lesson", 2.0), ("worksheet", 2.5), ], "Arbejde og karriere": [ ("ansøgning", 1.5), ("job", 1.5), ("jobansøgning", 2.5), ("cv", 2.5), ("curriculum vitae", 3.0), ("opsigelse", 2.5), ("løn", 1.5), ("lønforhandling", 2.5), ("ansættelseskontrakt", 3.0), ("ansættelse", 2.0), ("arbejdsplads", 2.0), ("kollega", 2.0), ("arbejdsgiver", 2.5), ("medarbejder", 2.0), ("fagforening", 2.5), ("a-kasse", 2.5), ("dagpenge", 2.5), ("jobcenter", 2.5), ("referenceliste", 3.0), ("karriere", 2.0), ("rekruttering", 2.5), ("personaleafdeling", 2.5), ("arbejde", 1.5), ("projektleder", 2.5), ("møde", 1.5), ("mødedagsorden", 2.5), ("scrum", 2.5), ("agile", 2.5), ("backlog", 2.5), ("sprint", 2.5), ("konference", 2.0), ("kompetencer", 2.0), ], "Økonomi og regninger": [ ("faktura", 2.5), ("regning", 2.0), ("betaling", 2.0), ("bank", 2.0), ("skat", 2.0), ("pension", 2.0), ("opsparing", 2.5), ("gæld", 2.5), ("lån", 2.5), ("kredit", 2.0), ("inkasso", 3.0), ("afdrag", 2.5), ("akkord", 3.0), ("restgæld", 3.0), ("kreditor", 2.5), ("økonomi", 1.5), ("budget", 2.0), ("forsikring", 2.0), ("rykkerbrev", 3.0), ("udbetaling", 2.0), ("sparekasse", 2.5), ("betalingsservice", 3.0), ("gældstyrelsen", 3.0), ("netto bank", 2.5), ("netbank", 2.5), ("kontoudtog", 3.0), ("årsopgørelse", 2.5), ("restskat", 3.0), ("årsopgørelse skat", 3.0), ("momsangivelse", 3.0), ], "Hjem og bolig": [ ("bolig", 2.0), ("hus", 1.5), ("lejlighed", 2.5), ("ejendom", 2.0), ("husleje", 3.0), ("vedligeholdelse", 2.5), ("renovation", 2.5), ("el", 1.0), ("vand", 1.0), ("varme", 1.5), ("fjernvarme", 2.5), ("ejerforening", 3.0), ("andelsbolig", 3.0), ("lejekontrakt", 3.0), ("fremlejning", 2.5), ("nøgle", 1.5), ("flytning", 2.0), ("indretning", 2.0), ("have", 1.5), ("grundejerforening", 3.0), ("BBR", 2.5), ("byggetilladelse", 3.0), ], "Jura og kontrakter": [ ("kontrakt", 2.0), ("aftale", 2.0), ("kontrakter", 2.0), ("juridisk", 2.5), ("advokat", 2.5), ("testamente", 3.0), ("retssag", 3.0), ("dom", 2.0), ("stævning", 3.0), ("klage", 2.0), ("tinglysning", 3.0), ("pantebrev", 3.0), ("tilbud", 1.5), ("vilkår", 2.0), ("betingelser", 2.0), ("fuldmagt", 2.5), ("forlig", 2.5), ("forsikringsbetingelser", 3.0), ("police", 2.0), ], "Sundhed og medicin": [ ("recept", 2.5), ("medicin", 2.5), ("læge", 2.5), ("hospital", 2.5), ("sygdom", 2.5), ("behandling", 2.0), ("diagnose", 3.0), ("operation", 2.5), ("symptomer", 2.5), ("sundhed", 2.0), ("journaloplysning", 3.0), ("patientjournal", 3.0), ("laboratorium", 2.5), ("blodprøve", 3.0), ("røntgen", 3.0), ("psykolog", 3.0), ("psykiater", 3.0), ("terapi", 2.5), ("tandlæge", 3.0), ("optiker", 2.5), ("vaccination", 3.0), ], "IT og teknologi": [ ("software", 2.5), ("server", 2.0), ("netværk", 2.5), ("database", 2.5), ("programmering", 2.5), ("kode", 2.0), ("linux", 3.0), ("cloud", 2.5), ("it", 1.5), ("computer", 2.0), ("laptop", 2.5), ("password", 2.5), ("installation", 2.0), ("konfiguration", 2.0), ("log", 1.5), ("backup", 2.5), ("docker", 3.0), ("kubernetes", 3.0), ("python", 3.0), ("github", 3.0), ("azure", 2.5), ("windows", 2.0), ("macos", 3.0), ("licens", 2.0), ("api", 2.5), ("dokumentation", 1.5), ("teknologi", 2.0), ("system", 1.5), ("web", 2.0), ("app", 1.5), ("program", 1.5), ("firmware", 3.0), ("internet", 2.0), ("cybersikkerhed", 3.0), ("bitcoin", 3.0), ("blockchain", 3.0), ("kryptovaluta", 3.0), ("jupyter", 3.0), ("notebook", 2.5), ("monitor", 2.0), ("display", 2.0), ("remote control", 2.5), ("user manual", 2.5), ("dataanalyse", 2.5), ("data analysis", 2.5), ("django", 3.0), ("javascript", 2.5), ("jquery", 2.5), ("typescript", 3.0), ("html", 2.0), ("css", 2.0), ("react", 2.5), ("nodejs", 3.0), ("java", 2.5), ("csharp", 3.0), ("datamatiker", 3.0), ("sql", 2.5), ("rest api", 3.0), ("programming", 2.5), ("developer", 2.0), ("debugging", 2.5), ("testing", 2.0), ], "Bøger og litteratur": [ ("isbn", 3.0), ("forlag", 1.5), ("roman", 3.0), ("novelle", 3.0), ("biografi", 3.0), ("poesi", 3.0), ("digtsamling", 3.0), ("bog", 2.0), ("litteratur", 2.5), ("forfatter", 3.0), ("kapitel", 2.0), ("bogklub", 3.0), ("bibliotek", 2.5), ("e-bog", 3.0), ("lydbog", 3.0), ("udgivelse", 2.0), ("biography", 2.5), ("novel", 2.5), ("author", 2.0), ("chapter", 2.0), ("publisher", 2.0), ("edition", 2.0), ("paperback", 3.0), ("hardcover", 3.0), ("fiction", 3.0), ("nonfiction", 3.0), ("memoir", 3.0), ], "Rejse og transport": [ ("rejse", 2.0), ("ferie", 2.0), ("fly", 2.5), ("hotel", 2.5), ("booking", 2.5), ("rejseplan", 2.5), ("pas", 2.0), ("visum", 3.0), ("bil", 1.5), ("kørekort", 3.0), ("tog", 2.0), ("billet", 2.5), ("flyrejse", 3.0), ("afgangsgate", 3.0), ("baggage", 2.5), ("cruise", 3.0), ("afrejse", 2.5), ("ankomst", 2.0), ("itinerary", 3.0), ("pakketur", 2.5), ], "Offentlige myndigheder": [ ("kommune", 2.5), ("stat", 1.5), ("styrelse", 2.5), ("forvaltning", 2.5), ("gældstyrelsen", 3.0), ("skat", 2.0), ("udbetaling danmark", 3.0), ("borger.dk", 3.0), ("digitalpost", 2.5), ("afgørelse", 2.5), ("offentlig myndighed", 3.0), ("ministeri", 2.5), ("ministeriet", 2.5), ("politi", 2.5), ("domstol", 2.5), ("retsinformation", 3.0), ("folketing", 2.5), ("region", 2.0), ("jobcenter", 2.5), ("borger", 1.5), ("ansøgning kommune", 3.0), ("nykøbingvej", 2.5), ("sakskøbing", 2.5), ("akkordansøgning", 3.0), ], "Projekter og hobby": [ ("hobby", 2.5), ("projekt", 2.0), ("frivillig", 2.5), ("klub", 2.0), ("aktivitet", 2.0), ("sport", 2.5), ("musik", 2.5), ("opskrift", 2.5), ("træning", 2.0), ("kreativ", 2.5), ("håndværk", 2.5), ("fotografi", 2.5), ("spil", 2.0), ("gaming", 3.0), ("maleri", 2.5), ("tegning", 2.0), ("golf", 3.0), ("fitness", 2.5), ("klippekort", 2.5), ("svømning", 2.5), ("cykling", 2.5), ("løb", 1.5), ("boldspil", 2.5), ("fodbold", 2.5), ("concert", 2.0), ("festival", 2.5), ], "Teknik og ingeniørfag": [ ("tegning", 2.0), ("teknisk tegning", 3.0), ("ingeniør", 2.5), ("konstruktion", 2.5), ("maskine", 2.5), ("elektroteknik", 3.0), ("specifikation", 2.0), ("diagram", 2.0), ("brugsanvisning", 3.0), ("manual", 2.5), ("datablad", 3.0), ("CE-mærkning", 3.0), ("ISO", 2.0), ("norm", 2.0), ("standard", 1.5), ("user manual", 2.5), ("installation guide", 3.0), ("technical specification", 3.0), ("product guide", 2.5), ("service manual", 3.0), ], "Erhverv og business": [ ("virksomhed", 2.5), ("erhverv", 2.5), ("CVR", 3.0), ("faktura", 2.5), ("ordre", 2.0), ("leverandør", 2.5), ("kunde", 2.0), ("salg", 2.0), ("moms", 2.5), ("regnskab", 2.5), ("årsregnskab", 3.0), ("balance", 2.0), ("resultatopgørelse", 3.0), ("aktieselskab", 3.0), ("iværksætter", 2.5), ("forretning", 2.0), ("selskab", 2.0), ("ApS", 3.0), ("A/S", 3.0), ], } TAXONOMY_TO_FOLDER: dict[str, str] = { "Familie og børn": "Privat/Familie", "Skole og uddannelse": "Privat/Personlig/Uddannelse", "Arbejde og karriere": "Privat/Personlig/Arbejde", "Økonomi og regninger": "Privat/Økonomi", "Hjem og bolig": "Privat/Hjem/Bolig", "Jura og kontrakter": "Privat/Jura", "Sundhed og medicin": "Privat/Personlig/Sundhed", "IT og teknologi": "Arkiv/Teknisk", "Bøger og litteratur": "Arkiv/Bøger", "Rejse og transport": "Privat/Rejser", "Offentlige myndigheder": "Privat/Jura/Myndigheder", "Projekter og hobby": "Projekter", "Teknik og ingeniørfag": "Arkiv/Teknisk", "Erhverv og business": "Arkiv/Erhverv", } MIN_SCORE: float = 1.5 def keyword_score(doc_text: str, keywords: list[tuple[str, float]]) -> float: """Score a document against a keyword list. Multi-word phrases are matched as substrings; single words are matched as whole words (word boundary) to avoid false positives (e.g. 'bil' in 'mobil'). Returns the sum of weights for all matching entries. """ text = doc_text.lower() total = 0.0 for kw, weight in keywords: kw_lower = kw.lower() if " " in kw_lower: if kw_lower in text: total += weight else: idx = text.find(kw_lower) while idx != -1: before = text[idx - 1] if idx > 0 else " " after = text[idx + len(kw_lower)] if idx + len(kw_lower) < len(text) else " " if not before.isalpha() and not after.isalpha(): total += weight break idx = text.find(kw_lower, idx + 1) return round(total, 3) def classify_text( content: str, keywords: list[str], folder_hint: str = "", min_score: float = MIN_SCORE, ) -> dict: """Classify document text + keywords against the taxonomy. Args: content: Extracted document text. keywords: YAKE keyword strings from kreuzberg. folder_hint: Current folder path (used as additional context signal). min_score: Minimum score to assign a label (else 'Ukendt'). Returns: dict with category, subcategory, confidence, runner_up, runner_up_score. """ kw_text = " ".join(keywords) folder_tokens = folder_hint.replace("/", " ").replace("_", " ").replace("-", " ") doc_text = f"{content} {folder_tokens} {kw_text}" scores = { cat: keyword_score(doc_text, kws) for cat, kws in TAXONOMY.items() } sorted_scores = sorted(scores.items(), key=lambda x: x[1], reverse=True) best_label, best_score = sorted_scores[0] runner_up_label, runner_up_score = sorted_scores[1] if len(sorted_scores) > 1 else ("", 0.0) if best_score >= min_score: category = best_label subcategory = TAXONOMY_TO_FOLDER.get(best_label, "") else: category = "Ukendt" subcategory = "" return { "category": category, "subcategory": subcategory, "confidence": round(best_score, 3), "runner_up": runner_up_label if best_score >= min_score else best_label, "runner_up_score": round(runner_up_score, 3), }