| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496 |
- #!/usr/bin/env python3
- import asyncio
- import json
- import sqlite3
- import urllib.request
- from collections import Counter
- from pathlib import Path
- BASE = Path(".")
- DB = BASE / "data/telephony.sqlite3"
- TAXONOMY_FILE = BASE / "config/telefonie_taxonomy.json"
- OUT = BASE / "data/historical_intent_v2.json"
- OLLAMA = "http://127.0.0.1:11434/api/generate"
- MODEL = "qwen3:8b"
- def ask_qwen(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("utf-8"),
- headers={
- "Content-Type": "application/json"
- },
- method="POST"
- )
- with urllib.request.urlopen(req, timeout=300) as response:
- data = json.loads(
- response.read().decode("utf-8")
- )
- return data["response"]
- def parse_json(text):
- text = text.strip()
- if text.startswith("```"):
- text = text.replace("```json", "", 1)
- text = text.replace("```", "")
- text = text.strip()
- return json.loads(text)
- def load_json(value):
- try:
- return json.loads(value or "{}")
- except Exception:
- return {}
- async def main():
- taxonomy = json.loads(
- TAXONOMY_FILE.read_text()
- )
- groups = taxonomy["groups"]
- intents = []
- for values in groups.values():
- intents.extend(values)
- complaint_types = list(
- taxonomy["complaint_types"].keys()
- )
- call_states = taxonomy["call_states"]
- 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",
- ]
- con = sqlite3.connect(DB)
- con.row_factory = sqlite3.Row
- rows = con.execute("""
- SELECT
- a.id AS analysis_id,
- a.cdr_row_id,
- a.analysis_json,
- t.transcript_json,
- c.source_caller_id,
- c.destination_caller_id,
- c.start_time
- FROM analyses a
- JOIN transcripts t
- ON t.id = a.transcript_id
- JOIN cdr_calls c
- ON c.id = a.cdr_row_id
- ORDER BY a.id
- """).fetchall()
- con.close()
- print("=" * 72)
- print("HISTORISCHE TELEFON-KI V2")
- print("=" * 72)
- print(f"Gespräche: {len(rows)}")
- print(f"Modell: {MODEL}")
- print(f"Taxonomie: {TAXONOMY_FILE}")
- results = []
- for number, row in enumerate(rows, 1):
- existing = load_json(
- row["analysis_json"]
- )
- transcript = row["transcript_json"] or ""
- old_analysis = existing.get(
- "analysis",
- {}
- )
- customer = existing.get(
- "customer",
- {}
- )
- products = existing.get(
- "products",
- []
- )
- evidence = {
- "existing_intent": old_analysis.get(
- "intent"
- ),
- "summary": old_analysis.get(
- "summary"
- ),
- "advice": old_analysis.get(
- "advice"
- ),
- "follow_up": old_analysis.get(
- "follow_up"
- ),
- "customer": customer,
- "products": products,
- "uncertainties": existing.get(
- "uncertainties",
- []
- )
- }
- print(
- f"[{number:02}/{len(rows):02}] "
- f"analysis={row['analysis_id']}"
- )
- prompt = f"""
- Du bist die Klassifikations-KI einer Telefon-Middleware
- für einen deutschen Gartenpflanzen-Webshop.
- Du analysierst ein bereits transkribiertes Kundentelefonat.
- Die zentrale Taxonomie ist verbindlich.
- TAXONOMIE:
- {json.dumps(groups, ensure_ascii=False, indent=2)}
- REKLAMATIONSGRÜNDE:
- {json.dumps(taxonomy["complaint_types"], ensure_ascii=False, indent=2)}
- CALL STATES:
- {json.dumps(call_states, ensure_ascii=False)}
- AKTIONEN:
- {json.dumps(actions, ensure_ascii=False)}
- WICHTIG:
- Ein Gespräch kann mehrere Intents enthalten.
- Zuerst CALL STATE bestimmen.
- HUMAN_CONVERSATION:
- Es findet ein echtes Gespräch mit verwertbarem Inhalt statt.
- NO_CONTENT:
- Kein verwertbarer Gesprächsinhalt.
- AUTOMATED_MESSAGE:
- Mailbox, automatische Ansage, Systemansage oder ähnliche Aufnahme.
- VOICEMAIL:
- Nachricht auf einer Mailbox.
- UNKNOWN:
- Inhalt vorhanden, aber nicht zuverlässig einzuordnen.
- Nur bei HUMAN_CONVERSATION werden geschäftliche Intents vergeben.
- DOKUMENTE ist ein echter Intent.
- Ein Anliegen zu Kaufvertrag, Rechnungskopie, Dokumentenkopie,
- Unterlagen usw. darf NICHT SONSTIGES sein.
- REKLAMATION ist ein echter Hauptintent.
- Wenn eine Reklamation vorliegt, prüfe zusätzlich
- complaint_type.
- Mögliche complaint_type:
- {json.dumps(complaint_types, ensure_ascii=False)}
- Beispiele:
- "Die Erde war verunreinigt."
- → primary_intent = REKLAMATION
- → complaint_type = PRODUKTQUALITÄT
- "Der Dünger war nicht dabei."
- → primary_intent = REKLAMATION
- → complaint_type = DUENGER_FEHLT
- "Die Pflanzen waren schlecht verpackt."
- → primary_intent = REKLAMATION
- → complaint_type = SCHLECHTVERPACKT
- "Ich habe eine andere Rosensorte bekommen."
- → primary_intent = REKLAMATION
- → complaint_type = FALSCHESORTEGELIEFERT
- "Die Pflanze wurde auf dem Transport beschädigt."
- → primary_intent = REKLAMATION
- → complaint_type = TRANSPORTSCHADEN
- "Der falsche Artikel wurde geliefert."
- → primary_intent = REKLAMATION
- → complaint_type = FEHLLIEFERUNG
- MEHRERE INTENTS:
- Wenn ein Gespräch mehrere Anliegen enthält,
- müssen diese getrennt werden.
- Beispiel:
- Bestellung ändern + Versandstatus prüfen
- primary_intent:
- BESTELLÄNDERUNG
- secondary_intents:
- ["VERSANDSTATUS"]
- Beispiel:
- Rechnung falsch + Gutschrift
- primary_intent:
- RECHNUNG
- actions:
- ["RECHNUNG_PRÜFEN", "RECHNUNG_KORRIGIEREN",
- "GUTSCHRIFT_ERSTELLEN"]
- SONSTIGES IST DER LETZTE FALLBACK.
- Wenn eine passende Kategorie existiert,
- darf SONSTIGES NICHT verwendet werden.
- BESTEHENDE QWEN-ANALYSE:
- {json.dumps(evidence, ensure_ascii=False, indent=2)}
- TRANSKRIPT:
- {transcript}
- Antworte ausschließlich mit diesem JSON:
- {{
- "call_state": "HUMAN_CONVERSATION",
- "primary_intent": "INTENT",
- "secondary_intents": [],
- "complaint_type": null,
- "customer_goal": "",
- "actions": [],
- "requires_customer_context": false,
- "requires_order_context": false,
- "requires_product_context": false,
- "requires_previous_contact_context": false,
- "requires_human_action": false,
- "confidence": 0.0,
- "reason": ""
- }}
- """
- try:
- raw = await asyncio.to_thread(
- ask_qwen,
- prompt
- )
- result = parse_json(raw)
- except Exception as exc:
- result = {
- "call_state": "UNKNOWN",
- "primary_intent": "SONSTIGES",
- "secondary_intents": [],
- "complaint_type": None,
- "customer_goal": "",
- "actions": [
- "MENSCHLICHE_BEARBEITUNG_ERFORDERLICH"
- ],
- "requires_human_action": True,
- "confidence": 0,
- "reason": str(exc)
- }
- if result.get("call_state") not in call_states:
- result["call_state"] = "UNKNOWN"
- 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"]
- ]
- complaint = result.get(
- "complaint_type"
- )
- if complaint not in complaint_types:
- result["complaint_type"] = None
- result["actions"] = [
- x
- for x in result.get(
- "actions",
- []
- )
- if x in actions
- ]
- result["_analysis_id"] = row[
- "analysis_id"
- ]
- result["_cdr_row_id"] = row[
- "cdr_row_id"
- ]
- result["_start_time"] = row[
- "start_time"
- ]
- results.append(result)
- primary = Counter()
- secondary = Counter()
- complaints = Counter()
- states = Counter()
- action_counts = Counter()
- for result in results:
- states[
- result["call_state"]
- ] += 1
- if result["call_state"] == "HUMAN_CONVERSATION":
- primary[
- result["primary_intent"]
- ] += 1
- secondary.update(
- result["secondary_intents"]
- )
- if result.get(
- "complaint_type"
- ):
- complaints[
- result["complaint_type"]
- ] += 1
- action_counts.update(
- result["actions"]
- )
- output = {
- "version": taxonomy["version"],
- "model": MODEL,
- "taxonomy_file": str(
- TAXONOMY_FILE
- ),
- "count": len(results),
- "call_states": dict(
- states.most_common()
- ),
- "primary_intents": dict(
- primary.most_common()
- ),
- "secondary_intents": dict(
- secondary.most_common()
- ),
- "complaint_types": dict(
- complaints.most_common()
- ),
- "actions": dict(
- action_counts.most_common()
- ),
- "calls": results
- }
- OUT.write_text(
- json.dumps(
- output,
- ensure_ascii=False,
- indent=2
- )
- )
- print()
- print("=" * 72)
- print("ERGEBNIS")
- print("=" * 72)
- print("\nCALL STATES")
- for k, v in states.most_common():
- print(f"{v:3} {k}")
- print("\nPRIMARY INTENTS")
- for k, v in primary.most_common():
- print(f"{v:3} {k}")
- print("\nSECONDARY INTENTS")
- for k, v in secondary.most_common():
- print(f"{v:3} {k}")
- print("\nREKLAMATIONSGRÜNDE")
- for k, v in complaints.most_common():
- print(f"{v:3} {k}")
- print("\nAKTIONEN")
- for k, v in action_counts.most_common():
- print(f"{v:3} {k}")
- print()
- print(f"Gespeichert: {OUT}")
- if __name__ == "__main__":
- asyncio.run(main())
|