Princeaka commited on
Commit
aa79c8d
·
verified ·
1 Parent(s): 1a4fd42

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +72 -148
app.py CHANGED
@@ -1,13 +1,15 @@
1
- # JusticeAI Backend — Improved Version (Backend-only, ready to deploy)
2
  #
3
  # Improvements:
4
- # 1. Leaderboard only shows refined ('learned') knowledge, never user chat/memory.
5
- # 2. Replies are always synthesized in English first, then translated to user language if needed.
6
- # 3. Synthesis logic enhanced: combines knowledge, LLM inspiration, intent, context, asks for clarification if uncertain.
7
- # 4. User chat/memory is never used for global replies or leaderboard.
8
- # 5. All endpoints preserved and improved.
 
 
 
9
 
10
- from sqlalchemy.pool import NullPool
11
  import os
12
  import time
13
  import json
@@ -18,7 +20,6 @@ import asyncio
18
  import re
19
  from datetime import datetime, timezone
20
  from collections import deque
21
- from pathlib import Path
22
  from typing import Optional, Dict, Any, List
23
 
24
  import requests
@@ -26,33 +27,43 @@ import psutil
26
  import torch
27
  import uvicorn
28
  from fastapi import FastAPI, Request, Body, Header, Query
29
- from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse, FileResponse
30
  from sqlalchemy import create_engine, text as sql_text
 
31
 
32
  import logging
33
  logging.basicConfig(level=logging.INFO)
34
  logger = logging.getLogger("justiceai")
35
 
36
- # ----- Environment & cache directories -----
37
- HF_CACHE_DIR = os.environ.get("HF_HOME", "/tmp/huggingface")
38
- os.environ["HF_HOME"] = HF_CACHE_DIR
39
- os.environ["TRANSFORMERS_CACHE"] = HF_CACHE_DIR
40
- os.environ["SENTENCE_TRANSFORMERS_HOME"] = HF_CACHE_DIR
 
 
 
 
 
41
 
42
- # ----- Optional helpers (soft fallbacks) -----
 
 
 
 
 
 
 
43
  try:
44
  from emojis import get_emoji, get_category_for_mood
45
  except Exception:
46
- def get_category_for_mood(mood: str) -> str:
47
- return "neutral"
48
- def get_emoji(cat: str, intensity: float = 0.5) -> str:
49
- return "🤖"
50
 
51
  try:
52
  from health import get_health_status
53
  except Exception:
54
- def get_health_status(engine_arg) -> Dict[str, Any]:
55
- return {"status": "starting", "db_status": "unknown", "stars": 0}
56
 
57
  try:
58
  from langdetect import detect as detect_lang
@@ -77,22 +88,7 @@ except Exception:
77
  AutoModelForCausalLM = None
78
  hf_pipeline = None
79
 
80
- # ----- Config (env) -----
81
- ADMIN_KEY = os.environ.get("ADMIN_KEY")
82
- DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///justice.db")
83
- EMBED_MODEL_NAME = os.environ.get("EMBED_MODEL_NAME", "paraphrase-multilingual-MiniLM-L12-v2")
84
- TRANSLATION_CACHE_DIR = os.environ.get("TRANSLATION_CACHE_DIR", "./translation_models")
85
- LLM_MODEL_PATH = os.environ.get("LLM_MODEL_PATH", "") # path to local LLM (optional)
86
- SAVE_MEMORY_CONFIDENCE = float(os.environ.get("SAVE_MEMORY_CONFIDENCE", "0.45"))
87
-
88
- app = FastAPI(title="JusticeAI — Backend (improved)")
89
- engine = create_engine(
90
- DATABASE_URL,
91
- poolclass=NullPool,
92
- connect_args={"check_same_thread": False} if DATABASE_URL.startswith("sqlite") else {}
93
- )
94
-
95
- # ----- Ensure DB schema -----
96
  def ensure_tables():
97
  dialect = engine.dialect.name
98
  with engine.begin() as conn:
@@ -162,7 +158,6 @@ def ensure_tables():
162
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
163
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
164
  );"""))
165
-
166
  ensure_tables()
167
 
168
  def ensure_column_exists(table: str, column: str, col_def_sql: str):
@@ -170,18 +165,12 @@ def ensure_column_exists(table: str, column: str, col_def_sql: str):
170
  try:
171
  with engine.begin() as conn:
172
  if dialect == "sqlite":
173
- try:
174
- rows = conn.execute(sql_text(f"PRAGMA table_info({table})")).fetchall()
175
- existing_cols = [r[1] for r in rows]
176
- if column not in existing_cols:
177
- conn.execute(sql_text(f"ALTER TABLE {table} ADD COLUMN {col_def_sql}"))
178
- except Exception:
179
- pass
180
  else:
181
- try:
182
- conn.execute(sql_text(f"ALTER TABLE {table} ADD COLUMN IF NOT EXISTS {col_def_sql}"))
183
- except Exception:
184
- pass
185
  except Exception:
186
  pass
187
 
@@ -190,7 +179,7 @@ ensure_column_exists("user_memory", "reply", "reply TEXT")
190
  ensure_column_exists("knowledge", "language", "language TEXT DEFAULT 'en'")
191
  ensure_column_exists("knowledge", "embedding", "embedding BYTEA" if engine.dialect.name != "sqlite" else "embedding BLOB")
192
 
193
- # ----- State & metrics -----
194
  app_start_time = time.time()
195
  last_heartbeat = {"time": datetime.utcnow().replace(tzinfo=timezone.utc).isoformat(), "ok": True}
196
  RECENT_WINDOW_SECONDS = 3600
@@ -221,10 +210,9 @@ moderator = None
221
  llm_tokenizer = None
222
  llm_model = None
223
  startup_time = 0.0
224
-
225
  _translation_model_cache: Dict[str, Any] = {}
226
 
227
- # ----- Helpers -----
228
  def record_request(duration_s: float):
229
  global response_time_ema
230
  ts = time.time()
@@ -377,19 +365,8 @@ def embed_text(text_data: str) -> bytes:
377
  logger.warning(f"Embedding fallback: {e}")
378
  raise
379
 
380
- # Boilerplate detection + creative reply generation
381
  def is_boilerplate_candidate(s: str) -> bool:
382
- s_low = (s or "").strip().lower()
383
- generic_phrases = [
384
- "justiceai is a unified intelligence dashboard providing chat, knowledge, and live metrics.",
385
- "justiceai es un panel de inteligencia unificado que proporciona chat, conocimiento y métricas en vivo."
386
- ]
387
- for g in generic_phrases:
388
- if s_low == g.strip().lower():
389
- return True
390
- if g.split(" ")[0].lower() in s_low and len(s_low) < 90:
391
- return True
392
- return False
393
 
394
  def generate_creative_reply(matches: List[str]) -> str:
395
  clean = []
@@ -401,10 +378,9 @@ def generate_creative_reply(matches: List[str]) -> str:
401
  seen.add(s)
402
  clean.append(s)
403
  if not clean:
404
- return "I’m not sure yet."
405
  if len(clean) == 1:
406
  return clean[0]
407
- # Return a concise, combined statement
408
  joined = ". ".join(clean[:3])
409
  return joined
410
 
@@ -427,10 +403,6 @@ def infer_topic_from_message(msg: str, known_topics=None) -> str:
427
  return "general"
428
 
429
  def refine_or_update(matches, new_text, new_reply, confidence, topic="general"):
430
- """
431
- Decide whether to update existing knowledge or insert a new entry based on similarity.
432
- Uses embed_model to compare and updates DB accordingly.
433
- """
434
  try:
435
  if embed_model is None:
436
  return
@@ -479,31 +451,21 @@ def detect_mood(text: str) -> str:
479
  return "neutral"
480
 
481
  def synthesize_final_reply(en_msg: str, matches: List[str], llm_suggestion: str, intent: str) -> str:
482
- """
483
- Central decision function: combine matches and llm_suggestion but JusticeAI always decides final text.
484
- Rules:
485
- - Only use knowledge marked as 'learned' (refined).
486
- - If a direct high-confidence match exists, prefer it.
487
- - Else combine top matches into meaningful synthesis.
488
- - If LLM suggestion exists, use it as additional candidate but do not accept it verbatim;
489
- instead, extract/merge useful sentences with knowledge matches.
490
- - Apply intent-specific formatting at the end.
491
- - If nothing is found, ask for clarification.
492
- """
493
  pieces = []
494
  for m in matches:
495
- if m and not is_boilerplate_candidate(m):
496
- pieces.append(dedupe_sentences(m))
 
 
497
  if llm_suggestion:
498
- # extract some sentences from the suggestion (avoid hallucinated facts)
499
- s = dedupe_sentences(llm_suggestion)
500
- for sent in re.split(r'(?<=[.?!])\s+', s):
501
- if len(sent.split()) < 60 and sent and sent not in pieces:
502
  pieces.append(sent)
503
  if not pieces:
504
  return "Can you provide more details so I can help better?"
505
  reply = ". ".join(pieces[:3])
506
- # Intent postprocessing
507
  if intent == "solution":
508
  bullets = [p.strip(" .") for p in re.split(r'\.\s+', reply) if p.strip()]
509
  pref = "Solutions:\n- "
@@ -517,14 +479,11 @@ def synthesize_final_reply(en_msg: str, matches: List[str], llm_suggestion: str,
517
  reply = reply
518
  return reply
519
 
520
- # ----- Startup: load models & background loops -----
521
  @app.on_event("startup")
522
  async def startup_event():
523
  global embed_model, spell, moderator, llm_tokenizer, llm_model, startup_time
524
  t0 = time.time()
525
  logger.info("[JusticeAI] Starting component loading...")
526
-
527
- # Embedding model
528
  try:
529
  if SentenceTransformer is not None:
530
  embed_model = SentenceTransformer(EMBED_MODEL_NAME, device="cpu")
@@ -539,8 +498,6 @@ async def startup_event():
539
  embed_model = None
540
  model_progress["embed"]["status"] = "error"
541
  logger.warning(f"[JusticeAI] Failed to load embedding model: {e}")
542
-
543
- # Spell checker
544
  try:
545
  if SpellChecker is not None:
546
  spell = SpellChecker()
@@ -555,8 +512,6 @@ async def startup_event():
555
  spell = None
556
  model_progress["spell"]["status"] = "error"
557
  logger.warning(f"[JusticeAI] SpellChecker load failed: {e}")
558
-
559
- # Moderator pipeline
560
  try:
561
  if AutoTokenizer is not None and hf_pipeline is not None:
562
  moderator = hf_pipeline("text-classification", model="unitary/toxic-bert", device=-1)
@@ -571,12 +526,11 @@ async def startup_event():
571
  moderator = None
572
  model_progress["moderator"]["status"] = "error"
573
  logger.warning(f"[JusticeAI] Moderator load error: {e}")
574
-
575
- # Local LLM for background learning/inspiration
576
  try:
577
- if LLM_MODEL_PATH and AutoTokenizer is not None and AutoModelForCausalLM is not None:
578
- llm_tokenizer = AutoTokenizer.from_pretrained(LLM_MODEL_PATH, cache_dir=HF_CACHE_DIR)
579
- llm_model = AutoModelForCausalLM.from_pretrained(LLM_MODEL_PATH, cache_dir=HF_CACHE_DIR)
 
580
  model_progress["llm"]["status"] = "ready"
581
  model_progress["llm"]["progress"] = 100.0
582
  logger.info(f"[JusticeAI] Loaded local LLM for background learning: {LLM_MODEL_PATH}")
@@ -588,11 +542,8 @@ async def startup_event():
588
  llm_tokenizer, llm_model = None, None
589
  model_progress["llm"]["status"] = "error"
590
  logger.warning(f"[JusticeAI] Could not load local LLM: {e}")
591
-
592
  startup_time = round(time.time() - t0, 2)
593
  logger.info(f"[JusticeAI] Startup completed in {startup_time}s")
594
-
595
- # Heartbeat loop
596
  def heartbeat_loop():
597
  while True:
598
  last_heartbeat["time"] = datetime.utcnow().replace(tzinfo=timezone.utc).isoformat()
@@ -603,17 +554,12 @@ async def startup_event():
603
  last_heartbeat["ok"] = False
604
  time.sleep(30)
605
  threading.Thread(target=heartbeat_loop, daemon=True).start()
606
-
607
- # Background learning loop (every minute)
608
  def background_learning_loop():
609
  while True:
610
  try:
611
- # Collect recent user interactions for learning
612
  with engine.begin() as conn:
613
  mem_rows = conn.execute(sql_text("SELECT text, reply, topic, confidence FROM user_memory ORDER BY created_at DESC LIMIT 200")).fetchall()
614
  knowledge_rows = conn.execute(sql_text("SELECT text, reply, topic FROM knowledge WHERE category='learned' ORDER BY created_at DESC LIMIT 200")).fetchall()
615
-
616
- # Use LLM for suggestions on each memory (if available)
617
  if llm_model and llm_tokenizer and mem_rows:
618
  for mem in mem_rows:
619
  user_text = mem[0] or ""
@@ -634,10 +580,9 @@ async def startup_event():
634
  except Exception as e:
635
  logger.warning(f"[Background AGI] Learning loop error: {e}")
636
  time.sleep(60)
637
-
638
  threading.Thread(target=background_learning_loop, daemon=True).start()
639
 
640
- # ----- Endpoints -----
641
  @app.get("/model-status")
642
  async def model_status():
643
  response_progress = {k: dict(v) for k, v in model_progress.items()}
@@ -847,7 +792,7 @@ async def chat(request: Request, data: dict = Body(...)):
847
  reply_lang = detected_lang
848
  user_force_save = bool(data.get("save_memory", False))
849
 
850
- # Optional spell correction
851
  if spell is not None:
852
  try:
853
  words = raw_msg.split()
@@ -861,19 +806,13 @@ async def chat(request: Request, data: dict = Body(...)):
861
  else:
862
  msg_corrected = raw_msg
863
 
864
- # Simple intent classifier
865
  def classify_intent_local(text: str) -> str:
866
  t = text.lower()
867
- if any(k in t for k in ["why", "para qué", "por qué"]):
868
- return "why"
869
- if any(k in t for k in ["solution", "solve", "how to", "how", "solución", "soluciona"]):
870
- return "solution"
871
- if any(k in t for k in ["disadvantage", "problem", "con ", "consecuencia", "desventaja", "issue"]):
872
- return "disadvantage"
873
- if any(k in t for k in ["benefit", "ventaja", "advantage", "pros"]):
874
- return "advantage"
875
  return "default"
876
-
877
  intent = classify_intent_local(raw_msg)
878
 
879
  # Infer topic if not provided
@@ -888,7 +827,7 @@ async def chat(request: Request, data: dict = Body(...)):
888
  else:
889
  topic = topic_hint
890
 
891
- # Load only refined knowledge
892
  try:
893
  with engine.begin() as conn:
894
  rows = conn.execute(sql_text("SELECT id, text, reply, language, embedding, topic FROM knowledge WHERE category='learned' ORDER BY created_at DESC")).fetchall()
@@ -901,7 +840,7 @@ async def chat(request: Request, data: dict = Body(...)):
901
  knowledge_langs = [r[3] or "en" for r in rows]
902
  knowledge_topics = [r[5] or "general" for r in rows]
903
 
904
- # Translate the user message to English if needed
905
  en_msg = msg_corrected
906
  if detected_lang and detected_lang.split("-")[0].lower() not in ("en", "eng"):
907
  en_msg = translate_to_english(msg_corrected, detected_lang)
@@ -923,22 +862,16 @@ async def chat(request: Request, data: dict = Body(...)):
923
  s = float(scores[i])
924
  candidate = knowledge_replies[i]
925
  candidate_lang = detect_language_safe(candidate)
926
- if candidate_lang != "en":
927
- candidate_en = translate_to_english(candidate, candidate_lang)
928
- else:
929
- candidate_en = candidate
930
  key = candidate_en.strip().lower()
931
- if is_boilerplate_candidate(candidate_en):
932
- continue
933
- if key in seen_text:
934
- continue
935
  seen_text.add(key)
936
  if s > 0.35:
937
  filtered.append((i, s, candidate_en))
938
  matches = [c for _, _, c in filtered]
939
  confidence = filtered[0][1] if filtered else 0.0
940
  else:
941
- # fallback simple substring matching
942
  for idx, ktext in enumerate(knowledge_texts):
943
  ktext_lang = detect_language_safe(ktext)
944
  ktext_en = translate_to_english(ktext, ktext_lang) if ktext_lang != "en" else ktext
@@ -951,7 +884,7 @@ async def chat(request: Request, data: dict = Body(...)):
951
  matches = knowledge_replies[:3] if knowledge_replies else []
952
  confidence = 0.0
953
 
954
- # Send query to local LLM for inspiration (not direct reply)
955
  llm_suggestion = ""
956
  try:
957
  if llm_model and llm_tokenizer:
@@ -965,19 +898,20 @@ async def chat(request: Request, data: dict = Body(...)):
965
  logger.debug(f"LLM suggestion error: {e}")
966
  llm_suggestion = ""
967
 
968
- # Compose final reply using JusticeAI's internal synthesis logic, always in English
969
- steps = []
970
  reply_en = synthesize_final_reply(en_msg, matches, llm_suggestion, intent)
971
  reply_en = dedupe_sentences(reply_en)
972
 
973
  # Translate to user's language if needed
974
  if reply_lang and reply_lang.split("-")[0].lower() not in ("en", "eng", "und"):
975
- reply_final = translate_from_english(reply_en, reply_lang)
976
- reply_final = dedupe_sentences(reply_final)
 
 
 
 
977
  else:
978
  reply_final = reply_en
979
 
980
- # Mood & emoji
981
  mood = detect_mood(raw_msg + " " + reply_final)
982
  emoji = ""
983
  try:
@@ -993,7 +927,6 @@ async def chat(request: Request, data: dict = Body(...)):
993
  except Exception:
994
  emoji = ""
995
 
996
- # Moderation (prevent toxic content from being saved)
997
  flags = {}
998
  try:
999
  if moderator is not None:
@@ -1006,7 +939,6 @@ async def chat(request: Request, data: dict = Body(...)):
1006
  except Exception:
1007
  pass
1008
 
1009
- # Persist user memory if meaningful and not toxic
1010
  try:
1011
  should_save = user_force_save or (confidence >= SAVE_MEMORY_CONFIDENCE and not flags.get('toxic', False))
1012
  if should_save:
@@ -1028,7 +960,6 @@ async def chat(request: Request, data: dict = Body(...)):
1028
  "topic": topic,
1029
  }
1030
  )
1031
- # Keep recent / high-confidence per topic
1032
  conn.execute(
1033
  sql_text("""
