| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348 |
- #!/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://127.0.0.1:11434/api/chat"
- MODEL = "qwen3:8b"
- WORKERS = 2
- TEST_LIMIT = 10
- TAXONOMY = """
- 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 / MERKMALE
- QUALITAETSPROBLEM
- VERWECHSLUNG
- SCHLECHTVERPACKT
- FALSCHESORTEGELIEFERT
- PROBLEMMITLIEFERDIENST
- LIEFERDIENST_DHL
- LIEFERDIENST_GLS
- LIEFERDIENST_UNBEKANNT
- TRANSPORTSCHADEN
- PFLANZENKRANKHEIT
- PFLANZENSCHADEN
- WACHSTUMSPROBLEM
- """
- SYSTEM = f"""
- Du analysierst historische Kundenservice-Tickets von Schmid Gartenpflanzen.
- Bestimme das tatsächliche Kundenanliegen anhand des KUNDENBEITRAGS.
- Die Antwort des Kundenservice dient nur als Kontext.
- {TAXONOMY}
- WICHTIGE KLASSIFIKATIONSREGELN:
- 1. BESTANDSANFRAGE
- Jede Frage nach aktueller oder zukünftiger Verfügbarkeit,
- Lieferbarkeit oder Bestellbarkeit eines Produkts.
- Beispiele:
- - "Wann ist die Rose wieder verfügbar?"
- - "Ab wann wieder bestellbar?"
- - "Ist die Sorte im Herbst wieder lieferbar?"
- - "Können Sie mich informieren, sobald sie wieder verfügbar ist?"
- - "Kann ich die Rose im Juli bekommen?"
- Alle diese Fälle sind BESTANDSANFRAGE.
- 2. VORBESTELLUNG
- VORBESTELLUNG nur verwenden, wenn der Kunde tatsächlich eine
- Vormerkung, Reservierung oder verbindliche Vorabbestellung verlangt.
- Eine reine Frage nach der zukünftigen Verfügbarkeit ist KEINE
- VORBESTELLUNG.
- 3. WIDERRUF
- Stornierung oder Rückabwicklung einer Bestellung auf Kundenwunsch,
- einschließlich Stornierung vor Versand.
- 4. REKLAMATION
- Beanstandung eines gelieferten Produkts oder einer erbrachten
- Leistung wegen Qualität, Zustand oder Beschädigung.
- 5. TRANSPORTSCHADEN
- Wenn ein Schaden offensichtlich durch Transport oder Versand
- entstanden ist. Kann zusätzlich als Tag TRANSPORTSCHADEN erscheinen.
- 6. FEHLLIEFERUNG
- Falscher Artikel, falsche Sorte oder falsches Produkt geliefert.
- 7. DÜNGER_FEHLT
- Bestellter Dünger fehlt in der Lieferung.
- 8. QUALITÄT
- QUALITAETSPROBLEM ist kein eigener Primärintent.
- Es ist ein Tag und kann zusammen mit REKLAMATION verwendet werden.
- PFLANZENKRANKHEIT, PFLANZENSCHADEN und WACHSTUMSPROBLEM sind
- ebenfalls Merkmale/Tags.
- 9. LIEFERDIENST
- PROBLEMMITLIEFERDIENST ist ein Problem-/Sekundärmerkmal.
- Wenn DHL ausdrücklich genannt wird:
- LIEFERDIENST_DHL
- Wenn GLS ausdrücklich genannt wird:
- LIEFERDIENST_GLS
- Wenn ein Paketdienstproblem vorliegt, aber der Dienst unbekannt ist:
- LIEFERDIENST_UNBEKANNT
- 10. MULTI-INTENT
- Mehrere gleichzeitig vorhandene Kundenanliegen müssen erfasst werden.
- primary_intent enthält das wichtigste Anliegen.
- secondary_intents enthält weitere relevante Primärintents.
- tags enthält konkrete Problem- und Kontextmerkmale.
- 11. TAXONOMIE-LÜCKEN
- Keine neuen Primärintents eigenmächtig erfinden.
- Wenn kein Primärintent sinnvoll passt:
- primary_intent = SONSTIGES
- taxonomy_fit = POOR oder PARTIAL
- taxonomy_candidate = möglicher fehlender Intent.
- Antworte ausschließlich als gültiges JSON.
- """
- OUTPUT_SCHEMA = """
- {
- "primary_intent": "...",
- "secondary_intents": [],
- "tags": [],
- "complaint_type": null,
- "actions": [],
- "summary": "...",
- "taxonomy_fit": "GOOD|PARTIAL|POOR",
- "taxonomy_candidate": null,
- "confidence": 0.0
- }
- """
- SYSTEM += "\nErwartetes JSON-Format:\n" + OUTPUT_SCHEMA
- def classify(row):
- prompt = f"""
- Ticket #{row["ticket_number"]}
- Betreff:
- {row["title"] or ""}
- KUNDENBEITRAG:
- {row["customer_text"] or ""}
- KUNDENSERVICE-KONTEXT:
- {row["agent_text"] or ""}
- """
- payload = {
- "model": MODEL,
- "messages": [
- {
- "role": "system",
- "content": SYSTEM,
- },
- {
- "role": "user",
- "content": prompt,
- },
- ],
- "stream": False,
- "format": "json",
- "options": {
- "temperature": 0,
- },
- }
- request = urllib.request.Request(
- URL,
- data=json.dumps(payload).encode(),
- headers={
- "Content-Type": "application/json",
- },
- )
- started = time.monotonic()
- with urllib.request.urlopen(
- request,
- timeout=300,
- ) as response:
- result = json.loads(response.read())
- elapsed = time.monotonic() - started
- content = result["message"]["content"]
- parsed = json.loads(content)
- return row, elapsed, parsed
- def main():
- db = sqlite3.connect(DB)
- db.row_factory = sqlite3.Row
- rows = db.execute("""
- SELECT
- ticket_id,
- ticket_number,
- title,
- customer_text,
- agent_text
- FROM cases
- WHERE analysis_status = 'ANALYZABLE'
- ORDER BY ticket_id
- LIMIT ?
- """, (TEST_LIMIT,)).fetchall()
- db.close()
- print("=" * 80)
- print("ZAMMAD TAXONOMIE V2 – PARALLEL BENCHMARK")
- print("=" * 80)
- print(f"Modell: {MODEL}")
- print(f"Tickets: {len(rows)}")
- print(f"Worker: {WORKERS}")
- print()
- started = time.monotonic()
- results = []
- with ThreadPoolExecutor(max_workers=WORKERS) as executor:
- futures = [
- executor.submit(classify, row)
- for row in rows
- ]
- for i, future in enumerate(
- as_completed(futures),
- 1,
- ):
- try:
- row, elapsed, result = future.result()
- results.append(
- (
- row,
- elapsed,
- result,
- )
- )
- print(
- f"[{i:02}/{len(rows)}] "
- f"#{row['ticket_number']} "
- f"{elapsed:6.1f}s → "
- f"{result.get('primary_intent')} / "
- f"{result.get('taxonomy_fit')} / "
- f"{result.get('confidence')}"
- )
- if result.get("secondary_intents"):
- print(
- f" secondary: "
- f"{result['secondary_intents']}"
- )
- if result.get("tags"):
- print(
- f" tags: "
- f"{result['tags']}"
- )
- if result.get("taxonomy_candidate"):
- print(
- f" candidate: "
- f"{result['taxonomy_candidate']}"
- )
- except Exception as exc:
- print(
- f"[{i:02}/{len(rows)}] ERROR: {exc}"
- )
- total = time.monotonic() - started
- print()
- print("=" * 80)
- print("BENCHMARK")
- print("=" * 80)
- if results:
- print(f"Erfolgreich: {len(results)}")
- print(f"Gesamtzeit: {total:.1f}s")
- print(
- f"Ø Einzelrequest: "
- f"{sum(x[1] for x in results) / len(results):.1f}s"
- )
- print(
- f"Durchsatz: "
- f"{len(results) / total * 3600:.1f} Tickets/Stunde"
- )
- throughput = len(results) / total * 3600
- print(
- f"Hochrechnung 4404: "
- f"{4404 / throughput:.1f} Stunden"
- )
- print(
- f"Hochrechnung Tage: "
- f"{4404 / throughput / 24:.2f}"
- )
- if __name__ == "__main__":
- main()
|