RedJul2110 commited on
Commit
ba15503
·
verified ·
1 Parent(s): f038276

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +192 -786
app.py CHANGED
@@ -1,889 +1,295 @@
 
1
  import os
2
  import json
3
- import re
4
  import time
5
  import sys
6
- import traceback
7
  import threading
8
- from datetime import datetime
9
-
10
- import torch
11
- import gradio as gr
12
  from transformers import AutoTokenizer, AutoModelForCausalLM
13
- from huggingface_hub import hf_hub_download, upload_file
14
-
15
- # =========================================================
16
- # KONFIG
17
- # =========================================================
18
- MODEL_NAME = "Qwen/Qwen2.5-0.5B-Instruct"
19
-
20
- HF_DATASET = "RedJul2110/wissen-datenbank"
21
- HF_TOKEN = os.environ.get("HF_TOKEN", "")
22
- ADMIN_CODE = os.environ.get("CODE", "1234")
23
-
24
- DATA_DIR = "/data" if os.path.isdir("/data") else "."
25
- os.makedirs(DATA_DIR, exist_ok=True)
26
-
27
- WISSEN_FILE = os.path.join(DATA_DIR, "wissen.json")
28
- CHAT_FILE = os.path.join(DATA_DIR, "chat_history.json")
29
- LOG_FILE = os.path.join(DATA_DIR, "ai_log.txt")
30
-
31
- FALLBACK_NO_INFO = "Das weiß ich leider nicht. Bitte bringe es mir im Lern-Tab bei."
32
 
33
- USE_QWEN_POLISH = True
34
-
35
- # =========================================================
36
- # GLOBALE VARIABLEN
37
- # =========================================================
38
  model = None
39
  tokenizer = None
40
- device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
41
-
42
- knowledge_lock = threading.Lock()
43
- chat_lock = threading.Lock()
44
 
45
- api_chat_historie = [] # list[dict]
46
- upload_in_progress = False
47
 
48
- letzter_hf_sync = None
49
- letzter_upload = None
50
- letzte_wissensänderung = None
51
- letzte_api_latenz = None
52
- letzter_fehler = None
53
 
54
- # =========================================================
55
- # HILFSFUNKTIONEN
56
- # =========================================================
57
- def now_str():
58
- return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
59
 
60
- def log_line(message):
61
- try:
62
- with open(LOG_FILE, "a", encoding="utf-8") as f:
63
- f.write(f"[{now_str()}] {message}\n")
64
- except:
65
- pass
66
 
67
- def log_error(where, exc):
68
- global letzter_fehler
69
- letzter_fehler = f"{where}: {exc}"
70
- log_line(f"[ERROR] {where}: {exc}\n{traceback.format_exc()}")
71
 
72
- def normalize_text(text):
73
- text = (text or "").lower().strip()
74
- text = (
75
- text.replace("ä", "ae")
76
- .replace("ö", "oe")
77
- .replace("ü", "ue")
78
- .replace("ß", "ss")
79
- )
80
- text = re.sub(r"[^a-z0-9]+", " ", text)
81
- return re.sub(r"\s+", " ", text).strip()
82
-
83
- def text_tokens(text):
84
- stopwords = {
85
- "der", "die", "das", "ein", "eine", "einer", "eines", "und", "oder", "ist",
86
- "sind", "war", "waren", "wie", "was", "wer", "wo", "wann", "warum", "wieso",
87
- "woher", "wieviel", "wieviele", "im", "in", "am", "an", "zu", "mit", "von",
88
- "für", "auf", "aus", "den", "dem", "des", "ich", "du", "er", "sie", "es",
89
- "man", "nicht", "nur", "auch", "noch"
90
- }
91
- tokens = normalize_text(text).split()
92
- return {t for t in tokens if t and t not in stopwords}
93
-
94
- def ensure_json_list_file(path):
95
- if not os.path.exists(path):
96
- save_json_list(path, [])
97
-
98
- def load_json_list(path):
99
- if not os.path.exists(path):
100
  return []
101
  try:
102
- with open(path, "r", encoding="utf-8") as f:
103
- data = json.load(f)
104
- return data if isinstance(data, list) else []
105
  except:
106
  return []
107
 
