build_zammad_analysis.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. #!/usr/bin/env python3
  2. import html
  3. import re
  4. import sqlite3
  5. from pathlib import Path
  6. SRC = Path("data/zammad_history.sqlite3")
  7. DST = Path("data/zammad_analysis.sqlite3")
  8. def clean_body(text):
  9. if not text:
  10. return ""
  11. text = html.unescape(text)
  12. # HTML entfernen
  13. text = re.sub(
  14. r"<(script|style).*?</\1>",
  15. " ",
  16. text,
  17. flags=re.I | re.S,
  18. )
  19. text = re.sub(r"<br\s*/?>", "\n", text, flags=re.I)
  20. text = re.sub(r"<[^>]+>", " ", text)
  21. # typische Mail-Header in Zitaten
  22. text = re.sub(
  23. r"\n\s*(Am .*? schrieb .*?:|"
  24. r"On .*? wrote:|"
  25. r"Gesendet: .*?\n|"
  26. r"Von: .*?\n|"
  27. r"From: .*?\n|"
  28. r"Betreff: .*?\n|"
  29. r"Subject: .*?\n)",
  30. "\n",
  31. text,
  32. flags=re.I,
  33. )
  34. # klassische Signaturen nur am Ende abschneiden
  35. text = re.split(
  36. r"\n\s*(Mit freundlichen Grüßen|"
  37. r"Viele Grüße|"
  38. r"Beste Grüße|"
  39. r"Freundliche Grüße)\b",
  40. text,
  41. maxsplit=1,
  42. flags=re.I,
  43. )[0]
  44. # Zitatzeilen
  45. lines = []
  46. for line in text.splitlines():
  47. if line.lstrip().startswith(">"):
  48. continue
  49. lines.append(line)
  50. text = "\n".join(lines)
  51. text = re.sub(r"[ \t]+", " ", text)
  52. text = re.sub(r"\n{3,}", "\n\n", text)
  53. return text.strip()
  54. def role(sender, internal):
  55. if internal:
  56. return "INTERNAL"
  57. sender = (sender or "").lower()
  58. if sender == "customer":
  59. return "CUSTOMER"
  60. if sender == "agent":
  61. return "AGENT"
  62. if sender == "system":
  63. return "UNKNOWN"
  64. return "UNKNOWN"
  65. # ------------------------------------------------------------
  66. src = sqlite3.connect(SRC)
  67. src.row_factory = sqlite3.Row
  68. if DST.exists():
  69. DST.unlink()
  70. dst = sqlite3.connect(DST)
  71. dst.executescript("""
  72. CREATE TABLE articles (
  73. id INTEGER PRIMARY KEY,
  74. ticket_id INTEGER NOT NULL,
  75. sender TEXT,
  76. role TEXT NOT NULL,
  77. internal INTEGER NOT NULL DEFAULT 0,
  78. type TEXT,
  79. subject TEXT,
  80. raw_body TEXT,
  81. clean_body TEXT,
  82. created_at TEXT
  83. );
  84. CREATE INDEX idx_articles_ticket
  85. ON articles(ticket_id);
  86. CREATE INDEX idx_articles_role
  87. ON articles(role);
  88. CREATE TABLE cases (
  89. ticket_id INTEGER PRIMARY KEY,
  90. ticket_number TEXT,
  91. title TEXT,
  92. group_name TEXT,
  93. state TEXT,
  94. created_at TEXT,
  95. updated_at TEXT,
  96. article_count INTEGER,
  97. customer_count INTEGER,
  98. agent_count INTEGER,
  99. unknown_count INTEGER,
  100. internal_count INTEGER,
  101. customer_text TEXT,
  102. agent_text TEXT,
  103. unknown_text TEXT,
  104. conversation TEXT,
  105. classification_status TEXT DEFAULT 'NEW',
  106. communication_classification TEXT,
  107. primary_intent TEXT,
  108. secondary_intents TEXT,
  109. complaint_type TEXT,
  110. actions TEXT,
  111. taxonomy_fit TEXT,
  112. taxonomy_candidate TEXT,
  113. summary TEXT,
  114. classification_json TEXT
  115. );
  116. CREATE INDEX idx_cases_status
  117. ON cases(classification_status);
  118. """)
  119. tickets = src.execute("""
  120. SELECT *
  121. FROM tickets
  122. ORDER BY id
  123. """).fetchall()
  124. print("=" * 72)
  125. print("ZAMMAD ANALYSE-DATENBANK")
  126. print("=" * 72)
  127. print(f"Tickets: {len(tickets)}")
  128. print()
  129. article_total = 0
  130. for pos, ticket in enumerate(tickets, 1):
  131. articles = src.execute("""
  132. SELECT *
  133. FROM articles
  134. WHERE ticket_id = ?
  135. ORDER BY created_at, id
  136. """, (ticket["id"],)).fetchall()
  137. customer = []
  138. agent = []
  139. unknown = []
  140. conversation = []
  141. counts = {
  142. "CUSTOMER": 0,
  143. "AGENT": 0,
  144. "UNKNOWN": 0,
  145. "INTERNAL": 0,
  146. }
  147. for a in articles:
  148. raw = a["body_text"] or ""
  149. clean = clean_body(raw)
  150. r = role(
  151. a["sender"],
  152. a["internal"],
  153. )
  154. counts[r] += 1
  155. article_total += 1
  156. dst.execute("""
  157. INSERT INTO articles (
  158. id,
  159. ticket_id,
  160. sender,
  161. role,
  162. internal,
  163. type,
  164. subject,
  165. raw_body,
  166. clean_body,
  167. created_at
  168. )
  169. VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  170. """, (
  171. a["id"],
  172. a["ticket_id"],
  173. a["sender"],
  174. r,
  175. int(a["internal"] or 0),
  176. a["type"],
  177. a["subject"],
  178. raw,
  179. clean,
  180. a["created_at"],
  181. ))
  182. if not clean:
  183. continue
  184. if r == "CUSTOMER":
  185. customer.append(clean)
  186. conversation.append(
  187. "[CUSTOMER]\n" + clean
  188. )
  189. elif r == "AGENT":
  190. agent.append(clean)
  191. conversation.append(
  192. "[AGENT]\n" + clean
  193. )
  194. elif r == "UNKNOWN":
  195. unknown.append(clean)
  196. conversation.append(
  197. "[UNKNOWN]\n" + clean
  198. )
  199. # INTERNAL bewusst nicht in conversation
  200. dst.execute("""
  201. INSERT INTO cases (
  202. ticket_id,
  203. ticket_number,
  204. title,
  205. group_name,
  206. state,
  207. created_at,
  208. updated_at,
  209. article_count,
  210. customer_count,
  211. agent_count,
  212. unknown_count,
  213. internal_count,
  214. customer_text,
  215. agent_text,
  216. unknown_text,
  217. conversation
  218. )
  219. VALUES (
  220. ?, ?, ?, ?, ?, ?, ?,
  221. ?, ?, ?, ?, ?,
  222. ?, ?, ?, ?
  223. )
  224. """, (
  225. ticket["id"],
  226. ticket["number"],
  227. ticket["title"],
  228. ticket["group_name"],
  229. ticket["state"],
  230. ticket["created_at"],
  231. ticket["updated_at"],
  232. len(articles),
  233. counts["CUSTOMER"],
  234. counts["AGENT"],
  235. counts["UNKNOWN"],
  236. counts["INTERNAL"],
  237. "\n\n".join(customer),
  238. "\n\n".join(agent),
  239. "\n\n".join(unknown),
  240. "\n\n".join(conversation),
  241. ))
  242. if pos % 500 == 0:
  243. dst.commit()
  244. print(
  245. f"[{pos:4}/{len(tickets)}] "
  246. f"Artikel: {article_total}"
  247. )
  248. dst.commit()
  249. print()
  250. print("=" * 72)
  251. print("FERTIG")
  252. print("=" * 72)
  253. for row in dst.execute("""
  254. SELECT role, COUNT(*)
  255. FROM articles
  256. GROUP BY role
  257. ORDER BY COUNT(*) DESC
  258. """):
  259. print(
  260. f"Artikel {row[0]:10} {row[1]}"
  261. )
  262. print()
  263. row = dst.execute("""
  264. SELECT
  265. COUNT(*),
  266. SUM(customer_count > 0),
  267. SUM(agent_count > 0),
  268. SUM(unknown_count > 0),
  269. SUM(internal_count > 0)
  270. FROM cases
  271. """).fetchone()
  272. print(f"Cases: {row[0]}")
  273. print(f"mit Kundenbeitrag: {row[1]}")
  274. print(f"mit Agentantwort: {row[2]}")
  275. print(f"mit UNKNOWN: {row[3]}")
  276. print(f"mit internen Notiz: {row[4]}")
  277. print()
  278. print(f"DB: {DST}")
  279. src.close()
  280. dst.close()