ps1811 commited on
Commit
71c058e
·
1 Parent(s): cc4d7d1

original llm.py and generate.py restored

Browse files
Files changed (2) hide show
  1. app/models/llm.py +41 -10
  2. app/recs/generate.py +97 -16
app/models/llm.py CHANGED
@@ -2,37 +2,68 @@ from __future__ import annotations
2
 
3
  import os
4
  import threading
5
- from typing import Any
6
 
7
  from huggingface_hub import hf_hub_download
 
8
 
9
- HF_REPO = os.getenv("LLAMA_HF_REPO", "ps1811/advisor-minicpm-finetuned-gguf")
10
- HF_FILENAME = os.getenv("LLAMA_HF_FILENAME", "advisor-minicpm-q4_k_m.gguf")
11
 
12
- _model: Any = None
13
  _init_lock = threading.Lock()
14
 
15
 
16
- def load_model() -> Any:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  global _model
 
18
 
19
  if _model is not None:
 
20
  return _model
21
 
22
  with _init_lock:
23
  if _model is not None:
24
  return _model
25
 
 
26
  model_path = hf_hub_download(repo_id=HF_REPO, filename=HF_FILENAME)
 
27
 
28
- from llama_cpp import Llama
 
 
 
 
 
 
 
 
29
 
30
  _model = Llama(
31
  model_path=model_path,
32
- n_ctx=int(os.getenv("LLAMA_N_CTX", "2048")),
33
- n_gpu_layers=int(os.getenv("LLAMA_GPU_LAYERS", "0")),
34
- n_threads=int(os.getenv("LLAMA_N_THREADS", "4")),
35
- verbose=os.getenv("LLAMA_VERBOSE", "0") == "1",
36
  )
 
37
 
38
  return _model
 
2
 
3
  import os
4
  import threading
 
5
 
6
  from huggingface_hub import hf_hub_download
7
+ from llama_cpp import Llama
8
 
9
+ HF_REPO = os.getenv("LLAMA_HF_REPO", "openbmb/MiniCPM5-1B-GGUF")
10
+ HF_FILENAME = os.getenv("LLAMA_HF_FILENAME", "MiniCPM5-1B-Q4_K_M.gguf")
11
 
12
+ _model: Llama | None = None
13
  _init_lock = threading.Lock()
14
 
15
 
16
+ def _preload_cuda_libs() -> None:
17
+ try:
18
+ import ctypes
19
+
20
+ import nvidia.cublas
21
+ import nvidia.cuda_runtime
22
+ except ImportError:
23
+ return
24
+
25
+ for module, lib_name in (
26
+ (nvidia.cublas, "libcublas.so.12"),
27
+ (nvidia.cuda_runtime, "libcudart.so.12"),
28
+ ):
29
+ lib_path = os.path.join(module.__path__[0], "lib", lib_name)
30
+ if os.path.isfile(lib_path):
31
+ ctypes.CDLL(lib_path, mode=ctypes.RTLD_GLOBAL)
32
+
33
+
34
+ def load_model() -> Llama:
35
  global _model
36
+ print("🧠 [load_model] called", flush=True)
37
 
38
  if _model is not None:
39
+ print("🧠 [load_model] returning cached model", flush=True)
40
  return _model
41
 
42
  with _init_lock:
43
  if _model is not None:
44
  return _model
45
 
46
+ print("⬇️ [load_model] downloading model...", flush=True)
47
  model_path = hf_hub_download(repo_id=HF_REPO, filename=HF_FILENAME)
48
+ print(f"✅ [load_model] model downloaded at {model_path}", flush=True)
49
 
50
+ _preload_cuda_libs()
51
+ gpu_layers = int(os.getenv("LLAMA_GPU_LAYERS", "-1"))
52
+ n_ctx = int(os.getenv("LLAMA_N_CTX", "2048"))
53
+ n_threads = int(os.getenv("LLAMA_N_THREADS", "4"))
54
+ print(
55
+ f"🚀 [load_model] initializing Llama "
56
+ f"(n_gpu_layers={gpu_layers}, n_ctx={n_ctx}, n_threads={n_threads})",
57
+ flush=True,
58
+ )
59
 
60
  _model = Llama(
61
  model_path=model_path,
62
+ n_ctx=n_ctx,
63
+ n_gpu_layers=gpu_layers,
64
+ n_threads=n_threads,
65
+ verbose=False,
66
  )
67
+ print("✅ [load_model] model initialized", flush=True)
68
 
69
  return _model
app/recs/generate.py CHANGED
@@ -11,8 +11,18 @@ from app.models.llm import load_model
11
 
12
  TARGET_CPL = 20.0
13
 
 
 
 
14
  _infer_lock = threading.Lock()
15
 
 
 
 
 
 
 
 
16
 
17
  def fallback_explanation(rec: Dict | None = None) -> str:
18
  return "This recommendation was generated from campaign performance metrics."