108
- def save_json_list(path, data):
109
- tmp = path + ".tmp"
110
- with open(tmp, "w", encoding="utf-8") as f:
111
- json.dump(data, f, ensure_ascii=False, indent=2)
112
- os.replace(tmp, path)
113
-
114
- def format_entry(item, idx=None):
115
- titel = item.get("frage", "").strip()
116
- text = item.get("antwort", "").strip()
117
- kategorie = item.get("kategorie", "").strip()
118
- created = item.get("created_at", "").strip()
119
-
120
- out = []
121
- if idx is None:
122
- out.append(f"{titel}")
123
- else:
124
- out.append(f"{idx}. {titel}")
125
- if kategorie:
126
- out.append(f"[Kategorie: {kategorie}]")
127
- if created:
128
- out.append(f"[Zeit: {created}]")
129
- out.append(text)
130
- return "\n".join(out)
131
-
132
- def history_to_context(history, max_turns=3):
133
- """
134
- history kann sein:
135
- - list[tuple(user, assistant)] -> UI-Chat
136
- - list[dict(role/content)] -> API-Chat
137
- """
138
- if not history:
139
- return ""
140
-
141
- lines = []
142
-
143
- # UI-Chat: tuples
144
- if isinstance(history[0], tuple):
145
- for user, assistant in history[-max_turns:]:
146
- lines.append(f"User: {user}")
147
- lines.append(f"Assistant: {assistant}")
148
- return "\n".join(lines)
149
-
150
- # API-Chat: dicts
151
- recent = history[-max_turns * 2:]
152
- for msg in recent:
153
- role = msg.get("role", "")
154
- content = msg.get("content", "")
155
- lines.append(f"{role}: {content}")
156
-
157
- return "\n".join(lines)
158
-
159
- def api_history_to_pairs(messages):
160
- pairs = []
161
- pending_user = None
162
-
163
- for msg in messages:
164
- role = msg.get("role", "")
165
- content = msg.get("content", "")
166
- if role == "user":
167
- pending_user = content
168
- elif role == "assistant" and pending_user is not None:
169
- pairs.append((pending_user, content))
170
- pending_user = None
171
-
172
- return pairs
173
-
174
- def trim_api_history(max_messages=20):
175
- global api_chat_historie
176
- if len(api_chat_historie) > max_messages:
177
- api_chat_historie = api_chat_historie[-max_messages:]
178
-
179
- # =========================================================
180
- # KNOWLEDGE / DATENBANK
181
- # =========================================================
182
- def load_wissen():
183
- ensure_json_list_file(WISSEN_FILE)
184
- return load_json_list(WISSEN_FILE)
185
-
186
- def sync_wissen_from_hf():
187
- """
188
- Holt die aktuelle wissen.json aus dem HF Dataset und schreibt sie lokal.
189
- Wenn keine Datei existiert oder der Sync fehlschlägt, bleibt lokal eine leere Liste.
190
- """
191
- global letzter_hf_sync
192
-
193
- ensure_json_list_file(WISSEN_FILE)
194
-
195
- if not HF_TOKEN:
196
- log_line("[WARN] HF_TOKEN fehlt. Lokale Datei wird genutzt.")
197
- return False, "HF_TOKEN fehlt. Lokale Datei wird genutzt."
198
-
199
- try:
200
- remote_path = hf_hub_download(
201
- repo_id=HF_DATASET,
202
- filename="wissen.json",
203
- repo_type="dataset",
204
- token=HF_TOKEN,
205
- force_download=True
206
- )
207
- remote_data = load_json_list(remote_path)
208
- save_json_list(WISSEN_FILE, remote_data)
209
- letzter_hf_sync = now_str()
210
- return True, f"✅ Wissen aus HF geladen ({len(remote_data)} Einträge)."
211
- except Exception as e:
212
- log_error("sync_wissen_from_hf", e)
213
- return False, f"⚠️ HF-Sync fehlgeschlagen, lokale Datei bleibt aktiv: {e}"
214
-
215
- def upload_wissen_background():
216
- """
217
- Lädt die lokale wissen.json im Hintergrund ins HF Dataset hoch.
218
- So blockiert der Space nicht.
219
- """
220
- global upload_in_progress, letzter_upload
221
-
222
- if not HF_TOKEN:
223
- log_line("[WARN] Upload übersprungen, weil HF_TOKEN fehlt.")
224
- return
225
-
226
- upload_in_progress = True
227
- try:
228
- upload_file(
229
- path_or_fileobj=WISSEN_FILE,
230
- path_in_repo="wissen.json",
231
- repo_id=HF_DATASET,
232
- repo_type="dataset",
233
- token=HF_TOKEN,
234
- commit_message=f"Update wissen.json ({now_str()})"
235
- )
236
- letzter_upload = now_str()
237
- log_line("[OK] wissen.json erfolgreich hochgeladen.")
238
- except Exception as e:
239
- log_error("upload_wissen_background", e)
240
- finally:
241
- upload_in_progress = False
242
 
243
  def exact_db_answer(user_message):
244
- q = normalize_text(user_message)
245
- if not q:
246
- return None
247
-
248
- data = load_wissen()
249
  for item in data:
250
- frage = normalize_text(item.get("frage", ""))
251
- antwort = item.get("antwort", "").strip()
252
-
253
- if q == frage:
254
- return antwort
255
-
256
  return None
257
 
258
- def score_entry(item, query_tokens, query_norm):
259
- frage = item.get("frage", "")
260
- antwort = item.get("antwort", "")
261
- kategorie = item.get("kategorie", "")
262
-
263
- blob = f"{frage} {antwort} {kategorie}"
264
- blob_norm = normalize_text(blob)
265
- blob_tokens = text_tokens(blob)
266
-
267
- score = len(query_tokens & blob_tokens)
268
-
269
- if query_norm and query_norm in blob_norm:
270
- score += 3
271
-
272
- if normalize_text(frage) == query_norm:
273
- score += 10
274
-
275
- if normalize_text(kategorie) == query_norm and query_norm:
276
- score += 4
277
-
278
- return score
279
-
280
- def find_relevant_facts(query, max_items=6):
281
- data = load_wissen()
282
- if not data:
283
- return []
284
-
285
- query_norm = normalize_text(query)
286
- query_tokens = text_tokens(query)
287
-
288
- if not query_tokens and not query_norm:
289
- return data[:max_items]
290
-
291
  scored = []
292
- for item in data:
293
- score = score_entry(item, query_tokens, query_norm)
294
- if score > 0:
295
- scored.append((score, item))
296
-
297
- scored.sort(key=lambda x: x[0], reverse=True)
298
- return [item for _, item in scored[:max_items]]
299
 
300
- def get_knowledge_stats():
301
- data = load_wissen()
302
- categories = []
303
  for item in data:
