Advisor / app /recs /generate.py
ps1811's picture
original llm.py and generate.py restored
71c058e
Raw
History Blame Contribute Delete
6.06 kB
from __future__ import annotations
import json
import os
import re
import threading
import traceback
from typing import Dict
from app.models.llm import load_model
TARGET_CPL = 20.0
_IM_END = "<|im_end|>"
_STOP_SEQUENCES = [_IM_END, "<|im_start|>", "</s>"]
_infer_lock = threading.Lock()
_SYSTEM = (
"You are a Google Ads analyst. "
"Reply with 3 to 5 markdown bullet points only. "
"Each bullet must be one short, actionable insight about the campaign data. "
"No introduction, no numbered lists, no step-by-step reasoning."
)
def fallback_explanation(rec: Dict | None = None) -> str:
return "This recommendation was generated from campaign performance metrics."
def _strip_thinking(text: str) -> str:
text = re.sub(r"<\s*think\s*>.*?<\s*/\s*think\s*>", "", text, flags=re.DOTALL | re.IGNORECASE)
text = re.sub(
r"<think>.*?</think>",
"",
text,
flags=re.DOTALL | re.IGNORECASE,
)
return text.strip()
def _looks_like_garbage(text: str) -> bool:
if not text:
return True
lower = text.lower()
if "return only" in lower or "no reasoning" in lower or "no explanation" in lower:
return True
if "google ads analyst" in lower and text.count("-") < 2:
return True
if "ads performance analyst" in lower and text.count("-") < 2:
return True
if re.search(r"(?:\d[\s\n]+){6,}", text):
return True
digit_ratio = sum(ch.isdigit() for ch in text) / max(len(text), 1)
return digit_ratio > 0.22
def is_fallback_output(text: str) -> bool:
return (
not text
or text.startswith("⚠️")
or text.startswith("This recommendation was generated")
)
def is_bad_llm_output(text: str) -> bool:
return is_fallback_output(text) or _looks_like_garbage(text)
def sanitize_explanation(text: str, rec: Dict | None = None) -> str:
text = _strip_thinking(text)
lines = [ln.strip() for ln in text.splitlines() if ln.strip()]
bullets: list[str] = []
for ln in lines:
if re.match(r"^[-•*]\s+\S", ln):
bullets.append(ln)
elif re.match(r"^\d+\.\s+\S", ln):
bullets.append(re.sub(r"^\d+\.\s+", "- ", ln))
if len(bullets) >= 2:
return "\n\n".join(bullets[:5])
flat = re.sub(r"[ \t]+", " ", text).strip()
if len(flat) < 20 or _looks_like_garbage(flat):
return fallback_explanation(rec)
return flat
def _messages_to_prompt(messages: list[dict[str, str]]) -> str:
chunks: list[str] = []
for msg in messages:
role = msg["role"]
content = msg["content"]
chunks.append(f"<|im_start|>{role}\n{content}{_IM_END}\n")
chunks.append("<|im_start|>assistant\n")
return "".join(chunks)
def _message_text(message: dict) -> str:
content = (message.get("content") or "").strip()
reasoning = (message.get("reasoning_content") or "").strip()
if content and reasoning and _looks_like_garbage(content):
return reasoning
return content or reasoning
def _infer(llm, messages: list[dict[str, str]]) -> str:
max_tokens = int(os.getenv("LLAMA_MAX_TOKENS", "384"))
temperature = float(os.getenv("LLAMA_TEMPERATURE", "0.35"))
try:
out = llm.create_chat_completion(
messages=messages,
max_tokens=max_tokens,
temperature=temperature,
)
raw = _message_text(out["choices"][0]["message"])
if raw and not _looks_like_garbage(raw):
print("✅ [generate_explanation] via create_chat_completion", flush=True)
return raw
print("⚠️ [generate_explanation] chat_completion empty/garbage — raw fallback", flush=True)
except TypeError as exc:
print(f"⚠️ [generate_explanation] chat_completion failed: {exc}", flush=True)
out = llm(
_messages_to_prompt(messages),
max_tokens=max_tokens,
temperature=temperature,
stop=_STOP_SEQUENCES,
echo=False,
)
return (out["choices"][0].get("text") or "").strip()
def _coerce_prompt(prompt: str | Dict, rec: Dict | None) -> tuple[str, Dict | None]:
if isinstance(prompt, dict):
rec = rec or prompt
reason = prompt.get("reason")
if reason:
return str(reason).strip(), rec
return json.dumps(prompt, default=str), rec
return str(prompt).strip(), rec
def generate_explanation(prompt: str | Dict, rec: Dict | None = None, stream: bool = False):
print("\n🔥 [generate_explanation] CALLED", flush=True)
try:
user_content, rec = _coerce_prompt(prompt, rec)
print(
f"🧾 [generate_explanation] prompt type={type(prompt).__name__} "
f"len={len(user_content)}",
flush=True,
)
if "/no_think" not in user_content:
user_content = f"{user_content}\n/no_think"
messages = [
{"role": "system", "content": _SYSTEM},
{"role": "user", "content": user_content},
]
with _infer_lock:
llm = load_model()
print("🧠 [generate_explanation] model loaded", flush=True)
print("🚀 [generate_explanation] calling LLM...", flush=True)
raw = _infer(llm, messages)
print("📡 [generate_explanation] response received", flush=True)
print("📄 [generate_explanation] raw output length:", len(raw), flush=True)
if raw:
print("📄 [generate_explanation] raw preview:", raw[:400], flush=True)
clean = sanitize_explanation(raw, rec)
if is_bad_llm_output(clean):
clean = fallback_explanation(rec)
print("✨ [generate_explanation] cleaned output ready", flush=True)
if stream:
return iter([clean])
return clean
except Exception as e:
print("❌ [generate_explanation] ERROR:", repr(e), flush=True)
traceback.print_exc()
err = f"⚠️ Analysis failed: {e}"
if stream:
return iter([err])
return err