File size: 4,548 Bytes
f1b0176
06feb89
f1b0176
06feb89
f1b0176
a1e6f13
 
 
 
 
 
 
f1b0176
 
 
06feb89
 
 
f1b0176
06feb89
f1b0176
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
06feb89
 
f1b0176
06feb89
 
f1b0176
 
06feb89
f1b0176
 
 
06feb89
 
 
 
 
 
f1b0176
 
 
 
 
 
 
 
06feb89
 
 
 
 
 
f1b0176
 
a1e6f13
 
 
 
 
06feb89
 
 
 
 
 
 
 
 
 
f1b0176
a1e6f13
f1b0176
 
06feb89
f1b0176
 
 
 
a1e6f13
06feb89
 
a1e6f13
 
06feb89
a1e6f13
 
 
06feb89
 
 
a1e6f13
 
 
 
 
06feb89
f1b0176
 
 
 
 
 
 
06feb89
f1b0176
06feb89
 
 
 
 
 
 
 
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
from functools import lru_cache
import logging
import os
from time import perf_counter

from training_coach.parser import (
    PARSER_MODEL,
    build_parser_messages,
    log_parser_messages,
    log_parser_response_text,
    parse_model_response,
)
from training_coach.models import ParsedCheckIn


MODEL_CACHE_ENV_VAR = "PARSER_MODEL_CACHE_DIR"
DEFAULT_TRANSFORMERS_MODEL = "Qwen/Qwen3-1.7B"
DEFAULT_MAX_NEW_TOKENS = 384
DEFAULT_ZEROGPU_DURATION_SECONDS = 120
logger = logging.getLogger(__name__)


class ParserRuntimeUnavailableError(RuntimeError):
    pass


def _gpu_decorator():
    try:
        import spaces
    except ImportError:
        return lambda function: function
    duration = int(
        os.getenv("ZEROGPU_DURATION_SECONDS", str(DEFAULT_ZEROGPU_DURATION_SECONDS))
    )
    return spaces.GPU(duration=duration)


def _load_transformers():
    try:
        from transformers import AutoModelForCausalLM, AutoTokenizer
    except ImportError as error:
        raise ParserRuntimeUnavailableError(
            "Install transformers, torch, and accelerate to run the parser model."
        ) from error

    return AutoModelForCausalLM, AutoTokenizer


@lru_cache(maxsize=1)
def load_parser_model(model_name: str = DEFAULT_TRANSFORMERS_MODEL):
    start_time = perf_counter()
    logger.info("event=parser_model_load_start model=%s", model_name)
    AutoModelForCausalLM, AutoTokenizer = _load_transformers()
    cache_dir = os.getenv(MODEL_CACHE_ENV_VAR) or None
    tokenizer = AutoTokenizer.from_pretrained(model_name, cache_dir=cache_dir)
    model = AutoModelForCausalLM.from_pretrained(
        model_name,
        cache_dir=cache_dir,
        device_map="auto",
        torch_dtype="auto",
    )
    logger.info(
        "event=parser_model_load_complete model=%s cache_dir_configured=%s elapsed_ms=%s",
        model_name,
        cache_dir is not None,
        round((perf_counter() - start_time) * 1000),
    )
    return tokenizer, model


@_gpu_decorator()
def generate_parser_response(
    raw_text: str,
    model_name: str = DEFAULT_TRANSFORMERS_MODEL,
) -> str:
    start_time = perf_counter()
    logger.info(
        "event=parser_generate_start model=%s text_chars=%s",
        model_name,
        len(raw_text),
    )
    tokenizer, model = load_parser_model(model_name)
    messages = build_parser_messages(raw_text)
    log_parser_messages(
        backend="transformers",
        model_name=model_name,
        messages=messages,
    )
    chat_template_kwargs = {
        "tokenize": False,
        "add_generation_prompt": True,
        "enable_thinking": False,
    }
    try:
        prompt = tokenizer.apply_chat_template(messages, **chat_template_kwargs)
    except TypeError:
        chat_template_kwargs.pop("enable_thinking")
        prompt = tokenizer.apply_chat_template(messages, **chat_template_kwargs)
    inputs = tokenizer([prompt], return_tensors="pt").to(model.device)
    input_token_count = inputs.input_ids.shape[-1]
    output_ids = model.generate(
        **inputs,
        max_new_tokens=int(os.getenv("PARSER_MAX_NEW_TOKENS", str(DEFAULT_MAX_NEW_TOKENS))),
        do_sample=False,
        pad_token_id=tokenizer.eos_token_id,
    )
    generated_ids = output_ids[:, inputs.input_ids.shape[-1] :]
    generated_token_count = generated_ids.shape[-1]
    response_text = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0].strip()
    logger.info(
        "event=parser_generate_complete model=%s prompt_chars=%s "
        "input_tokens=%s generated_tokens=%s response_chars=%s elapsed_ms=%s",
        model_name,
        len(prompt),
        input_token_count,
        generated_token_count,
        len(response_text),
        round((perf_counter() - start_time) * 1000),
    )
    log_parser_response_text(
        backend="transformers",
        model_name=model_name,
        response_text=response_text,
    )
    return response_text


def parse_check_in_with_model(
    raw_text: str,
    model_name: str | None = None,
) -> ParsedCheckIn:
    model_name = model_name or os.getenv("PARSER_MODEL_ID", DEFAULT_TRANSFORMERS_MODEL)
    logger.info("event=parser_model_parse_start model=%s", model_name)
    response_text = generate_parser_response(raw_text, model_name=model_name)
    parsed = parse_model_response(response_text)
    logger.info(
        "event=parser_model_parse_complete model=%s missing_fields=%s follow_up_questions=%s",
        model_name,
        len(parsed.missing_fields),
        len(parsed.follow_up_questions),
    )
    return parsed