304
- cat = item.get("kategorie", "").strip()
305
- if cat and cat not in categories:
306
- categories.append(cat)
307
- return {
308
- "count": len(data),
309
- "categories": categories[:10],
310
- }
311
-
312
- def search_knowledge(query, max_results=8):
313
- query = (query or "").strip()
314
- if not query:
315
- return "❌ Bitte gib einen Suchbegriff ein."
316
-
317
- data = load_wissen()
318
- if not data:
319
- return "Keine Einträge vorhanden."
320
-
321
- query_tokens = text_tokens(query)
322
- query_norm = normalize_text(query)
323
-
324
- scored = []
325
- for item in data:
326
- score = score_entry(item, query_tokens, query_norm)
327
  if score > 0:
328
  scored.append((score, item))
329
 
330
- scored.sort(key=lambda x: x[0], reverse=True)
331
- matches = [item for _, item in scored[:max_results]]
332
-
333
- if not matches:
334
- return "❌ Keine passenden Einträge gefunden."
335
-
336
- out = [f"✅ {len(matches)} Treffer:\n"]
337
- for i, item in enumerate(matches, 1):
338
- out.append(format_entry(item, i))
339
- out.append("\n" + "-" * 40 + "\n")
340
- return "\n".join(out).strip()
341
-
342
- def delete_knowledge(query):
343
- global letzte_wissensänderung
344
-
345
- query = (query or "").strip()
346
- if not query:
347
- return False, "❌ Bitte einen Suchbegriff zum Löschen eingeben."
348
-
349
- with knowledge_lock:
350
- sync_wissen_from_hf()
351
- data = load_wissen()
352
- if not data:
353
- return False, "Keine Einträge vorhanden."
354
-
355
- query_norm = normalize_text(query)
356
- query_tokens = text_tokens(query)
357
-
358
- new_data = []
359
- removed = []
360
-
361
- for item in data:
362
- item_score = score_entry(item, query_tokens, query_norm)
363
- if item_score > 0:
364
- removed.append(item)
365
- else:
366
- new_data.append(item)
367
-
368
- if not removed:
369
- return False, "❌ Nichts gefunden, was gelöscht werden kann."
370
-
371
- save_json_list(WISSEN_FILE, new_data)
372
- letzte_wissensänderung = now_str()
373
-
374
- threading.Thread(target=upload_wissen_background, daemon=True).start()
375
- return True, f"✅ {len(removed)} Eintrag/Einträge gelöscht."
376
-
377
- def delete_all_knowledge(admin_code):
378
- global letzte_wissensänderung
379
-
380
- if admin_code != ADMIN_CODE:
381
- return False, "❌ Falscher Admin-Code."
382
-
383
- with knowledge_lock:
384
- save_json_list(WISSEN_FILE, [])
385
- letzte_wissensänderung = now_str()
386
-
387
- threading.Thread(target=upload_wissen_background, daemon=True).start()
388
- return True, "✅ Alle Wissenseinträge wurden gelöscht."
389
-
390
- def save_knowledge_entry(frage, antwort, kategorie=""):
391
- global letzte_wissensänderung
392
-
393
- frage = (frage or "").strip()
394
- antwort = (antwort or "").strip()
395
- kategorie = (kategorie or "").strip()
396
-
397
- if not frage or not antwort:
398
- return False, "❌ Thema/Stichwort und Text dürfen nicht leer sein."
399
-
400
- with knowledge_lock:
401
- sync_wissen_from_hf()
402
-
403
- data = load_wissen()
404
- q_norm = normalize_text(frage)
405
 
406
- for item in data:
407
- if normalize_text(item.get("frage", "")) == q_norm:
408
- return False, "ℹ️ Dieser Eintrag ist schon vorhanden."
409
-
410
- entry = {
411
- "frage": frage,
412
- "antwort": antwort,
413
- "kategorie": kategorie,
414
- "created_at": now_str()
415
- }
416
- data.append(entry)
417
- save_json_list(WISSEN_FILE, data)
418
- letzte_wissensänderung = now_str()
419
-
420
- threading.Thread(target=upload_wissen_background, daemon=True).start()
421
- return True, f"✅ Lokal gespeichert. Upload läuft im Hintergrund.\n\nThema: {frage}"
422
-
423
- # =========================================================
424
- # CHAT / SPEICHER
425
- # =========================================================
426
- def load_chat_history():
427
- ensure_json_list_file(CHAT_FILE)
428
- return load_json_list(CHAT_FILE)
429
-
430
- def save_chat_history(history):
431
- save_json_list(CHAT_FILE, history)
432
-
433
- def reset_chat_history():
434
- global api_chat_historie
435
- with chat_lock:
436
- api_chat_historie = []
437
- save_chat_history(api_chat_historie)
438
- log_line("[CHAT] Chat-Historie zurückgesetzt.")
439
- return True, "✅ Chat-Historie gelöscht."
440
-
441
- def chat_history_status():
442
- history = load_chat_history()
443
- if not history:
444
- return "Chat-Historie ist leer."
445
-
446
- out = [f"📜 Gespeicherte Nachrichten: {len(history)}\n"]
447
- for i, msg in enumerate(history[-12:], 1):
448
- role = msg.get("role", "?")
449
- content = msg.get("content", "")
450
- out.append(f"{i}. {role}: {content[:250]}")
451
- out.append("\n")
452
- return "\n".join(out).strip()
453
-
454
- def load_visible_chat_history_for_ui():
455
- pairs = api_history_to_pairs(load_chat_history())
456
- return pairs, pairs
457
 
458
- # =========================================================
459
- # MODEL / QWEN
460
- # =========================================================
461
  def init_model_if_needed():
462
- global model, tokenizer, device
463
- if model is not None and tokenizer is not None:
464
  return
465
 
466
- print("=" * 60)
467
- print("🤖 Initialisiere Modell")
468
- print("=" * 60)
469
 
