| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826 |
- #!/usr/bin/env python3
- import json
- import sqlite3
- import time
- import urllib.request
- from concurrent.futures import ThreadPoolExecutor, as_completed
- DB = "data/zammad_analysis.sqlite3"
- URL = "http://192.168.1.101:11434/api/chat"
- MODEL = "qwen3:8b"
- WORKERS = 2
- PRIMARY_INTENTS = {
- "VERSANDSTATUS",
- "LIEFERVERZUG",
- "ADRESSÄNDERUNG",
- "REKLAMATION",
- "TRANSPORTSCHADEN",
- "FEHLLIEFERUNG",
- "RECHNUNG",
- "ZAHLUNG",
- "WIDERRUF",
- "RETOURE",
- "PFLANZENBERATUNG",
- "PFLANZENKRANKHEIT",
- "SORTENBERATUNG",
- "PFLEGEFRAGE",
- "BESTANDSANFRAGE",
- "VORBESTELLUNG",
- "B2B",
- "GROSSHANDEL",
- "RABATTANFRAGE",
- "SONSTIGES",
- }
- TAGS = {
- # Reklamation / Qualität
- "QUALITAETSPROBLEM",
- "VERWECHSLUNG",
- "SCHLECHTVERPACKT",
- "FALSCHESORTEGELIEFERT",
- # Pflanzenzustand / Pflanzenschäden
- "PFLANZE_ABGEBROCHEN",
- "PFLANZE_VERTROCKNET",
- "PFLANZE_KRANK",
- "PFLANZE_MICKRIG",
- "PFLANZE_NICHTANGEWACHSEN",
- "WEISSE_BLAETTER",
- "SCHIMMEL",
- "FAULIG",
- "PFLANZENKRANKHEIT",
- "PILZKRANKHEIT",
- "MEHLTAU",
- "STERNRUSSTAU",
- "SCHÄDLINGE",
- "LAUSE",
- "PFLANZENSCHADEN",
- "WACHSTUMSPROBLEM",
- # Mengen-/Lieferfehler
- "FALSCHEFARBE",
- "NUR_EIN_STUECK",
- "FALSCHE_MENGE",
- # Produktgruppe
- "PRODUKTGRUPPE_ROSE",
- "PRODUKTGRUPPE_CLEMATIS",
- # Produktform
- "PRODUKTFORM_CONTAINER",
- "PRODUKTFORM_WURZELWARE",
- "PRODUKTFORM_HOCHSTAMM",
- # Versand / Logistik
- "VERSAND",
- "VERSANDKOSTEN",
- "PROBLEMMITLIEFERDIENST",
- "LIEFERDIENST_DHL",
- "LIEFERDIENST_GLS",
- "LIEFERDIENST_UNBEKANNT",
- "TRANSPORTSCHADEN",
- # Service / sonstiger Kontext
- "KATALOG",
- "ABHOLUNG",
- "KUNDENSERVICE",
- # Retouren-Merkmale
- "TEILRETOURE",
- "VOLLRETOURE",
- # Rabattverhalten
- "RABATTANFRAGE",
- "RABATT_GEFORDERT",
- "RABATT_SELBSSTAENDIG_ABGEZOGEN",
- }
- SYSTEM = """
- Du analysierst historische Kundenservice-Tickets von Schmid Gartenpflanzen.
- Analysiere ausschließlich das tatsächliche Kundenanliegen.
- WICHTIG:
- Primärintent, Secondary-Intents und insbesondere TAGS dürfen nur
- aus dem tatsächlichen KUNDENBEITRAG abgeleitet werden.
- Der KUNDENSERVICE-KONTEXT darf NICHT dazu verwendet werden, neue
- Kundenprobleme, Zustände, Produktmerkmale oder Tags zu erfinden.
- Wenn ein Merkmal nur in der Antwort des Kundenservice vorkommt,
- aber nicht aus dem Kundenbeitrag hervorgeht, darf es NICHT als Tag
- ausgegeben werden.
- KUNDENBEITRAG:
- Die eigentliche Quelle für die Klassifikation.
- KUNDENSERVICE-KONTEXT:
- Nur ergänzender Kontext zur Interpretation bereits im Kundenbeitrag
- vorhandener Informationen.
- PRIMÄRINTENTS:
- VERSANDSTATUS
- LIEFERVERZUG
- ADRESSÄNDERUNG
- REKLAMATION
- TRANSPORTSCHADEN
- FEHLLIEFERUNG
- RECHNUNG
- ZAHLUNG
- WIDERRUF
- RETOURE
- PFLANZENBERATUNG
- SORTENBERATUNG
- PFLEGEFRAGE
- BESTANDSANFRAGE
- VORBESTELLUNG
- B2B
- GROSSHANDEL
- DÜNGER_FEHLT
- SONSTIGES
- TAGS:
- QUALITAETSPROBLEM
- VERWECHSLUNG
- SCHLECHTVERPACKT
- FALSCHESORTEGELIEFERT
- PROBLEMMITLIEFERDIENST
- LIEFERDIENST_DHL
- LIEFERDIENST_GLS
- LIEFERDIENST_UNBEKANNT
- TRANSPORTSCHADEN
- PFLANZENKRANKHEIT
- PFLANZENSCHADEN
- WACHSTUMSPROBLEM
- REGELN:
- BESTANDSANFRAGE:
- Jede Frage nach aktueller oder zukünftiger Verfügbarkeit,
- Lieferbarkeit oder Bestellbarkeit.
- VORBESTELLUNG:
- Nur wenn der Kunde ausdrücklich eine Vormerkung, Reservierung
- oder verbindliche Vorabbestellung für ein aktuell nicht verfügbares
- Produkt verlangt.
- Eine reine Frage nach Verfügbarkeit, Lieferbarkeit oder
- zukünftiger Bestellbarkeit ist IMMER BESTANDSANFRAGE.
- Auch Wünsche wie "ich hätte gerne welche", "ich möchte die gerne
- haben" oder "können Sie mir sagen, wann sie wieder da sind" sind
- KEINE VORBESTELLUNG, solange keine ausdrückliche Reservierung,
- Vormerkung oder Vorabbestellung verlangt wird.
- WIDERRUF:
- Stornierung oder Rückabwicklung einer Bestellung auf Kundenwunsch,
- einschließlich Stornierung vor Versand.
- REKLAMATION:
- Beanstandung eines gelieferten Produkts oder einer Leistung wegen
- Qualität, Zustand oder Beschädigung.
- TRANSPORTSCHADEN:
- Schaden, der offensichtlich durch Transport oder Versand entstanden ist.
- FEHLLIEFERUNG:
- Falscher Artikel, falsche Sorte oder falsches Produkt geliefert.
- DÜNGER_FEHLT:
- Bestellter Dünger fehlt.
- QUALITAETSPROBLEM:
- Tag, kein Primärintent.
- PFLANZENKRANKHEIT:
- Tag, kein Primärintent.
- PFLANZENSCHADEN:
- Tag, kein Primärintent.
- WACHSTUMSPROBLEM:
- Tag, kein Primärintent.
- PROBLEMMITLIEFERDIENST:
- Problem mit dem Paketdienst.
- LIEFERDIENST_DHL / LIEFERDIENST_GLS:
- Nur verwenden, wenn der jeweilige Dienst tatsächlich genannt wird.
- MULTI-INTENT:
- Mehrere Anliegen erfassen.
- TAGS DÜRFEN NIEMALS als secondary_intents ausgegeben werden.
- Die folgenden Begriffe sind ausschließlich TAGS:
- PRODUKTGRUPPE_ROSE
- PRODUKTGRUPPE_CLEMATIS
- PRODUKTFORM_CONTAINER
- PRODUKTFORM_WURZELWARE
- PRODUKTFORM_HOCHSTAMM
- VERSAND
- VERSANDKOSTEN
- PROBLEMMITLIEFERDIENST
- LIEFERDIENST_DHL
- LIEFERDIENST_GLS
- LIEFERDIENST_UNBEKANNT
- RETOURE
- KATALOG
- ABHOLUNG
- KUNDENSERVICE
- QUALITAETSPROBLEM
- VERWECHSLUNG
- SCHLECHTVERPACKT
- FALSCHESORTEGELIEFERT
- PFLANZE_ABGEBROCHEN
- PFLANZE_VERTROCKNET
- PFLANZE_KRANK
- PFLANZE_MICKRIG
- PFLANZE_NICHTANGEWACHSEN
- WEISSE_BLAETTER
- SCHIMMEL
- FAULIG
- PFLANZENKRANKHEIT
- PFLANZENSCHADEN
- WACHSTUMSPROBLEM
- FALSCHEFARBE
- NUR_EIN_STUECK
- FALSCHE_MENGE
- TRANSPORTSCHADEN
- secondary_intents darf ausschließlich einen PRIMÄRINTENT enthalten.
- RETOUREN:
- RETOURE ist ausschließlich ein PRIMÄRINTENT.
- RÜCKSENDUNG, RÜCKSENDEN, ZURÜCKSENDEN, RÜCKGABE,
- WARE ZURÜCKSCHICKEN und RETOURE sind Synonyme und
- werden als RETOURE klassifiziert.
- RÜCKSENDUNG darf NICHT als eigener Tag ausgegeben werden.
- TEILRETOURE:
- Der Kunde möchte nur einen Teil der Bestellung zurückgeben.
- VOLLRETOURE:
- Der Kunde möchte die komplette Bestellung zurückgeben.
- TEILRETOURE und VOLLRETOURE sind Tags.
- WIDERRUF und RETOURE sind zu unterscheiden:
- - RETOURE = Ware soll zurückgegeben/zurückgesendet werden.
- - WIDERRUF = Kunde erklärt den Widerruf des Kaufvertrags.
- Wenn ausdrücklich beides verlangt wird, kann RETOURE als
- secondary_intent zu WIDERRUF erscheinen.
- PFLANZENKRANKHEITEN / SCHÄDLINGE:
- PFLANZENKRANKHEIT = allgemeine oder konkrete Pflanzenkrankheit.
- PILZKRANKHEIT = Pilzerkrankung.
- MEHLTAU = konkreter Pilzkrankheitsbefund.
- STERNRUSSTAU = konkreter Pilzkrankheitsbefund.
- SCHÄDLINGE = allgemeiner Schädlingsbefall.
- LAUSE = konkreter Befall mit Läusen.
- Konkrete Merkmale dürfen zusätzlich zu PFLANZENKRANKHEIT
- ausgegeben werden.
- RABATTVERHALTEN:
- RABATTANFRAGE:
- Der Kunde fragt aktiv, ob ein Rabatt oder Preisnachlass möglich ist.
- RABATT_GEFORDERT:
- Der Kunde fordert ausdrücklich einen Rabatt, Preisnachlass oder
- eine bestimmte Rabatt-/Nachlasshöhe.
- RABATT_SELBSSTAENDIG_ABGEZOGEN:
- Der Kunde hat eigenmächtig einen Rabatt, Nachlass, Betrag oder
- Prozentsatz von einer Rechnung oder Zahlung abgezogen, ohne dass
- dieser Rabatt vorher vom Unternehmen gewährt wurde.
- Rabattmerkmale dürfen ausschließlich aus dem KUNDENBEITRAG abgeleitet
- werden. Ein vom Kundenservice angebotener oder gewährter Rabatt ist
- KEINE Rabattforderung des Kunden.
- Rabatt ist normalerweise kein eigener Primärintent.
- Beispiel:
- "Die Rose kam beschädigt an, dafür erwarte ich 20 % Rabatt."
- → primary_intent = REKLAMATION
- → tags = [QUALITAETSPROBLEM, RABATT_GEFORDERT]
- → discount.percentage = 20
- Beispiel:
- "Die Rechnung beträgt 100 Euro, ich habe nur 90 Euro überwiesen."
- → primary_intent = ZAHLUNG
- → tags = [RABATT_SELBSSTAENDIG_ABGEZOGEN]
- → discount.amount = 10
- → discount.currency = EUR
- → discount.self_deducted = true
- RABATT_WIEDERHOLT wird NICHT im einzelnen Ticket vergeben.
- Das ist ein späteres kundenbezogenes Aggregat.
- DISCOUNT-FELD:
- Extrahiere eine konkret genannte Rabatt-/Nachlasshöhe strukturiert.
- Wenn Prozent genannt:
- discount.percentage = Zahl
- Wenn ein konkreter Geldbetrag genannt:
- discount.amount = Zahl
- discount.currency = erkannte Währung
- Wenn keine Höhe genannt:
- entsprechender Wert = null
- discount.requested = true, wenn der Kunde aktiv Rabatt verlangt/fragt.
- discount.self_deducted = true, wenn der Kunde eigenmächtig gekürzt hat.
- Wenn kein Rabatt vorkommt:
- discount.requested = false
- discount.self_deducted = false
- discount.amount = null
- discount.percentage = null
- discount.currency = null
- primary_intent:
- Das wichtigste Anliegen.
- secondary_intents:
- Weitere Primärintents, falls vorhanden.
- tags:
- Nur konkrete Merkmale aus der TAG-Liste.
- NIEMALS Primärintents als Tags verwenden.
- taxonomy_fit:
- GOOD, wenn die Taxonomie gut passt.
- PARTIAL, wenn sie nur teilweise passt.
- POOR, wenn ein relevantes Anliegen fehlt.
- taxonomy_candidate:
- Nur bei PARTIAL oder POOR einen möglichen fehlenden Primärintent nennen.
- Sonst null.
- Keine neuen Primärintents eigenmächtig erfinden.
- Antworte ausschließlich mit gültigem JSON.
- """
- EXPECTED = {
- "primary_intent": "",
- "secondary_intents": [],
- "tags": [],
- "complaint_type": None,
- "actions": [],
- "summary": "",
- "taxonomy_fit": "",
- "taxonomy_candidate": None,
- "confidence": 0.0,
- "discount": {
- "requested": False,
- "self_deducted": False,
- "amount": None,
- "percentage": None,
- "currency": None
- },
- }
- def classify(row):
- prompt = f"""
- Ticket #{row["ticket_number"]}
- BETREFF:
- {row["title"] or ""}
- KUNDENBEITRAG:
- {row["customer_text"] or "(kein Kundentext vorhanden)"}
- KUNDENSERVICE-KONTEXT:
- {row["agent_text"] or "(kein Kundendiensttext vorhanden)"}
- WICHTIG:
- Auch ohne Kundentext muss das Ticket anhand von BETREFF und KUNDENSERVICE-KONTEXT klassifiziert werden.
- primary_intent MUSS exakt einem Wert aus der vorgegebenen Taxonomie entsprechen.
- Verwende KEINE selbst erfundenen Kategorien wie KUNDENSERVICE, ALLGEMEIN,
- KUNDENANFRAGE oder ähnliche freie Kategorien.
- Wenn kein spezifischer Intent sicher ableitbar ist, verwende SONSTIGES.
- taxonomy_fit MUSS exakt einer der Werte GOOD, PARTIAL oder POOR sein.
- confidence MUSS eine Zahl zwischen 0 und 1 sein.
- secondary_intents dürfen ausschließlich gültige Taxonomie-Intents enthalten.
- JSON-SCHEMA:
- {json.dumps(EXPECTED, ensure_ascii=False)}
- """
- payload = {
- "model": MODEL,
- "messages": [
- {"role": "system", "content": SYSTEM},
- {"role": "user", "content": prompt},
- ],
- "stream": False,
- "think": False,
- "format": "json",
- "options": {
- "temperature": 0,
- },
- }
- req = urllib.request.Request(
- URL,
- data=json.dumps(payload, ensure_ascii=False).encode(),
- headers={"Content-Type": "application/json"},
- )
- started = time.monotonic()
- with urllib.request.urlopen(req, timeout=300) as response:
- raw = json.loads(response.read())
- elapsed = time.monotonic() - started
- result = json.loads(raw["message"]["content"])
- # Harte Taxonomie-Grenze: Modell darf keine eigenen Kategorien erzeugen.
- primary = result.get("primary_intent")
- if not isinstance(primary, str) or primary.strip() not in PRIMARY_INTENTS:
- result["primary_intent"] = "SONSTIGES"
- else:
- result["primary_intent"] = primary.strip()
- fit = result.get("taxonomy_fit")
- if fit not in {"GOOD", "PARTIAL", "POOR"}:
- result["taxonomy_fit"] = "POOR"
- confidence = result.get("confidence")
- try:
- result["confidence"] = float(confidence)
- except (TypeError, ValueError):
- result["confidence"] = 0.0
- return row, result, elapsed
- def validate(result):
- if not isinstance(result, dict):
- return False, "JSON ist kein Objekt"
- primary = result.get("primary_intent")
- # Qwen darf bei fehlendem/unsinnigem Kundenanliegen
- # keinen leeren Primärintent zurückgeben.
- # Solche Fälle werden als SONSTIGES / POOR geführt.
- if not primary:
- fit = result.get("taxonomy_fit")
- if fit == "POOR":
- result["primary_intent"] = "SONSTIGES"
- primary = "SONSTIGES"
- if not result.get("taxonomy_candidate"):
- result["taxonomy_candidate"] = "KEINE_ANFRAGE"
- else:
- return False, "primary_intent fehlt"
- if primary not in PRIMARY_INTENTS:
- return False, f"Ungültiger primary_intent: {primary}"
- secondary = result.get("secondary_intents", [])
- tags = result.get("tags", [])
- if not isinstance(secondary, list):
- return False, "secondary_intents ist keine Liste"
- if not isinstance(tags, list):
- return False, "tags ist keine Liste"
- # Qwen verwechselt gelegentlich Tags mit secondary_intents.
- # Bekannte Tags werden automatisch in die Tag-Liste verschoben.
- secondary_clean = []
- for item in secondary:
- if item in TAGS:
- tags.append(item)
- elif item in PRIMARY_INTENTS:
- secondary_clean.append(item)
- else:
- return False, f"Ungültige secondary_intent: {item}"
- secondary = secondary_clean
- # Primärintent darf nicht zusätzlich als secondary_intent erscheinen.
- primary = result.get("primary_intent")
- secondary = [
- x for x in secondary
- if x != primary
- ]
- # Primärintents dürfen nicht als Tags erscheinen.
- tags = [
- x for x in tags
- if x not in PRIMARY_INTENTS
- ]
- tags = list(dict.fromkeys(tags))
- secondary = list(dict.fromkeys(secondary))
- invalid_tags = [
- x for x in tags
- if x not in TAGS
- ]
- if invalid_tags:
- return False, f"Ungültige Tags: {invalid_tags}"
- result["secondary_intents"] = secondary
- result["tags"] = tags
- fit = result.get("taxonomy_fit")
- if fit not in {"GOOD", "PARTIAL", "POOR"}:
- return False, f"Ungültiges taxonomy_fit: {fit}"
- # Rabattdaten validieren
- discount = result.get("discount", {})
- if not isinstance(discount, dict):
- return False, "discount ist kein Objekt"
- for key in ("requested", "self_deducted"):
- if key not in discount:
- discount[key] = False
- if not isinstance(discount[key], bool):
- return False, f"discount.{key} ist kein Boolean"
- for key in ("amount", "percentage"):
- if key not in discount:
- discount[key] = None
- if discount[key] is not None:
- try:
- discount[key] = float(discount[key])
- except Exception:
- return False, f"discount.{key} ist nicht numerisch"
- if discount[key] < 0:
- return False, f"discount.{key} ist negativ"
- if "currency" not in discount:
- discount["currency"] = None
- if discount["currency"] is not None:
- discount["currency"] = str(discount["currency"]).upper()
- result["discount"] = discount
- confidence = result.get("confidence")
- try:
- confidence = float(confidence)
- except Exception:
- return False, "confidence nicht numerisch"
- if not 0 <= confidence <= 1:
- return False, "confidence außerhalb 0..1"
- return True, ""
- def main():
- db = sqlite3.connect(DB)
- db.row_factory = sqlite3.Row
- # Zusätzliche Ergebnisfelder
- columns = {
- row[1]
- for row in db.execute("PRAGMA table_info(cases)")
- }
- fields = {
- "classification_json": "TEXT",
- "classification_error": "TEXT",
- "classification_seconds": "REAL",
- }
- for name, typ in fields.items():
- if name not in columns:
- db.execute(
- f"ALTER TABLE cases ADD COLUMN {name} {typ}"
- )
- db.commit()
- rows = db.execute("""
- SELECT
- ticket_id,
- ticket_number,
- title,
- customer_text,
- agent_text
- FROM cases
- WHERE analysis_status = 'ANALYZABLE'
- AND (
- classification_status IS NULL
- OR classification_status != 'DONE'
- )
- ORDER BY ticket_id
- """).fetchall()
- total = len(rows)
- print("=" * 80)
- print("ZAMMAD PRODUKTIONSKLASSIFIKATION")
- print("=" * 80)
- print(f"Modell: {MODEL}")
- print(f"Worker: {WORKERS}")
- print(f"Queue: {total}")
- print()
- if not rows:
- print("Keine offenen Tickets.")
- db.close()
- return
- completed = 0
- errors = 0
- started = time.monotonic()
- with ThreadPoolExecutor(max_workers=WORKERS) as executor:
- futures = {
- executor.submit(classify, row): row
- for row in rows
- }
- for future in as_completed(futures):
- row = futures[future]
- try:
- ticket, result, elapsed = future.result()
- valid, error = validate(result)
- if not valid:
- errors += 1
- db.execute("""
- UPDATE cases
- SET
- classification_status = 'ERROR',
- classification_error = ?,
- classification_seconds = ?
- WHERE ticket_id = ?
- """, (
- error,
- elapsed,
- ticket["ticket_id"],
- ))
- print(
- f"ERROR #{ticket['ticket_number']}: "
- f"{error}",
- flush=True,
- )
- else:
- # Normalisierte Tags
- tags = list(dict.fromkeys(
- result.get("tags", [])
- ))
- secondary = list(dict.fromkeys(
- result.get("secondary_intents", [])
- ))
- result["tags"] = tags
- result["secondary_intents"] = secondary
- db.execute("""
- UPDATE cases
- SET
- primary_intent = ?,
- secondary_intents = ?,
- tags = ?,
- classification_json = ?,
- taxonomy_fit = ?,
- taxonomy_candidate = ?,
- summary = ?,
- confidence = ?,
- classification_seconds = ?,
- classification_error = NULL,
- classification_status = 'DONE'
- WHERE ticket_id = ?
- """, (
- result["primary_intent"],
- json.dumps(
- secondary,
- ensure_ascii=False,
- ),
- json.dumps(
- tags,
- ensure_ascii=False,
- ),
- json.dumps(
- result,
- ensure_ascii=False,
- ),
- result["taxonomy_fit"],
- result.get("taxonomy_candidate"),
- result.get("summary"),
- float(result.get("confidence", 0)),
- elapsed,
- ticket["ticket_id"],
- ))
- completed += 1
- print(
- f"[{completed + errors}/{total}] "
- f"#{ticket['ticket_number']} "
- f"{elapsed:5.1f}s → "
- f"{result['primary_intent']} "
- f"secondary={result.get('secondary_intents') or []} "
- f"tags={result.get('tags') or []} "
- f"confidence={result.get('confidence')}",
- flush=True,
- )
- # Kleine Transaktionen:
- # Bei Abbruch bleibt praktisch alles bereits Geschriebene erhalten.
- db.commit()
- except Exception as exc:
- errors += 1
- db.execute("""
- UPDATE cases
- SET
- classification_status = 'ERROR',
- classification_error = ?
- WHERE ticket_id = ?
- """, (
- str(exc),
- row["ticket_id"],
- ))
- db.commit()
- print(
- f"ERROR #{row['ticket_number']}: {exc}",
- flush=True,
- )
- elapsed_total = time.monotonic() - started
- print()
- print("=" * 80)
- print("LAUF BEENDET")
- print("=" * 80)
- print(f"DONE: {completed}")
- print(f"ERROR: {errors}")
- print(f"Zeit: {elapsed_total / 3600:.2f} h")
- if elapsed_total:
- print(
- f"Durchsatz: "
- f"{completed / elapsed_total * 3600:.1f} Tickets/h"
- )
- db.close()
- if __name__ == "__main__":
- main()
|