1034
  DELETE FROM user_memory
@@ -1045,15 +976,8 @@ async def chat(request: Request, data: dict = Body(...)):
1045
  except Exception as e:
1046
  logger.warning(f"user_memory persist error: {e}")
1047
 
1048
- # OPTIONAL: include steps for debugging only if requested (default: False)
1049
- include_steps = bool(data.get("include_steps", False))
1050
- if include_steps and steps:
1051
- reasoning_text = " | ".join(str(s) for s in steps)
1052
- reply_final = f"{reply_final}\n\n[Reasoning steps: {reasoning_text}]"
1053
-
1054
  duration = time.time() - t0
1055
  record_request(duration)
1056
-
1057
  return {
1058
  "reply": reply_final,
1059
  "topic": topic,
 
1
+ # JusticeAI Backend — Ready to Deploy (Hugging Face Spaces & General Use)
2
  #
3
  # Improvements:
4
+ # - Translation cache set to /tmp/translation_models for Hugging Face Spaces.
5
+ # - No hardcoded replies/branding; knowledge-based/context-aware responses only.
6
+ # - Replies always synthesized in English and translated to user language at the final step.
7
+ # - Deduplication of sentences for uniqueness; language mismatch prevented.
8
+ # - Local LLM is now google/gemma-2b (small, fast, English chat).
9
+ # - Graceful fallback if LLM not available.
10
+ # - Leaderboard only from refined knowledge; user chat/memory never used for global answers.
11
+ # - All endpoints preserved and improved.
12
 
 
13
  import os
14
  import time
15
  import json
 
20
  import re
21
  from datetime import datetime, timezone
22
  from collections import deque
 
23
  from typing import Optional, Dict, Any, List
24
 
25
  import requests
 
27
  import torch
28
  import uvicorn
29
  from fastapi import FastAPI, Request, Body, Header, Query
30
+ from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
31
  from sqlalchemy import create_engine, text as sql_text
32
+ from sqlalchemy.pool import NullPool
33
 
34
  import logging
35
  logging.basicConfig(level=logging.INFO)
36
  logger = logging.getLogger("justiceai")
37
 
38
+ # Translation cache for Hugging Face Spaces
39
+ TRANSLATION_CACHE_DIR = os.environ.get("TRANSLATION_CACHE_DIR", "/tmp/translation_models")
40
+ os.environ["TRANSLATION_CACHE_DIR"] = TRANSLATION_CACHE_DIR
41
+
42
+ # Config
43
+ ADMIN_KEY = os.environ.get("ADMIN_KEY")
44
+ DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///justice.db")
45
+ EMBED_MODEL_NAME = os.environ.get("EMBED_MODEL_NAME", "paraphrase-multilingual-MiniLM-L12-v2")
46
+ LLM_MODEL_PATH = os.environ.get("LLM_MODEL_PATH", "google/gemma-2b")
47
+ SAVE_MEMORY_CONFIDENCE = float(os.environ.get("SAVE_MEMORY_CONFIDENCE", "0.45"))
48
 
49
+ app = FastAPI(title="JusticeAI Backend (final)")
50
+ engine = create_engine(
51
+ DATABASE_URL,
52
+ poolclass=NullPool,
53
+ connect_args={"check_same_thread": False} if DATABASE_URL.startswith("sqlite") else {}
54
+ )
55
+
56
+ # Optional helpers (soft fallbacks)
57
  try:
58
  from emojis import get_emoji, get_category_for_mood
59
  except Exception:
60
+ def get_category_for_mood(mood: str) -> str: return "neutral"
61
+ def get_emoji(cat: str, intensity: float = 0.5) -> str: return "🤖"
 
 
62
 
63
  try:
64
  from health import get_health_status
65
  except Exception:
66
+ def get_health_status(engine_arg) -> Dict[str, Any]: return {"status": "starting", "db_status": "unknown", "stars": 0}
 
67
 
68
  try:
69
  from langdetect import detect as detect_lang
 
88
  AutoModelForCausalLM = None
89
  hf_pipeline = None
90
 
91
+ # ===== Database Table Creation =====
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  def ensure_tables():
93
  dialect = engine.dialect.name
94
  with engine.begin() as conn:
 
158
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
159
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
160
  );"""))
 
161
  ensure_tables()
162
 
163
  def ensure_column_exists(table: str, column: str, col_def_sql: str):
 
165
  try:
166
  with engine.begin() as conn:
167
  if dialect == "sqlite":
168
+ rows = conn.execute(sql_text(f"PRAGMA table_info({table})")).fetchall()
169
+ existing_cols = [r[1] for r in rows]
170
+ if column not in existing_cols:
171
+ conn.execute(sql_text(f"ALTER TABLE {table} ADD COLUMN {col_def_sql}"))
 
 
 
172
  else:
173
+ conn.execute(sql_text(f"ALTER TABLE {table} ADD COLUMN IF NOT EXISTS {col_def_sql}"))
 
 
 
174
  except Exception:
175
  pass
176
 
 
179
  ensure_column_exists("knowledge", "language", "language TEXT DEFAULT 'en'")
180
  ensure_column_exists("knowledge", "embedding", "embedding BYTEA" if engine.dialect.name != "sqlite" else "embedding BLOB")
181
 
182
+ # ===== State & Metrics =====
183
  app_start_time = time.time()
184
  last_heartbeat = {"time": datetime.utcnow().replace(tzinfo=timezone.utc).isoformat(), "ok": True}
185
  RECENT_WINDOW_SECONDS = 3600
 
210
  llm_tokenizer = None
211
  llm_model = None
212
  startup_time = 0.0
 
213
  _translation_model_cache: Dict[str, Any] = {}
214
 
215
+ # ===== Helpers =====
216
  def record_request(duration_s: float):
217
  global response_time_ema
218
  ts = time.time()
 
365
  logger.warning(f"Embedding fallback: {e}")
366
  raise
367
 
 
368
  def is_boilerplate_candidate(s: str) -> bool:
369
+ return False # Remove all branding/boilerplate logic
 
 
 
 
 
 
 
 
 
 
370
 
371
  def generate_creative_reply(matches: List[str]) -> str:
372
  clean = []
 
378
  seen.add(s)
379
  clean.append(s)
380
  if not clean:
381
+ return "Can you provide more details so I can help better?"
382
  if len(clean) == 1:
383
  return clean[0]
 
384
  joined = ". ".join(clean[:3])
385
  return joined
386
 
 
403
  return "general"
404
 
405
  def refine_or_update(matches, new_text, new_reply, confidence, topic="general"):
 
 
 
 
406
  try:
407
  if embed_model is None:
408
  return
 
451
  return "neutral"
452
 
453
  def synthesize_final_reply(en_msg: str, matches: List[str], llm_suggestion: str, intent: str) -> str:
 
 
 
 
 
 
 
 
 
 
 
454
  pieces = []
455
  for m in matches:
456
+ if m:
457
+ deduped = dedupe_sentences(m)
458
+ if deduped not in pieces:
459
+ pieces.append(deduped)
460
  if llm_suggestion:
461
+ llm_sentences = re.split(r'(?<=[.?!])\s+', dedupe_sentences(llm_suggestion))
462
+ for sent in llm_sentences:
463
+ if sent and sent not in pieces and len(sent.split()) < 60:
 
464
  pieces.append(sent)
465
  if not pieces:
466
  return "Can you provide more details so I can help better?"
467
  reply = ". ".join(pieces[:3])
468
+ # Intent formatting
469
  if intent == "solution":
470
  bullets = [p.strip(" .") for p in re.split(r'\.\s+', reply) if p.strip()]
471
  pref = "Solutions:\n- "
 
479
  reply = reply
480
  return reply
481
 
 
482
  @app.on_event("startup")
483
  async def startup_event():
484
  global embed_model, spell, moderator, llm_tokenizer, llm_model, startup_time
485
  t0 = time.time()
486
  logger.info("[JusticeAI] Starting component loading...")
 
 
487
  try:
488
  if SentenceTransformer is not None:
489
  embed_model = SentenceTransformer(EMBED_MODEL_NAME, device="cpu")
 
498
  embed_model = None
499
  model_progress["embed"]["status"] = "error"
500
  logger.warning(f"[JusticeAI] Failed to load embedding model: {e}")
 
 
501
  try:
502
  if SpellChecker is not None:
503
  spell = SpellChecker()
 
512
  spell = None
513
  model_progress["spell"]["status"] = "error"
514
  logger.warning(f"[JusticeAI] SpellChecker load failed: {e}")
 
 
515
  try:
516
  if AutoTokenizer is not None and hf_pipeline is not None:
517
  moderator = hf_pipeline("text-classification", model="unitary/toxic-bert", device=-1)
 
526
  moderator = None
527
  model_progress["moderator"]["status"] = "error"
528
  logger.warning(f"[JusticeAI] Moderator load error: {e}")
 
 
529
  try:
530
+ # Use google/gemma-2b as the default local LLM (fast, small, chat-friendly)
531
+ if AutoTokenizer is not None and AutoModelForCausalLM is not None:
532
+ llm_tokenizer = AutoTokenizer.from_pretrained(LLM_MODEL_PATH, cache_dir="/tmp")
533
+ llm_model = AutoModelForCausalLM.from_pretrained(LLM_MODEL_PATH, cache_dir="/tmp")
534
  model_progress["llm"]["status"] = "ready"
535
  model_progress["llm"]["progress"] = 100.0
536
  logger.info(f"[JusticeAI] Loaded local LLM for background learning: {LLM_MODEL_PATH}")
 
542
  llm_tokenizer, llm_model = None, None
543
  model_progress["llm"]["status"] = "error"
544
  logger.warning(f"[JusticeAI] Could not load local LLM: {e}")
 
545
  startup_time = round(time.time() - t0, 2)
546
  logger.info(f"[JusticeAI] Startup completed in {startup_time}s")
 
 
547
  def heartbeat_loop():
548
  while True:
549
  last_heartbeat["time"] = datetime.utcnow().replace(tzinfo=timezone.utc).isoformat()
 
554
  last_heartbeat["ok"] = False
555
  time.sleep(30)
556
  threading.Thread(target=heartbeat_loop, daemon=True).start()
 
 
557
  def background_learning_loop():
558
  while True:
559
  try:
 
560
  with engine.begin() as conn:
561
  mem_rows = conn.execute(sql_text("SELECT text, reply, topic, confidence FROM user_memory ORDER BY created_at DESC LIMIT 200")).fetchall()
562
  knowledge_rows = conn.execute(sql_text("SELECT text, reply, topic FROM knowledge WHERE category='learned' ORDER BY created_at DESC LIMIT 200")).fetchall()
 
 
563
  if llm_model and llm_tokenizer and mem_rows:
564
  for mem in mem_rows:
565
  user_text = mem[0] or ""
 
580
  except Exception as e:
581
  logger.warning(f"[Background AGI] Learning loop error: {e}")
582
  time.sleep(60)
 
583
  threading.Thread(target=background_learning_loop, daemon=True).start()
584
 
585
+ # ===== ENDPOINTS =====
586
  @app.get("/model-status")
587
  async def model_status():
588
  response_progress = {k: dict(v) for k, v in model_progress.items()}
 
792
  reply_lang = detected_lang
793
  user_force_save = bool(data.get("save_memory", False))
794
 
795
+ # Spell correction
796
  if spell is not None:
797
  try:
798
  words = raw_msg.split()
 
806
  else:
807
  msg_corrected = raw_msg
808
 
 
809
  def classify_intent_local(text: str) -> str:
810
  t = text.lower()
811
+ if any(k in t for k in ["why", "para qué", "por qué"]): return "why"
812
+ if any(k in t for k in ["solution", "solve", "how to", "how", "solución", "soluciona"]): return "solution"
813
+ if any(k in t for k in ["disadvantage", "problem", "con ", "consecuencia", "desventaja", "issue"]): return "disadvantage"
814
+ if any(k in t for k in ["benefit", "ventaja", "advantage", "pros"]): return "advantage"
 
 
 
 
815
  return "default"
 
816
  intent = classify_intent_local(raw_msg)
817
 
818
  # Infer topic if not provided
 
827
  else:
828
  topic = topic_hint
829
 
830
+ # Load refined knowledge only
831
  try:
832
  with engine.begin() as conn:
833
  rows = conn.execute(sql_text("SELECT id, text, reply, language, embedding, topic FROM knowledge WHERE category='learned' ORDER BY created_at DESC")).fetchall()
 
840
  knowledge_langs = [r[3] or "en" for r in rows]
841
  knowledge_topics = [r[5] or "general" for r in rows]
842
 
843
+ # Translate user message to English if needed
844
  en_msg = msg_corrected
845
  if detected_lang and detected_lang.split("-")[0].lower() not in ("en", "eng"):
846
  en_msg = translate_to_english(msg_corrected, detected_lang)
 
862
  s = float(scores[i])
863
  candidate = knowledge_replies[i]
864
  candidate_lang = detect_language_safe(candidate)
865
+ candidate_en = translate_to_english(candidate, candidate_lang) if candidate_lang != "en" else candidate
 
 
 
866
  key = candidate_en.strip().lower()
867
+ if is_boilerplate_candidate(candidate_en): continue
868
+ if key in seen_text: continue
 
 
869
  seen_text.add(key)
870
  if s > 0.35:
871
  filtered.append((i, s, candidate_en))
872
  matches = [c for _, _, c in filtered]
873
  confidence = filtered[0][1] if filtered else 0.0
874
  else:
 
875
  for idx, ktext in enumerate(knowledge_texts):
876
  ktext_lang = detect_language_safe(ktext)
877
  ktext_en = translate_to_english(ktext, ktext_lang) if ktext_lang != "en" else ktext
 
884
  matches = knowledge_replies[:3] if knowledge_replies else []
885
  confidence = 0.0
886
 
887
+ # Local LLM inspiration (google/gemma-2b)
888
  llm_suggestion = ""
889
  try:
890
  if llm_model and llm_tokenizer:
 
898
  logger.debug(f"LLM suggestion error: {e}")
899
  llm_suggestion = ""
900
 
 
 
901
  reply_en = synthesize_final_reply(en_msg, matches, llm_suggestion, intent)
902
  reply_en = dedupe_sentences(reply_en)
903
 
904
  # Translate to user's language if needed
905
  if reply_lang and reply_lang.split("-")[0].lower() not in ("en", "eng", "und"):
906
+ try:
907
+ reply_final = translate_from_english(reply_en, reply_lang)
908
+ reply_final = dedupe_sentences(reply_final)
909
+ except Exception as e:
910
+ logger.warning(f"Translation failure: {e}")
911
+ reply_final = reply_en
912
  else:
913
  reply_final = reply_en
914
 
 
915
  mood = detect_mood(raw_msg + " " + reply_final)
916
  emoji = ""
917
  try:
 
927
  except Exception:
928
  emoji = ""
929
 
 
930
  flags = {}
931
  try:
932
  if moderator is not None:
 
939
  except Exception:
940
  pass
941
 
 
942
  try:
943
  should_save = user_force_save or (confidence >= SAVE_MEMORY_CONFIDENCE and not flags.get('toxic', False))
944
  if should_save:
 
960
  "topic": topic,
961
  }
962
  )
 
963
  conn.execute(
964
  sql_text("""
965
  DELETE FROM user_memory
 
976
  except Exception as e:
977
  logger.warning(f"user_memory persist error: {e}")
978
 
 
 
 
 
 
 
979
  duration = time.time() - t0
980
  record_request(duration)
 
981
  return {
982
  "reply": reply_final,
983
  "topic": topic,