470
- tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
471
  if tokenizer.pad_token is None:
472
  tokenizer.pad_token = tokenizer.eos_token
473
 
474
- dtype = torch.float16 if device.type == "cuda" else torch.float32
475
- model = AutoModelForCausalLM.from_pretrained(
476
- MODEL_NAME,
477
- torch_dtype=dtype,
478
- low_cpu_mem_usage=True
479
- )
480
  model.to(device)
481
- model.eval()
482
 
483
- print(f"✅ Modell geladen auf: {device}")
 
 
 
 
 
484
 
485
- def format_messages_for_model(messages_history):
486
- try:
487
- return tokenizer.apply_chat_template(
488
- messages_history,
489
- tokenize=False,
490
- add_generation_prompt=True
491
- )
492
- except Exception:
493
- lines = []
494
- for m in messages_history:
495
- role = m.get("role", "user").capitalize()
496
- content = m.get("content", "")
497
- lines.append(f"{role}: {content}")
498
- lines.append("Assistant:")
499
- return "\n".join(lines)
500
-
501
- def model_generate(messages_history, max_new_tokens=120):
502
- prompt_text = format_messages_for_model(messages_history)
503
- inputs = tokenizer(
504
- [prompt_text],
505
- return_tensors="pt",
506
- truncation=True,
507
- max_length=4096
508
- ).to(device)
509
 
510
  with torch.no_grad():
511
  output = model.generate(
512
  inputs.input_ids,
513
  max_new_tokens=max_new_tokens,
514
- do_sample=False,
515
- repetition_penalty=1.05,
 
516
  pad_token_id=tokenizer.eos_token_id
517
  )
518
 
519
- new_tokens = output[0][inputs.input_ids.shape[-1]:]
520
- text = tokenizer.decode(new_tokens, skip_special_tokens=True).strip()
521
- return text
522
-
523
- def build_system_prompt(user_message=""):
524
- facts = find_relevant_facts(user_message, max_items=6)
525
- if not facts:
526
- facts = load_wissen()[:6]
527
-
528
- fact_lines = []
529
- for idx, item in enumerate(facts, 1):
530
- fact_lines.append(
531
- f"Fakt {idx}:\n"
532
- f"Thema: {item.get('frage', '')}\n"
533
- f"Text: {item.get('antwort', '')}"
534
- )
535
-
536
- fact_block = "\n\n".join(fact_lines) if fact_lines else "Keine gespeicherten Fakten vorhanden."
537
-
538
- return f"""Du bist kein Wissensmodell.
539
- Du bist nur ein Sprach- und Grammatik-Assistent.
540
- Du darfst KEINE neuen Fakten hinzufügen.
541
- Du darfst nur die unten stehenden Fakten sprachlich sauber formulieren.
542
-
543
- Wenn die Fakten nicht reichen, antworte exakt:
544
- "{FALLBACK_NO_INFO}"
545
-
546
- --- SPEICHER ---
547
- {fact_block}
548
- ---------------"""
549
-
550
- def get_system_prompt():
551
- return build_system_prompt("")
552
-
553
- def compose_draft_from_facts(facts):
554
- if not facts:
555
- return ""
556
-
557
- answers = []
558
- for item in facts:
559
- ans = item.get("antwort", "").strip()
560
- if ans and ans not in answers:
561
- answers.append(ans)
562
-
563
- if not answers:
564
- return ""
565
 
566
- if len(answers) == 1:
567
- return answers[0]
 
 
 
 
568
 
569
- return " ".join(answers[:3])
 
 
 
 
 
 
 
 
570
 
571
  def polish_with_model(user_message, draft, facts, history_context=""):
572
- if not USE_QWEN_POLISH:
573
- return draft
574
-
575
- if model is None or tokenizer is None:
576
  return draft
577
 
578
- fact_lines = []
579
- for idx, item in enumerate(facts, 1):
580
- fact_lines.append(
581
- f"{idx}. Thema: {item.get('frage', '')}\n"
582
- f" Text: {item.get('antwort', '')}"
583
- )
584
- fact_block = "\n".join(fact_lines)
585
 
586
  messages = [
587
  {
588
  "role": "system",
589
- "content": (
590
- "Du bist nur ein Grammatik- und Formulierungsassistent. "
591
- "Du darfst KEINE neuen Fakten erfinden. "
592
- "Wenn der Rohentwurf leer oder unpassend ist, antworte exakt: "
593
- f'"{FALLBACK_NO_INFO}"'
594
- )
595
  },
596
  {
597
  "role": "user",
598
- "content": (
599
- f"Frage: {user_message}\n\n"
600
- f"Kontext: {history_context}\n\n"
601
- f"Gespeicherte Fakten:\n{fact_block}\n\n"
602
- f"Rohentwurf:\n{draft}\n\n"
603
- "Aufgabe: Formuliere den Rohentwurf natürlich, kurz und fehlerfrei auf Deutsch um. "
604
- "Füge keine neuen Fakten hinzu."
605
- )
606
  }
607
  ]
608
 
609
  try:
610
- out = model_generate(messages, max_new_tokens=120)
611
- if not out:
612
- return draft
613
- return out.strip()
614
- except Exception as e:
615
- log_error("polish_with_model", e)
616
  return draft
617
 
618
  def generate_reply(user_message, history_context=""):
619
- """
620
- 1) exakte DB-Antwort direkt zurück
621
- 2) sonst relevante Fakten suchen
622
- 3) Draft aus Fakten bauen
623
- 4) Qwen nur als Sprach-Polierer verwenden
624
- """
625
- query = f"{user_message} {history_context}".strip()
626
-
627
  exact = exact_db_answer(user_message)
