Files
fil/scripts/taxonomy.py
Henrik Jess Nielsen 58210207ea
Some checks failed
Deploy classify service / build-and-deploy (push) Failing after 24s
Deploy fil (kreuzberg) / deploy (push) Successful in 53s
feat: add taxonomy classify service + /classify endpoint
- scripts/taxonomy.py: shared taxonomy with 14 categories, keyword scorer
  and classify_text() function
- scripts/classify_server.py: FastAPI service — forwards to kreuzberg /extract,
  applies taxonomy, returns category/subcategory/confidence alongside full kreuzberg response
- Dockerfile.classify: lightweight Python image for classify service
- classify.nomad: Nomad job → classify.i80.dk
- .gitea/workflows/classify.yml: CI/CD pipeline (build + deploy)
- analyse_familie.py: refactored to import from taxonomy.py (no duplication)
- .gitignore: exclude dokumenter_keywords.* and extract_all.log
2026-06-05 19:57:39 +02:00

232 lines
12 KiB
Python

"""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),
}