Princeaka commited on
Commit
a3ee38f
Β·
verified Β·
1 Parent(s): dab77fb

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +427 -221
app.py CHANGED
@@ -1,4 +1,6 @@
1
- # JusticeAI Backend β€” FULL FILE, ALL ENDPOINTS, FIXED /chat, ENSEMBLE LLMS, READY FOR DEPLOYMENT
 
 
2
 
3
  import os
4
  import time
@@ -10,7 +12,7 @@ import asyncio
10
  import re
11
  from datetime import datetime, timezone
12
  from collections import deque
13
- from typing import Optional, Dict, Any, List
14
 
15
  import requests
16
  import psutil
@@ -25,48 +27,62 @@ import logging
25
  logging.basicConfig(level=logging.INFO)
26
  logger = logging.getLogger("justiceai")
27
 
 
28
  TRANSLATION_CACHE_DIR = os.environ.get("TRANSLATION_CACHE_DIR", "/tmp/translation_models")
29
  os.environ["TRANSLATION_CACHE_DIR"] = TRANSLATION_CACHE_DIR
30
 
31
  ADMIN_KEY = os.environ.get("ADMIN_KEY")
32
- DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///justice.db")
 
33
  EMBED_MODEL_NAME = os.environ.get("EMBED_MODEL_NAME", "paraphrase-multilingual-MiniLM-L12-v2")
34
  SAVE_MEMORY_CONFIDENCE = float(os.environ.get("SAVE_MEMORY_CONFIDENCE", "0.45"))
35
  LLM_MODEL_PATHS = [
 
36
  "TinyLlama/TinyLlama-1.1B-Chat-v1.0",
37
  "Qwen/Qwen1.5-0.5B-Chat",
38
  "microsoft/phi-2"
39
  ]
40
 
 
41
  app = FastAPI(title="JusticeAI β€” Backend (final)")
42
- engine = create_engine(
43
  DATABASE_URL,
44
  poolclass=NullPool,
45
  connect_args={"check_same_thread": False} if DATABASE_URL.startswith("sqlite") else {}
46
  )
 
 
 
 
 
47
 
48
- # --- Optional helper imports ---
49
  try:
50
  from emojis import get_emoji, get_category_for_mood
51
  except Exception:
52
  def get_category_for_mood(mood: str) -> str: return "neutral"
53
  def get_emoji(cat: str, intensity: float = 0.5) -> str: return "πŸ€–"
 
54
  try:
55
  from health import get_health_status
56
  except Exception:
57
  def get_health_status(engine_arg) -> Dict[str, Any]: return {"status": "starting", "db_status": "unknown", "stars": 0}
 
58
  try:
59
  from langdetect import detect as detect_lang
60
  except Exception:
61
  detect_lang = None
 
62
  try:
63
  from sentence_transformers import SentenceTransformer
64
  except Exception:
65
  SentenceTransformer = None
 
66
  try:
67
  from spellchecker import SpellChecker
68
  except Exception:
69
  SpellChecker = None
 
70
  try:
71
  from transformers import AutoTokenizer, AutoModelForSeq2SeqLM, AutoModelForCausalLM, pipeline as hf_pipeline
72
  except Exception:
@@ -75,82 +91,91 @@ except Exception:
75
  AutoModelForCausalLM = None
76
  hf_pipeline = None
77
 
78
- # --- Database creation ---
79
- def ensure_tables():
80
- dialect = engine.dialect.name
81
- with engine.begin() as conn:
 
 
82
  if dialect == "sqlite":
83
- conn.execute(sql_text("""
84
- CREATE TABLE IF NOT EXISTS knowledge (
85
- id INTEGER PRIMARY KEY AUTOINCREMENT,
86
- text TEXT,
87
- reply TEXT,
88
- language TEXT DEFAULT 'en',
89
- embedding BLOB,
90
- category TEXT DEFAULT 'learned',
91
- topic TEXT DEFAULT 'general',
92
- confidence FLOAT DEFAULT 0,
93
- source TEXT,
94
- meta TEXT,
95
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
96
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
97
- );"""))
98
- conn.execute(sql_text("""
99
- CREATE TABLE IF NOT EXISTS user_memory (
100
- id INTEGER PRIMARY KEY AUTOINCREMENT,
101
- user_id TEXT,
102
- username TEXT,
103
- ip TEXT,
104
- text TEXT,
105
- reply TEXT,
106
- language TEXT DEFAULT 'en',
107
- mood TEXT,
108
- confidence FLOAT DEFAULT 0,
109
- topic TEXT DEFAULT 'general',
110
- source TEXT,
111
- meta TEXT,
112
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
113
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
114
- );"""))
 
 
115
  else:
116
- conn.execute(sql_text("""
117
- CREATE TABLE IF NOT EXISTS knowledge (
118
- id SERIAL PRIMARY KEY,
119
- text TEXT,
120
- reply TEXT,
121
- language TEXT DEFAULT 'en',
122
- embedding BYTEA,
123
- category TEXT DEFAULT 'learned',
124
- topic TEXT DEFAULT 'general',
125
- confidence FLOAT DEFAULT 0,
126
- source TEXT,
127
- meta JSONB,
128
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
129
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
130
- );"""))
131
- conn.execute(sql_text("""
132
- CREATE TABLE IF NOT EXISTS user_memory (
133
- id SERIAL PRIMARY KEY,
134
- user_id TEXT,
135
- username TEXT,
136
- ip TEXT,
137
- text TEXT,
138
- reply TEXT,
139
- language TEXT DEFAULT 'en',
140
- mood TEXT,
141
- confidence FLOAT DEFAULT 0,
142
- topic TEXT DEFAULT 'general',
143
- source TEXT,
144
- meta JSONB,
145
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
146
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
147
- );"""))
148
- ensure_tables()
149
-
150
- def ensure_column_exists(table: str, column: str, col_def_sql: str):
151
- dialect = engine.dialect.name
 
 
 
 
 
152
  try:
153
- with engine.begin() as conn:
154
  if dialect == "sqlite":
155
  rows = conn.execute(sql_text(f"PRAGMA table_info({table})")).fetchall()
156
  existing_cols = [r[1] for r in rows]
@@ -160,12 +185,17 @@ def ensure_column_exists(table: str, column: str, col_def_sql: str):
160
  conn.execute(sql_text(f"ALTER TABLE {table} ADD COLUMN IF NOT EXISTS {col_def_sql}"))
161
  except Exception:
162
  pass
163
- ensure_column_exists("knowledge", "reply", "reply TEXT")
164
- ensure_column_exists("user_memory", "reply", "reply TEXT")
165
- ensure_column_exists("knowledge", "language", "language TEXT DEFAULT 'en'")
166
- ensure_column_exists("knowledge", "embedding", "embedding BYTEA" if engine.dialect.name != "sqlite" else "embedding BLOB")
167
 
168
- # --- State ---
 
 
 
 
 
 
 
 
 
169
  app_start_time = time.time()
170
  last_heartbeat = {"time": datetime.utcnow().replace(tzinfo=timezone.utc).isoformat(), "ok": True}
171
  RECENT_WINDOW_SECONDS = 3600
@@ -175,11 +205,13 @@ recent_requests_timestamps = deque()
175
  recent_learning_timestamps = deque()
176
  response_time_ema: Optional[float] = None
177
  EMA_ALPHA = 0.2
 
178
  SPARKLINE_LEN = 60
179
  cpu_history = deque(maxlen=SPARKLINE_LEN)
180
  mem_history = deque(maxlen=SPARKLINE_LEN)
181
  latency_history = deque(maxlen=SPARKLINE_LEN)
182
  recent_metrics = deque(maxlen=600)
 