628
  if exact:
629
  return exact
630
 
631
- facts = find_relevant_facts(query, max_items=6)
632
- if not facts:
633
- return FALLBACK_NO_INFO
634
 
635
- draft = compose_draft_from_facts(facts)
636
- if not draft:
 
 
 
637
  return FALLBACK_NO_INFO
638
 
639
- reply = polish_with_model(user_message, draft, facts, history_context)
640
- return reply if reply else draft
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
641
 
642
- # =========================================================
643
  # API
644
- # =========================================================
645
  def gradio_simple_api(user_message):
646
- global api_chat_historie, letzte_api_latenz
 
647
 
648
- start = time.perf_counter()
649
 
650
- with chat_lock:
651
- history_context = history_to_context(api_chat_historie)
652
- reply = generate_reply(user_message, history_context=history_context)
653
 
654
- api_chat_historie.append({"role": "user", "content": user_message})
655
- api_chat_historie.append({"role": "assistant", "content": reply})
656
- trim_api_history(20)
657
- save_chat_history(api_chat_historie)
658
 
659
- log_line(f"[USER] {user_message}")
660
- log_line(f"[ASSISTANT] {reply}")
 
 
 
 
661
 
662
- letzte_api_latenz = f"{(time.perf_counter() - start) * 1000:.2f} ms"
663
- return reply
 
 
664
 
665
- # =========================================================
666
- # UI FUNKTIONEN
667
- # =========================================================
668
- def ui_zeige_status():
669
- facts = load_wissen()
670
- stats = get_knowledge_stats()
671
- chat_entries = len(load_chat_history())
672
-
673
- return f"""🟢 SYSTEM ONLINE
674
-
675
- 🤖 Modell: {MODEL_NAME}
676
- 🖥️ Device: {device}
677
- 🏠 Space: RedJul2110/MyfirstAI
678
- 📦 Datenbank: {HF_DATASET}
679
- 💾 Gespeicherte Fakten: {len(facts)}
680
- 🗂️ Kategorien: {", ".join(stats["categories"]) if stats["categories"] else "keine"}
681
- 💬 Gespeicherte Chat-Nachrichten: {chat_entries}
682
- ⏱️ Letzte API-Antwortzeit: {letzte_api_latenz if letzte_api_latenz else "noch keine"}
683
- 🔁 Letzter HF-Sync: {letzter_hf_sync if letzter_hf_sync else "noch keiner"}
684
- ⬆️ Letzter Upload: {letzter_upload if letzter_upload else "noch keiner"}
685
- 🧠 Letzte Wissensänderung: {letzte_wissensänderung if letzte_wissensänderung else "noch keine"}
686
- 🔄 Upload läuft: {"ja" if upload_in_progress else "nein"}
687
- ⚠️ Letzter Fehler: {letzter_fehler if letzter_fehler else "keiner"}
688
-
689
- Lokale Wissensdatei: {WISSEN_FILE}
690
- Chat-Datei: {CHAT_FILE}
691
- Log-Datei: {LOG_FILE}
692
- """
693
-
694
- def ui_sync_wissen():
695
- ok, msg = sync_wissen_from_hf()
696
- return msg
697
-
698
- def ui_web_lernen(passwort, frage, antwort, kategorie):
699
- if passwort != ADMIN_CODE:
700
- return "❌ Zugriff verweigert! Falscher Admin-Code."
701
-
702
- ok, msg = save_knowledge_entry(frage, antwort, kategorie)
703
- return msg
704
-
705
- def ui_wissen_suchen(suchbegriff):
706
- return search_knowledge(suchbegriff)
707
-
708
- def ui_wissen_loeschen(passwort, suchbegriff):
709
- if passwort != ADMIN_CODE:
710
- return "❌ Zugriff verweigert! Falscher Admin-Code."
711
- ok, msg = delete_knowledge(suchbegriff)
712
- return msg
713
-
714
- def ui_wissen_alle_loeschen(passwort):
715
- if passwort != ADMIN_CODE:
716
- return "❌ Zugriff verweigert! Falscher Admin-Code."
717
- ok, msg = delete_all_knowledge(passwort)
718
- return msg
719
-
720
- def ui_chat_send(user_message, visible_history):
721
- """
722
- Echter Chat-Tab:
723
- - zeigt Verlauf
724
- - nutzt dieselbe Antwortlogik
725
- - speichert den Verlauf auch für die API
726
- """
727
- global api_chat_historie, letzte_api_latenz
728
-
729
- user_message = (user_message or "").strip()
730
- if not user_message:
731
- return visible_history, "", visible_history
732
-
733
- start = time.perf_counter()
734
-
735
- if visible_history is None:
736
- visible_history = []
737
-
738
- history_context = history_to_context(visible_history)
739
- reply = generate_reply(user_message, history_context=history_context)
740
-
741
- visible_history = visible_history + [(user_message, reply)]
742
-
743
- with chat_lock:
744
- api_chat_historie.append({"role": "user", "content": user_message})
745
- api_chat_historie.append({"role": "assistant", "content": reply})
746
- trim_api_history(20)
747
- save_chat_history(api_chat_historie)
748
-
749
- log_line(f"[CHAT USER] {user_message}")
750
- log_line(f"[CHAT BOT] {reply}")
751
-
752
- letzte_api_latenz = f"{(time.perf_counter() - start) * 1000:.2f} ms"
753
- return visible_history, "", visible_history
754
 
755
  def ui_chat_reset():
756
- ok, msg = reset_chat_history()
757
- return [], [], msg
758
 
759
- def ui_chat_status():
760
- return chat_history_status()
 