@@ -20,7 +30,12 @@ def fallback_explanation(rec: Dict | None = None) -> str:
20
 
21
  def _strip_thinking(text: str) -> str:
22
  text = re.sub(r"<\s*think\s*>.*?<\s*/\s*think\s*>", "", text, flags=re.DOTALL | re.IGNORECASE)
23
- text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL | re.IGNORECASE)
 
 
 
 
 
24
  return text.strip()
25
 
26
 
@@ -32,15 +47,20 @@ def _looks_like_garbage(text: str) -> bool:
32
  return True
33
  if "google ads analyst" in lower and text.count("-") < 2:
34
  return True
 
 
35
  if re.search(r"(?:\d[\s\n]+){6,}", text):
36
  return True
37
  digit_ratio = sum(ch.isdigit() for ch in text) / max(len(text), 1)
38
- bullet_count = sum(1 for ln in text.splitlines() if ln.strip().startswith("-"))
39
- return digit_ratio > 0.35 and bullet_count < 2
40
 
41
 
42
  def is_fallback_output(text: str) -> bool:
43
- return not text or text.startswith("WARNING:") or text.startswith("This recommendation was generated")
 
 
 
 
44
 
45
 
46
  def is_bad_llm_output(text: str) -> bool:
@@ -53,7 +73,7 @@ def sanitize_explanation(text: str, rec: Dict | None = None) -> str:
53
 
54
  bullets: list[str] = []
55
  for ln in lines:
56
- if re.match(r"^[-*]\s+\S", ln):
57
  bullets.append(ln)
58
  elif re.match(r"^\d+\.\s+\S", ln):
59
  bullets.append(re.sub(r"^\d+\.\s+", "- ", ln))
@@ -67,6 +87,52 @@ def sanitize_explanation(text: str, rec: Dict | None = None) -> str:
67
  return flat
68
 
69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  def _coerce_prompt(prompt: str | Dict, rec: Dict | None) -> tuple[str, Dict | None]:
71
  if isinstance(prompt, dict):
72
  rec = rec or prompt
