Coverage for apps / ai / models.py: 90%
39 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-14 19:13 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-14 19:13 +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, choices=SUGGESTION_TYPES, help_text="Type of discovery suggestion"
21 )
22 search_query = models.CharField(max_length=255, help_text="Search query to execute for this suggestion")
23 title = models.CharField(max_length=255, help_text="Display title for this suggestion")
24 description = models.TextField(help_text="Explanation of why this suggestion fits the user")
25 created_at = models.DateTimeField(auto_now_add=True)
27 class Meta:
28 ordering = ["-created_at"]
29 verbose_name = "AI Discovery Suggestion"
30 verbose_name_plural = "AI Discovery Suggestions"
31 indexes = [
32 models.Index(fields=["profile", "suggestion_type", "created_at"]),
33 ]
35 def __str__(self):
36 return f"{self.suggestion_type}: {self.title}"
39class AIPrompt(models.Model):
40 """Stores customizable AI prompts for various features."""
42 PROMPT_TYPES = [
43 ("recipe_remix", "Recipe Remix"),
44 ("serving_adjustment", "Serving Adjustment"),
45 ("tips_generation", "Tips Generation"),
46 ("nutrition_estimate", "Nutrition Estimate"),
47 ("discover_favorites", "Discover from Favorites"),
48 ("discover_seasonal", "Discover Seasonal/Holiday"),
49 ("discover_new", "Discover Try Something New"),
50 ("search_ranking", "Search Result Ranking"),
51 ("timer_naming", "Timer Naming"),
52 ("remix_suggestions", "Remix Suggestions"),
53 ("selector_repair", "CSS Selector Repair"),
54 ]
56 AVAILABLE_MODELS = [
57 # Anthropic Claude
58 ("anthropic/claude-3.5-haiku", "Claude 3.5 Haiku (Fast)"),
59 ("anthropic/claude-sonnet-4", "Claude Sonnet 4"),
60 ("anthropic/claude-opus-4", "Claude Opus 4"),
61 ("anthropic/claude-opus-4.5", "Claude Opus 4.5"),
62 # OpenAI GPT
63 ("openai/gpt-4o", "GPT-4o"),
64 ("openai/gpt-4o-mini", "GPT-4o Mini (Fast)"),
65 ("openai/gpt-5-mini", "GPT-5 Mini"),
66 ("openai/o3-mini", "o3 Mini (Reasoning)"),
67 # Google Gemini
68 ("google/gemini-2.5-pro-preview", "Gemini 2.5 Pro"),
69 ("google/gemini-2.5-flash-preview", "Gemini 2.5 Flash (Fast)"),
70 ]
72 prompt_type = models.CharField(
73 max_length=50, choices=PROMPT_TYPES, unique=True, help_text="Unique identifier for this prompt type"
74 )
75 name = models.CharField(max_length=100, help_text="Human-readable name for this prompt")
76 description = models.TextField(blank=True, help_text="Description of what this prompt does")
77 system_prompt = models.TextField(help_text="System message sent to the AI model")
78 user_prompt_template = models.TextField(
79 help_text="User message template with {placeholders} for variable substitution"
80 )
81 model = models.CharField(
82 max_length=100,
83 choices=AVAILABLE_MODELS,
84 default="anthropic/claude-3.5-haiku",
85 help_text="AI model to use for this prompt",
86 )
87 is_active = models.BooleanField(default=True, help_text="Whether this prompt is enabled")
88 created_at = models.DateTimeField(auto_now_add=True)
89 updated_at = models.DateTimeField(auto_now=True)
91 class Meta:
92 ordering = ["prompt_type"]
93 verbose_name = "AI Prompt"
94 verbose_name_plural = "AI Prompts"
96 def __str__(self):
97 return self.name
99 def format_user_prompt(self, **kwargs) -> str:
100 """Format the user prompt template with provided variables."""
101 return self.user_prompt_template.format(**kwargs)
103 @classmethod
104 def get_prompt(cls, prompt_type: str) -> "AIPrompt":
105 """Get an active prompt by type, raises DoesNotExist if not found."""
106 return cls.objects.get(prompt_type=prompt_type, is_active=True)