761
 
762
- def load_visible_chat_history_for_ui():
763
- pairs = api_history_to_pairs(load_chat_history())
764
- return pairs, pairs
 
 
765
 
766
- # =========================================================
767
- # APP
768
- # =========================================================
769
  def erzeuge_gradio_app():
770
- with gr.Blocks(title="Privates KI Kontrollzentrum", theme="soft") as demo:
771
- gr.Markdown("# 🤖 Privates KI Kontrollzentrum")
772
- gr.Markdown("Die KI nutzt zuerst die Datenbank. Qwen darf nur bei der Formulierung helfen.")
773
-
774
- with gr.Tab("📊 Status"):
775
- status_text = gr.Textbox(label="Systembericht", lines=16, interactive=False)
776
- with gr.Row():
777
- refresh_btn = gr.Button("Status aktualisieren")
778
- sync_btn = gr.Button("Wissen von HF neu laden")
779
- refresh_btn.click(ui_zeige_status, outputs=status_text)
780
- sync_btn.click(ui_sync_wissen, outputs=status_text)
781
- demo.load(ui_zeige_status, outputs=status_text)
782
-
783
- with gr.Tab("🧠 Lernen (Admin)"):
784
- gr.Markdown("Hier speicherst du neue Fakten in die Datenbank.")
785
- pw_input = gr.Textbox(label="Geheimer Code", type="password")
786
- k_input = gr.Textbox(label="Kategorie / Bereich (optional)", placeholder="z. B. Geschichte, Geo, Technik")
787
- q_input = gr.Textbox(label="Thema / Stichwort", placeholder="z. B. Frankreich, Mars, Bundeskanzler")
788
- a_input = gr.Textbox(label="Text", placeholder="Langer Infotext", lines=6)
789
- lern_btn = gr.Button("Wissen speichern", variant="primary")
790
- lern_out = gr.Textbox(label="Ergebnis", interactive=False)
791
- lern_btn.click(ui_web_lernen, inputs=[pw_input, q_input, a_input, k_input], outputs=lern_out)
792
-
793
- with gr.Tab("🔍 Suchen / Löschen"):
794
- gr.Markdown("Suche in der Datenbank oder l��sche Einträge wieder.")
795
- search_box = gr.Textbox(label="Suchbegriff", placeholder="z. B. Frankreich")
796
- search_btn = gr.Button("Suchen")
797
- search_out = gr.Textbox(label="Treffer", lines=12, interactive=False)
798
-
799
- del_pw = gr.Textbox(label="Admin-Code", type="password")
800
- del_box = gr.Textbox(label="Löschen nach Begriff", placeholder="z. B. Frankreich")
801
- del_btn = gr.Button("Löschen", variant="secondary")
802
- del_out = gr.Textbox(label="Lösch-Ergebnis", interactive=False)
803
-
804
- all_del_btn = gr.Button("ALLES löschen", variant="stop")
805
- all_del_out = gr.Textbox(label="Alles löschen", interactive=False)
806
-
807
- search_btn.click(ui_wissen_suchen, inputs=[search_box], outputs=search_out)
808
- del_btn.click(ui_wissen_loeschen, inputs=[del_pw, del_box], outputs=del_out)
809
- all_del_btn.click(ui_wissen_alle_loeschen, inputs=[del_pw], outputs=all_del_out)
810
 
811
  with gr.Tab("Chat"):
812
- gr.Markdown("Echter Chat mit Verlauf. API und Chat nutzen dieselbe Wissenslogik.")
813
- chatbot = gr.Chatbot(label="Chat", height=420)
814
- chat_state = gr.State([])
815
-
816
- with gr.Row():
817
- chat_input = gr.Textbox(label="Nachricht", placeholder="Schreib etwas ...", scale=5)
818
- chat_send = gr.Button("Senden", scale=1)
819
-
820
- with gr.Row():
821
- chat_clear = gr.Button("Chat leeren")
822
- chat_history_btn = gr.Button("Gespeicherte Chat-Historie anzeigen")
823
-
824
- chat_history_text = gr.Textbox(label="Gespeicherte Chat-Historie", lines=12, interactive=False)
825
-
826
- demo.load(load_visible_chat_history_for_ui, outputs=[chatbot, chat_state])
827
-
828
- chat_send.click(
829
- ui_chat_send,
830
- inputs=[chat_input, chat_state],
831
- outputs=[chatbot, chat_input, chat_state]
832
- )
833
-
834
- chat_clear.click(
835
- ui_chat_reset,
836
- outputs=[chatbot, chat_state, chat_history_text]
837
- )
838
-
839
- chat_history_btn.click(
840
- ui_chat_status,
841
- outputs=chat_history_text
842
- )
843
- demo.load(ui_chat_status, outputs=chat_history_text)
844
-
845
- # Unsichtbare API bleibt erhalten
846
- api_eingabe = gr.Textbox(visible=False)
847
- api_ausgabe = gr.Textbox(visible=False)
848
- api_btn = gr.Button(visible=False)
849
- api_btn.click(gradio_simple_api, inputs=api_eingabe, outputs=api_ausgabe, api_name="predict")
850
-
851
- demo.queue(default_concurrency_limit=8)
852
- return demo
853
 
