benchmark_zammad_taxonomy.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. #!/usr/bin/env python3
  2. import json
  3. import sqlite3
  4. import time
  5. import urllib.request
  6. from concurrent.futures import ThreadPoolExecutor, as_completed
  7. DB = "data/zammad_analysis.sqlite3"
  8. URL = "http://127.0.0.1:11434/api/chat"
  9. MODEL = "qwen3:8b"
  10. WORKERS = 2
  11. TEST_LIMIT = 10
  12. TAXONOMY = """
  13. PRIMÄRINTENTS
  14. VERSANDSTATUS
  15. LIEFERVERZUG
  16. ADRESSÄNDERUNG
  17. REKLAMATION
  18. TRANSPORTSCHADEN
  19. FEHLLIEFERUNG
  20. RECHNUNG
  21. ZAHLUNG
  22. WIDERRUF
  23. RETOURE
  24. PFLANZENBERATUNG
  25. SORTENBERATUNG
  26. PFLEGEFRAGE
  27. BESTANDSANFRAGE
  28. VORBESTELLUNG
  29. B2B
  30. GROSSHANDEL
  31. DÜNGER_FEHLT
  32. SONSTIGES
  33. TAGS / MERKMALE
  34. QUALITAETSPROBLEM
  35. VERWECHSLUNG
  36. SCHLECHTVERPACKT
  37. FALSCHESORTEGELIEFERT
  38. PROBLEMMITLIEFERDIENST
  39. LIEFERDIENST_DHL
  40. LIEFERDIENST_GLS
  41. LIEFERDIENST_UNBEKANNT
  42. TRANSPORTSCHADEN
  43. PFLANZENKRANKHEIT
  44. PFLANZENSCHADEN
  45. WACHSTUMSPROBLEM
  46. """
  47. SYSTEM = f"""
  48. Du analysierst historische Kundenservice-Tickets von Schmid Gartenpflanzen.
  49. Bestimme das tatsächliche Kundenanliegen anhand des KUNDENBEITRAGS.
  50. Die Antwort des Kundenservice dient nur als Kontext.
  51. {TAXONOMY}
  52. WICHTIGE KLASSIFIKATIONSREGELN:
  53. 1. BESTANDSANFRAGE
  54. Jede Frage nach aktueller oder zukünftiger Verfügbarkeit,
  55. Lieferbarkeit oder Bestellbarkeit eines Produkts.
  56. Beispiele:
  57. - "Wann ist die Rose wieder verfügbar?"
  58. - "Ab wann wieder bestellbar?"
  59. - "Ist die Sorte im Herbst wieder lieferbar?"
  60. - "Können Sie mich informieren, sobald sie wieder verfügbar ist?"
  61. - "Kann ich die Rose im Juli bekommen?"
  62. Alle diese Fälle sind BESTANDSANFRAGE.
  63. 2. VORBESTELLUNG
  64. VORBESTELLUNG nur verwenden, wenn der Kunde tatsächlich eine
  65. Vormerkung, Reservierung oder verbindliche Vorabbestellung verlangt.
  66. Eine reine Frage nach der zukünftigen Verfügbarkeit ist KEINE
  67. VORBESTELLUNG.
  68. 3. WIDERRUF
  69. Stornierung oder Rückabwicklung einer Bestellung auf Kundenwunsch,
  70. einschließlich Stornierung vor Versand.
  71. 4. REKLAMATION
  72. Beanstandung eines gelieferten Produkts oder einer erbrachten
  73. Leistung wegen Qualität, Zustand oder Beschädigung.
  74. 5. TRANSPORTSCHADEN
  75. Wenn ein Schaden offensichtlich durch Transport oder Versand
  76. entstanden ist. Kann zusätzlich als Tag TRANSPORTSCHADEN erscheinen.
  77. 6. FEHLLIEFERUNG
  78. Falscher Artikel, falsche Sorte oder falsches Produkt geliefert.
  79. 7. DÜNGER_FEHLT
  80. Bestellter Dünger fehlt in der Lieferung.
  81. 8. QUALITÄT
  82. QUALITAETSPROBLEM ist kein eigener Primärintent.
  83. Es ist ein Tag und kann zusammen mit REKLAMATION verwendet werden.
  84. PFLANZENKRANKHEIT, PFLANZENSCHADEN und WACHSTUMSPROBLEM sind
  85. ebenfalls Merkmale/Tags.
  86. 9. LIEFERDIENST
  87. PROBLEMMITLIEFERDIENST ist ein Problem-/Sekundärmerkmal.
  88. Wenn DHL ausdrücklich genannt wird:
  89. LIEFERDIENST_DHL
  90. Wenn GLS ausdrücklich genannt wird:
  91. LIEFERDIENST_GLS
  92. Wenn ein Paketdienstproblem vorliegt, aber der Dienst unbekannt ist:
  93. LIEFERDIENST_UNBEKANNT
  94. 10. MULTI-INTENT
  95. Mehrere gleichzeitig vorhandene Kundenanliegen müssen erfasst werden.
  96. primary_intent enthält das wichtigste Anliegen.
  97. secondary_intents enthält weitere relevante Primärintents.
  98. tags enthält konkrete Problem- und Kontextmerkmale.
  99. 11. TAXONOMIE-LÜCKEN
  100. Keine neuen Primärintents eigenmächtig erfinden.
  101. Wenn kein Primärintent sinnvoll passt:
  102. primary_intent = SONSTIGES
  103. taxonomy_fit = POOR oder PARTIAL
  104. taxonomy_candidate = möglicher fehlender Intent.
  105. Antworte ausschließlich als gültiges JSON.
  106. """
  107. OUTPUT_SCHEMA = """
  108. {
  109. "primary_intent": "...",
  110. "secondary_intents": [],
  111. "tags": [],
  112. "complaint_type": null,
  113. "actions": [],
  114. "summary": "...",
  115. "taxonomy_fit": "GOOD|PARTIAL|POOR",
  116. "taxonomy_candidate": null,
  117. "confidence": 0.0
  118. }
  119. """
  120. SYSTEM += "\nErwartetes JSON-Format:\n" + OUTPUT_SCHEMA
  121. def classify(row):
  122. prompt = f"""
  123. Ticket #{row["ticket_number"]}
  124. Betreff:
  125. {row["title"] or ""}
  126. KUNDENBEITRAG:
  127. {row["customer_text"] or ""}
  128. KUNDENSERVICE-KONTEXT:
  129. {row["agent_text"] or ""}
  130. """
  131. payload = {
  132. "model": MODEL,
  133. "messages": [
  134. {
  135. "role": "system",
  136. "content": SYSTEM,
  137. },
  138. {
  139. "role": "user",
  140. "content": prompt,
  141. },
  142. ],
  143. "stream": False,
  144. "format": "json",
  145. "options": {
  146. "temperature": 0,
  147. },
  148. }
  149. request = urllib.request.Request(
  150. URL,
  151. data=json.dumps(payload).encode(),
  152. headers={
  153. "Content-Type": "application/json",
  154. },
  155. )
  156. started = time.monotonic()
  157. with urllib.request.urlopen(
  158. request,
  159. timeout=300,
  160. ) as response:
  161. result = json.loads(response.read())
  162. elapsed = time.monotonic() - started
  163. content = result["message"]["content"]
  164. parsed = json.loads(content)
  165. return row, elapsed, parsed
  166. def main():
  167. db = sqlite3.connect(DB)
  168. db.row_factory = sqlite3.Row
  169. rows = db.execute("""
  170. SELECT
  171. ticket_id,
  172. ticket_number,
  173. title,
  174. customer_text,
  175. agent_text
  176. FROM cases
  177. WHERE analysis_status = 'ANALYZABLE'
  178. ORDER BY ticket_id
  179. LIMIT ?
  180. """, (TEST_LIMIT,)).fetchall()
  181. db.close()
  182. print("=" * 80)
  183. print("ZAMMAD TAXONOMIE V2 – PARALLEL BENCHMARK")
  184. print("=" * 80)
  185. print(f"Modell: {MODEL}")
  186. print(f"Tickets: {len(rows)}")
  187. print(f"Worker: {WORKERS}")
  188. print()
  189. started = time.monotonic()
  190. results = []
  191. with ThreadPoolExecutor(max_workers=WORKERS) as executor:
  192. futures = [
  193. executor.submit(classify, row)
  194. for row in rows
  195. ]
  196. for i, future in enumerate(
  197. as_completed(futures),
  198. 1,
  199. ):
  200. try:
  201. row, elapsed, result = future.result()
  202. results.append(
  203. (
  204. row,
  205. elapsed,
  206. result,
  207. )
  208. )
  209. print(
  210. f"[{i:02}/{len(rows)}] "
  211. f"#{row['ticket_number']} "
  212. f"{elapsed:6.1f}s → "
  213. f"{result.get('primary_intent')} / "
  214. f"{result.get('taxonomy_fit')} / "
  215. f"{result.get('confidence')}"
  216. )
  217. if result.get("secondary_intents"):
  218. print(
  219. f" secondary: "
  220. f"{result['secondary_intents']}"
  221. )
  222. if result.get("tags"):
  223. print(
  224. f" tags: "
  225. f"{result['tags']}"
  226. )
  227. if result.get("taxonomy_candidate"):
  228. print(
  229. f" candidate: "
  230. f"{result['taxonomy_candidate']}"
  231. )
  232. except Exception as exc:
  233. print(
  234. f"[{i:02}/{len(rows)}] ERROR: {exc}"
  235. )
  236. total = time.monotonic() - started
  237. print()
  238. print("=" * 80)
  239. print("BENCHMARK")
  240. print("=" * 80)
  241. if results:
  242. print(f"Erfolgreich: {len(results)}")
  243. print(f"Gesamtzeit: {total:.1f}s")
  244. print(
  245. f"Ø Einzelrequest: "
  246. f"{sum(x[1] for x in results) / len(results):.1f}s"
  247. )
  248. print(
  249. f"Durchsatz: "
  250. f"{len(results) / total * 3600:.1f} Tickets/Stunde"
  251. )
  252. throughput = len(results) / total * 3600
  253. print(
  254. f"Hochrechnung 4404: "
  255. f"{4404 / throughput:.1f} Stunden"
  256. )
  257. print(
  258. f"Hochrechnung Tage: "
  259. f"{4404 / throughput / 24:.2f}"
  260. )
  261. if __name__ == "__main__":
  262. main()