Update app.py
Browse files
app.py
CHANGED
|
@@ -1,13 +1,15 @@
|
|
| 1 |
-
# JusticeAI Backend —
|
| 2 |
#
|
| 3 |
# Improvements:
|
| 4 |
-
#
|
| 5 |
-
#
|
| 6 |
-
#
|
| 7 |
-
#
|
| 8 |
-
#
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
#
|
| 37 |
-
|
| 38 |
-
os.environ["
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
#
|
| 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 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
conn.execute(sql_text(f"ALTER TABLE {table} ADD COLUMN {col_def_sql}"))
|
| 178 |
-
except Exception:
|
| 179 |
-
pass
|
| 180 |
else:
|
| 181 |
-
|
| 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 |
-
#
|
| 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 |
-
#
|
| 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 |
-
|
| 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
|
| 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
|
| 496 |
-
|
|
|
|
|
|
|
| 497 |
if llm_suggestion:
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 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
|
| 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 |
-
|
| 578 |
-
|
| 579 |
-
|
|
|
|
| 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 |
-
#
|
| 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 |
-
#
|
| 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 |
-
|
| 869 |
-
if any(k in t for k in ["
|
| 870 |
-
|
| 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
|
| 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
|
| 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 |
-
|
| 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 |
-
#
|
| 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 |
-
|
| 976 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|