183
  model_progress = {
184
  "embed": {"status": "pending", "progress": 0.0},
185
  "spell": {"status": "pending", "progress": 0.0},
@@ -190,11 +222,13 @@ model_load_times = {"embed": None, "spell": None, "moderator": None, "llm": None
190
  embed_model = None
191
  spell = None
192
  moderator = None
193
- ensemble_llms = []
194
  startup_time = 0.0
195
  _translation_model_cache: Dict[str, Any] = {}
196
 
197
- # --- Utility functions ---
 
 
198
  def record_request(duration_s: float):
199
  global response_time_ema
200
  ts = time.time()
@@ -232,7 +266,7 @@ def sanitize_knowledge_text(t: Any) -> str:
232
  s = s[1:-1].strip()
233
  return " ".join(s.split())
234
 
235
- def dedupe_sentences(text):
236
  parts = re.split(r'([.?!]\s+)', text)
237
  out = []
238
  seen = set()
@@ -264,21 +298,16 @@ def detect_language_safe(text: str) -> str:
264
  def embed_text(text_data: str) -> bytes:
265
  global embed_model
266
  if embed_model is None:
267
- logger.warning("Embedding model not available; fallback.")
268
  raise RuntimeError("Embedding model not available.")
269
- try:
270
- emb = embed_model.encode(text_data, convert_to_tensor=True)
271
- return emb.cpu().numpy().tobytes()
272
- except Exception as e:
273
- logger.warning(f"Embedding fallback: {e}")
274
- raise
275
 
276
  def is_boilerplate_candidate(s: str) -> bool:
277
  s_low = (s or "").strip().lower()
278
  return "justiceai" in s_low or "dashboard" in s_low or "intelligence" in s_low
279
 
280
- def ensemble_llm_suggestions(prompt):
281
- replies = []
282
  for tokenizer, model in ensemble_llms:
283
  try:
284
  inputs = tokenizer(prompt, return_tensors="pt", truncation=True, max_length=512)
@@ -290,6 +319,148 @@ def ensemble_llm_suggestions(prompt):
290
  logger.debug(f"LLM error ({getattr(tokenizer, 'name_or_path', 'unknown')}): {e}")
291
  return replies
292
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  @app.on_event("startup")
294
  async def startup_event():
295
  global embed_model, spell, moderator, ensemble_llms, startup_time
@@ -337,6 +508,7 @@ async def startup_event():
337
  moderator = None
338
  model_progress["moderator"]["status"] = "error"
339
  logger.warning(f"[JusticeAI] Moderator load error: {e}")
 
340
  ensemble_llms.clear()
341
  if AutoTokenizer is not None and AutoModelForCausalLM is not None:
342
  for path in LLM_MODEL_PATHS:
@@ -347,22 +519,41 @@ async def startup_event():
347
  logger.info(f"[JusticeAI] Loaded ensemble LLM: {path}")
348
  except Exception as e:
349
  logger.warning(f"[JusticeAI] Could not load ensemble LLM {path}: {e}")
 
350
  startup_time = round(time.time() - t0, 2)
351
  logger.info(f"[JusticeAI] Startup completed in {startup_time}s")
 
 
352
  initial_knowledge = [
353
  {"text": "Justice is fairness in protection of rights and punishment of wrongs.", "reply": "Justice means fairness.", "topic": "general"},
354
  {"text": "Law is a system of rules created and enforced through social or governmental institutions.", "reply": "Law is a set of rules.", "topic": "general"},
355
  ]
356
- with engine.begin() as conn:
357
  for item in initial_knowledge:
358
  exists = conn.execute(sql_text("SELECT COUNT(*) FROM knowledge WHERE text = :t"), {"t": item["text"]}).scalar()
359
  if not exists:
360
- emb = embed_text(item["text"]) if embed_model else None
361
- conn.execute(
362
- sql_text("INSERT INTO knowledge (text, reply, language, embedding, category, topic, confidence) VALUES (:t, :r, 'en', :e, 'learned', :topic, 1.0)"),
363
- {"t": item["text"], "r": item["reply"], "e": emb, "topic": item["topic"]}
364
- )
365
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
  @app.post("/chat")
367
  async def chat(request: Request, data: dict = Body(...)):
368
  t0 = time.time()
@@ -372,36 +563,40 @@ async def chat(request: Request, data: dict = Body(...)):
372
  username = data.get("username", "anonymous")
373
  user_ip = request.client.host if request.client else "0.0.0.0"
374
  user_id = hashlib.sha256(f"{user_ip}-{username}".encode()).hexdigest()
375
- topic_hint = str(data.get("topic", "") or "").strip()
376
  detected_lang = detect_language_safe(raw_msg)
377
  reply_lang = detected_lang
378
  user_force_save = bool(data.get("save_memory", False))
379
 
 
380
  msg_corrected = raw_msg
381
  if spell is not None:
382
  try:
383
  words = raw_msg.split()
384
- corrected = [spell.correction(w) for w in words]
 
 
 
385
  msg_corrected = " ".join(corrected)
386
  except Exception:
387
  pass
388
 
389
- topic = topic_hint if topic_hint else "general"
390
-
391
  try:
392
- with engine.begin() as conn:
393
- rows = conn.execute(sql_text("SELECT id, text, reply, language, embedding, topic FROM knowledge WHERE category='learned' ORDER BY created_at DESC")).fetchall()
394
  except Exception as e:
395
  record_request(time.time() - t0)
396
  return JSONResponse(status_code=500, content={"error": "failed to read knowledge", "details": str(e)})
397
 
398
  knowledge_texts = [r[1] or "" for r in rows]
399
  knowledge_replies = [r[2] or r[1] or "" for r in rows]
400
- knowledge_langs = [r[3] or "en" for r in rows]
401
  knowledge_topics = [r[5] or "general" for r in rows]
 
402
 
403
- matches = []
404
- confidence = 0.0
 
405
  similarity_threshold = 0.35
406
  try:
407
  if embed_model is not None and knowledge_texts:
@@ -409,98 +604,133 @@ async def chat(request: Request, data: dict = Body(...)):
409
  msg_emb = embed_model.encode(msg_corrected, convert_to_tensor=True)
410
  if msg_emb.shape[-1] == knowledge_embeddings.shape[-1]:
411
  scores = torch.nn.functional.cosine_similarity(msg_emb.unsqueeze(0), knowledge_embeddings)
412
- topk = min(10, scores.shape[0])
413
  top_indices = torch.topk(scores, k=topk).indices.tolist()
414
  seen_text = set()
415
  filtered = []
416
  for i in top_indices:
417
  s = float(scores[i])
418
  candidate = knowledge_replies[i]
419
- key = candidate.strip().lower()
420
  if is_boilerplate_candidate(candidate): continue
 
421
  if key in seen_text: continue
422
  seen_text.add(key)
423
- if s > similarity_threshold:
424
- filtered.append((i, s, candidate))
 
 
 
425
  matches = [c for _, _, c in filtered]
426
  confidence = filtered[0][1] if filtered else 0.0
427
  else:
428
- logger.warning("Embedding dimension mismatch for confidence scoring.")
429
  matches = []
430
  else:
 
431
  for idx, ktext in enumerate(knowledge_texts):
432
- if topic and topic.lower() in (knowledge_topics[idx] or "").lower():
433
- if msg_corrected.lower() in ktext.lower():
434
- matches.append(ktext)
435
  confidence = 0.0
436
  except Exception as e:
437
- logger.warning(f"Knowledge retrieval fallback: {e}")
438
  matches = knowledge_replies[:3] if knowledge_replies else []
439
  confidence = 0.0
440
 
 
441
  loop = asyncio.get_running_loop()
442
- def run_llm(prompt):
443
- return ensemble_llm_suggestions(prompt)
444
  try:
445
- llm_replies = await loop.run_in_executor(None, run_llm, msg_corrected)
 
446
  except Exception as e:
447
  logger.warning(f"LLM ensemble failed: {e}")
448
  llm_replies = []
449
 
450
- unique_llm_replies = []
 
451
  if embed_model is not None and matches and llm_replies:
452
- match_embs = embed_model.encode(matches, convert_to_tensor=True)
453
- for llm_text in llm_replies:
454
- try:
455
- llm_emb = embed_model.encode(llm_text, convert_to_tensor=True)
456
- sims = torch.nn.functional.cosine_similarity(llm_emb.unsqueeze(0), match_embs)
457
- max_sim = float(sims.max().item())
458
- if max_sim < 0.60:
459
- unique_llm_replies.append(llm_text)
460
- except Exception:
461
- if llm_text not in matches:
462
- unique_llm_replies.append(llm_text)
 
 
 
463
  else:
464
- for llm_text in llm_replies:
465
- if llm_text not in matches:
466
- unique_llm_replies.append(llm_text)
467
 
 
468
  all_candidates = []
469
  for m in matches:
470
  if m and not is_boilerplate_candidate(m):
471
  all_candidates.append(dedupe_sentences(m))
472
- for llm_r in unique_llm_replies:
473
- if llm_r and not is_boilerplate_candidate(llm_r):
474
- all_candidates.append(dedupe_sentences(llm_r))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
475
 
476
- seen = set()
477
- merged = []
478
- for s in all_candidates:
479
- for sent in re.split(r'(?<=[.?!])\s+', s):
480
- sent = sent.strip()
481
- if sent and sent not in seen and not is_boilerplate_candidate(sent):
482
- seen.add(sent)
483
- merged.append(sent)
484
- reply_en = " ".join(merged[:3]) if merged else "Can you provide more details so I can help better?"
485
-
486
- reply_final = reply_en
487
- mood = "neutral"
488
- emoji = ""
489
- flags = {}
490
 
491
  duration = time.time() - t0
492
  record_request(duration)
 
 
 
493
  return {
494
- "reply": reply_final,
495
- "topic": topic,
496
  "language": reply_lang,
497
  "emoji": emoji,
498
  "confidence": round(confidence, 2),
499
  "flags": flags
500
  }
501
 
 
 
 
502
  @app.post("/add")
503
- async def add_knowledge(data: dict = Body(...)):
 
 
 
 
504
  text_data = sanitize_knowledge_text(data.get("text", "") or "")
505
  reply = sanitize_knowledge_text(data.get("reply", "") or "")
506
  topic = str(data.get("topic", "") or "").strip()
@@ -508,7 +738,6 @@ async def add_knowledge(data: dict = Body(...)):
508
  return JSONResponse(status_code=400, content={"error": "Topic is required"})
509
  if not text_data:
510
  return JSONResponse(status_code=400, content={"error": "Text is required"})
511
- detected = detect_language_safe(text_data)
512
  try:
513
  emb = None
514
  if embed_model is not None:
@@ -517,17 +746,13 @@ async def add_knowledge(data: dict = Body(...)):
517
  except Exception as e:
518
  logger.warning(f"embed_text failed in /add: {e}")
519
  emb = None
520
- with engine.begin() as conn:
521
  if emb is not None:
522
- conn.execute(
523
- sql_text("INSERT INTO knowledge (text, reply, language, embedding, category, topic) VALUES (:t, :r, :lang, :e, 'learned', :topic)"),
524
- {"t": text_data, "r": reply, "lang": "en", "e": emb, "topic": topic}
525
- )
526
  else:
527
- conn.execute(
528
- sql_text("INSERT INTO knowledge (text, reply, language, category, topic) VALUES (:t, :r, :lang, 'learned', :topic)"),
529
- {"t": text_data, "r": reply, "lang": "en", "topic": topic}
530
- )
531
  record_learn_event()
532
  res = {"status": "βœ… Knowledge added", "text": text_data, "topic": topic, "language": "en"}
533
  if embed_model is None or emb is None:
@@ -536,47 +761,14 @@ async def add_knowledge(data: dict = Body(...)):
536
  except Exception as e:
537
  return JSONResponse(status_code=500, content={"error": "failed to store knowledge", "details": str(e)})
538
 
539
- @app.post("/add-bulk")
540
- async def add_bulk(data: List[dict] = Body(...)):
541
- added = 0
542
- errors = []
543
- for i, item in enumerate(data):
544
- try:
545
- text_data = sanitize_knowledge_text(item.get("text", "") or "")
546
- reply = sanitize_knowledge_text(item.get("reply", "") or "")
547
- topic = str(item.get("topic", "") or "").strip()
548
- if not text_data or not topic:
549
- errors.append({"index": i, "error": "missing text or topic"})
550
- continue
551
- detected = detect_language_safe(text_data)
552
- try:
553
- emb = embed_text(text_data) if embed_model is not None else None
554
- except Exception as e:
555
- emb = None
556
- errors.append({"index": i, "error": f"embed failed: {e}"})
557
- continue
558
- with engine.begin() as conn:
559
- if emb is not None:
560
- conn.execute(
561
- sql_text("INSERT INTO knowledge (text, reply, language, embedding, category, topic) VALUES (:t, :r, :lang, :e, 'learned', :topic)"),
562
- {"t": text_data, "r": reply, "lang": "en", "e": emb, "topic": topic}
563
- )
564
- else:
565
- conn.execute(
566
- sql_text("INSERT INTO knowledge (text, reply, language, category, topic) VALUES (:t, :r, :lang, 'learned', :topic)"),
567
- {"t": text_data, "r": reply, "lang": "en", "topic": topic}
568
- )
569
- record_learn_event()
570
- added += 1
571
- except Exception as e:
572
- errors.append({"index": i, "error": str(e)})
573
- return {"added": added, "errors": errors}
574
-
575
  @app.get("/leaderboard")
576
  async def leaderboard(topic: str = Query("general")):
577
  topic = str(topic or "general").strip() or "general"
578
  try:
579
- with engine.begin() as conn:
580
  rows = conn.execute(sql_text("""
581
  SELECT id, text, reply, language, category, confidence, created_at
582
  FROM knowledge
@@ -602,6 +794,9 @@ async def leaderboard(topic: str = Query("general")):
602
  except Exception as e:
603
  return JSONResponse(status_code=500, content={"error": "failed to fetch leaderboard", "details": str(e)})
604
 
 
 
 
605
  @app.get("/model-status")
606
  async def model_status():
607
  response_progress = {k: dict(v) for k, v in model_progress.items()}
@@ -617,13 +812,13 @@ async def health_check():
617
  elapsed = round(time.time() - start, 2)
618
  health_data["response_time_s"] = elapsed
619
  try:
620
- with engine.connect() as conn:
621
- k = conn.execute(sql_text("SELECT COUNT(*) FROM knowledge WHERE category='learned'")).scalar() or 0
622
- u = conn.execute(sql_text("SELECT COUNT(*) FROM user_memory")).scalar() or 0
623
  except Exception:
624
  k, u = -1, -1
625
  try:
626
- with engine.begin() as conn:
627
  rows = conn.execute(sql_text("SELECT DISTINCT topic FROM knowledge WHERE category='learned'")).fetchall()
628
  topics = [r[0] for r in rows if r and r[0]]
629
  except Exception:
@@ -638,6 +833,7 @@ async def health_check():
638
  health_data["learn_rate_per_min"] = sum(1 for t in recent_learning_timestamps if t >= time.time() - 60)
639
  return health_data
640
 
 
641
  async def metrics_producer():
642
  while True:
643
  try:
@@ -654,9 +850,9 @@ async def metrics_producer():
654
  async def _get_counts():
655
  def blocking_counts():
656
  try:
657
- with engine.connect() as conn:
658
- kcount = conn.execute(sql_text("SELECT COUNT(*) FROM knowledge WHERE category='learned'")).scalar() or 0
659
- ucount = conn.execute(sql_text("SELECT COUNT(*) FROM user_memory")).scalar() or 0
660
  return int(kcount), int(ucount)
661
  except Exception:
662
  return 0, 0
@@ -701,6 +897,9 @@ async def metrics_recent(limit: int = Query(100, ge=1, le=600)):
701
  items = list(recent_metrics)[-limit:]
702
  return {"count": len(items), "metrics": items}
703
 
 
 
 
704
  @app.post("/verify-admin")
705
  async def verify_admin(x_admin_key: str = Header(None, alias="X-Admin-Key")):
706
  if ADMIN_KEY is None:
@@ -719,12 +918,13 @@ async def clear_database(data: dict = Body(...), x_admin_key: str = Header(None,
719
  if confirm != "CLEAR_DATABASE":
720
  return JSONResponse(status_code=400, content={"error": "confirm token required."})
721
  try:
722
- with engine.begin() as conn:
723
- k_count = conn.execute(sql_text("SELECT COUNT(*) FROM knowledge")).scalar() or 0
724
- u_count = conn.execute(sql_text("SELECT COUNT(*) FROM user_memory")).scalar() or 0
725
- conn.execute(sql_text("DELETE FROM knowledge"))
726
- conn.execute(sql_text("DELETE FROM user_memory"))
727
- return {"status": "βœ… Cleared database", "deleted_knowledge": int(k_count), "deleted_user_memory": int(u_count)}
 
728
  except Exception as e:
729
  return JSONResponse(status_code=500, content={"error": "failed to clear database", "details": str(e)})
730
 
@@ -741,7 +941,7 @@ async def reembed_all(data: dict = Body(...), x_admin_key: str = Header(None, al
741
  return JSONResponse(status_code=400, content={"error": "confirm token required."})
742
  batch_size = int(data.get("batch_size", 100))
743
  try:
744
- with engine.begin() as conn:
745
  rows = conn.execute(sql_text("SELECT id, text FROM knowledge WHERE category='learned' ORDER BY id")).fetchall()
746
  ids_texts = [(r[0], r[1]) for r in rows]
747
  total = len(ids_texts)
@@ -752,13 +952,16 @@ async def reembed_all(data: dict = Body(...), x_admin_key: str = Header(None, al
752
  embs = embed_model.encode(texts, convert_to_tensor=True)
753
  for j, (kid, _) in enumerate(batch):
754
  emb_bytes = embs[j].cpu().numpy().tobytes()
755
- with engine.begin() as conn:
756
  conn.execute(sql_text("UPDATE knowledge SET embedding = :e, updated_at = CURRENT_TIMESTAMP WHERE id = :id"), {"e": emb_bytes, "id": kid})
757
  updated += 1
758
  return {"status": "βœ… Re-embed complete", "total_rows": total, "updated": updated}
759
  except Exception as e:
760
  return JSONResponse(status_code=500, content={"error": "re-embed failed", "details": str(e)})
761
 
 
 
 
762
  @app.get("/", response_class=HTMLResponse)
763
  async def frontend_dashboard():
764
  try:
@@ -789,6 +992,9 @@ async def frontend_dashboard():
789
  html = html.replace("%%STARTUP_TIME%%", str(startup_time_local))
790
  return HTMLResponse(html)
791
 
 
 
 
792
  if __name__ == "__main__":
793
  port = int(os.environ.get("PORT", 7860))
794
- uvicorn.run("app:app", host="0.0.0.0", port=port)
 
1
+ # JusticeAI β€” Full updated app.py
2
+ # Key change: separate knowledge DB (KNOWLEDGEDATABASE_URL) and user DB (DATABASE_URL).
3
+ # /chat now only writes to user_memory (user DB). knowledge DB is only written by /add and background refinement.
4
 
5
  import os
6
  import time
 
12
  import re
13
  from datetime import datetime, timezone
14
  from collections import deque
15
+ from typing import Optional, Dict, Any, List, Tuple
16
 
17
  import requests
18
  import psutil
 
27
  logging.basicConfig(level=logging.INFO)
28
  logger = logging.getLogger("justiceai")
29
 
30
+ # env config
31
  TRANSLATION_CACHE_DIR = os.environ.get("TRANSLATION_CACHE_DIR", "/tmp/translation_models")
32
  os.environ["TRANSLATION_CACHE_DIR"] = TRANSLATION_CACHE_DIR
33
 
34
  ADMIN_KEY = os.environ.get("ADMIN_KEY")
35
+ DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///justice.db") # user DB (user_memory, etc.)
36
+ KNOWLEDGE_DATABASE_URL = os.environ.get("KNOWLEDGEDATABASE_URL", DATABASE_URL) # knowledge DB (knowledge table)
37
  EMBED_MODEL_NAME = os.environ.get("EMBED_MODEL_NAME", "paraphrase-multilingual-MiniLM-L12-v2")
38
  SAVE_MEMORY_CONFIDENCE = float(os.environ.get("SAVE_MEMORY_CONFIDENCE", "0.45"))
39
  LLM_MODEL_PATHS = [
40
+ # Examples β€” replace with local / available checkpoints
41
  "TinyLlama/TinyLlama-1.1B-Chat-v1.0",
42
  "Qwen/Qwen1.5-0.5B-Chat",
43
  "microsoft/phi-2"
44
  ]
45
 
46
+ # app + engines
47
  app = FastAPI(title="JusticeAI β€” Backend (final)")
48
+ engine = create_engine( # user DB (user_memory)
49
  DATABASE_URL,
50
  poolclass=NullPool,
51
  connect_args={"check_same_thread": False} if DATABASE_URL.startswith("sqlite") else {}
52
  )
53
+ knowledge_engine = create_engine( # knowledge DB (knowledge)
54
+ KNOWLEDGE_DATABASE_URL,
55
+ poolclass=NullPool,
56
+ connect_args={"check_same_thread": False} if KNOWLEDGE_DATABASE_URL.startswith("sqlite") else {}
57
+ )
58
 
59
+ # Optional helpers
60
  try:
61
  from emojis import get_emoji, get_category_for_mood
62
  except Exception:
63
  def get_category_for_mood(mood: str) -> str: return "neutral"
64
  def get_emoji(cat: str, intensity: float = 0.5) -> str: return "πŸ€–"
65
+
66
  try:
67
  from health import get_health_status
68
  except Exception:
69
  def get_health_status(engine_arg) -> Dict[str, Any]: return {"status": "starting", "db_status": "unknown", "stars": 0}
70
+
71
  try:
72
  from langdetect import detect as detect_lang
73
  except Exception:
74
  detect_lang = None
75
+
76
  try:
77
  from sentence_transformers import SentenceTransformer
78
  except Exception:
79
  SentenceTransformer = None
80
+
81
  try:
82
  from spellchecker import SpellChecker
83
  except Exception:
84
  SpellChecker = None
85
+
86
  try:
87
  from transformers import AutoTokenizer, AutoModelForSeq2SeqLM, AutoModelForCausalLM, pipeline as hf_pipeline
88
  except Exception:
 
91
  AutoModelForCausalLM = None
92
  hf_pipeline = None
93
 
94
+ # -------------------------
95
+ # Schema setup (both DBs)
96
+ # -------------------------
97
+ def ensure_tables_for_engine(engine_obj, is_knowledge: bool):
98
+ dialect = engine_obj.dialect.name
99
+ with engine_obj.begin() as conn:
100
  if dialect == "sqlite":
101
+ if is_knowledge:
102
+ conn.execute(sql_text("""
103
+ CREATE TABLE IF NOT EXISTS knowledge (
104
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
105
+ text TEXT,
106
+ reply TEXT,
107
+ language TEXT DEFAULT 'en',
108
+ embedding BLOB,
109
+ category TEXT DEFAULT 'learned',
110
+ topic TEXT DEFAULT 'general',
111
+ confidence FLOAT DEFAULT 0,
112
+ source TEXT,
113
+ meta TEXT,
114
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
115
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
116
+ );"""))
117
+ else:
118
+ conn.execute(sql_text("""
119
+ CREATE TABLE IF NOT EXISTS user_memory (
120
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
121
+ user_id TEXT,
122
+ username TEXT,
123
+ ip TEXT,
124
+ text TEXT,
125
+ reply TEXT,
126
+ language TEXT DEFAULT 'en',
127
+ mood TEXT,
128
+ confidence FLOAT DEFAULT 0,
129
+ topic TEXT DEFAULT 'general',
130
+ source TEXT,
131
+ meta TEXT,
132
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
133
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
134
+ );"""))
135
  else:
136
+ if is_knowledge:
137
+ conn.execute(sql_text("""
138
+ CREATE TABLE IF NOT EXISTS knowledge (
139
+ id SERIAL PRIMARY KEY,
140
+ text TEXT,
141
+ reply TEXT,
142
+ language TEXT DEFAULT 'en',
143
+ embedding BYTEA,
144
+ category TEXT DEFAULT 'learned',
145
+ topic TEXT DEFAULT 'general',
146
+ confidence FLOAT DEFAULT 0,
147
+ source TEXT,
148
+ meta JSONB,
149
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
150
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
151
+ );"""))
152
+ else:
153
+ conn.execute(sql_text("""
154
+ CREATE TABLE IF NOT EXISTS user_memory (
155
+ id SERIAL PRIMARY KEY,
156
+ user_id TEXT,
157
+ username TEXT,
158
+ ip TEXT,
159
+ text TEXT,
160
+ reply TEXT,
161
+ language TEXT DEFAULT 'en',
162
+ mood TEXT,
163
+ confidence FLOAT DEFAULT 0,
164
+ topic TEXT DEFAULT 'general',
165
+ source TEXT,
166
+ meta JSONB,
167
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
168
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
169
+ );"""))
170
+
171
+ # Ensure tables exist in both DBs
172
+ ensure_tables_for_engine(knowledge_engine, is_knowledge=True)
173
+ ensure_tables_for_engine(engine, is_knowledge=False)
174
+
175
+ def ensure_column_exists(engine_obj, table: str, column: str, col_def_sql: str):
176
+ dialect = engine_obj.dialect.name
177
  try:
178
+ with engine_obj.begin() as conn:
179
  if dialect == "sqlite":
180
  rows = conn.execute(sql_text(f"PRAGMA table_info({table})")).fetchall()
181
  existing_cols = [r[1] for r in rows]
 
185
  conn.execute(sql_text(f"ALTER TABLE {table} ADD COLUMN IF NOT EXISTS {col_def_sql}"))
186
  except Exception:
187
  pass
 
 
 
 
188
 
189
+ # keep migrations safe
190
+ ensure_column_exists(knowledge_engine, "knowledge", "reply", "reply TEXT")
191
+ ensure_column_exists(knowledge_engine, "knowledge", "language", "language TEXT DEFAULT 'en'")
192
+ ensure_column_exists(knowledge_engine, "knowledge", "embedding", "embedding BYTEA" if knowledge_engine.dialect.name != "sqlite" else "embedding BLOB")
193
+ ensure_column_exists(engine, "user_memory", "reply", "reply TEXT")
194
+ ensure_column_exists(engine, "user_memory", "language", "language TEXT DEFAULT 'en'")
195
+
196
+ # -------------------------
197
+ # State + telemetry
198
+ # -------------------------
199
  app_start_time = time.time()
200
  last_heartbeat = {"time": datetime.utcnow().replace(tzinfo=timezone.utc).isoformat(), "ok": True}
201
  RECENT_WINDOW_SECONDS = 3600
 
205
  recent_learning_timestamps = deque()
206
  response_time_ema: Optional[float] = None
207
  EMA_ALPHA = 0.2
208
+
209
  SPARKLINE_LEN = 60
210
  cpu_history = deque(maxlen=SPARKLINE_LEN)
211
  mem_history = deque(maxlen=SPARKLINE_LEN)
212
  latency_history = deque(maxlen=SPARKLINE_LEN)
213
  recent_metrics = deque(maxlen=600)
214
+
215
  model_progress = {
216
  "embed": {"status": "pending", "progress": 0.0},
217
  "spell": {"status": "pending", "progress": 0.0},
 
222
  embed_model = None
223
  spell = None
224
  moderator = None
225
+ ensemble_llms: List[Tuple[Any, Any]] = []
226
  startup_time = 0.0
227
  _translation_model_cache: Dict[str, Any] = {}
228
 
229
+ # -------------------------
230
+ # Helpers: text, detection, embedding, LLM ensemble
231
+ # -------------------------
232
  def record_request(duration_s: float):
233
  global response_time_ema
234
  ts = time.time()
 
266
  s = s[1:-1].strip()
267
  return " ".join(s.split())
268
 
269
+ def dedupe_sentences(text: str) -> str:
270
  parts = re.split(r'([.?!]\s+)', text)
271
  out = []
272
  seen = set()
 
298
  def embed_text(text_data: str) -> bytes:
299
  global embed_model
300
  if embed_model is None:
 
301
  raise RuntimeError("Embedding model not available.")
302
+ emb = embed_model.encode(text_data, convert_to_tensor=True)
303
+ return emb.cpu().numpy().tobytes()
 
 
 
 
304
 
305
  def is_boilerplate_candidate(s: str) -> bool:
306
  s_low = (s or "").strip().lower()
307
  return "justiceai" in s_low or "dashboard" in s_low or "intelligence" in s_low
308
 
309
+ def ensemble_llm_suggestions(prompt: str) -> List[str]:
310
+ replies: List[str] = []
311
  for tokenizer, model in ensemble_llms:
312
  try:
313
  inputs = tokenizer(prompt, return_tensors="pt", truncation=True, max_length=512)
 
319
  logger.debug(f"LLM error ({getattr(tokenizer, 'name_or_path', 'unknown')}): {e}")
320
  return replies
321
 
322
+ # -------------------------
323
+ # Synthesis & knowledge utilities (operate on knowledge_engine ONLY)
324
+ # -------------------------
325
+ def create_composite_idea(candidates: List[str], context_prompt: Optional[str] = None) -> str:
326
+ prompt = "Synthesize a single clear, actionable idea that integrates these proposals:\n\n"
327
+ for i, c in enumerate(candidates[:8], 1):
328
+ prompt += f"{i}) {c}\n"
329
+ if context_prompt:
330
+ prompt += f"\nContext: {context_prompt}\n"
331
+ prompt += "\nProduce a concise, integrated plan with benefits and steps."
332
+ synths = []
333
+ try:
334
+ synths = ensemble_llm_suggestions(prompt)
335
+ except Exception:
336
+ synths = []
337
+ if synths:
338
+ seen = set()
339
+ merged = []
340
+ for s in synths:
341
+ for sent in re.split(r'(?<=[.?!])\s+', s):
342
+ sent = sent.strip()
343
+ key = sent.lower()
344
+ if sent and key not in seen and not is_boilerplate_candidate(sent):
345
+ seen.add(key)
346
+ merged.append(sent)
347
+ if len(merged) >= 4:
348
+ break
349
+ result = " ".join(merged[:6])
350
+ if result:
351
+ return dedupe_sentences(result)
352
+ reduced = []
353
+ for c in candidates:
354
+ c = dedupe_sentences(c)
355
+ if c and c not in reduced and not is_boilerplate_candidate(c):
356
+ reduced.append(c)
357
+ if not reduced:
358
+ return "I could not synthesize a composite idea; please provide more details."
359
+ if len(reduced) == 1:
360
+ return reduced[0]
361
+ composite = f"Combine: {', '.join(reduced[:3])}."
362
+ return dedupe_sentences(composite)
363
+
364
+ def find_similar_knowledge_in_knowledge_db(text: str, topic: str, threshold: float = 0.75) -> Optional[int]:
365
+ if embed_model is None:
366
+ return None
367
+ try:
368
+ with knowledge_engine.begin() as conn:
369
+ rows = conn.execute(sql_text("SELECT id, text FROM knowledge WHERE topic = :topic"), {"topic": topic}).fetchall()
370
+ if not rows:
371
+ return None
372
+ ids = [r[0] for r in rows]
373
+ texts = [r[1] for r in rows]
374
+ match_embs = embed_model.encode(texts, convert_to_tensor=True)
375
+ query_emb = embed_model.encode(text, convert_to_tensor=True)
376
+ sims = torch.nn.functional.cosine_similarity(query_emb.unsqueeze(0), match_embs)
377
+ best_idx = int(torch.argmax(sims).item())
378
+ best_score = float(sims[best_idx])
379
+ if best_score >= threshold:
380
+ return ids[best_idx]
381
+ except Exception as e:
382
+ logger.debug(f"find_similar_knowledge error: {e}")
383
+ return None
384
+ return None
385
+
386
+ def store_or_refine_knowledge_in_knowledge_db(text: str, reply: str, topic: str = "general", confidence: float = 0.5):
387
+ text = sanitize_knowledge_text(text)
388
+ reply = sanitize_knowledge_text(reply)
389
+ try:
390
+ emb_bytes = None
391
+ if embed_model is not None:
392
+ try:
393
+ emb_bytes = embed_text(text)
394
+ except Exception:
395
+ emb_bytes = None
396
+ existing_id = None
397
+ try:
398
+ existing_id = find_similar_knowledge_in_knowledge_db(text, topic, threshold=0.75)
399
+ except Exception:
400
+ existing_id = None
401
+ with knowledge_engine.begin() as conn:
402
+ if existing_id:
403
+ # update existing
404
+ conn.execute(sql_text("""
405
+ UPDATE knowledge
406
+ SET reply = :reply, text = :text, confidence = GREATEST(coalesce(confidence, 0), :conf), updated_at = CURRENT_TIMESTAMP
407
+ WHERE id = :id
408
+ """), {"reply": reply, "text": text, "conf": float(confidence), "id": existing_id})
409
+ else:
410
+ if emb_bytes is not None:
411
+ conn.execute(sql_text("""
412
+ INSERT INTO knowledge (text, reply, language, embedding, category, topic, confidence)
413
+ VALUES (:t, :r, 'en', :e, 'learned', :topic, :conf)
414
+ """), {"t": text, "r": reply, "e": emb_bytes, "topic": topic, "conf": float(confidence)})
415
+ else:
416
+ conn.execute(sql_text("""
417
+ INSERT INTO knowledge (text, reply, language, category, topic, confidence)
418
+ VALUES (:t, :r, 'en', 'learned', :topic, :conf)
419
+ """), {"t": text, "r": reply, "topic": topic, "conf": float(confidence)})
420
+ record_learn_event()
421
+ return True
422
+ except Exception as e:
423
+ logger.warning(f"store_or_refine_knowledge failed: {e}")
424
+ return False
425
+
426
+ def deep_refinement_pass():
427
+ try:
428
+ with knowledge_engine.begin() as conn:
429
+ topics_rows = conn.execute(sql_text("SELECT DISTINCT topic FROM knowledge WHERE category='learned'")).fetchall()
430
+ topics = [r[0] for r in topics_rows if r and r[0]] or ["general"]
431
+ for t in topics:
432
+ with knowledge_engine.begin() as conn:
433
+ rows = conn.execute(sql_text("""
434
+ SELECT text, reply, confidence FROM knowledge WHERE topic = :topic AND category='learned'
435
+ ORDER BY confidence DESC NULLS LAST, updated_at DESC LIMIT 12
436
+ """), {"topic": t}).fetchall()
437
+ candidates = []
438
+ for r in rows:
439
+ if r and (r[1] or r[0]):
440
+ candidates.append(r[1] or r[0])
441
+ if not candidates:
442
+ continue
443
+ composite = create_composite_idea(candidates, context_prompt=f"topic: {t}")
444
+ vals = [float(r[2] or 0.0) for r in rows]
445
+ avg_conf = (sum(vals) / len(vals)) if vals else 0.0
446
+ composite_conf = min(1.0, avg_conf + 0.15)
447
+ # store to knowledge DB only
448
+ store_or_refine_knowledge_in_knowledge_db(composite, composite, topic=t, confidence=composite_conf)
449
+ except Exception as e:
450
+ logger.warning(f"deep_refinement_pass error: {e}")
451
+
452
+ def deep_refinement_loop(interval_minutes: int = 60):
453
+ while True:
454
+ try:
455
+ logger.info("[JusticeAI] Deep refinement tick")
456
+ deep_refinement_pass()
457
+ except Exception as e:
458
+ logger.warning(f"deep_refinement_loop exception: {e}")
459
+ time.sleep(max(60, interval_minutes * 60))
460
+
461
+ # -------------------------
462
+ # Startup: load models + background thread
463
+ # -------------------------
464
  @app.on_event("startup")
465
  async def startup_event():
466
  global embed_model, spell, moderator, ensemble_llms, startup_time
 
508
  moderator = None
509
  model_progress["moderator"]["status"] = "error"
510
  logger.warning(f"[JusticeAI] Moderator load error: {e}")
511
+
512
  ensemble_llms.clear()
513
  if AutoTokenizer is not None and AutoModelForCausalLM is not None:
514
  for path in LLM_MODEL_PATHS:
 
519
  logger.info(f"[JusticeAI] Loaded ensemble LLM: {path}")
520
  except Exception as e:
521
  logger.warning(f"[JusticeAI] Could not load ensemble LLM {path}: {e}")
522
+
523
  startup_time = round(time.time() - t0, 2)
524
  logger.info(f"[JusticeAI] Startup completed in {startup_time}s")
525
+
526
+ # seed some initial knowledge (into knowledge DB only)
527
  initial_knowledge = [
528
  {"text": "Justice is fairness in protection of rights and punishment of wrongs.", "reply": "Justice means fairness.", "topic": "general"},
529
  {"text": "Law is a system of rules created and enforced through social or governmental institutions.", "reply": "Law is a set of rules.", "topic": "general"},
530
  ]
531
+ with knowledge_engine.begin() as conn:
532
  for item in initial_knowledge:
533
  exists = conn.execute(sql_text("SELECT COUNT(*) FROM knowledge WHERE text = :t"), {"t": item["text"]}).scalar()
534
  if not exists:
535
+ emb = None
536
+ if embed_model is not None:
537
+ try:
538
+ emb = embed_text(item["text"])
539
+ except Exception:
540
+ emb = None
541
+ if emb is not None:
542
+ conn.execute(sql_text("INSERT INTO knowledge (text, reply, language, embedding, category, topic, confidence) VALUES (:t, :r, 'en', :e, 'learned', :topic, 1.0)"),
543
+ {"t": item["text"], "r": item["reply"], "e": emb, "topic": item["topic"]})
544
+ else:
545
+ conn.execute(sql_text("INSERT INTO knowledge (text, reply, language, category, topic, confidence) VALUES (:t, :r, 'en', 'learned', :topic, 1.0)"),
546
+ {"t": item["text"], "r": item["reply"], "topic": item["topic"]})
547
+
548
+ # start deep refinement background thread (runs on knowledge DB)
549
+ t = threading.Thread(target=deep_refinement_loop, kwargs={"interval_minutes": 60}, daemon=True)
550
+ t.start()
551
+
552
+ # -------------------------
553
+ # /chat endpoint β€” IMPORTANT: only writes to user_memory (engine). DOES NOT write to knowledge.
554
+ # It will use user input to expand internal queries and create composite replies, but will not save
555
+ # those composites directly to knowledge. Knowledge DB is updated only by /add or the background pass.
556
+ # -------------------------
557
  @app.post("/chat")
558
  async def chat(request: Request, data: dict = Body(...)):
559
  t0 = time.time()
 
563
  username = data.get("username", "anonymous")
564
  user_ip = request.client.host if request.client else "0.0.0.0"
565
  user_id = hashlib.sha256(f"{user_ip}-{username}".encode()).hexdigest()
566
+ topic_hint = str(data.get("topic", "") or "").strip() or "general"
567
  detected_lang = detect_language_safe(raw_msg)
568
  reply_lang = detected_lang
569
  user_force_save = bool(data.get("save_memory", False))
570
 
571
+ # spell correction
572
  msg_corrected = raw_msg
573
  if spell is not None:
574
  try:
575
  words = raw_msg.split()
576
+ corrected = []
577
+ for w in words:
578
+ cor = spell.correction(w) if hasattr(spell, "correction") else w
579
+ corrected.append(cor or w)
580
  msg_corrected = " ".join(corrected)
581
  except Exception:
582
  pass
583
 
584
+ # retrieve candidates from knowledge DB (read-only)
 
585
  try:
586
+ with knowledge_engine.begin() as conn:
587
+ rows = conn.execute(sql_text("SELECT id, text, reply, language, embedding, topic, confidence FROM knowledge WHERE category='learned' ORDER BY confidence DESC, updated_at DESC")).fetchall()
588
  except Exception as e:
589
  record_request(time.time() - t0)
590
  return JSONResponse(status_code=500, content={"error": "failed to read knowledge", "details": str(e)})
591
 
592
  knowledge_texts = [r[1] or "" for r in rows]
593
  knowledge_replies = [r[2] or r[1] or "" for r in rows]
 
594
  knowledge_topics = [r[5] or "general" for r in rows]
595
+ knowledge_confidences = [float(r[6] or 0.0) for r in rows]
596
 
597
+ # semantic retrieval (local)
598
+ matches: List[str] = []
599
+ confidence: float = 0.0
600
  similarity_threshold = 0.35
601
  try:
602
  if embed_model is not None and knowledge_texts:
 
604
  msg_emb = embed_model.encode(msg_corrected, convert_to_tensor=True)
605
  if msg_emb.shape[-1] == knowledge_embeddings.shape[-1]:
606
  scores = torch.nn.functional.cosine_similarity(msg_emb.unsqueeze(0), knowledge_embeddings)
607
+ topk = min(12, scores.shape[0])
608
  top_indices = torch.topk(scores, k=topk).indices.tolist()
609
  seen_text = set()
610
  filtered = []
611
  for i in top_indices:
612
  s = float(scores[i])
613
  candidate = knowledge_replies[i]
614
+ key = (candidate or "").strip().lower()
615
  if is_boilerplate_candidate(candidate): continue
616
+ if not key: continue
617
  if key in seen_text: continue
618
  seen_text.add(key)
619
+ topic_bonus = 0.05 if topic_hint.lower() in (knowledge_topics[i] or "").lower() else 0.0
620
+ final_score = s + topic_bonus
621
+ if final_score >= similarity_threshold:
622
+ filtered.append((i, final_score, candidate))
623
+ filtered.sort(key=lambda x: x[1], reverse=True)
624
  matches = [c for _, _, c in filtered]
625
  confidence = filtered[0][1] if filtered else 0.0
626
  else:
627
+ logger.warning("Embedding dimension mismatch")
628
  matches = []
629
  else:
630
+ # fallback substring search
631
  for idx, ktext in enumerate(knowledge_texts):
632
+ if topic_hint and topic_hint.lower() in (knowledge_topics[idx] or "").lower():
633
+ if msg_corrected.lower() in ktext.lower() or ktext.lower() in msg_corrected.lower():
634
+ matches.append(knowledge_replies[idx])
635
  confidence = 0.0
636
  except Exception as e:
637
+ logger.warning(f"Retrieval failure: {e}")
638
  matches = knowledge_replies[:3] if knowledge_replies else []
639
  confidence = 0.0
640
 
641
+ # ask ensemble LLMs for suggestions (non-blocking via executor)
642
  loop = asyncio.get_running_loop()
643
+ def run_llm(prompt_in: str):
644
+ return ensemble_llm_suggestions(prompt_in)
645
  try:
646
+ prompt_for_llms = f"Respond to: {msg_corrected}\nProvide concise proposals/answers."
647
+ llm_replies = await loop.run_in_executor(None, run_llm, prompt_for_llms)
648
  except Exception as e:
649
  logger.warning(f"LLM ensemble failed: {e}")
650
  llm_replies = []
651
 
652
+ # dedupe LLMs vs matches (prefer fresh ideas)
653
+ unique_llm_replies: List[str] = []
654
  if embed_model is not None and matches and llm_replies:
655
+ try:
656
+ match_embs = embed_model.encode(matches, convert_to_tensor=True)
657
+ for llm_text in llm_replies:
658
+ try:
659
+ llm_emb = embed_model.encode(llm_text, convert_to_tensor=True)
660
+ sims = torch.nn.functional.cosine_similarity(llm_emb.unsqueeze(0), match_embs)
661
+ max_sim = float(sims.max().item())
662
+ if max_sim < 0.60:
663
+ unique_llm_replies.append(llm_text)
664
+ except Exception:
665
+ if llm_text not in matches:
666
+ unique_llm_replies.append(llm_text)
667
+ except Exception:
668
+ unique_llm_replies = [r for r in llm_replies if r not in matches]
669
  else:
670
+ unique_llm_replies = [r for r in llm_replies if r not in matches]
 
 
671
 
672
+ # combine candidates (knowledge matches + unique LLM replies)
673
  all_candidates = []
674
  for m in matches:
675
  if m and not is_boilerplate_candidate(m):
676
  all_candidates.append(dedupe_sentences(m))
677
+ for l in unique_llm_replies:
678
+ if l and not is_boilerplate_candidate(l):
679
+ all_candidates.append(dedupe_sentences(l))
680
+ # if too few candidates, add user message only as seed (but do not store to knowledge)
681
+ if not all_candidates:
682
+ all_candidates.append(msg_corrected)
683
+
684
+ # composite idea created (ephemeral). NOTE: do NOT store into knowledge directly here.
685
+ composite = create_composite_idea(all_candidates, context_prompt=f"topic: {topic_hint}")
686
+ reply_en = composite if composite else (all_candidates[0] if all_candidates else "I need more details.")
687
+
688
+ # ALWAYS: store raw user interaction into user_memory (user DB) β€” but not to knowledge.
689
+ try:
690
+ with engine.begin() as conn:
691
+ conn.execute(sql_text("""
692
+ INSERT INTO user_memory (user_id, username, ip, text, reply, language, mood, confidence, topic)
693
+ VALUES (:uid, :uname, :ip, :text, :reply, :lang, :mood, :conf, :topic)
694
+ """), {
695
+ "uid": user_id,
696
+ "uname": username,
697
+ "ip": user_ip,
698
+ "text": raw_msg,
699
+ "reply": reply_en,
700
+ "lang": detected_lang,
701
+ "mood": "neutral",
702
+ "conf": float(confidence),
703
+ "topic": topic_hint
704
+ })
705
+ except Exception as e:
706
+ logger.warning(f"/chat user_memory save failed: {e}")
707
 
708
+ # IMPORTANT: do NOT call store_or_refine_knowledge_in_knowledge_db() here.
709
+ # The background deep refinement will pick up aggregated data and update knowledge DB.
 
 
 
 
 
 
 
 
 
 
 
 
710
 
711
  duration = time.time() - t0
712
  record_request(duration)
713
+ emoji = get_emoji(get_category_for_mood("neutral"), intensity=random.random())
714
+ flags = {}
715
+
716
  return {
717
+ "reply": reply_en,
718
+ "topic": topic_hint,
719
  "language": reply_lang,
720
  "emoji": emoji,
721
  "confidence": round(confidence, 2),
722
  "flags": flags
723
  }
724
 
725
+ # -------------------------
726
+ # /add endpoint β€” explicitly writes to knowledge DB only (admin or trusted)
727
+ # -------------------------
728
  @app.post("/add")
729
+ async def add_knowledge(data: dict = Body(...), x_admin_key: str = Header(None, alias="X-Admin-Key")):
730
+ # optional admin guard; if ADMIN_KEY set and header missing/invalid, deny
731
+ if ADMIN_KEY:
732
+ if not x_admin_key or x_admin_key != ADMIN_KEY:
733
+ return JSONResponse(status_code=403, content={"error": "Invalid or missing admin key."})
734
  text_data = sanitize_knowledge_text(data.get("text", "") or "")
735
  reply = sanitize_knowledge_text(data.get("reply", "") or "")
736
  topic = str(data.get("topic", "") or "").strip()
 
738
  return JSONResponse(status_code=400, content={"error": "Topic is required"})
739
  if not text_data:
740
  return JSONResponse(status_code=400, content={"error": "Text is required"})
 
741
  try:
742
  emb = None
743
  if embed_model is not None:
 
746
  except Exception as e:
747
  logger.warning(f"embed_text failed in /add: {e}")
748
  emb = None
749
+ with knowledge_engine.begin() as conn:
750
  if emb is not None:
751
+ conn.execute(sql_text("INSERT INTO knowledge (text, reply, language, embedding, category, topic) VALUES (:t, :r, :lang, :e, 'learned', :topic)"),
752
+ {"t": text_data, "r": reply, "lang": "en", "e": emb, "topic": topic})
 
 
753
  else:
754
+ conn.execute(sql_text("INSERT INTO knowledge (text, reply, language, category, topic) VALUES (:t, :r, :lang, 'learned', :topic)"),
755
+ {"t": text_data, "r": reply, "lang": "en", "topic": topic})
 
 
756
  record_learn_event()
757
  res = {"status": "βœ… Knowledge added", "text": text_data, "topic": topic, "language": "en"}
758
  if embed_model is None or emb is None:
 
761
  except Exception as e:
762
  return JSONResponse(status_code=500, content={"error": "failed to store knowledge", "details": str(e)})
763
 
764
+ # -------------------------
765
+ # /leaderboard β€” reads from knowledge DB ONLY
766
+ # -------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
767
  @app.get("/leaderboard")
768
  async def leaderboard(topic: str = Query("general")):
769
  topic = str(topic or "general").strip() or "general"
770
  try:
771
+ with knowledge_engine.begin() as conn:
772
  rows = conn.execute(sql_text("""
773
  SELECT id, text, reply, language, category, confidence, created_at
774
  FROM knowledge
 
794
  except Exception as e:
795
  return JSONResponse(status_code=500, content={"error": "failed to fetch leaderboard", "details": str(e)})
796
 
797
+ # -------------------------
798
+ # model-status, health, metrics (some read both DBs)
799
+ # -------------------------
800
  @app.get("/model-status")
801
  async def model_status():
802
  response_progress = {k: dict(v) for k, v in model_progress.items()}
 
812
  elapsed = round(time.time() - start, 2)
813
  health_data["response_time_s"] = elapsed
814
  try:
815
+ with knowledge_engine.connect() as kconn, engine.connect() as uconn:
816
+ k = kconn.execute(sql_text("SELECT COUNT(*) FROM knowledge WHERE category='learned'")).scalar() or 0
817
+ u = uconn.execute(sql_text("SELECT COUNT(*) FROM user_memory")).scalar() or 0
818
  except Exception:
819
  k, u = -1, -1
820
  try:
821
+ with knowledge_engine.begin() as conn:
822
  rows = conn.execute(sql_text("SELECT DISTINCT topic FROM knowledge WHERE category='learned'")).fetchall()
823
  topics = [r[0] for r in rows if r and r[0]]
824
  except Exception:
 
833
  health_data["learn_rate_per_min"] = sum(1 for t in recent_learning_timestamps if t >= time.time() - 60)
834
  return health_data
835
 
836
+ # SSE metrics
837
  async def metrics_producer():
838
  while True:
839
  try:
 
850
  async def _get_counts():
851
  def blocking_counts():
852
  try:
853
+ with knowledge_engine.connect() as kconn, engine.connect() as uconn:
854
+ kcount = kconn.execute(sql_text("SELECT COUNT(*) FROM knowledge WHERE category='learned'")).scalar() or 0
855
+ ucount = uconn.execute(sql_text("SELECT COUNT(*) FROM user_memory")).scalar() or 0
856
  return int(kcount), int(ucount)
857
  except Exception:
858
  return 0, 0
 
897
  items = list(recent_metrics)[-limit:]
898
  return {"count": len(items), "metrics": items}
899
 
900
+ # -------------------------
901
+ # Admin endpoints β€” operate on knowledge DB for knowledge operations and user DB for user memory operations
902
+ # -------------------------
903
  @app.post("/verify-admin")
904
  async def verify_admin(x_admin_key: str = Header(None, alias="X-Admin-Key")):
905
  if ADMIN_KEY is None:
 
918
  if confirm != "CLEAR_DATABASE":
919
  return JSONResponse(status_code=400, content={"error": "confirm token required."})
920
  try:
921
+ with knowledge_engine.begin() as kconn:
922
+ k_count = kconn.execute(sql_text("SELECT COUNT(*) FROM knowledge")).scalar() or 0
923
+ kconn.execute(sql_text("DELETE FROM knowledge"))
924
+ with engine.begin() as uconn:
925
+ u_count = uconn.execute(sql_text("SELECT COUNT(*) FROM user_memory")).scalar() or 0
926
+ uconn.execute(sql_text("DELETE FROM user_memory"))
927
+ return {"status": "βœ… Cleared both databases", "deleted_knowledge": int(k_count), "deleted_user_memory": int(u_count)}
928
  except Exception as e:
929
  return JSONResponse(status_code=500, content={"error": "failed to clear database", "details": str(e)})
930
 
 
941
  return JSONResponse(status_code=400, content={"error": "confirm token required."})
942
  batch_size = int(data.get("batch_size", 100))
943
  try:
944
+ with knowledge_engine.begin() as conn:
945
  rows = conn.execute(sql_text("SELECT id, text FROM knowledge WHERE category='learned' ORDER BY id")).fetchall()
946
  ids_texts = [(r[0], r[1]) for r in rows]
947
  total = len(ids_texts)
 
952
  embs = embed_model.encode(texts, convert_to_tensor=True)
953
  for j, (kid, _) in enumerate(batch):
954
  emb_bytes = embs[j].cpu().numpy().tobytes()
955
+ with knowledge_engine.begin() as conn:
956
  conn.execute(sql_text("UPDATE knowledge SET embedding = :e, updated_at = CURRENT_TIMESTAMP WHERE id = :id"), {"e": emb_bytes, "id": kid})
957
  updated += 1
958
  return {"status": "βœ… Re-embed complete", "total_rows": total, "updated": updated}
959
  except Exception as e:
960
  return JSONResponse(status_code=500, content={"error": "re-embed failed", "details": str(e)})
961
 
962
+ # -------------------------
963
+ # Frontend dashboard
964
+ # -------------------------
965
  @app.get("/", response_class=HTMLResponse)
966
  async def frontend_dashboard():
967
  try:
 
992
  html = html.replace("%%STARTUP_TIME%%", str(startup_time_local))
993
  return HTMLResponse(html)
994
 
995
+ # -------------------------
996
+ # Main
997
+ # -------------------------
998
  if __name__ == "__main__":
999
  port = int(os.environ.get("PORT", 7860))
1000
+ uvicorn.run("app:app", host="0.0.0.0", port=port)