Coverage for apps / ai / services / timer.py: 25%
24 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 00:40 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 00:40 +0000
1"""Timer naming service using AI."""
3import logging
5from ..models import AIPrompt
6from .openrouter import OpenRouterService
7from .validator import AIResponseValidator
9logger = logging.getLogger(__name__)
12def generate_timer_name(step_text: str, duration_minutes: int) -> dict:
13 """Generate a descriptive name for a cooking timer.
15 Args:
16 step_text: The cooking instruction text.
17 duration_minutes: The timer duration in minutes.
19 Returns:
20 Dict with the generated label.
22 Raises:
23 AIUnavailableError: If AI service is not available.
24 AIResponseError: If AI returns invalid response.
25 ValidationError: If response doesn't match expected schema.
26 """
27 # Get the timer_naming prompt
28 prompt = AIPrompt.get_prompt('timer_naming')
30 # Format duration nicely
31 if duration_minutes >= 60:
32 hours = duration_minutes // 60
33 mins = duration_minutes % 60
34 if mins > 0:
35 duration_str = f'{hours} hour{"s" if hours > 1 else ""} {mins} minute{"s" if mins > 1 else ""}'
36 else:
37 duration_str = f'{hours} hour{"s" if hours > 1 else ""}'
38 else:
39 duration_str = f'{duration_minutes} minute{"s" if duration_minutes > 1 else ""}'
41 # Format the user prompt
42 user_prompt = prompt.format_user_prompt(
43 instruction=step_text,
44 duration=duration_str,
45 )
47 # Call AI service
48 service = OpenRouterService()
49 response = service.complete(
50 system_prompt=prompt.system_prompt,
51 user_prompt=user_prompt,
52 model=prompt.model,
53 json_response=True,
54 )
56 # Validate response
57 validator = AIResponseValidator()
58 result = validator.validate('timer_naming', response)
60 # Truncate label if too long (max 30 chars as per spec)
61 label = result['label']
62 if len(label) > 30:
63 label = label[:27] + '...'
65 logger.info(f'Generated timer name: "{label}" for {duration_minutes}min timer')
67 return {
68 'label': label,
69 }