| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297 |
- #!/usr/bin/env python3
- import asyncio
- import json
- import sqlite3
- import urllib.request
- from collections import Counter
- from pathlib import Path
- DB = "data/telephony.sqlite3"
- OUT = Path("data/historical_multi_intent.json")
- OLLAMA = "http://127.0.0.1:11434/api/generate"
- MODEL = "qwen3:8b"
- INTENTS = [
- "VERSANDSTATUS",
- "LIEFERVERZUG",
- "ADRESSÄNDERUNG",
- "REKLAMATION",
- "TRANSPORTSCHADEN",
- "FEHLLIEFERUNG",
- "RECHNUNG",
- "ZAHLUNG",
- "WIDERRUF",
- "RETOURE",
- "PFLANZENBERATUNG",
- "SORTENBERATUNG",
- "PFLEGEFRAGE",
- "BESTANDSANFRAGE",
- "VORBESTELLUNG",
- "BESTELLUNG",
- "BESTELLÄNDERUNG",
- "STORNIERUNG",
- "B2B",
- "GROSSHANDEL",
- "SONSTIGES",
- ]
- ACTIONS = [
- "KEINE_AKTION",
- "KUNDENKONTEXT_PRÜFEN",
- "BESTELLUNG_PRÜFEN",
- "BESTELLUNG_ÄNDERN",
- "BESTELLUNG_STORNIEREN",
- "BESTELLUNGEN_ZUSAMMENFÜHREN",
- "VERSANDSTATUS_PRÜFEN",
- "LIEFERTERMIN_PRÜFEN",
- "ADRESSE_ÄNDERN",
- "REKLAMATION_ERFASSEN",
- "TRANSPORTSCHADEN_ERFASSEN",
- "FEHLLIEFERUNG_PRÜFEN",
- "RECHNUNG_PRÜFEN",
- "RECHNUNG_KORRIGIEREN",
- "ZAHLUNG_PRÜFEN",
- "GUTSCHRIFT_ERSTELLEN",
- "RETOURE_ERFASSEN",
- "PRODUKT_IDENTIFIZIEREN",
- "BESTAND_PRÜFEN",
- "SORTEN_ALTERNATIVE_PRÜFEN",
- "PFLEGEHINWEIS_GEBEN",
- "BILD_ANFORDERN",
- "MENSCHLICHE_BEARBEITUNG_ERFORDERLICH",
- "RÜCKRUF",
- "E-MAIL_SENDEN",
- ]
- def ask(prompt):
- payload = {
- "model": MODEL,
- "prompt": prompt,
- "stream": False,
- "format": "json",
- "options": {"temperature": 0},
- }
- req = urllib.request.Request(
- OLLAMA,
- data=json.dumps(payload, ensure_ascii=False).encode(),
- headers={"Content-Type": "application/json"},
- method="POST",
- )
- with urllib.request.urlopen(req, timeout=300) as r:
- return json.loads(r.read().decode())["response"]
- def parse(text):
- text = text.strip()
- if text.startswith("```"):
- text = text.replace("```json", "", 1)
- text = text.replace("```", "").strip()
- return json.loads(text)
- async def main():
- con = sqlite3.connect(DB)
- con.row_factory = sqlite3.Row
- rows = con.execute("""
- SELECT id AS analysis_id, analysis_json
- FROM analyses
- WHERE cdr_row_id IS NOT NULL
- ORDER BY id
- """).fetchall()
- con.close()
- results = []
- for n, row in enumerate(rows, 1):
- old = json.loads(row["analysis_json"] or "{}")
- a = old.get("analysis") or {}
- customer = old.get("customer") or {}
- products = old.get("products") or []
- print(f"[{n}/{len(rows)}] analysis={row['analysis_id']}")
- evidence = {
- "old_intent": a.get("intent"),
- "summary": a.get("summary"),
- "advice": a.get("advice"),
- "follow_up": a.get("follow_up"),
- "customer_number": customer.get("customer_number"),
- "order_number": customer.get("order_number"),
- "products": products,
- "uncertainties": old.get("uncertainties"),
- }
- prompt = f"""
- Du bist der Intent- und Aktionsklassifikator einer Telefon-KI.
- Analysiere die folgende bereits erstellte Gesprächsanalyse.
- WICHTIG:
- Ein Gespräch kann mehrere Anliegen enthalten.
- Verwende deshalb primary_intent UND secondary_intents.
- Die vorhandene Analyse wurde bereits aus dem echten Telefonat
- erzeugt. Nutze sie als Tatsachengrundlage. Erfinde nichts.
- INTENTS:
- {json.dumps(INTENTS, ensure_ascii=False)}
- AKTIONEN:
- {json.dumps(ACTIONS, ensure_ascii=False)}
- KLASSIFIKATIONSREGELN:
- - Eine konkrete Bestellung bearbeiten → BESTELLUNG
- - Eine bestehende Bestellung ergänzen/verändern → BESTELLÄNDERUNG
- - Bestellung aufheben → STORNIERUNG
- - Verfügbarkeit → BESTANDSANFRAGE
- - Lieferung verfolgen → VERSANDSTATUS
- - verspätete Lieferung → LIEFERVERZUG
- - Rechnung/Gutschrift/Rechnungsfehler → RECHNUNG
- - Zahlung → ZAHLUNG
- - beschädigte Lieferung → TRANSPORTSCHADEN
- - falsche Lieferung → FEHLLIEFERUNG
- - Qualitätsbeanstandung → REKLAMATION
- - Pflanzenauswahl/Standort → PFLANZENBERATUNG
- - konkrete Sortenauswahl → SORTENBERATUNG
- - Pflege/Krankheit/Schnitt/Düngung → PFLEGEFRAGE
- - Rückgabe → RETOURE
- - Rücktritt vom Kauf → WIDERRUF
- MEHRERE THEMEN:
- Wenn z.B. eine Bestellung ergänzt UND gleichzeitig der
- Versandstatus einer anderen Bestellung besprochen wird:
- primary_intent = BESTELLÄNDERUNG
- secondary_intents = [BESTELLUNG, VERSANDSTATUS]
- Wenn eine Reklamation wegen beschädigter Pflanzen besprochen wird:
- primary_intent = REKLAMATION
- secondary_intents = [TRANSPORTSCHADEN]
- Wenn eine Rechnung korrigiert und eine Gutschrift erstellt werden soll:
- primary_intent = RECHNUNG
- actions = [RECHNUNG_PRÜFEN, RECHNUNG_KORRIGIEREN, GUTSCHRIFT_ERSTELLEN]
- SONSTIGES:
- Nur verwenden, wenn wirklich kein Intent aus der Liste passt.
- Wenn die vorhandene Analyse einen eindeutigen Geschäftsfall
- beschreibt, DARF primary_intent NICHT SONSTIGES sein.
- EVIDENZ:
- {json.dumps(evidence, ensure_ascii=False, indent=2)}
- Antworte ausschließlich als JSON:
- {{
- "call_state": "HUMAN_CONVERSATION",
- "primary_intent": "INTENT",
- "secondary_intents": [],
- "customer_goal": "",
- "actions": [],
- "requires_customer_context": true,
- "requires_order_context": false,
- "requires_product_context": false,
- "requires_previous_contact_context": false,
- "requires_human_action": false,
- "confidence": 0.0,
- "reason": ""
- }}
- """
- try:
- result = parse(
- await asyncio.to_thread(ask, prompt)
- )
- except Exception as exc:
- result = {
- "call_state": "UNKNOWN",
- "primary_intent": "SONSTIGES",
- "secondary_intents": [],
- "customer_goal": "",
- "actions": [
- "MENSCHLICHE_BEARBEITUNG_ERFORDERLICH"
- ],
- "confidence": 0,
- "reason": str(exc),
- }
- if result.get("primary_intent") not in INTENTS:
- result["primary_intent"] = "SONSTIGES"
- result["secondary_intents"] = [
- x for x in result.get("secondary_intents", [])
- if x in INTENTS and x != result["primary_intent"]
- ]
- result["actions"] = [
- x for x in result.get("actions", [])
- if x in ACTIONS
- ]
- result["_analysis_id"] = row["analysis_id"]
- results.append(result)
- primary = Counter(r["primary_intent"] for r in results)
- secondary = Counter(
- x
- for r in results
- for x in r["secondary_intents"]
- )
- actions = Counter(
- x
- for r in results
- for x in r["actions"]
- )
- states = Counter(
- r.get("call_state", "UNKNOWN")
- for r in results
- )
- output = {
- "model": MODEL,
- "source": "existing_analysis_json",
- "count": len(results),
- "call_states": dict(states.most_common()),
- "primary_intents": dict(primary.most_common()),
- "secondary_intents": dict(secondary.most_common()),
- "actions": dict(actions.most_common()),
- "calls": results,
- }
- OUT.write_text(
- json.dumps(output, ensure_ascii=False, indent=2)
- )
- print("\n" + "=" * 72)
- print("RE-KLASSIFIKATION FERTIG")
- print("=" * 72)
- print("\nCALL STATES")
- for k, v in states.most_common():
- print(f"{v:3} {k}")
- print("\nPRIMARY")
- for k, v in primary.most_common():
- print(f"{v:3} {k}")
- print("\nSECONDARY")
- for k, v in secondary.most_common():
- print(f"{v:3} {k}")
- print("\nACTIONS")
- for k, v in actions.most_common():
- print(f"{v:3} {k}")
- print(f"\nGespeichert: {OUT}")
- if __name__ == "__main__":
- asyncio.run(main())
|