reclassify_historical_from_analysis.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. #!/usr/bin/env python3
  2. import asyncio
  3. import json
  4. import sqlite3
  5. import urllib.request
  6. from collections import Counter
  7. from pathlib import Path
  8. DB = "data/telephony.sqlite3"
  9. OUT = Path("data/historical_multi_intent.json")
  10. OLLAMA = "http://127.0.0.1:11434/api/generate"
  11. MODEL = "qwen3:8b"
  12. INTENTS = [
  13. "VERSANDSTATUS",
  14. "LIEFERVERZUG",
  15. "ADRESSÄNDERUNG",
  16. "REKLAMATION",
  17. "TRANSPORTSCHADEN",
  18. "FEHLLIEFERUNG",
  19. "RECHNUNG",
  20. "ZAHLUNG",
  21. "WIDERRUF",
  22. "RETOURE",
  23. "PFLANZENBERATUNG",
  24. "SORTENBERATUNG",
  25. "PFLEGEFRAGE",
  26. "BESTANDSANFRAGE",
  27. "VORBESTELLUNG",
  28. "BESTELLUNG",
  29. "BESTELLÄNDERUNG",
  30. "STORNIERUNG",
  31. "B2B",
  32. "GROSSHANDEL",
  33. "SONSTIGES",
  34. ]
  35. ACTIONS = [
  36. "KEINE_AKTION",
  37. "KUNDENKONTEXT_PRÜFEN",
  38. "BESTELLUNG_PRÜFEN",
  39. "BESTELLUNG_ÄNDERN",
  40. "BESTELLUNG_STORNIEREN",
  41. "BESTELLUNGEN_ZUSAMMENFÜHREN",
  42. "VERSANDSTATUS_PRÜFEN",
  43. "LIEFERTERMIN_PRÜFEN",
  44. "ADRESSE_ÄNDERN",
  45. "REKLAMATION_ERFASSEN",
  46. "TRANSPORTSCHADEN_ERFASSEN",
  47. "FEHLLIEFERUNG_PRÜFEN",
  48. "RECHNUNG_PRÜFEN",
  49. "RECHNUNG_KORRIGIEREN",
  50. "ZAHLUNG_PRÜFEN",
  51. "GUTSCHRIFT_ERSTELLEN",
  52. "RETOURE_ERFASSEN",
  53. "PRODUKT_IDENTIFIZIEREN",
  54. "BESTAND_PRÜFEN",
  55. "SORTEN_ALTERNATIVE_PRÜFEN",
  56. "PFLEGEHINWEIS_GEBEN",
  57. "BILD_ANFORDERN",
  58. "MENSCHLICHE_BEARBEITUNG_ERFORDERLICH",
  59. "RÜCKRUF",
  60. "E-MAIL_SENDEN",
  61. ]
  62. def ask(prompt):
  63. payload = {
  64. "model": MODEL,
  65. "prompt": prompt,
  66. "stream": False,
  67. "format": "json",
  68. "options": {"temperature": 0},
  69. }
  70. req = urllib.request.Request(
  71. OLLAMA,
  72. data=json.dumps(payload, ensure_ascii=False).encode(),
  73. headers={"Content-Type": "application/json"},
  74. method="POST",
  75. )
  76. with urllib.request.urlopen(req, timeout=300) as r:
  77. return json.loads(r.read().decode())["response"]
  78. def parse(text):
  79. text = text.strip()
  80. if text.startswith("```"):
  81. text = text.replace("```json", "", 1)
  82. text = text.replace("```", "").strip()
  83. return json.loads(text)
  84. async def main():
  85. con = sqlite3.connect(DB)
  86. con.row_factory = sqlite3.Row
  87. rows = con.execute("""
  88. SELECT id AS analysis_id, analysis_json
  89. FROM analyses
  90. WHERE cdr_row_id IS NOT NULL
  91. ORDER BY id
  92. """).fetchall()
  93. con.close()
  94. results = []
  95. for n, row in enumerate(rows, 1):
  96. old = json.loads(row["analysis_json"] or "{}")
  97. a = old.get("analysis") or {}
  98. customer = old.get("customer") or {}
  99. products = old.get("products") or []
  100. print(f"[{n}/{len(rows)}] analysis={row['analysis_id']}")
  101. evidence = {
  102. "old_intent": a.get("intent"),
  103. "summary": a.get("summary"),
  104. "advice": a.get("advice"),
  105. "follow_up": a.get("follow_up"),
  106. "customer_number": customer.get("customer_number"),
  107. "order_number": customer.get("order_number"),
  108. "products": products,
  109. "uncertainties": old.get("uncertainties"),
  110. }
  111. prompt = f"""
  112. Du bist der Intent- und Aktionsklassifikator einer Telefon-KI.
  113. Analysiere die folgende bereits erstellte Gesprächsanalyse.
  114. WICHTIG:
  115. Ein Gespräch kann mehrere Anliegen enthalten.
  116. Verwende deshalb primary_intent UND secondary_intents.
  117. Die vorhandene Analyse wurde bereits aus dem echten Telefonat
  118. erzeugt. Nutze sie als Tatsachengrundlage. Erfinde nichts.
  119. INTENTS:
  120. {json.dumps(INTENTS, ensure_ascii=False)}
  121. AKTIONEN:
  122. {json.dumps(ACTIONS, ensure_ascii=False)}
  123. KLASSIFIKATIONSREGELN:
  124. - Eine konkrete Bestellung bearbeiten → BESTELLUNG
  125. - Eine bestehende Bestellung ergänzen/verändern → BESTELLÄNDERUNG
  126. - Bestellung aufheben → STORNIERUNG
  127. - Verfügbarkeit → BESTANDSANFRAGE
  128. - Lieferung verfolgen → VERSANDSTATUS
  129. - verspätete Lieferung → LIEFERVERZUG
  130. - Rechnung/Gutschrift/Rechnungsfehler → RECHNUNG
  131. - Zahlung → ZAHLUNG
  132. - beschädigte Lieferung → TRANSPORTSCHADEN
  133. - falsche Lieferung → FEHLLIEFERUNG
  134. - Qualitätsbeanstandung → REKLAMATION
  135. - Pflanzenauswahl/Standort → PFLANZENBERATUNG
  136. - konkrete Sortenauswahl → SORTENBERATUNG
  137. - Pflege/Krankheit/Schnitt/Düngung → PFLEGEFRAGE
  138. - Rückgabe → RETOURE
  139. - Rücktritt vom Kauf → WIDERRUF
  140. MEHRERE THEMEN:
  141. Wenn z.B. eine Bestellung ergänzt UND gleichzeitig der
  142. Versandstatus einer anderen Bestellung besprochen wird:
  143. primary_intent = BESTELLÄNDERUNG
  144. secondary_intents = [BESTELLUNG, VERSANDSTATUS]
  145. Wenn eine Reklamation wegen beschädigter Pflanzen besprochen wird:
  146. primary_intent = REKLAMATION
  147. secondary_intents = [TRANSPORTSCHADEN]
  148. Wenn eine Rechnung korrigiert und eine Gutschrift erstellt werden soll:
  149. primary_intent = RECHNUNG
  150. actions = [RECHNUNG_PRÜFEN, RECHNUNG_KORRIGIEREN, GUTSCHRIFT_ERSTELLEN]
  151. SONSTIGES:
  152. Nur verwenden, wenn wirklich kein Intent aus der Liste passt.
  153. Wenn die vorhandene Analyse einen eindeutigen Geschäftsfall
  154. beschreibt, DARF primary_intent NICHT SONSTIGES sein.
  155. EVIDENZ:
  156. {json.dumps(evidence, ensure_ascii=False, indent=2)}
  157. Antworte ausschließlich als JSON:
  158. {{
  159. "call_state": "HUMAN_CONVERSATION",
  160. "primary_intent": "INTENT",
  161. "secondary_intents": [],
  162. "customer_goal": "",
  163. "actions": [],
  164. "requires_customer_context": true,
  165. "requires_order_context": false,
  166. "requires_product_context": false,
  167. "requires_previous_contact_context": false,
  168. "requires_human_action": false,
  169. "confidence": 0.0,
  170. "reason": ""
  171. }}
  172. """
  173. try:
  174. result = parse(
  175. await asyncio.to_thread(ask, prompt)
  176. )
  177. except Exception as exc:
  178. result = {
  179. "call_state": "UNKNOWN",
  180. "primary_intent": "SONSTIGES",
  181. "secondary_intents": [],
  182. "customer_goal": "",
  183. "actions": [
  184. "MENSCHLICHE_BEARBEITUNG_ERFORDERLICH"
  185. ],
  186. "confidence": 0,
  187. "reason": str(exc),
  188. }
  189. if result.get("primary_intent") not in INTENTS:
  190. result["primary_intent"] = "SONSTIGES"
  191. result["secondary_intents"] = [
  192. x for x in result.get("secondary_intents", [])
  193. if x in INTENTS and x != result["primary_intent"]
  194. ]
  195. result["actions"] = [
  196. x for x in result.get("actions", [])
  197. if x in ACTIONS
  198. ]
  199. result["_analysis_id"] = row["analysis_id"]
  200. results.append(result)
  201. primary = Counter(r["primary_intent"] for r in results)
  202. secondary = Counter(
  203. x
  204. for r in results
  205. for x in r["secondary_intents"]
  206. )
  207. actions = Counter(
  208. x
  209. for r in results
  210. for x in r["actions"]
  211. )
  212. states = Counter(
  213. r.get("call_state", "UNKNOWN")
  214. for r in results
  215. )
  216. output = {
  217. "model": MODEL,
  218. "source": "existing_analysis_json",
  219. "count": len(results),
  220. "call_states": dict(states.most_common()),
  221. "primary_intents": dict(primary.most_common()),
  222. "secondary_intents": dict(secondary.most_common()),
  223. "actions": dict(actions.most_common()),
  224. "calls": results,
  225. }
  226. OUT.write_text(
  227. json.dumps(output, ensure_ascii=False, indent=2)
  228. )
  229. print("\n" + "=" * 72)
  230. print("RE-KLASSIFIKATION FERTIG")
  231. print("=" * 72)
  232. print("\nCALL STATES")
  233. for k, v in states.most_common():
  234. print(f"{v:3} {k}")
  235. print("\nPRIMARY")
  236. for k, v in primary.most_common():
  237. print(f"{v:3} {k}")
  238. print("\nSECONDARY")
  239. for k, v in secondary.most_common():
  240. print(f"{v:3} {k}")
  241. print("\nACTIONS")
  242. for k, v in actions.most_common():
  243. print(f"{v:3} {k}")
  244. print(f"\nGespeichert: {OUT}")
  245. if __name__ == "__main__":
  246. asyncio.run(main())