| import json |
| import logging |
| from time import perf_counter |
| from urllib import request |
|
|
| from training_coach.parser import ( |
| PARSER_SYSTEM_PROMPT, |
| expected_response_format, |
| log_parser_messages, |
| log_parser_response_text, |
| parse_model_response, |
| ) |
| from training_coach.models import ParsedCheckIn |
|
|
|
|
| OLLAMA_URL = "http://127.0.0.1:11434/api/generate" |
| OLLAMA_MODEL = "qwen3:1.7B" |
| OLLAMA_TINY_CANDIDATE = "qwen3:4b" |
| logger = logging.getLogger(__name__) |
|
|
|
|
| def build_ollama_prompt(raw_text: str) -> str: |
| schema = json.dumps(expected_response_format(), indent=2) |
| return ( |
| "Parse this check-in into the expected response JSON schema.\n" |
| "Return only the JSON object.\n\n" |
| "Allowed top-level keys are: check_in, missing_fields, follow_up_items, " |
| "follow_up_questions, context_signals, notes.\n\n" |
| "The check_in object must use only these keys: raw_text, " |
| "time_available_minutes, energy_level, sleep_quality, sleep_hours, " |
| "soreness, pain_or_injury, pain_issues, mood_stress, notes.\n\n" |
| f"Expected response JSON schema:\n{schema}\n\n" |
| f"Check-in:\n{raw_text}" |
| ) |
|
|
|
|
| def generate_parser_response_ollama( |
| raw_text: str, |
| model_name: str = OLLAMA_MODEL, |
| url: str = OLLAMA_URL, |
| ) -> str: |
| start_time = perf_counter() |
| prompt = build_ollama_prompt(raw_text) |
| logger.info( |
| "event=parser_ollama_request model=%s prompt_chars=%s text_chars=%s", |
| model_name, |
| len(prompt), |
| len(raw_text), |
| ) |
| log_parser_messages( |
| backend="ollama", |
| model_name=model_name, |
| messages=[ |
| {"role": "system", "content": PARSER_SYSTEM_PROMPT}, |
| {"role": "user", "content": prompt}, |
| ], |
| ) |
| payload = { |
| "model": model_name, |
| "system": PARSER_SYSTEM_PROMPT, |
| "prompt": prompt, |
| "format": expected_response_format(), |
| "stream": False, |
| "think": False, |
| "options": { |
| "temperature": 0, |
| "num_predict": 512, |
| }, |
| } |
| body = json.dumps(payload).encode("utf-8") |
| http_request = request.Request( |
| url, |
| data=body, |
| headers={"Content-Type": "application/json"}, |
| method="POST", |
| ) |
|
|
| with request.urlopen(http_request, timeout=120) as response: |
| data = json.loads(response.read().decode("utf-8")) |
|
|
| response_text = data["response"].strip() |
| logger.info( |
| "event=parser_ollama_response model=%s response_chars=%s " |
| "prompt_eval_count=%s eval_count=%s total_duration_ms=%s " |
| "load_duration_ms=%s prompt_eval_duration_ms=%s eval_duration_ms=%s " |
| "wall_elapsed_ms=%s", |
| model_name, |
| len(response_text), |
| data.get("prompt_eval_count"), |
| data.get("eval_count"), |
| _ns_to_ms(data.get("total_duration")), |
| _ns_to_ms(data.get("load_duration")), |
| _ns_to_ms(data.get("prompt_eval_duration")), |
| _ns_to_ms(data.get("eval_duration")), |
| round((perf_counter() - start_time) * 1000), |
| ) |
| log_parser_response_text( |
| backend="ollama", |
| model_name=model_name, |
| response_text=response_text, |
| ) |
| return response_text |
|
|
|
|
| def _ns_to_ms(value): |
| if value is None: |
| return None |
| return round(value / 1_000_000) |
|
|
|
|
| def parse_check_in_with_ollama( |
| raw_text: str, |
| model_name: str = OLLAMA_MODEL, |
| ) -> ParsedCheckIn: |
| response_text = generate_parser_response_ollama(raw_text, model_name=model_name) |
| return parse_model_response(response_text) |
|
|