Coverage for apps / ai / services / tips.py: 24%
33 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"""Tips generation service using AI."""
3import logging
5from apps.recipes.models import Recipe
7from ..models import AIPrompt
8from .openrouter import OpenRouterService, AIUnavailableError, AIResponseError
9from .validator import AIResponseValidator, ValidationError
11logger = logging.getLogger(__name__)
14def generate_tips(recipe_id: int) -> dict:
15 """Generate cooking tips for a recipe.
17 Tips are cached in the Recipe.ai_tips field for efficiency.
19 Args:
20 recipe_id: The ID of the recipe to generate tips for.
22 Returns:
23 Dict with tips array and cache status.
25 Raises:
26 Recipe.DoesNotExist: If recipe not found.
27 AIUnavailableError: If AI service is not available.
28 AIResponseError: If AI returns invalid response.
29 ValidationError: If response doesn't match expected schema.
30 """
31 recipe = Recipe.objects.get(id=recipe_id)
33 # Check for cached tips
34 if recipe.ai_tips:
35 logger.info(f'Returning cached tips for recipe {recipe_id}')
36 return {
37 'tips': recipe.ai_tips,
38 'cached': True,
39 }
41 # Get the tips_generation prompt
42 prompt = AIPrompt.get_prompt('tips_generation')
44 # Format ingredients as a string
45 ingredients_str = '\n'.join(f'- {ing}' for ing in recipe.ingredients)
47 # Format instructions
48 if isinstance(recipe.instructions, list):
49 instructions_str = '\n'.join(
50 f'{i+1}. {step.get("text", step) if isinstance(step, dict) else step}'
51 for i, step in enumerate(recipe.instructions)
52 )
53 else:
54 instructions_str = recipe.instructions_text or str(recipe.instructions)
56 # Format the user prompt
57 user_prompt = prompt.format_user_prompt(
58 title=recipe.title,
59 ingredients=ingredients_str,
60 instructions=instructions_str,
61 )
63 # Call AI service
64 service = OpenRouterService()
65 response = service.complete(
66 system_prompt=prompt.system_prompt,
67 user_prompt=user_prompt,
68 model=prompt.model,
69 json_response=True,
70 )
72 # Validate response - tips_generation returns an array directly
73 validator = AIResponseValidator()
74 tips = validator.validate('tips_generation', response)
76 # Cache the tips on the recipe
77 recipe.ai_tips = tips
78 recipe.save(update_fields=['ai_tips'])
80 logger.info(f'Generated and cached {len(tips)} tips for recipe {recipe_id}')
82 return {
83 'tips': tips,
84 'cached': False,
85 }
88def clear_tips(recipe_id: int) -> bool:
89 """Clear cached tips for a recipe.
91 Args:
92 recipe_id: The ID of the recipe to clear tips for.
94 Returns:
95 True if tips were cleared, False if no tips existed.
97 Raises:
98 Recipe.DoesNotExist: If recipe not found.
99 """
100 recipe = Recipe.objects.get(id=recipe_id)
102 if recipe.ai_tips:
103 recipe.ai_tips = []
104 recipe.save(update_fields=['ai_tips'])
105 logger.info(f'Cleared tips for recipe {recipe_id}')
106 return True
108 return False