854
- # =========================================================
855
- # LOKALER CHAT (FALLBACK)
856
- # =========================================================
857
- def local_terminal_chat():
858
- print("Lokaler Chat gestartet. Tippe 'exit' zum Beenden.")
859
- while True:
860
- user = input("Du: ").strip()
861
- if user.lower() in {"exit", "quit", "ende"}:
862
- break
863
- if not user:
864
- continue
865
- reply = gradio_simple_api(user)
866
- print("Bot:", reply)
867
-
868
- # =========================================================
869
- # BOOTSTRAP
870
- # =========================================================
871
- def bootstrap():
872
- global api_chat_historie
873
-
874
- ensure_json_list_file(WISSEN_FILE)
875
- ensure_json_list_file(CHAT_FILE)
876
-
877
- sync_wissen_from_hf()
878
- api_chat_historie = load_chat_history()
879
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
880
  init_model_if_needed()
881
 
882
  if os.environ.get("SPACE_ID"):
883
  app = erzeuge_gradio_app()
884
  app.launch()
885
  else:
886
- local_terminal_chat()
887
-
888
- if __name__ == "__main__":
889
- bootstrap()
 
1
+ import torch
2
  import os
3
  import json
 
4
  import time
5
  import sys
 
6
  import threading
7
+ import re
 
 
 
8
  from transformers import AutoTokenizer, AutoModelForCausalLM
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
+ # =========================
11
+ # GLOBALS
12
+ # =========================
 
 
13
  model = None
14
  tokenizer = None
15
+ device = torch.device("cpu")
 
 
 
16
 
17
+ WISSEN_FILE = "wissen.json"
18
+ CHAT_FILE = "chat_history.json"
19
 
20
+ api_chat_historie = []
 
 
 
 
21
 
22
+ FALLBACK_NO_INFO = "Dazu habe ich nichts in meiner Datenbank."
 
 
 
 
23
 
24
+ # =========================
25
+ # BASIS FUNKTIONEN
26
+ # =========================
27
+ def normalize_text(text):
28
+ return re.sub(r"[^a-z0-9 ]", "", text.lower())
 
29
 
30
+ def now_str():
31
+ return time.strftime("%Y-%m-%d %H:%M:%S")
 
 
32
 
33
+ # =========================
34
+ # WISSEN
35
+ # =========================
36
+ def wissen_laden():
37
+ if not os.path.exists(WISSEN_FILE):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  return []
39
  try:
40
+ with open(WISSEN_FILE, "r", encoding="utf-8") as f:
41
+ return json.load(f)
 
42
  except:
43
  return []
44
 
45
+ def wissen_speichern(frage, antwort):
46
+ data = wissen_laden()
47
+ data.append({
48
+ "frage": frage.strip(),
49
+ "antwort": antwort.strip()
50
+ })
51
+ with open(WISSEN_FILE, "w", encoding="utf-8") as f:
52
+ json.dump(data, f, ensure_ascii=False, indent=4)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
  def exact_db_answer(user_message):
55
+ data = wissen_laden()
56
+ msg = normalize_text(user_message)
 
 
 
57
  for item in data:
58
+ if normalize_text(item["frage"]) == msg:
59
+ return item["antwort"]
 
 
 
 
60
  return None
61
 
62
+ def find_relevant_facts(query, max_items=5):
63
+ data = wissen_laden()
64
+ query_words = set(normalize_text(query).split())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  scored = []
 
 
 
 
 
 
 
66
 
 
 
 
67
  for item in data:
68
+ words = set(normalize_text(item["frage"]).split())
69
+ score = len(query_words & words)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  if score > 0:
71
  scored.append((score, item))
72
 
73
+ scored.sort(reverse=True, key=lambda x: x[0])
74
+ return [x[1] for x in scored[:max_items]]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
+ def compose_draft_from_facts(facts):
77
+ texts = [f["antwort"] for f in facts if f.get("antwort")]
78
+ return " ".join(texts).strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
+ # =========================
81
+ # KI
82
+ # =========================
83
  def init_model_if_needed():
84
+ global model, tokenizer
85
+ if model:
86
  return
87
 
88
+ print("🤖 Lade Modell...")
89
+ model_name = "Qwen/Qwen2.5-0.5B-Instruct"
 
90
 
91
+ tokenizer = AutoTokenizer.from_pretrained(model_name)
92
  if tokenizer.pad_token is None:
93
  tokenizer.pad_token = tokenizer.eos_token
94
 
95
+ model = AutoModelForCausalLM.from_pretrained(model_name)
 
 
 
 
 
96
  model.to(device)
97
+ print("✅ Modell bereit")
98
 
99
+ def model_generate(messages, max_new_tokens=120):
100
+ text = tokenizer.apply_chat_template(
101
+ messages,
102
+ tokenize=False,
103
+ add_generation_prompt=True
104
+ )
105
 
106
+ inputs = tokenizer([text], return_tensors="pt").to(device)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
  with torch.no_grad():
109
  output = model.generate(
110
  inputs.input_ids,
111
  max_new_tokens=max_new_tokens,
112
+ temperature=0.6,
113
+ top_p=0.9,
114
+ do_sample=True,
115
  pad_token_id=tokenizer.eos_token_id
116
  )
117
 
