"""Shared text generation utilities."""
from __future__ import annotations
import re
import logging
from typing import List, Dict, Any
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from src.config import DEFAULT_MAX_NEW_TOKENS, DEFAULT_TEMPERATURE
logger = logging.getLogger(__name__)
def strip_think_blocks(text: str) -> str:
"""Remove ... blocks if the model emits them."""
return re.sub(r".*?", "", text, flags=re.DOTALL).strip()
def extract_assistant_reply(text: str) -> str:
"""Extract the assistant reply from a chat-templated output string."""
# Common delimiters used by chat templates
markers = [
"assistant\n",
"assistant",
"<|im_start|>assistant",
"<|start_header_id|>assistant<|end_header_id|>",
]
for marker in markers:
if marker in text:
reply = text.split(marker, 1)[-1]
return strip_think_blocks(reply).strip()
return strip_think_blocks(text).strip()
def chat_generate(
model: AutoModelForCausalLM,
tokenizer: AutoTokenizer,
messages: List[Dict[str, Any]],
max_new_tokens: int = DEFAULT_MAX_NEW_TOKENS,
temperature: float = DEFAULT_TEMPERATURE,
do_sample: bool = False,
) -> str:
"""Generate an assistant reply from a list of chat messages."""
prompt = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True,
)
inputs = tokenizer(
prompt,
return_tensors="pt",
padding=True,
truncation=True,
)
if hasattr(model, "device") and model.device.type != "meta":
inputs = {k: v.to(model.device) for k, v in inputs.items()}
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=max_new_tokens,
do_sample=do_sample,
temperature=temperature if do_sample else None,
pad_token_id=tokenizer.pad_token_id,
eos_token_id=tokenizer.eos_token_id,
)
generated = outputs[0][inputs["input_ids"].shape[1]:]
text = tokenizer.decode(generated, skip_special_tokens=True)
return extract_assistant_reply(text)