File size: 9,767 Bytes
6c05976 1db4430 6c05976 f19a6db 8e1f5bc 6c05976 b9db94b 6c05976 1db4430 6c05976 48fccfa 6c05976 1db4430 48fccfa 1db4430 48fccfa 1db4430 48fccfa 1db4430 fe43097 1db4430 6c05976 1db4430 48fccfa 1db4430 6c05976 48fccfa 6c05976 1db4430 6c05976 1db4430 6c05976 1db4430 f19a6db 48fccfa 1db4430 f19a6db 48fccfa 1db4430 f19a6db 1db4430 6c05976 1db4430 6c05976 48fccfa 6c05976 1db4430 6c05976 1db4430 6c05976 1db4430 6c05976 1db4430 6c05976 48fccfa 6c05976 1db4430 6c05976 48fccfa 6c05976 1db4430 6c05976 1db4430 6c05976 1db4430 6c05976 f1d5b31 1db4430 f1d5b31 1db4430 f1d5b31 1db4430 f1d5b31 1db4430 f1d5b31 48fccfa f1d5b31 1db4430 f1d5b31 48fccfa f1d5b31 1db4430 f1d5b31 1db4430 f1d5b31 1db4430 f1d5b31 1db4430 f1d5b31 1db4430 f1d5b31 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 | from __future__ import annotations
import os
import re
from typing import Any
import torch
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
from env.config import ADAPTER_REPO_ID, MODEL_ID
# Keep one model instance warm after the first request.
_model: Any = None
_tokenizer: Any = None
_adapter_applied = False
# Reasoning-capable models sometimes emit hidden thinking tags.
_THINK_BLOCK_PATTERN = re.compile(r"<think>.*?</think>", re.IGNORECASE | re.DOTALL)
_THINK_START_PATTERN = re.compile(r"<think>.*", re.IGNORECASE | re.DOTALL)
def clean_generated_text(text: str, max_sentences: int | None = None) -> str:
"""Removes hidden reasoning and trims rambling generated text."""
# Prefer visible content after a completed hidden-reasoning block.
if "</think>" in text.lower():
text = re.split(r"</think>", text, flags=re.IGNORECASE, maxsplit=1)[-1]
# Remove complete or dangling hidden-reasoning blocks.
text = _THINK_BLOCK_PATTERN.sub("", text)
text = _THINK_START_PATTERN.sub("", text)
if "=== EMOTIONS ===" in text:
text = text.rsplit("=== EMOTIONS ===", 1)[1]
text = f"=== EMOTIONS ==={text}"
# Drop accidental chat-template continuations.
for marker in ("<|im_end|>", "<|im_start|>", "\nUser:", "\nAssistant:"):
if marker in text:
text = text.split(marker, 1)[0]
# Normalize horizontal whitespace, but preserve newlines.
text = re.sub(r"[ \t]+", " ", text)
text = "\n".join(line.strip() for line in text.splitlines())
text = re.sub(r"\n{3,}", "\n\n", text).strip()
# Apply conversational meta-prefix stripping only for chat replies (when max_sentences is set)
if max_sentences is not None:
lines = text.splitlines()
cleaned_lines = []
for line in lines:
line_strip = line.strip()
if not line_strip:
continue
# Skip conversational meta-introductions
normalized_line = line_strip.lower().rstrip(".:")
if normalized_line in (
"here goes",
"here is a reply",
"here is the reply",
"sure",
"here's a reply",
"here's the reply",
"here is my reply",
"here is a cbt reflection",
"here is a cbt reflection",
"here is the reflection",
"cbt reflection",
):
continue
# Strip prefixes like "AI:", "Assistant:", "Coach:", "InnerSpace:", "Response:"
prefix_pattern = re.compile(
r"^(ai|assistant|coach|innerspace|response):\s*", re.IGNORECASE
)
line_strip = prefix_pattern.sub("", line_strip)
if line_strip:
cleaned_lines.append(line_strip)
text = "\n\n".join(cleaned_lines)
if max_sentences is None or not text:
return text
# Keep coach replies compact for the visible chat panel.
sentences = re.findall(r"[^.!?]+[.!?]?", text)
compact = "".join(sentences[:max_sentences]).strip()
return compact or text
def _format_chat_prompt(tokenizer: Any, messages: list[dict[str, str]]) -> str:
"""Formats chat input while disabling model-side hidden reasoning when supported."""
try:
return tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True,
enable_thinking=False,
)
except TypeError:
return tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True,
)
def get_model_and_tokenizer(log_lines: list[str]) -> tuple[Any, Any]:
"""Loads and caches the Hugging Face model and tokenizer lazily."""
global _adapter_applied, _model, _tokenizer
if _model is None:
# Load tokenizer before model so prompt formatting is ready.
log_lines.append(f"Loading tokenizer: {MODEL_ID}")
_tokenizer = AutoTokenizer.from_pretrained(
MODEL_ID,
token=os.environ.get("HF_TOKEN"),
)
# Use bfloat16 on CUDA and float32 elsewhere for compatibility.
log_lines.append(f"Loading model: {MODEL_ID}")
dtype = torch.bfloat16 if torch.cuda.is_available() else torch.float32
_model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
dtype=dtype,
low_cpu_mem_usage=True,
token=os.environ.get("HF_TOKEN"),
)
# Apply the CBT adapter when it is available.
log_lines.append(f"Loading LoRA adapter: {ADAPTER_REPO_ID}")
try:
_model = PeftModel.from_pretrained(
_model,
ADAPTER_REPO_ID,
token=os.environ.get("HF_TOKEN"),
)
_adapter_applied = True
log_lines.append("LoRA adapter applied.")
except Exception as adapter_error:
_adapter_applied = False
log_lines.append(
f"Warning: Could not load adapter ({adapter_error}). Using base model."
)
# Move the loaded model to GPU memory when ZeroGPU is active.
if torch.cuda.is_available():
log_lines.append("Moving model to CUDA.")
_model = _model.to("cuda")
else:
# Make cached-request diagnostics explicit without reloading weights.
adapter_status = "with LoRA adapter" if _adapter_applied else "without adapter"
log_lines.append(f"Using cached model {adapter_status}.")
return _model, _tokenizer
def run_model_inference(prompt: str) -> tuple[str, str]:
"""Executes inference using local hardware in the app runtime."""
log_lines: list[str] = []
try:
# Reuse the cached local model for the journal analysis.
log_lines.append("Initializing local model execution...")
model, tokenizer = get_model_and_tokenizer(log_lines)
device = str(model.device)
log_lines.append(f"Running local model execution on device: {device}...")
# Format the prompt with the model chat template.
messages = [{"role": "user", "content": prompt}]
text = _format_chat_prompt(tokenizer, messages)
model_inputs = tokenizer([text], return_tensors="pt").to(device)
# Generate a structured CBT reflection.
log_lines.append("Generating reflection...")
generated_ids = model.generate(
**model_inputs,
max_new_tokens=512,
do_sample=False,
)
# Decode only the generated assistant tokens.
generated_ids = [
output_ids[len(input_ids) :]
for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
]
response: str = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[
0
]
response = clean_generated_text(response)
log_lines.append("Local model execution completed successfully.")
return response, "\n".join(log_lines)
except Exception as e:
# Keep private text local even when inference fails.
log_lines.append(f"Local model execution failed: {e}.")
log_lines.append("Inference stopped. No serverless fallback is configured.")
return "", "\n".join(log_lines)
def run_chat_inference(
history: list[dict[str, str]],
system_prompt: str,
) -> tuple[str, str]:
"""Executes stateful multi-turn chat generation using local hardware."""
log_lines: list[str] = []
try:
# Reuse the cached local model for follow-up coaching.
log_lines.append("Initializing local model for chat...")
model, tokenizer = get_model_and_tokenizer(log_lines)
device = str(model.device)
log_lines.append(f"Running local chat model on device: {device}...")
# Include the system prompt and visible chat history.
messages = [{"role": "system", "content": system_prompt}] + history
text = _format_chat_prompt(tokenizer, messages)
model_inputs = tokenizer([text], return_tensors="pt").to(device)
# Keep chat replies shorter than the initial report.
log_lines.append("Generating chat response...")
generated_ids = model.generate(
**model_inputs,
max_new_tokens=96,
do_sample=False,
repetition_penalty=1.18,
no_repeat_ngram_size=4,
)
# Decode only the latest assistant response.
generated_ids = [
output_ids[len(input_ids) :]
for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
]
response: str = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[
0
]
response = clean_generated_text(response, max_sentences=4)
if not response:
response = (
"That sounds like a painful thought to sit with. "
"Can we look at one concrete piece of evidence for and against it?"
)
log_lines.append("Local chat generation completed successfully.")
return response, "\n".join(log_lines)
except Exception as e:
# Preserve privacy by failing locally instead of routing to an API.
log_lines.append(f"Local chat execution failed: {e}.")
log_lines.append(
"Chat inference stopped. No serverless fallback is configured."
)
error_msg = (
"I'm sorry, local model execution is currently unavailable. "
"Please try again after the Space finishes loading the model."
)
return error_msg, "\n".join(log_lines)
|