118
+ generated = output[0][len(inputs.input_ids[0]):]
119
+ return tokenizer.decode(generated, skip_special_tokens=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
 
121
+ # =========================
122
+ # CHAT INTELLIGENZ
123
+ # =========================
124
+ def looks_like_factual_question(text):
125
+ t = normalize_text(text)
126
+ return "?" in text or t.startswith(("was", "wer", "wie", "wann", "wo", "warum"))
127
 
128
+ def general_chat_reply(user_message, history_context=""):
129
+ messages = [
130
+ {"role": "system", "content": "Du bist ein freundlicher Chat-Assistent."},
131
+ {"role": "user", "content": user_message}
132
+ ]
133
+ try:
134
+ return model_generate(messages, 80)
135
+ except:
136
+ return FALLBACK_NO_INFO
137
 
138
  def polish_with_model(user_message, draft, facts, history_context=""):
139
+ if not draft:
 
 
 
140
  return draft
141
 
142
+ fact_text = "\n".join([f["antwort"] for f in facts])
 
 
 
 
 
 
143
 
144
  messages = [
145
  {
146
  "role": "system",
147
+ "content": "Formuliere den Text schöner, füge aber keine neuen Infos hinzu."
 
 
 
 
 
148
  },
149
  {
150
  "role": "user",
151
+ "content": f"{draft}\n\nFakten:\n{fact_text}"
 
 
 
 
 
 
 
152
  }
153
  ]
154
 
155
  try:
156
+ return model_generate(messages, 120)
157
+ except:
 
 
 
 
158
  return draft
159
 
160
  def generate_reply(user_message, history_context=""):
 
 
 
 
 
 
 
 
161
  exact = exact_db_answer(user_message)
162
  if exact:
163
  return exact
164
 
165
+ facts = find_relevant_facts(user_message)
 
 
166
 
167
+ if facts:
168
+ draft = compose_draft_from_facts(facts)
169
+ return polish_with_model(user_message, draft, facts)
170
+
171
+ if looks_like_factual_question(user_message):
172
  return FALLBACK_NO_INFO
173
 
174
+ return general_chat_reply(user_message)
175
+
176
+ # =========================
177
+ # CHAT HISTORY
178
+ # =========================
179
+ def load_chat_history():
180
+ if not os.path.exists(CHAT_FILE):
181
+ return []
182
+ try:
183
+ with open(CHAT_FILE, "r", encoding="utf-8") as f:
184
+ return json.load(f)
185
+ except:
186
+ return []
187
+
188
+ def save_chat_history(history):
189
+ with open(CHAT_FILE, "w", encoding="utf-8") as f:
190
+ json.dump(history, f, ensure_ascii=False, indent=2)
191
+
192
+ def api_history_to_pairs(history):
193
+ pairs = []
194
+ for i in range(0, len(history)-1, 2):
195
+ if history[i]["role"] == "user":
196
+ pairs.append((history[i]["content"], history[i+1]["content"]))
197
+ return pairs
198
+
199
+ def load_visible_chat_history_for_ui():
200
+ pairs = api_history_to_pairs(load_chat_history())
201
+ return pairs, pairs
202
 
203
+ # =========================
204
  # API
205
+ # =========================
206
  def gradio_simple_api(user_message):
207
+ history = load_chat_history()
208
+ history.append({"role": "user", "content": user_message})
209
 
210
+ reply = generate_reply(user_message)
211
 
212
+ history.append({"role": "assistant", "content": reply})
213
+ save_chat_history(history)
 
214
 
215
+ return reply
 
 
 
216
 
217
+ # =========================
218
+ # UI
219
+ # =========================
220
+ def ui_chat_send(message, history):
221
+ reply = generate_reply(message)
222
+ history.append((message, reply))
223
 
224
+ hist = load_chat_history()
225
+ hist.append({"role": "user", "content": message})
226
+ hist.append({"role": "assistant", "content": reply})
227
+ save_chat_history(hist)
228
 
229
+ return "", history
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
 
231
  def ui_chat_reset():
232
+ save_chat_history([])
233
+ return [], []
234
 
235
+ def ui_status():
236
+ data = wissen_laden()
237
+ return f"Fakten: {len(data)}"
238
 
239
+ def ui_learn(code, frage, antwort):
240
+ if code != os.environ.get("CODE", "1234"):
241
+ return "❌ Falscher Code"
242
+ wissen_speichern(frage, antwort)
243
+ return "✅ Gespeichert"
244
 
 
 
 
245
  def erzeuge_gradio_app():
246
+ import gradio as gr
247
+
248
+ with gr.Blocks() as demo:
249
+ gr.Markdown("# 🤖 KI")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
 
251
  with gr.Tab("Chat"):
252
+ chatbot = gr.Chatbot(height=400, type="tuples")
253
+ msg = gr.Textbox()
254
+ send = gr.Button("Senden")
255
+ reset = gr.Button("Reset")
256
+
257
+ send.click(ui_chat_send, [msg, chatbot], [msg, chatbot])
258
+ reset.click(ui_chat_reset, None, [chatbot, chatbot])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
 
260
+ demo.load(load_visible_chat_history_for_ui, None, [chatbot, chatbot])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
 
262
+ with gr.Tab("Lernen"):
263
+ code = gr.Textbox(label="Code", type="password")
264
+ frage = gr.Textbox(label="Frage")
265
+ antwort = gr.Textbox(label="Antwort")
266
+ btn = gr.Button("Speichern")
267
+ out = gr.Textbox()
268
+
269
+ btn.click(ui_learn, [code, frage, antwort], out)
270
+
271
+ with gr.Tab("Status"):
272
+ txt = gr.Textbox()
273
+ demo.load(ui_status, None, txt)
274
+
275
+ # API
276
+ inp = gr.Textbox(visible=False)
277
+ out = gr.Textbox(visible=False)
278
+ btn = gr.Button(visible=False)
279
+ btn.click(gradio_simple_api, inp, out, api_name="predict")
280
+
281
+ return demo
282
+
283
+ # =========================
284
+ # START
285
+ # =========================
286
+ if __name__ == "__main__":
287
  init_model_if_needed()
288
 
289
  if os.environ.get("SPACE_ID"):
290
  app = erzeuge_gradio_app()
291
  app.launch()
292
  else:
293
+ while True:
294
+ msg = input("Du: ")
295
+ print("KI:", generate_reply(msg))