| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265 |
- #!/usr/bin/env python3
- import json
- import re
- import sqlite3
- import subprocess
- import time
- DB = "data/zammad_analysis.sqlite3"
- MODEL = "qwen3:8b"
- TAXONOMY = """
- VERSANDSTATUS
- LIEFERVERZUG
- ADRESSÄNDERUNG
- REKLAMATION
- TRANSPORTSCHADEN
- FEHLLIEFERUNG
- RECHNUNG
- ZAHLUNG
- WIDERRUF
- RETOURE
- PFLANZENBERATUNG
- SORTENBERATUNG
- PFLEGEFRAGE
- BESTANDSANFRAGE
- VORBESTELLUNG
- B2B
- GROSSHANDEL
- DÜNGER_FEHLT
- SONSTIGES
- Regeln:
- - PRODUKTQUALITÄT gehört unter REKLAMATION oder PFLANZENBERATUNG.
- - SCHLECHTVERPACKT gehört unter REKLAMATION bzw. TRANSPORTSCHADEN, wenn ein Transportschaden vorliegt.
- - FALSCHESORTEGELIEFERT gehört unter FEHLLIEFERUNG.
- - DÜNGER_FEHLT ist ein eigener wichtiger Intent.
- - Mehrere gleichzeitig vorhandene Anliegen müssen als Multi-Intent erfasst werden.
- - primary_intent ist das wichtigste Kundenanliegen.
- - secondary_intents enthält weitere relevante Anliegen.
- - Erfinde keinen neuen Primärintent.
- - Wenn kein bestehender Intent passt, nutze SONSTIGES und schlage taxonomy_candidate vor.
- """
- SYSTEM = f"""
- Du analysierst historische Kundenservice-Tickets von Schmid Gartenpflanzen.
- Bewerte das tatsächliche Kundenanliegen, nicht nur die Antwort des Mitarbeiters.
- Bestehende Taxonomie:
- {TAXONOMY}
- Gib ausschließlich gültiges JSON zurück:
- {{
- "primary_intent": "...",
- "secondary_intents": [],
- "complaint_type": null,
- "actions": [],
- "summary": "...",
- "taxonomy_fit": "GOOD|PARTIAL|POOR",
- "taxonomy_candidate": null,
- "confidence": 0.0
- }}
- Bei REKLAMATION oder FEHLLIEFERUNG soll complaint_type möglichst konkret sein.
- actions beschreibt, was der Kundenservice getan oder als Lösung angeboten hat.
- """
- def parse_json(text):
- text = text.strip()
- # Markdown-Codeblock entfernen
- text = re.sub(
- r"^```(?:json)?\s*",
- "",
- text,
- flags=re.I,
- )
- text = re.sub(
- r"\s*```$",
- "",
- text,
- )
- start = text.find("{")
- end = text.rfind("}")
- if start < 0 or end <= start:
- raise ValueError("Kein JSON gefunden")
- return json.loads(
- text[start:end + 1]
- )
- def run_qwen(case_text):
- prompt = f"""
- {SYSTEM}
- TICKET:
- {case_text}
- """
- result = subprocess.run(
- [
- "ollama",
- "run",
- MODEL,
- prompt,
- ],
- capture_output=True,
- text=True,
- timeout=600,
- )
- if result.returncode != 0:
- raise RuntimeError(
- result.stderr[-2000:]
- )
- return parse_json(result.stdout)
- db = sqlite3.connect(DB)
- db.row_factory = sqlite3.Row
- rows = db.execute("""
- SELECT
- ticket_id,
- ticket_number,
- title,
- customer_text,
- agent_text,
- unknown_text
- FROM cases
- WHERE classification_status = 'NEW'
- ORDER BY ticket_id
- """).fetchall()
- print("=" * 72)
- print("ZAMMAD TAXONOMIE – QWEN")
- print("=" * 72)
- print(f"Cases offen: {len(rows)}")
- print(f"Modell: {MODEL}")
- print("Worker: 1")
- print()
- started = time.time()
- done = 0
- errors = 0
- for pos, row in enumerate(rows, 1):
- case_text = f"""
- Ticket #{row['ticket_number']}
- Betreff: {row['title'] or ''}
- KUNDENBEITRÄGE:
- {row['customer_text'] or ''}
- ANTWORTEN DES KUNDENSERVICE:
- {row['agent_text'] or ''}
- SONSTIGE FACHLICHE SYSTEMANTWORTEN:
- {row['unknown_text'] or ''}
- """
- try:
- result = run_qwen(case_text)
- db.execute("""
- UPDATE cases
- SET classification_status = 'CLASSIFIED',
- primary_intent = ?,
- secondary_intents = ?,
- complaint_type = ?,
- actions = ?,
- summary = ?,
- taxonomy_fit = ?,
- taxonomy_candidate = ?,
- classification_json = ?
- WHERE ticket_id = ?
- """, (
- result.get("primary_intent"),
- json.dumps(
- result.get("secondary_intents", []),
- ensure_ascii=False,
- ),
- result.get("complaint_type"),
- json.dumps(
- result.get("actions", []),
- ensure_ascii=False,
- ),
- result.get("summary"),
- result.get("taxonomy_fit"),
- result.get("taxonomy_candidate"),
- json.dumps(
- result,
- ensure_ascii=False,
- ),
- row["ticket_id"],
- ))
- db.commit()
- done += 1
- elapsed = time.time() - started
- rate = done / elapsed if elapsed else 0
- remaining = (
- (len(rows) - done) / rate
- if rate
- else 0
- )
- print(
- f"[{pos}/{len(rows)}] "
- f"#{row['ticket_number']} "
- f"→ {result.get('primary_intent')} "
- f"({result.get('confidence', 0)}) "
- f"| noch ~{remaining / 60:.0f} min",
- flush=True,
- )
- except Exception as exc:
- errors += 1
- db.execute("""
- UPDATE cases
- SET classification_status = 'ERROR'
- WHERE ticket_id = ?
- """, (row["ticket_id"],))
- db.commit()
- print(
- f"[{pos}/{len(rows)}] "
- f"#{row['ticket_number']} "
- f"ERROR: {exc}",
- flush=True,
- )
- print()
- print("=" * 72)
- print("QWEN-LAUF BEENDET")
- print("=" * 72)
- print(f"Erfolgreich: {done}")
- print(f"Fehler: {errors}")
- for status, count in db.execute("""
- SELECT classification_status, COUNT(*)
- FROM cases
- GROUP BY classification_status
- ORDER BY classification_status
- """):
- print(f"{status:20} {count}")
- db.close()
|