99 lines
4.3 KiB
Python
99 lines
4.3 KiB
Python
from django.db import models
|
|
from django.contrib.auth.models import User
|
|
|
|
class Brief(models.Model):
|
|
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name="briefs")
|
|
product = models.TextField()
|
|
audience = models.TextField()
|
|
usp = models.TextField(blank=True, null=True)
|
|
benefits = models.JSONField(default=list, blank=True)
|
|
constraints = models.TextField(blank=True, null=True)
|
|
tone = models.CharField(max_length=120, blank=True, null=True)
|
|
|
|
formats = models.JSONField(default=list) # client chooses formats
|
|
variants_per_format = models.PositiveIntegerField(default=3)
|
|
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class TextVariant(models.Model):
|
|
brief = models.ForeignKey(Brief, on_delete=models.CASCADE, related_name="variants")
|
|
format = models.CharField(max_length=50)
|
|
payload = models.JSONField(default=dict)
|
|
placement_tips = models.TextField(blank=True, default="")
|
|
expected_effect = models.TextField(blank=True, default="")
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Test(models.Model):
|
|
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name="tests")
|
|
brief = models.ForeignKey(Brief, on_delete=models.CASCADE, related_name="tests")
|
|
name = models.CharField(max_length=200, default="Тест")
|
|
channel = models.CharField(max_length=120, blank=True, default="")
|
|
duration_days = models.PositiveIntegerField(default=3)
|
|
sample_size = models.PositiveIntegerField(default=0)
|
|
objective = models.CharField(max_length=32, default="leads") # leads|conversions|clicks
|
|
status = models.CharField(max_length=20, default="draft")
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Segment(models.Model):
|
|
test = models.ForeignKey(Test, on_delete=models.CASCADE, related_name="segments")
|
|
name = models.CharField(max_length=200)
|
|
description = models.TextField(blank=True, default="")
|
|
|
|
class Assignment(models.Model):
|
|
test = models.ForeignKey(Test, on_delete=models.CASCADE, related_name="assignments")
|
|
segment = models.ForeignKey(Segment, on_delete=models.CASCADE, related_name="assignments")
|
|
variant = models.ForeignKey(TextVariant, on_delete=models.CASCADE, related_name="assignments")
|
|
|
|
class ResultEntry(models.Model):
|
|
test = models.ForeignKey(Test, on_delete=models.CASCADE, related_name="results")
|
|
segment = models.ForeignKey(Segment, on_delete=models.CASCADE, related_name="results")
|
|
variant = models.ForeignKey(TextVariant, on_delete=models.CASCADE, related_name="results")
|
|
date = models.DateField()
|
|
impressions = models.PositiveIntegerField(default=0)
|
|
clicks = models.PositiveIntegerField(default=0)
|
|
conversions = models.PositiveIntegerField(default=0)
|
|
leads = models.PositiveIntegerField(default=0)
|
|
spend = models.FloatField(default=0.0)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class MetricsSnapshot(models.Model):
|
|
test = models.OneToOneField(Test, on_delete=models.CASCADE, related_name="snapshot")
|
|
ranking = models.JSONField(default=list)
|
|
recommendations = models.JSONField(default=list)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
|
|
class OptimizationPolicy(models.Model):
|
|
"""User-defined optimization rules for Agent #2.
|
|
|
|
Supports two modes:
|
|
- thresholds: structured KPI + thresholds
|
|
- text: free-form query that describes ranking/thresholds
|
|
"""
|
|
MODE_CHOICES = [
|
|
("thresholds", "thresholds"),
|
|
("text", "text"),
|
|
]
|
|
KPI_CHOICES = [
|
|
("cpa", "cpa"),
|
|
("cpl", "cpl"),
|
|
("cpc", "cpc"),
|
|
("ctr", "ctr"),
|
|
("cr", "cr"),
|
|
]
|
|
test = models.OneToOneField("Test", on_delete=models.CASCADE, related_name="policy")
|
|
mode = models.CharField(max_length=16, choices=MODE_CHOICES, default="thresholds")
|
|
|
|
# Structured mode
|
|
primary_kpi = models.CharField(max_length=8, choices=KPI_CHOICES, default="cpl")
|
|
direction = models.CharField(max_length=8, default="min") # min or max
|
|
good_threshold = models.FloatField(null=True, blank=True)
|
|
ok_threshold = models.FloatField(null=True, blank=True)
|
|
min_impressions = models.IntegerField(default=0)
|
|
min_clicks = models.IntegerField(default=0)
|
|
|
|
# Text mode
|
|
query_text = models.TextField(blank=True, null=True)
|
|
|
|
updated_at = models.DateTimeField(auto_now=True)
|