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

1"""Tips generation service using AI.""" 

2 

3import logging 

4 

5from apps.recipes.models import Recipe 

6 

7from ..models import AIPrompt 

8from .openrouter import OpenRouterService, AIUnavailableError, AIResponseError 

9from .validator import AIResponseValidator, ValidationError 

10 

11logger = logging.getLogger(__name__) 

12 

13 

14def generate_tips(recipe_id: int) -> dict: 

15 """Generate cooking tips for a recipe. 

16 

17 Tips are cached in the Recipe.ai_tips field for efficiency. 

18 

19 Args: 

20 recipe_id: The ID of the recipe to generate tips for. 

21 

22 Returns: 

23 Dict with tips array and cache status. 

24 

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) 

32 

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 } 

40 

41 # Get the tips_generation prompt 

42 prompt = AIPrompt.get_prompt('tips_generation') 

43 

44 # Format ingredients as a string 

45 ingredients_str = '\n'.join(f'- {ing}' for ing in recipe.ingredients) 

46 

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) 

55 

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 ) 

62 

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 ) 

71 

72 # Validate response - tips_generation returns an array directly 

73 validator = AIResponseValidator() 

74 tips = validator.validate('tips_generation', response) 

75 

76 # Cache the tips on the recipe 

77 recipe.ai_tips = tips 

78 recipe.save(update_fields=['ai_tips']) 

79 

80 logger.info(f'Generated and cached {len(tips)} tips for recipe {recipe_id}') 

81 

82 return { 

83 'tips': tips, 

84 'cached': False, 

85 } 

86 

87 

88def clear_tips(recipe_id: int) -> bool: 

89 """Clear cached tips for a recipe. 

90 

91 Args: 

92 recipe_id: The ID of the recipe to clear tips for. 

93 

94 Returns: 

95 True if tips were cleared, False if no tips existed. 

96 

97 Raises: 

98 Recipe.DoesNotExist: If recipe not found. 

99 """ 

100 recipe = Recipe.objects.get(id=recipe_id) 

101 

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 

107 

108 return False 

← Back to Dashboard