Coverage for apps / ai / models.py: 90%
39 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
1from django.db import models
4class AIDiscoverySuggestion(models.Model):
5 """Caches AI-generated discovery suggestions per profile."""
7 SUGGESTION_TYPES = [
8 ('favorites', 'Based on Favorites'),
9 ('seasonal', 'Seasonal/Holiday'),
10 ('new', 'Try Something New'),
11 ]
13 profile = models.ForeignKey(
14 'profiles.Profile',
15 on_delete=models.CASCADE,
16 related_name='ai_discovery_suggestions',
17 help_text='Profile this suggestion belongs to'
18 )
19 suggestion_type = models.CharField(
20 max_length=50,
21 choices=SUGGESTION_TYPES,
22 help_text='Type of discovery suggestion'
23 )
24 search_query = models.CharField(
25 max_length=255,
26 help_text='Search query to execute for this suggestion'
27 )
28 title = models.CharField(
29 max_length=255,
30 help_text='Display title for this suggestion'
31 )
32 description = models.TextField(
33 help_text='Explanation of why this suggestion fits the user'
34 )
35 created_at = models.DateTimeField(auto_now_add=True)
37 class Meta:
38 ordering = ['-created_at']
39 verbose_name = 'AI Discovery Suggestion'
40 verbose_name_plural = 'AI Discovery Suggestions'
41 indexes = [
42 models.Index(fields=['profile', 'suggestion_type', 'created_at']),
43 ]
45 def __str__(self):
46 return f'{self.suggestion_type}: {self.title}'
49class AIPrompt(models.Model):
50 """Stores customizable AI prompts for various features."""
52 PROMPT_TYPES = [
53 ('recipe_remix', 'Recipe Remix'),
54 ('serving_adjustment', 'Serving Adjustment'),
55 ('tips_generation', 'Tips Generation'),
56 ('nutrition_estimate', 'Nutrition Estimate'),
57 ('discover_favorites', 'Discover from Favorites'),
58 ('discover_seasonal', 'Discover Seasonal/Holiday'),
59 ('discover_new', 'Discover Try Something New'),
60 ('search_ranking', 'Search Result Ranking'),
61 ('timer_naming', 'Timer Naming'),
62 ('remix_suggestions', 'Remix Suggestions'),
63 ('selector_repair', 'CSS Selector Repair'),
64 ]
66 AVAILABLE_MODELS = [
67 # Anthropic Claude
68 ('anthropic/claude-3.5-haiku', 'Claude 3.5 Haiku (Fast)'),
69 ('anthropic/claude-sonnet-4', 'Claude Sonnet 4'),
70 ('anthropic/claude-opus-4', 'Claude Opus 4'),
71 ('anthropic/claude-opus-4.5', 'Claude Opus 4.5'),
72 # OpenAI GPT
73 ('openai/gpt-4o', 'GPT-4o'),
74 ('openai/gpt-4o-mini', 'GPT-4o Mini (Fast)'),
75 ('openai/gpt-5-mini', 'GPT-5 Mini'),
76 ('openai/o3-mini', 'o3 Mini (Reasoning)'),
77 # Google Gemini
78 ('google/gemini-2.5-pro-preview', 'Gemini 2.5 Pro'),
79 ('google/gemini-2.5-flash-preview', 'Gemini 2.5 Flash (Fast)'),
80 ]
82 prompt_type = models.CharField(
83 max_length=50,
84 choices=PROMPT_TYPES,
85 unique=True,
86 help_text='Unique identifier for this prompt type'
87 )
88 name = models.CharField(
89 max_length=100,
90 help_text='Human-readable name for this prompt'
91 )
92 description = models.TextField(
93 blank=True,
94 help_text='Description of what this prompt does'
95 )
96 system_prompt = models.TextField(
97 help_text='System message sent to the AI model'
98 )
99 user_prompt_template = models.TextField(
100 help_text='User message template with {placeholders} for variable substitution'
101 )
102 model = models.CharField(
103 max_length=100,
104 choices=AVAILABLE_MODELS,
105 default='anthropic/claude-3.5-haiku',
106 help_text='AI model to use for this prompt'
107 )
108 is_active = models.BooleanField(
109 default=True,
110 help_text='Whether this prompt is enabled'
111 )
112 created_at = models.DateTimeField(auto_now_add=True)
113 updated_at = models.DateTimeField(auto_now=True)
115 class Meta:
116 ordering = ['prompt_type']
117 verbose_name = 'AI Prompt'
118 verbose_name_plural = 'AI Prompts'
120 def __str__(self):
121 return self.name
123 def format_user_prompt(self, **kwargs) -> str:
124 """Format the user prompt template with provided variables."""
125 return self.user_prompt_template.format(**kwargs)
127 @classmethod
128 def get_prompt(cls, prompt_type: str) -> 'AIPrompt':
129 """Get an active prompt by type, raises DoesNotExist if not found."""
130 return cls.objects.get(prompt_type=prompt_type, is_active=True)