Source code for cw.core.models

"""
Core models for LLM model management.

This module provides database models for tracking:
- LLM models (HuggingFace model IDs) used for text generation
- Prompt templates (Jinja2 templates for LLM prompts)

Note: Geographic models (Region, Country, Language) have been relocated to cw.audiences.
"""

from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.utils import timezone


[docs] class LLMModel(models.Model): """HuggingFace language model configuration. Stores model identifiers and metadata for LLMs used in adaptation tasks. Each model can be marked as primary or alternative for specific languages. Example: >>> model = LLMModel.objects.create( ... model_id="Qwen/Qwen2.5-7B-Instruct", ... name="Qwen 2.5 7B Instruct", ... notes="Good multilingual support" ... ) """ model_id = models.CharField( max_length=200, unique=True, help_text="HuggingFace model ID (e.g., 'Qwen/Qwen2.5-7B-Instruct').", ) name = models.CharField( max_length=100, help_text="Friendly display name (e.g., 'Qwen 2.5 7B').", ) notes = models.TextField( blank=True, help_text="Notes about model capabilities, strengths, or limitations.", ) is_active = models.BooleanField( default=True, help_text="Whether this model is available for use.", ) load_in_4bit = models.BooleanField( default=False, help_text="Load model with 4-bit quantization (requires bitsandbytes).", ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = "core_llmmodel" ordering = ["name"] verbose_name = "LLM Model" verbose_name_plural = "LLM Models" def __str__(self): return self.name
[docs] class PromptTemplate(models.Model): """Database-backed LLM prompt template with versioning and validation.""" # Template categories CATEGORY_CHOICES = [ ("enhancement", "Prompt Enhancement"), ("adaptation", "Cultural Adaptation"), ("evaluation", "Quality Evaluation"), ("concept", "Concept Analysis"), ] # Identity slug = models.SlugField( max_length=100, db_index=True, help_text="Template identifier (e.g., 'adaptation', 'concept-extraction')", ) name = models.CharField( max_length=200, help_text="Human-readable name (e.g., 'Cultural Adaptation')" ) # Categorization category = models.CharField( max_length=50, choices=CATEGORY_CHOICES, help_text="Template category" ) # Template Content template = models.TextField(help_text="Jinja2 template content") # Schema/Variables (optional metadata for validation) expected_variables = models.JSONField( default=dict, blank=True, help_text="Expected template variables: {name: {type, required, description}}", ) # Versioning version = models.PositiveIntegerField( default=1, help_text="Template version number (auto-incremented on save)" ) is_active = models.BooleanField( default=True, help_text="Whether this version is the active version" ) # Documentation description = models.TextField(blank=True, help_text="Purpose and usage notes") # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="created_prompt_templates", help_text="User who created this template version", ) # Analytics usage_count = models.PositiveIntegerField( default=0, help_text="Number of times this template has been rendered" ) last_used_at = models.DateTimeField( null=True, blank=True, help_text="Last time this template was rendered" ) class Meta: db_table = "core_prompttemplate" constraints = [ # Each slug+version combination must be unique models.UniqueConstraint( fields=["slug", "version"], name="unique_slug_version" ), # Only one active version per slug models.UniqueConstraint( fields=["slug"], condition=models.Q(is_active=True), name="unique_active_slug" ), ] ordering = ["-version", "slug"] indexes = [ models.Index(fields=["slug", "is_active"]), models.Index(fields=["category"]), ] verbose_name = "Prompt Template" verbose_name_plural = "Prompt Templates" def __str__(self): active_indicator = " [ACTIVE]" if self.is_active else "" return f"{self.name} (v{self.version}){active_indicator}"
[docs] def clean(self): """Validate Jinja2 syntax on save.""" from jinja2 import Environment, TemplateSyntaxError env = Environment(trim_blocks=True, lstrip_blocks=True) try: env.from_string(self.template) except TemplateSyntaxError as e: raise ValidationError( {"template": f"Invalid Jinja2 syntax: {e.message} at line {e.lineno}"} )
[docs] def save(self, *args, **kwargs): """ Save the template, auto-incrementing version if template content changed. If this is an update to an existing template and the template content has changed, we: 1. Deactivate the old version 2. Create a new version (incrementing version number) 3. Mark it as active """ # Check if this is an update to existing template with changed content if self.pk: try: old_version = self.__class__.objects.get(pk=self.pk) if old_version.template != self.template: # Template changed - create new version # Deactivate all other versions of this slug self.__class__.objects.filter(slug=self.slug, is_active=True).update( is_active=False ) # Get highest version number for this slug max_version = ( self.__class__.objects.filter(slug=self.slug).aggregate( models.Max("version") )["version__max"] or 0 ) # Create new version self.pk = None # Force creation of new record self.version = max_version + 1 self.is_active = True except self.__class__.DoesNotExist: pass # Run Jinja2 validation self.full_clean() super().save(*args, **kwargs)
[docs] def render(self, **context): """ Render this template with the provided context. Args: **context: Variables to pass to the Jinja2 template Returns: Rendered template string Updates usage analytics (usage_count, last_used_at). """ from jinja2 import Environment env = Environment(trim_blocks=True, lstrip_blocks=True) template = env.from_string(self.template) # Update usage analytics (using F() to avoid race conditions) self.__class__.objects.filter(pk=self.pk).update( usage_count=models.F("usage_count") + 1, last_used_at=timezone.now() ) return template.render(**context)
[docs] class PipelineSettings(models.Model): """Singleton app-level defaults for per-node LLM model selection. Each pipeline node (concept analyst, cultural researcher, evaluation gates) can have its own default LLM model. If not set, falls back to global_default_model. Writer node is excluded — it defaults to the Language's primary LLM. """ global_default_model = models.ForeignKey( "LLMModel", on_delete=models.SET_NULL, null=True, blank=True, related_name="+", help_text="Fallback model when no node-specific default is set", ) concept_default_model = models.ForeignKey( "LLMModel", on_delete=models.SET_NULL, null=True, blank=True, related_name="+", help_text="Default model for Concept Analyst node", ) culture_default_model = models.ForeignKey( "LLMModel", on_delete=models.SET_NULL, null=True, blank=True, related_name="+", help_text="Default model for Cultural Researcher node", ) format_gate_default_model = models.ForeignKey( "LLMModel", on_delete=models.SET_NULL, null=True, blank=True, related_name="+", help_text="Default model for Format Gate node", ) culture_gate_default_model = models.ForeignKey( "LLMModel", on_delete=models.SET_NULL, null=True, blank=True, related_name="+", help_text="Default model for Culture Gate node", ) concept_gate_default_model = models.ForeignKey( "LLMModel", on_delete=models.SET_NULL, null=True, blank=True, related_name="+", help_text="Default model for Concept Gate node", ) brand_gate_default_model = models.ForeignKey( "LLMModel", on_delete=models.SET_NULL, null=True, blank=True, related_name="+", help_text="Default model for Brand Gate node", ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = "core_pipelinesettings" verbose_name = "Pipeline Settings" verbose_name_plural = "Pipeline Settings" def __str__(self): return "Pipeline Settings"
[docs] @classmethod def get_instance(cls): """Get or create the singleton PipelineSettings instance.""" instance, _ = cls.objects.get_or_create(pk=1) return instance