@@ -78,31 +144,46 @@ def _coerce_prompt(prompt: str | Dict, rec: Dict | None) -> tuple[str, Dict | No
78
 
79
 
80
  def generate_explanation(prompt: str | Dict, rec: Dict | None = None, stream: bool = False):
 
 
81
  try:
82
  user_content, rec = _coerce_prompt(prompt, rec)
 
 
 
 
 
83
  if "/no_think" not in user_content:
84
  user_content = f"{user_content}\n/no_think"
85
 
 
 
 
 
 
86
  with _infer_lock:
87
  llm = load_model()
88
- out = llm(
89
- user_content,
90
- max_tokens=int(os.getenv("LLAMA_MAX_TOKENS", "384")),
91
- temperature=float(os.getenv("LLAMA_TEMPERATURE", "0.35")),
92
- stop=["</s>"],
93
- echo=False,
94
- )
95
-
96
- raw = (out["choices"][0].get("text") or "").strip()
97
- clean = sanitize_explanation(raw, rec)
98
 
 
 
 
 
 
 
 
 
 
99
  if stream:
100
  return iter([clean])
101
  return clean
102
 
103
  except Exception as e:
 
104
  traceback.print_exc()
105
- err = f"WARNING: Analysis failed: {e}"
106
  if stream:
107
  return iter([err])
108
  return err
 
11
 
12
  TARGET_CPL = 20.0
13
 
14
+ _IM_END = "<|im_end|>"
15
+ _STOP_SEQUENCES = [_IM_END, "<|im_start|>", "</s>"]
16
+
17
  _infer_lock = threading.Lock()
18
 
19
+ _SYSTEM = (
20
+ "You are a Google Ads analyst. "
21
+ "Reply with 3 to 5 markdown bullet points only. "
22
+ "Each bullet must be one short, actionable insight about the campaign data. "
23
+ "No introduction, no numbered lists, no step-by-step reasoning."
24
+ )
25
+
26
 
27
  def fallback_explanation(rec: Dict | None = None) -> str:
28
  return "This recommendation was generated from campaign performance metrics."
 
30
 
31
  def _strip_thinking(text: str) -> str:
32
  text = re.sub(r"<\s*think\s*>.*?<\s*/\s*think\s*>", "", text, flags=re.DOTALL | re.IGNORECASE)
33
+ text = re.sub(
34
+ r"<think>.*?</think>",
35
+ "",
36
+ text,
37
+ flags=re.DOTALL | re.IGNORECASE,
38
+ )
39
  return text.strip()
40
 
41
 
 
47
  return True
48
  if "google ads analyst" in lower and text.count("-") < 2:
49
  return True
50
+ if "ads performance analyst" in lower and text.count("-") < 2:
51
+ return True
52
  if re.search(r"(?:\d[\s\n]+){6,}", text):
53
  return True
54
  digit_ratio = sum(ch.isdigit() for ch in text) / max(len(text), 1)
55
+ return digit_ratio > 0.22
 
56
 
57
 
58
  def is_fallback_output(text: str) -> bool:
59
+ return (
60
+ not text
61
+ or text.startswith("⚠️")
62
+ or text.startswith("This recommendation was generated")
63
+ )
64
 
65
 
66
  def is_bad_llm_output(text: str) -> bool:
 
73
 
74
  bullets: list[str] = []
75
  for ln in lines:
76
+ if re.match(r"^[-*]\s+\S", ln):
77
  bullets.append(ln)
78
  elif re.match(r"^\d+\.\s+\S", ln):
79
  bullets.append(re.sub(r"^\d+\.\s+", "- ", ln))
 
87
  return flat
88
 
89
 
90
+ def _messages_to_prompt(messages: list[dict[str, str]]) -> str:
91
+ chunks: list[str] = []
92
+ for msg in messages:
93
+ role = msg["role"]
94
+ content = msg["content"]
95
+ chunks.append(f"<|im_start|>{role}\n{content}{_IM_END}\n")
96
+ chunks.append("<|im_start|>assistant\n")
97
+ return "".join(chunks)
98
+
99
+
100
+ def _message_text(message: dict) -> str:
101
+ content = (message.get("content") or "").strip()
102
+ reasoning = (message.get("reasoning_content") or "").strip()
103
+ if content and reasoning and _looks_like_garbage(content):
104
+ return reasoning
105
+ return content or reasoning
106
+
107
+
108
+ def _infer(llm, messages: list[dict[str, str]]) -> str:
109
+ max_tokens = int(os.getenv("LLAMA_MAX_TOKENS", "384"))
110
+ temperature = float(os.getenv("LLAMA_TEMPERATURE", "0.35"))
111
+
112
+ try:
113
+ out = llm.create_chat_completion(
114
+ messages=messages,
115
+ max_tokens=max_tokens,
116
+ temperature=temperature,
117
+ )
118
+ raw = _message_text(out["choices"][0]["message"])
119
+ if raw and not _looks_like_garbage(raw):
120
+ print("✅ [generate_explanation] via create_chat_completion", flush=True)
121
+ return raw
122
+ print("⚠️ [generate_explanation] chat_completion empty/garbage — raw fallback", flush=True)
123
+ except TypeError as exc:
124
+ print(f"⚠️ [generate_explanation] chat_completion failed: {exc}", flush=True)
125
+
126
+ out = llm(
127
+ _messages_to_prompt(messages),
128
+ max_tokens=max_tokens,
129
+ temperature=temperature,
130
+ stop=_STOP_SEQUENCES,
131
+ echo=False,
132
+ )
133
+ return (out["choices"][0].get("text") or "").strip()
134
+
135
+
136
  def _coerce_prompt(prompt: str | Dict, rec: Dict | None) -> tuple[str, Dict | None]:
137
  if isinstance(prompt, dict):
138
  rec = rec or prompt
 
144
 
145
 
146
  def generate_explanation(prompt: str | Dict, rec: Dict | None = None, stream: bool = False):
147
+ print("\n🔥 [generate_explanation] CALLED", flush=True)
148
+
149
  try:
150
  user_content, rec = _coerce_prompt(prompt, rec)
151
+ print(
152
+ f"🧾 [generate_explanation] prompt type={type(prompt).__name__} "
153
+ f"len={len(user_content)}",
154
+ flush=True,
155
+ )
156
  if "/no_think" not in user_content:
157
  user_content = f"{user_content}\n/no_think"
158
 
159
+ messages = [
160
+ {"role": "system", "content": _SYSTEM},
161
+ {"role": "user", "content": user_content},
162
+ ]
163
+
164
  with _infer_lock:
165
  llm = load_model()
166
+ print("🧠 [generate_explanation] model loaded", flush=True)
167
+ print("🚀 [generate_explanation] calling LLM...", flush=True)
168
+ raw = _infer(llm, messages)
 
 
 
 
 
 
 
169
 
170
+ print("📡 [generate_explanation] response received", flush=True)
171
+ print("📄 [generate_explanation] raw output length:", len(raw), flush=True)
172
+ if raw:
173
+ print("📄 [generate_explanation] raw preview:", raw[:400], flush=True)
174
+
175
+ clean = sanitize_explanation(raw, rec)
176
+ if is_bad_llm_output(clean):
177
+ clean = fallback_explanation(rec)
178
+ print("✨ [generate_explanation] cleaned output ready", flush=True)
179
  if stream:
180
  return iter([clean])
181
  return clean
182
 
183
  except Exception as e:
184
+ print("❌ [generate_explanation] ERROR:", repr(e), flush=True)
185
  traceback.print_exc()
186
+ err = f"⚠️ Analysis failed: {e}"
187
  if stream:
188
  return iter([err])
189
  return err