Update app.py
Browse files
app.py
CHANGED
|
@@ -1,16 +1,20 @@
|
|
| 1 |
# JusticeAI Backend — Upgraded & Integrated (Backend-only; does NOT create or overwrite frontend)
|
| 2 |
#
|
| 3 |
-
#
|
| 4 |
-
#
|
| 5 |
-
#
|
| 6 |
-
# - Prefer a local language model (language.py or language.bin) for translations; fall back to Helsinki-NLP via transformers if needed.
|
| 7 |
-
# - Ensure /chat and all retrieval/refinement operate strictly within the resolved topic (no cross-topic lookup/updates).
|
| 8 |
-
# - Stop joining sentences into run-on paragraphs. dedupe_sentences preserves sentences as separate lines.
|
| 9 |
-
# - Enhance emoji understanding: detect emojis in user input, adjust mood detection using emojis, and apply safer rules for when to append or echo emojis in replies.
|
| 10 |
-
# - Minor safety: never append emojis to replies that already contain them or when moderation flags high toxicity.
|
| 11 |
#
|
| 12 |
-
#
|
| 13 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
from sqlalchemy.pool import NullPool
|
| 16 |
import os
|
|
@@ -49,9 +53,6 @@ os.environ["SENTENCE_TRANSFORMERS_HOME"] = HF_CACHE_DIR
|
|
| 49 |
# ----- Optional helpers (soft fallbacks) -----
|
| 50 |
# Prefer user's emojis.py
|
| 51 |
try:
|
| 52 |
-
# emojis.py is expected to provide at least:
|
| 53 |
-
# - get_emoji(category: str, intensity: float=0.5) -> str
|
| 54 |
-
# - get_category_for_mood(mood: str) -> str
|
| 55 |
from emojis import get_emoji, get_category_for_mood # type: ignore
|
| 56 |
logger.info("[JusticeAI] Using local emojis.py")
|
| 57 |
except Exception:
|
|
@@ -85,37 +86,53 @@ except Exception:
|
|
| 85 |
AutoModelForCausalLM = None
|
| 86 |
hf_pipeline = None
|
| 87 |
|
| 88 |
-
# ----- Local language
|
| 89 |
-
|
| 90 |
|
| 91 |
-
def
|
| 92 |
-
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
try:
|
| 95 |
-
import language as
|
| 96 |
-
|
| 97 |
-
logger.info("[JusticeAI] Loaded
|
| 98 |
return
|
| 99 |
except Exception:
|
| 100 |
pass
|
| 101 |
-
#
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
try:
|
| 106 |
-
|
| 107 |
-
logger.info("[JusticeAI] Loaded
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
-
#
|
| 118 |
-
|
| 119 |
|
| 120 |
# ----- Config (env) -----
|
| 121 |
ADMIN_KEY = os.environ.get("ADMIN_KEY")
|
|
@@ -288,7 +305,7 @@ class JusticeBrain:
|
|
| 288 |
self.capabilities["embed_available"] = embed_model is not None
|
| 289 |
self.capabilities["moderator"] = moderator is not None
|
| 290 |
self.capabilities["llm_reflect"] = llm_model is not None and llm_tokenizer is not None
|
| 291 |
-
self.capabilities["translation"] = (AutoTokenizer is not None and AutoModelForSeq2SeqLM is not None) or (
|
| 292 |
self.capabilities["ann"] = False # FAISS not wired yet (scaffold)
|
| 293 |
logger.info(f"[JusticeBrain] Capabilities: {self.capabilities}")
|
| 294 |
|
|
@@ -360,17 +377,18 @@ def sanitize_knowledge_text(t: Any) -> str:
|
|
| 360 |
s = s[1:-1].strip()
|
| 361 |
return " ".join(s.split())
|
| 362 |
|
| 363 |
-
def dedupe_sentences(text):
|
| 364 |
"""
|
| 365 |
-
Split text into sentences (respecting newlines) and dedupe while preserving order.
|
| 366 |
-
Return
|
| 367 |
"""
|
| 368 |
if not text:
|
| 369 |
return text
|
| 370 |
sentences = []
|
| 371 |
seen = set()
|
| 372 |
-
# Respect explicit newlines
|
| 373 |
for chunk in re.split(r'\n+', text):
|
|
|
|
| 374 |
parts = re.split(r'(?<=[.?!])\s+', chunk)
|
| 375 |
for sent in parts:
|
| 376 |
s = sent.strip()
|
|
@@ -382,8 +400,7 @@ def dedupe_sentences(text):
|
|
| 382 |
sentences.append(s)
|
| 383 |
return "\n".join(sentences)
|
| 384 |
|
| 385 |
-
# Emoji
|
| 386 |
-
# This is a conservative heuristic and will be used to inform mood detection and emoji decisions.
|
| 387 |
_EMOJI_PATTERN = re.compile(
|
| 388 |
"["
|
| 389 |
"\U0001F600-\U0001F64F" # emoticons
|
|
@@ -395,31 +412,21 @@ _EMOJI_PATTERN = re.compile(
|
|
| 395 |
"]+", flags=re.UNICODE
|
| 396 |
)
|
| 397 |
|
| 398 |
-
_POS_EMOJI_RANGES = [
|
| 399 |
-
("\U0001F600", "\U0001F606"), # grinning, smiling
|
| 400 |
-
("\U0001F60A", "\U0001F60F"), # smiling variants
|
| 401 |
-
("\U0001F642", "\U0001F60D")
|
| 402 |
-
]
|
| 403 |
-
_NEG_EMOJI_RANGES = [
|
| 404 |
-
("\U0001F61E", "\U0001F626"), # sad/concerned faces
|
| 405 |
-
("\U0001F62A", "\U0001F626")
|
| 406 |
-
]
|
| 407 |
-
|
| 408 |
def extract_emojis(text: str) -> List[str]:
|
| 409 |
if not text:
|
| 410 |
return []
|
| 411 |
return _EMOJI_PATTERN.findall(text)
|
| 412 |
|
| 413 |
def emoji_sentiment_score(emojis: List[str]) -> float:
|
| 414 |
-
# Returns score in [-1.0, 1.0], positive -> positive sentiment
|
| 415 |
if not emojis:
|
| 416 |
return 0.0
|
| 417 |
score = 0.0
|
|
|
|
| 418 |
for e in "".join(emojis):
|
| 419 |
ord_val = ord(e)
|
| 420 |
-
|
| 421 |
-
if 0x1F600 <= ord_val <= 0x1F64F:
|
| 422 |
-
# smiles
|
| 423 |
if ord_val in range(0x1F600, 0x1F607) or ord_val in range(0x1F60A, 0x1F60F):
|
| 424 |
score += 1.0
|
| 425 |
elif ord_val in range(0x1F61E, 0x1F626):
|
|
@@ -430,55 +437,112 @@ def emoji_sentiment_score(emojis: List[str]) -> float:
|
|
| 430 |
score += 0.1
|
| 431 |
else:
|
| 432 |
score += 0.0
|
| 433 |
-
return max(-1.0, min(1.0, score / max(1,
|
| 434 |
|
| 435 |
def detect_language_safe(text: str) -> str:
|
| 436 |
"""
|
| 437 |
-
|
| 438 |
-
|
| 439 |
"""
|
| 440 |
text = (text or "").strip()
|
| 441 |
if not text:
|
| 442 |
return "en"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 443 |
lower = text.lower()
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 450 |
letters = re.findall(r'[A-Za-z]', text)
|
| 451 |
if len(letters) >= max(1, len(text) / 4):
|
| 452 |
return "en"
|
|
|
|
|
|
|
| 453 |
return "und"
|
| 454 |
|
| 455 |
def translate_to_english(text: str, src_lang: str) -> str:
|
| 456 |
"""
|
| 457 |
-
|
| 458 |
-
If not available, fall back to Helsinki-NLP models via transformers.
|
| 459 |
"""
|
| 460 |
if not text:
|
| 461 |
return text
|
| 462 |
src = (src_lang.split('-')[0].lower() if src_lang else "und")
|
| 463 |
if src in ("en", "eng", "", "und"):
|
| 464 |
return text
|
| 465 |
-
#
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
if hasattr(
|
| 470 |
-
|
| 471 |
-
|
|
|
|
|
|
|
|
|
|
| 472 |
try:
|
| 473 |
-
return
|
| 474 |
except TypeError:
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 482 |
if not re.fullmatch(r"[a-z]{2,3}", src):
|
| 483 |
return text
|
| 484 |
try:
|
|
@@ -495,7 +559,7 @@ def translate_to_english(text: str, src_lang: str) -> str:
|
|
| 495 |
model_name = f"Helsinki-NLP/opus-mt-{src}-en"
|
| 496 |
tokenizer = AutoTokenizer.from_pretrained(model_name, cache_dir=TRANSLATION_CACHE_DIR)
|
| 497 |
model = AutoModelForSeq2SeqLM.from_pretrained(model_name, cache_dir=TRANSLATION_CACHE_DIR)
|
| 498 |
-
_translation_model_cache[
|
| 499 |
inputs = tokenizer([text], return_tensors="pt", truncation=True)
|
| 500 |
outputs = model.generate(**inputs, max_length=1024)
|
| 501 |
return tokenizer.batch_decode(outputs, skip_special_tokens=True)[0]
|
|
@@ -514,27 +578,37 @@ def translate_to_english(text: str, src_lang: str) -> str:
|
|
| 514 |
|
| 515 |
def translate_from_english(text: str, tgt_lang: str) -> str:
|
| 516 |
"""
|
| 517 |
-
|
| 518 |
"""
|
| 519 |
if not text:
|
| 520 |
return text
|
| 521 |
tgt = (tgt_lang.split('-')[0].lower() if tgt_lang else "und")
|
| 522 |
if tgt in ("en", "eng", "", "und"):
|
| 523 |
return text
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
if hasattr(language_model, "translate"):
|
| 529 |
try:
|
| 530 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 531 |
except TypeError:
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 538 |
if not re.fullmatch(r"[a-z]{2,3}", tgt):
|
| 539 |
return text
|
| 540 |
try:
|
|
@@ -551,7 +625,7 @@ def translate_from_english(text: str, tgt_lang: str) -> str:
|
|
| 551 |
model_name = f"Helsinki-NLP/opus-mt-en-{tgt}"
|
| 552 |
tokenizer = AutoTokenizer.from_pretrained(model_name, cache_dir=TRANSLATION_CACHE_DIR)
|
| 553 |
model = AutoModelForSeq2SeqLM.from_pretrained(model_name, cache_dir=TRANSLATION_CACHE_DIR)
|
| 554 |
-
_translation_model_cache[
|
| 555 |
inputs = tokenizer([text], return_tensors="pt", truncation=True)
|
| 556 |
outputs = model.generate(**inputs, max_length=1024)
|
| 557 |
return tokenizer.batch_decode(outputs, skip_special_tokens=True)[0]
|
|
@@ -597,7 +671,7 @@ def is_boilerplate_candidate(s: str) -> bool:
|
|
| 597 |
def generate_creative_reply(matches: List[str]) -> str:
|
| 598 |
"""
|
| 599 |
Combine up to three matches into a concise reply.
|
| 600 |
-
Preserve
|
| 601 |
"""
|
| 602 |
clean = []
|
| 603 |
seen = set()
|
|
@@ -606,14 +680,12 @@ def generate_creative_reply(matches: List[str]) -> str:
|
|
| 606 |
if not s or s in seen or is_boilerplate_candidate(s):
|
| 607 |
continue
|
| 608 |
seen.add(s)
|
| 609 |
-
# Keep answer sentences separate
|
| 610 |
s = dedupe_sentences(s)
|
| 611 |
clean.append(s)
|
| 612 |
if not clean:
|
| 613 |
return "I’m not sure yet."
|
| 614 |
if len(clean) == 1:
|
| 615 |
return clean[0]
|
| 616 |
-
# Return as separate lines; preserve sentence boundaries inside each match
|
| 617 |
return "\n\n".join(clean[:3])
|
| 618 |
|
| 619 |
def infer_topic_from_message(msg: str, known_topics=None) -> str:
|
|
@@ -636,8 +708,7 @@ def infer_topic_from_message(msg: str, known_topics=None) -> str:
|
|
| 636 |
|
| 637 |
def refine_or_update(matches, new_text, new_reply, confidence, topic="general"):
|
| 638 |
"""
|
| 639 |
-
|
| 640 |
-
This function ONLY touches rows in the provided topic (enforced).
|
| 641 |
"""
|
| 642 |
try:
|
| 643 |
if embed_model is None:
|
|
@@ -661,7 +732,6 @@ def refine_or_update(matches, new_text, new_reply, confidence, topic="general"):
|
|
| 661 |
best_score = float(sims[best_idx])
|
| 662 |
if best_score > 0.75:
|
| 663 |
kid = ids[best_idx]
|
| 664 |
-
# Check manual flag and prevent contradictory overwrite within topic
|
| 665 |
with engine.begin() as conn:
|
| 666 |
row = conn.execute(sql_text("SELECT meta FROM knowledge WHERE id = :id"), {"id": kid}).fetchone()
|
| 667 |
is_manual = False
|
|
@@ -672,7 +742,6 @@ def refine_or_update(matches, new_text, new_reply, confidence, topic="general"):
|
|
| 672 |
except Exception:
|
| 673 |
is_manual = False
|
| 674 |
if is_manual and confidence < 0.85:
|
| 675 |
-
# Do not overwrite manual high-confidence entries unless very confident; append refined reply note
|
| 676 |
with engine.begin() as conn:
|
| 677 |
conn.execute(
|
| 678 |
sql_text("UPDATE knowledge SET reply = :r, updated_at = CURRENT_TIMESTAMP WHERE id = :id"),
|
|
@@ -691,7 +760,6 @@ def refine_or_update(matches, new_text, new_reply, confidence, topic="general"):
|
|
| 691 |
sql_text("INSERT INTO knowledge (text, reply, language, embedding, category, topic, confidence, meta) VALUES (:t, :r, :lang, :e, 'learned', :topic, :conf, :meta)"),
|
| 692 |
{"t": new_text, "r": new_reply or "", "lang": "en", "e": emb, "topic": topic, "conf": min(0.7, float(confidence)), "meta": json.dumps({"refined": True})}
|
| 693 |
)
|
| 694 |
-
# bump version on update
|
| 695 |
global knowledge_version
|
| 696 |
knowledge_version += 1
|
| 697 |
except Exception as e:
|
|
@@ -699,13 +767,11 @@ def refine_or_update(matches, new_text, new_reply, confidence, topic="general"):
|
|
| 699 |
|
| 700 |
def detect_mood(text: str) -> str:
|
| 701 |
"""
|
| 702 |
-
Detect mood
|
| 703 |
-
This integrates emoji sentiment heuristics.
|
| 704 |
"""
|
| 705 |
lower = (text or "").lower()
|
| 706 |
positive = ["great", "thanks", "awesome", "happy", "love", "excellent", "cool", "yes", "good", "success", "helpful", "useful", "thank you"]
|
| 707 |
-
negative = ["sad", "bad", "problem", "angry", "hate", "fail", "no", "error", "not working", "disadvantage", "issue"
|
| 708 |
-
# emoji influence
|
| 709 |
emojis = extract_emojis(text)
|
| 710 |
e_score = emoji_sentiment_score(emojis)
|
| 711 |
if any(w in lower for w in positive) or e_score > 0.3:
|
|
@@ -717,35 +783,25 @@ def detect_mood(text: str) -> str:
|
|
| 717 |
def should_append_emoji(user_text: str, reply_text: str, mood: str, flags: Dict[str, Any]) -> str:
|
| 718 |
"""
|
| 719 |
Decide whether to append/echo an emoji and which one.
|
| 720 |
-
|
| 721 |
-
- Do not append if moderation flagged toxic.
|
| 722 |
-
- Do not append if the reply already contains emoji.
|
| 723 |
-
- Prefer echoing user's emoji if present (amplify or acknowledge).
|
| 724 |
-
- Only append for short replies and when mood non-neutral or user used emojis.
|
| 725 |
-
- Use get_emoji/get_category_for_mood (from emojis.py) if available.
|
| 726 |
-
Returns the emoji string to append or empty string.
|
| 727 |
"""
|
| 728 |
if flags.get("toxic"):
|
| 729 |
return ""
|
| 730 |
-
# If reply already
|
| 731 |
if extract_emojis(reply_text):
|
| 732 |
return ""
|
| 733 |
user_emojis = extract_emojis(user_text)
|
| 734 |
if user_emojis:
|
| 735 |
-
# Echo the first user emoji if it's positive-ish, otherwise map mood
|
| 736 |
user_score = emoji_sentiment_score(user_emojis)
|
| 737 |
if user_score >= 0.2:
|
| 738 |
-
# echo a positive emoji
|
| 739 |
try:
|
| 740 |
cat = get_category_for_mood("positive")
|
| 741 |
return get_emoji(cat, intensity=min(1.0, 0.5 + user_score))
|
| 742 |
except Exception:
|
| 743 |
return user_emojis[0] if user_emojis else ""
|
| 744 |
elif user_score <= -0.2:
|
| 745 |
-
# avoid adding negative emoji; reflect neutrally
|
| 746 |
return ""
|
| 747 |
else:
|
| 748 |
-
# for neutral user emoji, optionally add a small positive emoji for short replies
|
| 749 |
if len(reply_text) < 200:
|
| 750 |
try:
|
| 751 |
cat = get_category_for_mood("neutral")
|
|
@@ -753,13 +809,10 @@ def should_append_emoji(user_text: str, reply_text: str, mood: str, flags: Dict[
|
|
| 753 |
except Exception:
|
| 754 |
return ""
|
| 755 |
return ""
|
| 756 |
-
# No user emoji: use mood and reply constraints
|
| 757 |
if mood == "neutral":
|
| 758 |
return ""
|
| 759 |
-
# do not add emoji for long or formal replies
|
| 760 |
if len(reply_text) > 400:
|
| 761 |
return ""
|
| 762 |
-
# avoid adding when reply contains code-like chars
|
| 763 |
if re.search(r'[\{\}\[\]\(\)]', reply_text):
|
| 764 |
return ""
|
| 765 |
try:
|
|
@@ -770,9 +823,8 @@ def should_append_emoji(user_text: str, reply_text: str, mood: str, flags: Dict[
|
|
| 770 |
|
| 771 |
def synthesize_final_reply(en_msg: str, matches: List[str], llm_suggestion: str, intent: str, detected_lang: str) -> str:
|
| 772 |
"""
|
| 773 |
-
Combine matches and
|
| 774 |
-
|
| 775 |
-
- Return reply in English (later translated if needed).
|
| 776 |
"""
|
| 777 |
pieces = []
|
| 778 |
for m in matches:
|
|
@@ -785,9 +837,7 @@ def synthesize_final_reply(en_msg: str, matches: List[str], llm_suggestion: str,
|
|
| 785 |
pieces.append(sent)
|
| 786 |
if not pieces:
|
| 787 |
return "I don't have enough context — could you add more details or add knowledge with /add?"
|
| 788 |
-
# Compose using up to 3 pieces; keep them separated by blank lines
|
| 789 |
reply = "\n\n".join(pieces[:3])
|
| 790 |
-
# Intent-specific formatting: solutions -> bullets; others keep lines
|
| 791 |
if intent == "solution":
|
| 792 |
bullets = []
|
| 793 |
for p in re.split(r'\n+', reply):
|
|
@@ -806,7 +856,7 @@ def synthesize_final_reply(en_msg: str, matches: List[str], llm_suggestion: str,
|
|
| 806 |
# ----- Startup: load models & background loops -----
|
| 807 |
@app.on_event("startup")
|
| 808 |
async def startup_event():
|
| 809 |
-
global embed_model, spell, moderator, llm_tokenizer, llm_model, startup_time
|
| 810 |
t0 = time.time()
|
| 811 |
logger.info("[JusticeAI] Starting component loading...")
|
| 812 |
|
|
@@ -875,8 +925,18 @@ async def startup_event():
|
|
| 875 |
model_progress["llm"]["status"] = "error"
|
| 876 |
logger.warning(f"[JusticeAI] Could not load local LLM: {e}")
|
| 877 |
|
| 878 |
-
# reload language
|
| 879 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 880 |
|
| 881 |
startup_time = round(time.time() - t0, 2)
|
| 882 |
logger.info(f"[JusticeAI] Startup completed in {startup_time}s")
|
|
@@ -903,12 +963,9 @@ async def startup_event():
|
|
| 903 |
def background_learning_loop():
|
| 904 |
while True:
|
| 905 |
try:
|
| 906 |
-
# Collect recent user interactions for learning
|
| 907 |
with engine.begin() as conn:
|
| 908 |
mem_rows = conn.execute(sql_text("SELECT text, reply, topic, confidence FROM user_memory ORDER BY created_at DESC LIMIT 200")).fetchall()
|
| 909 |
knowledge_rows = conn.execute(sql_text("SELECT text, reply, topic FROM knowledge ORDER BY created_at DESC LIMIT 200")).fetchall()
|
| 910 |
-
|
| 911 |
-
# Use LLM for suggestions on each memory (if available)
|
| 912 |
if llm_model and llm_tokenizer and mem_rows:
|
| 913 |
for mem in mem_rows:
|
| 914 |
user_text = mem[0] or ""
|
|
@@ -919,27 +976,29 @@ async def startup_event():
|
|
| 919 |
inputs = llm_tokenizer(prompt, return_tensors="pt", truncation=True, max_length=512)
|
| 920 |
outputs = llm_model.generate(**inputs, max_length=256, do_sample=True, temperature=0.7)
|
| 921 |
suggestion = llm_tokenizer.decode(outputs[0], skip_special_tokens=True)
|
| 922 |
-
# Use suggestion to refine knowledge in a conservative way
|
| 923 |
conf = float(mem[3] or 0)
|
| 924 |
if suggestion and conf >= 0.2:
|
| 925 |
-
# refine/update is topic-scoped
|
| 926 |
refine_or_update([], user_text, suggestion, conf, topic=topic)
|
| 927 |
logger.debug(f"[Background AGI] Refined knowledge for topic={topic}")
|
| 928 |
except Exception as e:
|
| 929 |
logger.debug(f"[Background AGI] LLM suggestion error for memory: {e}")
|
| 930 |
-
# Mark learning event
|
| 931 |
record_learn_event()
|
| 932 |
except Exception as e:
|
| 933 |
logger.warning(f"[Background AGI] Learning loop error: {e}")
|
| 934 |
-
time.sleep(60)
|
| 935 |
-
|
| 936 |
threading.Thread(target=background_learning_loop, daemon=True).start()
|
| 937 |
|
| 938 |
# ----- Endpoints -----
|
| 939 |
@app.get("/model-status")
|
| 940 |
async def model_status():
|
| 941 |
response_progress = {k: dict(v) for k, v in model_progress.items()}
|
| 942 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 943 |
|
| 944 |
@app.get("/health")
|
| 945 |
async def health_check():
|
|
@@ -1048,7 +1107,7 @@ async def add_knowledge(data: dict = Body(...)):
|
|
| 1048 |
return JSONResponse(status_code=400, content={"error": "Text is required"})
|
| 1049 |
detected = detect_language_safe(text_data)
|
| 1050 |
if detected and detected.split("-")[0].lower() not in ("en", "eng", "und"):
|
| 1051 |
-
if AutoTokenizer is not None and AutoModelForSeq2SeqLM is not None or
|
| 1052 |
try:
|
| 1053 |
text_data = translate_to_english(text_data, detected)
|
| 1054 |
detected = "en"
|
|
@@ -1081,7 +1140,6 @@ async def add_knowledge(data: dict = Body(...)):
|
|
| 1081 |
sql_text("INSERT INTO knowledge (text, reply, language, category, topic, confidence, meta) VALUES (:t, :r, :lang, 'general', :topic, :conf, :meta)"),
|
| 1082 |
{"t": text_data, "r": reply, "lang": "en", "topic": topic, "conf": 0.9, "meta": json.dumps({"manual": True})}
|
| 1083 |
)
|
| 1084 |
-
# bump version for caches and indexes
|
| 1085 |
global knowledge_version
|
| 1086 |
knowledge_version += 1
|
| 1087 |
record_learn_event()
|
|
@@ -1136,7 +1194,7 @@ async def add_bulk(data: List[dict] = Body(...)):
|
|
| 1136 |
errors.append({"index": i, "error": str(e)})
|
| 1137 |
return {"added": added, "errors": errors}
|
| 1138 |
|
| 1139 |
-
#
|
| 1140 |
@app.post("/chat")
|
| 1141 |
async def chat(request: Request, data: dict = Body(...)):
|
| 1142 |
t0 = time.time()
|
|
@@ -1146,12 +1204,11 @@ async def chat(request: Request, data: dict = Body(...)):
|
|
| 1146 |
user_id = hashlib.sha256(f"{user_ip}-{username}".encode()).hexdigest()
|
| 1147 |
topic_hint = str(data.get("topic", "") or "").strip()
|
| 1148 |
detected_lang = detect_language_safe(raw_msg)
|
| 1149 |
-
#
|
| 1150 |
-
|
| 1151 |
-
reply_lang = "en" if (detected_lang in ("und", "") or likely_en) else detected_lang
|
| 1152 |
user_force_save = bool(data.get("save_memory", False))
|
| 1153 |
|
| 1154 |
-
#
|
| 1155 |
if spell is not None:
|
| 1156 |
try:
|
| 1157 |
words = raw_msg.split()
|
|
@@ -1165,7 +1222,7 @@ async def chat(request: Request, data: dict = Body(...)):
|
|
| 1165 |
else:
|
| 1166 |
msg_corrected = raw_msg
|
| 1167 |
|
| 1168 |
-
#
|
| 1169 |
def classify_intent_local(text: str) -> str:
|
| 1170 |
t = text.lower()
|
| 1171 |
if any(k in t for k in ["why", "para qué", "por qué"]):
|
|
@@ -1192,7 +1249,7 @@ async def chat(request: Request, data: dict = Body(...)):
|
|
| 1192 |
else:
|
| 1193 |
topic = topic_hint
|
| 1194 |
|
| 1195 |
-
# Load knowledge
|
| 1196 |
try:
|
| 1197 |
with engine.begin() as conn:
|
| 1198 |
rows = conn.execute(sql_text("SELECT id, text, reply, language, embedding, topic FROM knowledge WHERE topic = :topic ORDER BY created_at DESC"), {"topic": topic}).fetchall()
|
|
@@ -1205,12 +1262,12 @@ async def chat(request: Request, data: dict = Body(...)):
|
|
| 1205 |
knowledge_langs = [r[3] or "en" for r in rows]
|
| 1206 |
knowledge_topics = [r[5] or "general" for r in rows]
|
| 1207 |
|
| 1208 |
-
# Translate the user message to English if needed
|
| 1209 |
en_msg = msg_corrected
|
| 1210 |
if detected_lang and detected_lang.split("-")[0].lower() not in ("en", "eng", "", "und"):
|
| 1211 |
en_msg = translate_to_english(msg_corrected, detected_lang)
|
| 1212 |
|
| 1213 |
-
# Embedding-based retrieval
|
| 1214 |
matches = []
|
| 1215 |
confidence = 0.0
|
| 1216 |
knowledge_embeddings = None
|
|
@@ -1242,7 +1299,7 @@ async def chat(request: Request, data: dict = Body(...)):
|
|
| 1242 |
matches = [c for _, _, c in filtered]
|
| 1243 |
confidence = filtered[0][1] if filtered else 0.0
|
| 1244 |
else:
|
| 1245 |
-
# fallback
|
| 1246 |
for idx, ktext in enumerate(knowledge_texts):
|
| 1247 |
ktext_lang = detect_language_safe(ktext)
|
| 1248 |
ktext_en = translate_to_english(ktext, ktext_lang) if ktext_lang != "en" else ktext
|
|
@@ -1254,7 +1311,7 @@ async def chat(request: Request, data: dict = Body(...)):
|
|
| 1254 |
matches = knowledge_replies[:3] if knowledge_replies else []
|
| 1255 |
confidence = 0.0
|
| 1256 |
|
| 1257 |
-
#
|
| 1258 |
def build_reasoning_scratchpad(question_en: str, facts_en: List[str]) -> Dict[str, Any]:
|
| 1259 |
scratch = {
|
| 1260 |
"question": question_en,
|
|
@@ -1290,17 +1347,12 @@ async def chat(request: Request, data: dict = Body(...)):
|
|
| 1290 |
return "Solutions:\n- " + "\n- ".join(steps[:5])
|
| 1291 |
if intent_label == "why":
|
| 1292 |
return base + " It is useful because it provides direct access to relevant information and supports faster decision-making."
|
| 1293 |
-
if intent_label == "advantage":
|
| 1294 |
-
return base
|
| 1295 |
-
if intent_label == "disadvantage":
|
| 1296 |
-
return base
|
| 1297 |
return base
|
| 1298 |
|
| 1299 |
-
# Build scratchpad and synthesize answer (LLM not used for user reply)
|
| 1300 |
scratchpad = build_reasoning_scratchpad(en_msg, matches)
|
| 1301 |
reply_en = synthesize_from_scratchpad(scratchpad, intent)
|
| 1302 |
|
| 1303 |
-
# Optional
|
| 1304 |
llm_suggestion = ""
|
| 1305 |
try:
|
| 1306 |
if llm_model and llm_tokenizer and matches:
|
|
@@ -1318,17 +1370,7 @@ async def chat(request: Request, data: dict = Body(...)):
|
|
| 1318 |
logger.debug(f"LLM reflection error: {e}")
|
| 1319 |
llm_suggestion = ""
|
| 1320 |
|
| 1321 |
-
#
|
| 1322 |
-
steps = []
|
| 1323 |
-
if matches and confidence >= 0.6:
|
| 1324 |
-
steps.append(f"Direct match with confidence={confidence:.2f}")
|
| 1325 |
-
elif matches and confidence >= 0.35:
|
| 1326 |
-
steps.append(f"Synthesized from top matches with confidence ~{confidence:.2f}")
|
| 1327 |
-
else:
|
| 1328 |
-
steps.append("Scratchpad synthesis")
|
| 1329 |
-
|
| 1330 |
-
# Compose final reply using Justice Brain's internal synthesis logic
|
| 1331 |
-
reply_en = ""
|
| 1332 |
steps = []
|
| 1333 |
if matches and confidence >= 0.6:
|
| 1334 |
reply_en = matches[0]
|
|
@@ -1341,22 +1383,14 @@ async def chat(request: Request, data: dict = Body(...)):
|
|
| 1341 |
if matches or llm_suggestion:
|
| 1342 |
reply_en = synthesize_final_reply(en_msg, matches, llm_suggestion, intent, "en")
|
| 1343 |
else:
|
| 1344 |
-
|
| 1345 |
-
|
| 1346 |
-
reply_en = synthesize_final_reply(en_msg, [], llm_suggestion, intent, "en")
|
| 1347 |
-
steps.append("Synthesized from limited items")
|
| 1348 |
-
except Exception:
|
| 1349 |
-
reply_en = "I don't have enough context yet — can you give more details?"
|
| 1350 |
-
steps.append("Fallback related-items failure")
|
| 1351 |
-
else:
|
| 1352 |
-
reply_en = "I don't have enough context yet — can you give more details?"
|
| 1353 |
-
steps.append("No embedding model available")
|
| 1354 |
except Exception as e:
|
| 1355 |
logger.warning(f"Synthesis error: {e}")
|
| 1356 |
reply_en = "I don't have enough context yet — can you give more details?"
|
| 1357 |
steps.append("Synthesis fallback")
|
| 1358 |
|
| 1359 |
-
# Postprocess for intent
|
| 1360 |
def postprocess_for_intent_en(reply_text: str, intent_label: str) -> str:
|
| 1361 |
if intent_label == "why":
|
| 1362 |
suf = " It is useful because it provides direct access to relevant information and supports faster decision-making."
|
|
@@ -1384,12 +1418,9 @@ async def chat(request: Request, data: dict = Body(...)):
|
|
| 1384 |
reply_en = postprocess_for_intent_en(reply_en, intent)
|
| 1385 |
reply_en = dedupe_sentences(reply_en)
|
| 1386 |
|
| 1387 |
-
#
|
| 1388 |
-
mood = detect_mood(raw_msg + " " + reply_en)
|
| 1389 |
-
emoji = ""
|
| 1390 |
flags = {}
|
| 1391 |
try:
|
| 1392 |
-
# Moderation (prevent toxic content from being saved)
|
| 1393 |
if moderator is not None:
|
| 1394 |
mod_result = moderator(raw_msg[:1024])
|
| 1395 |
if isinstance(mod_result, list) and len(mod_result) > 0:
|
|
@@ -1400,11 +1431,12 @@ async def chat(request: Request, data: dict = Body(...)):
|
|
| 1400 |
except Exception:
|
| 1401 |
pass
|
| 1402 |
|
| 1403 |
-
#
|
|
|
|
|
|
|
| 1404 |
try:
|
| 1405 |
chosen_emoji = should_append_emoji(raw_msg, reply_en, mood, flags)
|
| 1406 |
if chosen_emoji:
|
| 1407 |
-
# Append safely (space separator) and ensure length constraint
|
| 1408 |
if len(reply_en) + len(chosen_emoji) < 1200:
|
| 1409 |
reply_en = reply_en + " " + chosen_emoji
|
| 1410 |
emoji = chosen_emoji
|
|
@@ -1433,7 +1465,6 @@ async def chat(request: Request, data: dict = Body(...)):
|
|
| 1433 |
"topic": topic,
|
| 1434 |
}
|
| 1435 |
)
|
| 1436 |
-
# Keep recent / high-confidence per topic
|
| 1437 |
conn.execute(
|
| 1438 |
sql_text("""
|
| 1439 |
DELETE FROM user_memory
|
|
@@ -1450,18 +1481,21 @@ async def chat(request: Request, data: dict = Body(...)):
|
|
| 1450 |
except Exception as e:
|
| 1451 |
logger.warning(f"user_memory persist error: {e}")
|
| 1452 |
|
| 1453 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1454 |
include_steps = bool(data.get("include_steps", False))
|
| 1455 |
if include_steps and steps:
|
| 1456 |
reasoning_text = " | ".join(str(s) for s in steps)
|
| 1457 |
-
|
| 1458 |
-
|
| 1459 |
-
# Always translate once at the end to the fixed user language target
|
| 1460 |
-
if reply_lang and reply_lang.split("-")[0].lower() not in ("en", "eng"):
|
| 1461 |
-
reply_final = translate_from_english(reply_en, reply_lang)
|
| 1462 |
-
reply_final = dedupe_sentences(reply_final)
|
| 1463 |
-
else:
|
| 1464 |
-
reply_final = reply_en
|
| 1465 |
|
| 1466 |
duration = time.time() - t0
|
| 1467 |
record_request(duration)
|
|
|
|
| 1 |
# JusticeAI Backend — Upgraded & Integrated (Backend-only; does NOT create or overwrite frontend)
|
| 2 |
#
|
| 3 |
+
# This is the updated app.py requested: it prefers a local language model (language.py or language.bin),
|
| 4 |
+
# enforces strict topic scoping, preserves sentence boundaries (no run-on joining), understands and
|
| 5 |
+
# reasons about emojis, and uses the provided emojis.py when present.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
#
|
| 7 |
+
# Key behaviors:
|
| 8 |
+
# - Loads language.py if present; otherwise attempts to load language.bin (torch.load then pickle).
|
| 9 |
+
# - If the language module exposes translate/translate_to_en/translate_from_en/detect, those are used.
|
| 10 |
+
# - detect_language_safe will consult the language module for detection if available, then fall back to heuristics.
|
| 11 |
+
# - All knowledge retrieval and refinement in /chat is strictly within the resolved topic.
|
| 12 |
+
# - dedupe_sentences preserves sentences as separate lines and avoids turning them into run-ons.
|
| 13 |
+
# - Emoji extraction and a small emoji-sentiment heuristic are used to decide when to append/echo emojis.
|
| 14 |
+
# - Moderation prevents saving toxic memory and prevents adding emojis to responses flagged toxic.
|
| 15 |
+
#
|
| 16 |
+
# Place language.bin and/or language.py and emojis.py in the same folder as this file.
|
| 17 |
+
# Restart the app after placing those files.
|
| 18 |
|
| 19 |
from sqlalchemy.pool import NullPool
|
| 20 |
import os
|
|
|
|
| 53 |
# ----- Optional helpers (soft fallbacks) -----
|
| 54 |
# Prefer user's emojis.py
|
| 55 |
try:
|
|
|
|
|
|
|
|
|
|
| 56 |
from emojis import get_emoji, get_category_for_mood # type: ignore
|
| 57 |
logger.info("[JusticeAI] Using local emojis.py")
|
| 58 |
except Exception:
|
|
|
|
| 86 |
AutoModelForCausalLM = None
|
| 87 |
hf_pipeline = None
|
| 88 |
|
| 89 |
+
# ----- Local language loader (language.py or language.bin) -----
|
| 90 |
+
language_module = None
|
| 91 |
|
| 92 |
+
def load_local_language_module():
|
| 93 |
+
"""
|
| 94 |
+
Attempt to import language.py first. If not present, attempt to load language.bin
|
| 95 |
+
via torch.load or pickle. The resulting object is stored in `language_module`.
|
| 96 |
+
The module/object should ideally expose:
|
| 97 |
+
- translate(text, src, tgt)
|
| 98 |
+
- translate_to_en(text, src)
|
| 99 |
+
- translate_from_en(text, tgt)
|
| 100 |
+
- detect(text) or detect_language(text)
|
| 101 |
+
- model_info() (optional)
|
| 102 |
+
"""
|
| 103 |
+
global language_module
|
| 104 |
+
# Try language.py module import
|
| 105 |
try:
|
| 106 |
+
import language as lm # type: ignore
|
| 107 |
+
language_module = lm
|
| 108 |
+
logger.info("[JusticeAI] Loaded language.py module")
|
| 109 |
return
|
| 110 |
except Exception:
|
| 111 |
pass
|
| 112 |
+
# Try language.bin next (torch.load then pickle)
|
| 113 |
+
bin_path = Path("language.bin")
|
| 114 |
+
if bin_path.exists():
|
| 115 |
+
try:
|
| 116 |
try:
|
| 117 |
+
language_module = torch.load(str(bin_path), map_location="cpu")
|
| 118 |
+
logger.info("[JusticeAI] Loaded language.bin via torch.load")
|
| 119 |
+
return
|
| 120 |
+
except Exception as e:
|
| 121 |
+
logger.info(f"[JusticeAI] torch.load failed for language.bin: {e}")
|
| 122 |
+
# fallback to pickle
|
| 123 |
+
import pickle
|
| 124 |
+
with open(bin_path, "rb") as f:
|
| 125 |
+
language_module = pickle.load(f)
|
| 126 |
+
logger.info("[JusticeAI] Loaded language.bin via pickle")
|
| 127 |
+
return
|
| 128 |
+
except Exception as e:
|
| 129 |
+
language_module = None
|
| 130 |
+
logger.warning(f"[JusticeAI] Failed to load language.bin: {e}")
|
| 131 |
+
else:
|
| 132 |
+
logger.info("[JusticeAI] No language.py or language.bin found in cwd")
|
| 133 |
|
| 134 |
+
# attempt early load
|
| 135 |
+
load_local_language_module()
|
| 136 |
|
| 137 |
# ----- Config (env) -----
|
| 138 |
ADMIN_KEY = os.environ.get("ADMIN_KEY")
|
|
|
|
| 305 |
self.capabilities["embed_available"] = embed_model is not None
|
| 306 |
self.capabilities["moderator"] = moderator is not None
|
| 307 |
self.capabilities["llm_reflect"] = llm_model is not None and llm_tokenizer is not None
|
| 308 |
+
self.capabilities["translation"] = (AutoTokenizer is not None and AutoModelForSeq2SeqLM is not None) or (language_module is not None)
|
| 309 |
self.capabilities["ann"] = False # FAISS not wired yet (scaffold)
|
| 310 |
logger.info(f"[JusticeBrain] Capabilities: {self.capabilities}")
|
| 311 |
|
|
|
|
| 377 |
s = s[1:-1].strip()
|
| 378 |
return " ".join(s.split())
|
| 379 |
|
| 380 |
+
def dedupe_sentences(text: str) -> str:
|
| 381 |
"""
|
| 382 |
+
Split text into sentences (respecting existing newlines) and dedupe while preserving order.
|
| 383 |
+
Return a string where sentences are separated by single newlines (no joining into run-on paragraphs).
|
| 384 |
"""
|
| 385 |
if not text:
|
| 386 |
return text
|
| 387 |
sentences = []
|
| 388 |
seen = set()
|
| 389 |
+
# Respect explicit newlines
|
| 390 |
for chunk in re.split(r'\n+', text):
|
| 391 |
+
# Split on punctuation boundaries but keep them
|
| 392 |
parts = re.split(r'(?<=[.?!])\s+', chunk)
|
| 393 |
for sent in parts:
|
| 394 |
s = sent.strip()
|
|
|
|
| 400 |
sentences.append(s)
|
| 401 |
return "\n".join(sentences)
|
| 402 |
|
| 403 |
+
# Emoji detection and heuristics
|
|
|
|
| 404 |
_EMOJI_PATTERN = re.compile(
|
| 405 |
"["
|
| 406 |
"\U0001F600-\U0001F64F" # emoticons
|
|
|
|
| 412 |
"]+", flags=re.UNICODE
|
| 413 |
)
|
| 414 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
def extract_emojis(text: str) -> List[str]:
|
| 416 |
if not text:
|
| 417 |
return []
|
| 418 |
return _EMOJI_PATTERN.findall(text)
|
| 419 |
|
| 420 |
def emoji_sentiment_score(emojis: List[str]) -> float:
|
|
|
|
| 421 |
if not emojis:
|
| 422 |
return 0.0
|
| 423 |
score = 0.0
|
| 424 |
+
total = 0
|
| 425 |
for e in "".join(emojis):
|
| 426 |
ord_val = ord(e)
|
| 427 |
+
total += 1
|
| 428 |
+
if 0x1F600 <= ord_val <= 0x1F64F:
|
| 429 |
+
# smiles a bit positive, frowns negative
|
| 430 |
if ord_val in range(0x1F600, 0x1F607) or ord_val in range(0x1F60A, 0x1F60F):
|
| 431 |
score += 1.0
|
| 432 |
elif ord_val in range(0x1F61E, 0x1F626):
|
|
|
|
| 437 |
score += 0.1
|
| 438 |
else:
|
| 439 |
score += 0.0
|
| 440 |
+
return max(-1.0, min(1.0, score / max(1, total)))
|
| 441 |
|
| 442 |
def detect_language_safe(text: str) -> str:
|
| 443 |
"""
|
| 444 |
+
Prefer the local language module detection if available (language.detect or language.detect_language).
|
| 445 |
+
Then use greeting heuristics and Unicode ranges to detect CJK/JP. Conservative fallback is 'en'.
|
| 446 |
"""
|
| 447 |
text = (text or "").strip()
|
| 448 |
if not text:
|
| 449 |
return "en"
|
| 450 |
+
# 1) local language module detection
|
| 451 |
+
try:
|
| 452 |
+
global language_module
|
| 453 |
+
if language_module is not None:
|
| 454 |
+
# Prefer explicit detect functions if provided
|
| 455 |
+
if hasattr(language_module, "detect_language"):
|
| 456 |
+
try:
|
| 457 |
+
lang = language_module.detect_language(text)
|
| 458 |
+
if lang:
|
| 459 |
+
return lang
|
| 460 |
+
except Exception:
|
| 461 |
+
pass
|
| 462 |
+
if hasattr(language_module, "detect"):
|
| 463 |
+
try:
|
| 464 |
+
lang = language_module.detect(text)
|
| 465 |
+
if lang:
|
| 466 |
+
return lang
|
| 467 |
+
except Exception:
|
| 468 |
+
pass
|
| 469 |
+
# Some wrappers expose model_info with detection capability indication
|
| 470 |
+
if hasattr(language_module, "model_info"):
|
| 471 |
+
try:
|
| 472 |
+
info = language_module.model_info()
|
| 473 |
+
# no rigid rule; if model_info exposes a 'detect' attribute we could try it
|
| 474 |
+
except Exception:
|
| 475 |
+
pass
|
| 476 |
+
except Exception:
|
| 477 |
+
pass
|
| 478 |
+
|
| 479 |
+
# 2) greeting/keyword heuristics
|
| 480 |
lower = text.lower()
|
| 481 |
+
greeting_map = {
|
| 482 |
+
"hola": "es", "gracias": "es", "adios": "es",
|
| 483 |
+
"bonjour": "fr", "salut": "fr",
|
| 484 |
+
"hallo": "de", "guten morgen": "de",
|
| 485 |
+
"ciao": "it", "buongiorno": "it",
|
| 486 |
+
"olá": "pt", "obrigado": "pt",
|
| 487 |
+
"привет": "ru", "здравствуйте": "ru",
|
| 488 |
+
"こんにちは": "ja", "こんばんは": "ja",
|
| 489 |
+
"你好": "zh", "谢谢": "zh", "안녕하세요": "ko"
|
| 490 |
+
}
|
| 491 |
+
for k, v in greeting_map.items():
|
| 492 |
+
if k in lower:
|
| 493 |
+
return v
|
| 494 |
+
|
| 495 |
+
# 3) Unicode heuristics: Hiragana/Katakana -> Japanese, CJK -> Chinese, Hangul -> Korean
|
| 496 |
+
if re.search(r'[\u3040-\u30ff]', text):
|
| 497 |
+
return "ja"
|
| 498 |
+
if re.search(r'[\u4e00-\u9fff]', text):
|
| 499 |
+
return "zh"
|
| 500 |
+
if re.search(r'[\uac00-\ud7af]', text):
|
| 501 |
+
return "ko"
|
| 502 |
+
|
| 503 |
+
# 4) ASCII fallback: if text contains mostly ASCII letters and common english words, treat as 'en'
|
| 504 |
letters = re.findall(r'[A-Za-z]', text)
|
| 505 |
if len(letters) >= max(1, len(text) / 4):
|
| 506 |
return "en"
|
| 507 |
+
|
| 508 |
+
# Conservative default
|
| 509 |
return "und"
|
| 510 |
|
| 511 |
def translate_to_english(text: str, src_lang: str) -> str:
|
| 512 |
"""
|
| 513 |
+
Use the local language module (language_module) if present. Otherwise fall back to Helsinki models.
|
|
|
|
| 514 |
"""
|
| 515 |
if not text:
|
| 516 |
return text
|
| 517 |
src = (src_lang.split('-')[0].lower() if src_lang else "und")
|
| 518 |
if src in ("en", "eng", "", "und"):
|
| 519 |
return text
|
| 520 |
+
# prefer language_module
|
| 521 |
+
try:
|
| 522 |
+
global language_module
|
| 523 |
+
if language_module is not None:
|
| 524 |
+
if hasattr(language_module, "translate_to_en"):
|
| 525 |
+
try:
|
| 526 |
+
return language_module.translate_to_en(text, src)
|
| 527 |
+
except Exception:
|
| 528 |
+
pass
|
| 529 |
+
if hasattr(language_module, "translate"):
|
| 530 |
try:
|
| 531 |
+
return language_module.translate(text, src, "en")
|
| 532 |
except TypeError:
|
| 533 |
+
try:
|
| 534 |
+
return language_module.translate(text)
|
| 535 |
+
except Exception:
|
| 536 |
+
pass
|
| 537 |
+
# If language_module is an object with callable method
|
| 538 |
+
if hasattr(language_module, "__call__") and callable(language_module):
|
| 539 |
+
try:
|
| 540 |
+
return language_module(text, src, "en")
|
| 541 |
+
except Exception:
|
| 542 |
+
pass
|
| 543 |
+
except Exception as e:
|
| 544 |
+
logger.debug(f"Local language_module translate attempt failed: {e}")
|
| 545 |
+
# fallback to Helsinki/transformers if available
|
| 546 |
if not re.fullmatch(r"[a-z]{2,3}", src):
|
| 547 |
return text
|
| 548 |
try:
|
|
|
|
| 559 |
model_name = f"Helsinki-NLP/opus-mt-{src}-en"
|
| 560 |
tokenizer = AutoTokenizer.from_pretrained(model_name, cache_dir=TRANSLATION_CACHE_DIR)
|
| 561 |
model = AutoModelForSeq2SeqLM.from_pretrained(model_name, cache_dir=TRANSLATION_CACHE_DIR)
|
| 562 |
+
_translation_model_cache[cache_key] = (tokenizer, model)
|
| 563 |
inputs = tokenizer([text], return_tensors="pt", truncation=True)
|
| 564 |
outputs = model.generate(**inputs, max_length=1024)
|
| 565 |
return tokenizer.batch_decode(outputs, skip_special_tokens=True)[0]
|
|
|
|
| 578 |
|
| 579 |
def translate_from_english(text: str, tgt_lang: str) -> str:
|
| 580 |
"""
|
| 581 |
+
Use the local language module if available; otherwise fall back to Helsinki/transformers.
|
| 582 |
"""
|
| 583 |
if not text:
|
| 584 |
return text
|
| 585 |
tgt = (tgt_lang.split('-')[0].lower() if tgt_lang else "und")
|
| 586 |
if tgt in ("en", "eng", "", "und"):
|
| 587 |
return text
|
| 588 |
+
try:
|
| 589 |
+
global language_module
|
| 590 |
+
if language_module is not None:
|
| 591 |
+
if hasattr(language_module, "translate_from_en"):
|
|
|
|
| 592 |
try:
|
| 593 |
+
return language_module.translate_from_en(text, tgt)
|
| 594 |
+
except Exception:
|
| 595 |
+
pass
|
| 596 |
+
if hasattr(language_module, "translate"):
|
| 597 |
+
try:
|
| 598 |
+
return language_module.translate(text, "en", tgt)
|
| 599 |
except TypeError:
|
| 600 |
+
try:
|
| 601 |
+
return language_module.translate(text)
|
| 602 |
+
except Exception:
|
| 603 |
+
pass
|
| 604 |
+
if hasattr(language_module, "__call__") and callable(language_module):
|
| 605 |
+
try:
|
| 606 |
+
return language_module(text, "en", tgt)
|
| 607 |
+
except Exception:
|
| 608 |
+
pass
|
| 609 |
+
except Exception as e:
|
| 610 |
+
logger.debug(f"Local language_module translate_from_en attempt failed: {e}")
|
| 611 |
+
# fallback to Helsinki/transformers
|
| 612 |
if not re.fullmatch(r"[a-z]{2,3}", tgt):
|
| 613 |
return text
|
| 614 |
try:
|
|
|
|
| 625 |
model_name = f"Helsinki-NLP/opus-mt-en-{tgt}"
|
| 626 |
tokenizer = AutoTokenizer.from_pretrained(model_name, cache_dir=TRANSLATION_CACHE_DIR)
|
| 627 |
model = AutoModelForSeq2SeqLM.from_pretrained(model_name, cache_dir=TRANSLATION_CACHE_DIR)
|
| 628 |
+
_translation_model_cache[cache_key] = (tokenizer, model)
|
| 629 |
inputs = tokenizer([text], return_tensors="pt", truncation=True)
|
| 630 |
outputs = model.generate(**inputs, max_length=1024)
|
| 631 |
return tokenizer.batch_decode(outputs, skip_special_tokens=True)[0]
|
|
|
|
| 671 |
def generate_creative_reply(matches: List[str]) -> str:
|
| 672 |
"""
|
| 673 |
Combine up to three matches into a concise reply.
|
| 674 |
+
Preserve sentence lines (no joining into run-ons).
|
| 675 |
"""
|
| 676 |
clean = []
|
| 677 |
seen = set()
|
|
|
|
| 680 |
if not s or s in seen or is_boilerplate_candidate(s):
|
| 681 |
continue
|
| 682 |
seen.add(s)
|
|
|
|
| 683 |
s = dedupe_sentences(s)
|
| 684 |
clean.append(s)
|
| 685 |
if not clean:
|
| 686 |
return "I’m not sure yet."
|
| 687 |
if len(clean) == 1:
|
| 688 |
return clean[0]
|
|
|
|
| 689 |
return "\n\n".join(clean[:3])
|
| 690 |
|
| 691 |
def infer_topic_from_message(msg: str, known_topics=None) -> str:
|
|
|
|
| 708 |
|
| 709 |
def refine_or_update(matches, new_text, new_reply, confidence, topic="general"):
|
| 710 |
"""
|
| 711 |
+
Update or insert knowledge but ONLY inside the given topic.
|
|
|
|
| 712 |
"""
|
| 713 |
try:
|
| 714 |
if embed_model is None:
|
|
|
|
| 732 |
best_score = float(sims[best_idx])
|
| 733 |
if best_score > 0.75:
|
| 734 |
kid = ids[best_idx]
|
|
|
|
| 735 |
with engine.begin() as conn:
|
| 736 |
row = conn.execute(sql_text("SELECT meta FROM knowledge WHERE id = :id"), {"id": kid}).fetchone()
|
| 737 |
is_manual = False
|
|
|
|
| 742 |
except Exception:
|
| 743 |
is_manual = False
|
| 744 |
if is_manual and confidence < 0.85:
|
|
|
|
| 745 |
with engine.begin() as conn:
|
| 746 |
conn.execute(
|
| 747 |
sql_text("UPDATE knowledge SET reply = :r, updated_at = CURRENT_TIMESTAMP WHERE id = :id"),
|
|
|
|
| 760 |
sql_text("INSERT INTO knowledge (text, reply, language, embedding, category, topic, confidence, meta) VALUES (:t, :r, :lang, :e, 'learned', :topic, :conf, :meta)"),
|
| 761 |
{"t": new_text, "r": new_reply or "", "lang": "en", "e": emb, "topic": topic, "conf": min(0.7, float(confidence)), "meta": json.dumps({"refined": True})}
|
| 762 |
)
|
|
|
|
| 763 |
global knowledge_version
|
| 764 |
knowledge_version += 1
|
| 765 |
except Exception as e:
|
|
|
|
| 767 |
|
| 768 |
def detect_mood(text: str) -> str:
|
| 769 |
"""
|
| 770 |
+
Detect mood using words and emoji heuristics.
|
|
|
|
| 771 |
"""
|
| 772 |
lower = (text or "").lower()
|
| 773 |
positive = ["great", "thanks", "awesome", "happy", "love", "excellent", "cool", "yes", "good", "success", "helpful", "useful", "thank you"]
|
| 774 |
+
negative = ["sad", "bad", "problem", "angry", "hate", "fail", "no", "error", "not working", "disadvantage", "issue"]
|
|
|
|
| 775 |
emojis = extract_emojis(text)
|
| 776 |
e_score = emoji_sentiment_score(emojis)
|
| 777 |
if any(w in lower for w in positive) or e_score > 0.3:
|
|
|
|
| 783 |
def should_append_emoji(user_text: str, reply_text: str, mood: str, flags: Dict[str, Any]) -> str:
|
| 784 |
"""
|
| 785 |
Decide whether to append/echo an emoji and which one.
|
| 786 |
+
Conservative rules to avoid inappropriate emoji use.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 787 |
"""
|
| 788 |
if flags.get("toxic"):
|
| 789 |
return ""
|
| 790 |
+
# If reply already contains emoji, do not add
|
| 791 |
if extract_emojis(reply_text):
|
| 792 |
return ""
|
| 793 |
user_emojis = extract_emojis(user_text)
|
| 794 |
if user_emojis:
|
|
|
|
| 795 |
user_score = emoji_sentiment_score(user_emojis)
|
| 796 |
if user_score >= 0.2:
|
|
|
|
| 797 |
try:
|
| 798 |
cat = get_category_for_mood("positive")
|
| 799 |
return get_emoji(cat, intensity=min(1.0, 0.5 + user_score))
|
| 800 |
except Exception:
|
| 801 |
return user_emojis[0] if user_emojis else ""
|
| 802 |
elif user_score <= -0.2:
|
|
|
|
| 803 |
return ""
|
| 804 |
else:
|
|
|
|
| 805 |
if len(reply_text) < 200:
|
| 806 |
try:
|
| 807 |
cat = get_category_for_mood("neutral")
|
|
|
|
| 809 |
except Exception:
|
| 810 |
return ""
|
| 811 |
return ""
|
|
|
|
| 812 |
if mood == "neutral":
|
| 813 |
return ""
|
|
|
|
| 814 |
if len(reply_text) > 400:
|
| 815 |
return ""
|
|
|
|
| 816 |
if re.search(r'[\{\}\[\]\(\)]', reply_text):
|
| 817 |
return ""
|
| 818 |
try:
|
|
|
|
| 823 |
|
| 824 |
def synthesize_final_reply(en_msg: str, matches: List[str], llm_suggestion: str, intent: str, detected_lang: str) -> str:
|
| 825 |
"""
|
| 826 |
+
Combine knowledge matches and optional LLM suggestion into a final English reply.
|
| 827 |
+
Preserve lines, do not join sentences into run-ons.
|
|
|
|
| 828 |
"""
|
| 829 |
pieces = []
|
| 830 |
for m in matches:
|
|
|
|
| 837 |
pieces.append(sent)
|
| 838 |
if not pieces:
|
| 839 |
return "I don't have enough context — could you add more details or add knowledge with /add?"
|
|
|
|
| 840 |
reply = "\n\n".join(pieces[:3])
|
|
|
|
| 841 |
if intent == "solution":
|
| 842 |
bullets = []
|
| 843 |
for p in re.split(r'\n+', reply):
|
|
|
|
| 856 |
# ----- Startup: load models & background loops -----
|
| 857 |
@app.on_event("startup")
|
| 858 |
async def startup_event():
|
| 859 |
+
global embed_model, spell, moderator, llm_tokenizer, llm_model, startup_time, language_module
|
| 860 |
t0 = time.time()
|
| 861 |
logger.info("[JusticeAI] Starting component loading...")
|
| 862 |
|
|
|
|
| 925 |
model_progress["llm"]["status"] = "error"
|
| 926 |
logger.warning(f"[JusticeAI] Could not load local LLM: {e}")
|
| 927 |
|
| 928 |
+
# reload language module in case files were placed before startup
|
| 929 |
+
load_local_language_module()
|
| 930 |
+
if language_module is not None:
|
| 931 |
+
try:
|
| 932 |
+
if hasattr(language_module, "model_info"):
|
| 933 |
+
info = language_module.model_info()
|
| 934 |
+
logger.info(f"[JusticeAI] language module info: {info}")
|
| 935 |
+
else:
|
| 936 |
+
# attempt a small introspection
|
| 937 |
+
logger.info(f"[JusticeAI] language module type: {type(language_module)}")
|
| 938 |
+
except Exception as e:
|
| 939 |
+
logger.debug(f"[JusticeAI] language module introspect failed: {e}")
|
| 940 |
|
| 941 |
startup_time = round(time.time() - t0, 2)
|
| 942 |
logger.info(f"[JusticeAI] Startup completed in {startup_time}s")
|
|
|
|
| 963 |
def background_learning_loop():
|
| 964 |
while True:
|
| 965 |
try:
|
|
|
|
| 966 |
with engine.begin() as conn:
|
| 967 |
mem_rows = conn.execute(sql_text("SELECT text, reply, topic, confidence FROM user_memory ORDER BY created_at DESC LIMIT 200")).fetchall()
|
| 968 |
knowledge_rows = conn.execute(sql_text("SELECT text, reply, topic FROM knowledge ORDER BY created_at DESC LIMIT 200")).fetchall()
|
|
|
|
|
|
|
| 969 |
if llm_model and llm_tokenizer and mem_rows:
|
| 970 |
for mem in mem_rows:
|
| 971 |
user_text = mem[0] or ""
|
|
|
|
| 976 |
inputs = llm_tokenizer(prompt, return_tensors="pt", truncation=True, max_length=512)
|
| 977 |
outputs = llm_model.generate(**inputs, max_length=256, do_sample=True, temperature=0.7)
|
| 978 |
suggestion = llm_tokenizer.decode(outputs[0], skip_special_tokens=True)
|
|
|
|
| 979 |
conf = float(mem[3] or 0)
|
| 980 |
if suggestion and conf >= 0.2:
|
|
|
|
| 981 |
refine_or_update([], user_text, suggestion, conf, topic=topic)
|
| 982 |
logger.debug(f"[Background AGI] Refined knowledge for topic={topic}")
|
| 983 |
except Exception as e:
|
| 984 |
logger.debug(f"[Background AGI] LLM suggestion error for memory: {e}")
|
|
|
|
| 985 |
record_learn_event()
|
| 986 |
except Exception as e:
|
| 987 |
logger.warning(f"[Background AGI] Learning loop error: {e}")
|
| 988 |
+
time.sleep(60)
|
|
|
|
| 989 |
threading.Thread(target=background_learning_loop, daemon=True).start()
|
| 990 |
|
| 991 |
# ----- Endpoints -----
|
| 992 |
@app.get("/model-status")
|
| 993 |
async def model_status():
|
| 994 |
response_progress = {k: dict(v) for k, v in model_progress.items()}
|
| 995 |
+
lang_info = None
|
| 996 |
+
try:
|
| 997 |
+
if language_module is not None and hasattr(language_module, "model_info"):
|
| 998 |
+
lang_info = language_module.model_info()
|
| 999 |
+
except Exception:
|
| 1000 |
+
lang_info = {"info": "unavailable"}
|
| 1001 |
+
return {"model_loaded": embed_model is not None, "model_progress": response_progress, "model_load_times": model_load_times, "startup_time_s": startup_time, "language_module": lang_info}
|
| 1002 |
|
| 1003 |
@app.get("/health")
|
| 1004 |
async def health_check():
|
|
|
|
| 1107 |
return JSONResponse(status_code=400, content={"error": "Text is required"})
|
| 1108 |
detected = detect_language_safe(text_data)
|
| 1109 |
if detected and detected.split("-")[0].lower() not in ("en", "eng", "und"):
|
| 1110 |
+
if AutoTokenizer is not None and AutoModelForSeq2SeqLM is not None or language_module is not None:
|
| 1111 |
try:
|
| 1112 |
text_data = translate_to_english(text_data, detected)
|
| 1113 |
detected = "en"
|
|
|
|
| 1140 |
sql_text("INSERT INTO knowledge (text, reply, language, category, topic, confidence, meta) VALUES (:t, :r, :lang, 'general', :topic, :conf, :meta)"),
|
| 1141 |
{"t": text_data, "r": reply, "lang": "en", "topic": topic, "conf": 0.9, "meta": json.dumps({"manual": True})}
|
| 1142 |
)
|
|
|
|
| 1143 |
global knowledge_version
|
| 1144 |
knowledge_version += 1
|
| 1145 |
record_learn_event()
|
|
|
|
| 1194 |
errors.append({"index": i, "error": str(e)})
|
| 1195 |
return {"added": added, "errors": errors}
|
| 1196 |
|
| 1197 |
+
# ----- /chat endpoint -----
|
| 1198 |
@app.post("/chat")
|
| 1199 |
async def chat(request: Request, data: dict = Body(...)):
|
| 1200 |
t0 = time.time()
|
|
|
|
| 1204 |
user_id = hashlib.sha256(f"{user_ip}-{username}".encode()).hexdigest()
|
| 1205 |
topic_hint = str(data.get("topic", "") or "").strip()
|
| 1206 |
detected_lang = detect_language_safe(raw_msg)
|
| 1207 |
+
# If detection returns 'und', keep und; otherwise set reply_lang to detected language.
|
| 1208 |
+
reply_lang = detected_lang if detected_lang and detected_lang != "und" else "en"
|
|
|
|
| 1209 |
user_force_save = bool(data.get("save_memory", False))
|
| 1210 |
|
| 1211 |
+
# Optional spell correction
|
| 1212 |
if spell is not None:
|
| 1213 |
try:
|
| 1214 |
words = raw_msg.split()
|
|
|
|
| 1222 |
else:
|
| 1223 |
msg_corrected = raw_msg
|
| 1224 |
|
| 1225 |
+
# Intent classifier
|
| 1226 |
def classify_intent_local(text: str) -> str:
|
| 1227 |
t = text.lower()
|
| 1228 |
if any(k in t for k in ["why", "para qué", "por qué"]):
|
|
|
|
| 1249 |
else:
|
| 1250 |
topic = topic_hint
|
| 1251 |
|
| 1252 |
+
# Load knowledge strictly for this topic only
|
| 1253 |
try:
|
| 1254 |
with engine.begin() as conn:
|
| 1255 |
rows = conn.execute(sql_text("SELECT id, text, reply, language, embedding, topic FROM knowledge WHERE topic = :topic ORDER BY created_at DESC"), {"topic": topic}).fetchall()
|
|
|
|
| 1262 |
knowledge_langs = [r[3] or "en" for r in rows]
|
| 1263 |
knowledge_topics = [r[5] or "general" for r in rows]
|
| 1264 |
|
| 1265 |
+
# Translate the user message to English if needed (for retrieval/synthesis)
|
| 1266 |
en_msg = msg_corrected
|
| 1267 |
if detected_lang and detected_lang.split("-")[0].lower() not in ("en", "eng", "", "und"):
|
| 1268 |
en_msg = translate_to_english(msg_corrected, detected_lang)
|
| 1269 |
|
| 1270 |
+
# Embedding-based retrieval (topic-scoped)
|
| 1271 |
matches = []
|
| 1272 |
confidence = 0.0
|
| 1273 |
knowledge_embeddings = None
|
|
|
|
| 1299 |
matches = [c for _, _, c in filtered]
|
| 1300 |
confidence = filtered[0][1] if filtered else 0.0
|
| 1301 |
else:
|
| 1302 |
+
# fallback: substring search inside topic texts
|
| 1303 |
for idx, ktext in enumerate(knowledge_texts):
|
| 1304 |
ktext_lang = detect_language_safe(ktext)
|
| 1305 |
ktext_en = translate_to_english(ktext, ktext_lang) if ktext_lang != "en" else ktext
|
|
|
|
| 1311 |
matches = knowledge_replies[:3] if knowledge_replies else []
|
| 1312 |
confidence = 0.0
|
| 1313 |
|
| 1314 |
+
# Build scratchpad and synthesize
|
| 1315 |
def build_reasoning_scratchpad(question_en: str, facts_en: List[str]) -> Dict[str, Any]:
|
| 1316 |
scratch = {
|
| 1317 |
"question": question_en,
|
|
|
|
| 1347 |
return "Solutions:\n- " + "\n- ".join(steps[:5])
|
| 1348 |
if intent_label == "why":
|
| 1349 |
return base + " It is useful because it provides direct access to relevant information and supports faster decision-making."
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1350 |
return base
|
| 1351 |
|
|
|
|
| 1352 |
scratchpad = build_reasoning_scratchpad(en_msg, matches)
|
| 1353 |
reply_en = synthesize_from_scratchpad(scratchpad, intent)
|
| 1354 |
|
| 1355 |
+
# Optional LLM reflection for knowledge refinement (not for user reply)
|
| 1356 |
llm_suggestion = ""
|
| 1357 |
try:
|
| 1358 |
if llm_model and llm_tokenizer and matches:
|
|
|
|
| 1370 |
logger.debug(f"LLM reflection error: {e}")
|
| 1371 |
llm_suggestion = ""
|
| 1372 |
|
| 1373 |
+
# Compose final reply (knowledge-first, topic-scoped)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1374 |
steps = []
|
| 1375 |
if matches and confidence >= 0.6:
|
| 1376 |
reply_en = matches[0]
|
|
|
|
| 1383 |
if matches or llm_suggestion:
|
| 1384 |
reply_en = synthesize_final_reply(en_msg, matches, llm_suggestion, intent, "en")
|
| 1385 |
else:
|
| 1386 |
+
reply_en = "I don't have enough context yet — can you give more details?"
|
| 1387 |
+
steps.append("No relevant matches")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1388 |
except Exception as e:
|
| 1389 |
logger.warning(f"Synthesis error: {e}")
|
| 1390 |
reply_en = "I don't have enough context yet — can you give more details?"
|
| 1391 |
steps.append("Synthesis fallback")
|
| 1392 |
|
| 1393 |
+
# Postprocess for intent
|
| 1394 |
def postprocess_for_intent_en(reply_text: str, intent_label: str) -> str:
|
| 1395 |
if intent_label == "why":
|
| 1396 |
suf = " It is useful because it provides direct access to relevant information and supports faster decision-making."
|
|
|
|
| 1418 |
reply_en = postprocess_for_intent_en(reply_en, intent)
|
| 1419 |
reply_en = dedupe_sentences(reply_en)
|
| 1420 |
|
| 1421 |
+
# Moderation check for user message (prevent saving toxic memory)
|
|
|
|
|
|
|
| 1422 |
flags = {}
|
| 1423 |
try:
|
|
|
|
| 1424 |
if moderator is not None:
|
| 1425 |
mod_result = moderator(raw_msg[:1024])
|
| 1426 |
if isinstance(mod_result, list) and len(mod_result) > 0:
|
|
|
|
| 1431 |
except Exception:
|
| 1432 |
pass
|
| 1433 |
|
| 1434 |
+
# Mood & emoji: detect mood from user message and reply, then decide emoji
|
| 1435 |
+
mood = detect_mood(raw_msg + " " + reply_en)
|
| 1436 |
+
emoji = ""
|
| 1437 |
try:
|
| 1438 |
chosen_emoji = should_append_emoji(raw_msg, reply_en, mood, flags)
|
| 1439 |
if chosen_emoji:
|
|
|
|
| 1440 |
if len(reply_en) + len(chosen_emoji) < 1200:
|
| 1441 |
reply_en = reply_en + " " + chosen_emoji
|
| 1442 |
emoji = chosen_emoji
|
|
|
|
| 1465 |
"topic": topic,
|
| 1466 |
}
|
| 1467 |
)
|
|
|
|
| 1468 |
conn.execute(
|
| 1469 |
sql_text("""
|
| 1470 |
DELETE FROM user_memory
|
|
|
|
| 1481 |
except Exception as e:
|
| 1482 |
logger.warning(f"user_memory persist error: {e}")
|
| 1483 |
|
| 1484 |
+
# Translate final reply into user's language if needed (use language_module if available)
|
| 1485 |
+
reply_final = reply_en
|
| 1486 |
+
try:
|
| 1487 |
+
if reply_lang and reply_lang.split("-")[0].lower() not in ("en", "eng", "", "und"):
|
| 1488 |
+
reply_final = translate_from_english(reply_en, reply_lang)
|
| 1489 |
+
reply_final = dedupe_sentences(reply_final)
|
| 1490 |
+
except Exception as e:
|
| 1491 |
+
logger.debug(f"Final translation error: {e}")
|
| 1492 |
+
reply_final = reply_en
|
| 1493 |
+
|
| 1494 |
+
# Optional debug steps
|
| 1495 |
include_steps = bool(data.get("include_steps", False))
|
| 1496 |
if include_steps and steps:
|
| 1497 |
reasoning_text = " | ".join(str(s) for s in steps)
|
| 1498 |
+
reply_final = f"{reply_final}\n\n[Reasoning steps: {reasoning_text}]"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1499 |
|
| 1500 |
duration = time.time() - t0
|
| 1501 |
record_request(duration)
|