django-agentic 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,44 @@
1
+ """django-agentic: AI framework for Django — LangGraph + LangChain.
2
+
3
+ Add 'django_agentic' to INSTALLED_APPS, run migrations, configure models in admin.
4
+
5
+ Checkpointer Configuration
6
+ --------------------------
7
+ Chat history is stored via LangGraph checkpointers. Configure in settings:
8
+
9
+ DJANGO_AGENTIC = {
10
+ "CHECKPOINTER": <checkpointer_instance>,
11
+ }
12
+
13
+ Default: InMemorySaver (works out of the box, loses history on server restart).
14
+
15
+ For production with PostgreSQL:
16
+
17
+ # pip install langgraph-checkpoint-postgres
18
+ from langgraph.checkpoint.postgres import PostgresSaver
19
+ DJANGO_AGENTIC = {
20
+ "CHECKPOINTER": PostgresSaver.from_conn_string(
21
+ "postgresql://user:pass@host:5432/dbname"
22
+ ),
23
+ }
24
+ # Call DJANGO_AGENTIC["CHECKPOINTER"].setup() once (e.g. in a management command
25
+ # or AppConfig.ready()) to create the checkpoint tables.
26
+
27
+ For production with Redis:
28
+
29
+ # pip install langgraph-checkpoint-redis
30
+ from langgraph.checkpoint.redis import RedisSaver
31
+ DJANGO_AGENTIC = {
32
+ "CHECKPOINTER": RedisSaver(redis_url="redis://localhost:6379"),
33
+ }
34
+
35
+ For SQLite (local dev with persistence across restarts):
36
+
37
+ # pip install langgraph-checkpoint-sqlite
38
+ import sqlite3
39
+ from langgraph.checkpoint.sqlite import SqliteSaver
40
+ DJANGO_AGENTIC = {
41
+ "CHECKPOINTER": SqliteSaver(sqlite3.connect("checkpoints.db", check_same_thread=False)),
42
+ }
43
+ """
44
+ __version__ = "0.3.0"
@@ -0,0 +1,163 @@
1
+ import json
2
+ import datetime
3
+ from django.contrib import admin
4
+ from django.db.models import Sum, Count
5
+ from django.utils import timezone
6
+ from django.utils.html import escape
7
+ from django.utils.safestring import mark_safe
8
+ from .models import AIModel, AIUsageLog, SiteAIConfig, UserAIProfile
9
+
10
+
11
+ def _daily_usage_chart(qs, title="Usage (Last 30 Days)", days=30):
12
+ """Generate Chart.js HTML for daily usage trends."""
13
+ from django.db.models.functions import TruncDate
14
+ cutoff = timezone.now() - datetime.timedelta(days=days)
15
+ daily = (
16
+ qs.filter(created_at__gte=cutoff)
17
+ .annotate(date=TruncDate("created_at"))
18
+ .values("date")
19
+ .annotate(
20
+ requests=Count("id"), cost=Sum("cost_usd"),
21
+ input_tokens=Sum("prompt_tokens"), output_tokens=Sum("completion_tokens"),
22
+ cache_read=Sum("cache_read_tokens"), cache_write=Sum("cache_creation_tokens"),
23
+ )
24
+ .order_by("date")
25
+ )
26
+ daily_map = {r["date"]: r for r in daily}
27
+ today = timezone.now().date()
28
+
29
+ dates, costs, inputs, outputs, cache_r, cache_w, reqs = [], [], [], [], [], [], []
30
+ for i in range(days - 1, -1, -1):
31
+ d = today - datetime.timedelta(days=i)
32
+ r = daily_map.get(d)
33
+ dates.append(d.strftime("%b %d"))
34
+ costs.append(float(r["cost"] or 0) if r else 0)
35
+ inputs.append(r["input_tokens"] or 0 if r else 0)
36
+ outputs.append(r["output_tokens"] or 0 if r else 0)
37
+ cache_r.append(r["cache_read"] or 0 if r else 0)
38
+ cache_w.append(r["cache_write"] or 0 if r else 0)
39
+ reqs.append(r["requests"] or 0 if r else 0)
40
+
41
+ total_cost = f"${sum(costs):.4f}"
42
+ total_reqs = sum(reqs)
43
+ total_tokens = f"{sum(inputs)+sum(outputs)+sum(cache_r)+sum(cache_w):,}"
44
+ cid = f"aichart_{id(qs) % 99999}"
45
+ data = json.dumps({"d": dates, "c": costs, "i": inputs,
46
+ "o": outputs, "cr": cache_r, "cw": cache_w})
47
+
48
+ safe_title = escape(title)
49
+ js = (
50
+ '(function(){'
51
+ 'var d=' + data + ';'
52
+ 'var ctx=document.getElementById("' + cid + '");'
53
+ 'if(ctx&&d.d.length>0){new Chart(ctx,{type:"line",data:{labels:d.d,datasets:['
54
+ '{label:"Cost ($)",data:d.c,borderColor:"rgb(75,192,192)",backgroundColor:"rgba(75,192,192,0.1)",yAxisID:"y",tension:0.3,fill:true},'
55
+ '{label:"Input",data:d.i,borderColor:"rgb(54,162,235)",backgroundColor:"rgba(54,162,235,0.1)",yAxisID:"y1",tension:0.3,fill:true},'
56
+ '{label:"Output",data:d.o,borderColor:"rgb(255,159,64)",backgroundColor:"rgba(255,159,64,0.1)",yAxisID:"y1",tension:0.3,fill:true},'
57
+ '{label:"Cache Read",data:d.cr,borderColor:"rgb(153,102,255)",backgroundColor:"rgba(153,102,255,0.1)",yAxisID:"y1",tension:0.3,fill:true},'
58
+ '{label:"Cache Write",data:d.cw,borderColor:"rgb(255,99,132)",backgroundColor:"rgba(255,99,132,0.1)",yAxisID:"y1",tension:0.3,fill:true}'
59
+ ']},options:{responsive:true,interaction:{mode:"index",intersect:false},'
60
+ 'scales:{y:{type:"linear",position:"left",title:{display:true,text:"Cost ($)"},ticks:{callback:function(v){return "$"+v.toFixed(3)}}},'
61
+ 'y1:{type:"linear",position:"right",grid:{drawOnChartArea:false},title:{display:true,text:"Tokens"},'
62
+ 'ticks:{callback:function(v){return v>=1e6?(v/1e6).toFixed(1)+"M":v>=1e3?(v/1e3).toFixed(0)+"K":v}}}'
63
+ '}}})}})();'
64
+ )
65
+ return mark_safe(
66
+ '<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>'
67
+ '<div style="background:#fff;padding:20px;border:1px solid #ddd;border-radius:4px;margin:10px 0">'
68
+ '<h3 style="margin-top:0">' + str(safe_title) + '</h3>'
69
+ '<div style="display:flex;gap:40px;margin-bottom:15px">'
70
+ '<div><strong>Requests:</strong> ' + str(total_reqs) + '</div>'
71
+ '<div><strong>Cost:</strong> ' + str(total_cost) + '</div>'
72
+ '<div><strong>Tokens:</strong> ' + str(total_tokens) + '</div></div>'
73
+ '<canvas id="' + cid + '" style="max-height:300px"></canvas></div>'
74
+ '<script>' + js + '</script>'
75
+ )
76
+
77
+
78
+ @admin.register(AIModel)
79
+ class AIModelAdmin(admin.ModelAdmin):
80
+ list_display = ("display_name", "name", "model_type", "provider", "input_cost_per_1m", "output_cost_per_1m",
81
+ "allowed_for_free", "allowed_for_paid", "active", "total_cost_30d")
82
+ list_filter = ("model_type", "provider", "active", "allowed_for_free", "allowed_for_paid")
83
+ list_editable = ("allowed_for_free", "allowed_for_paid", "active")
84
+ readonly_fields = ("usage_charts",)
85
+
86
+ def total_cost_30d(self, obj):
87
+ cutoff = timezone.now() - datetime.timedelta(days=30)
88
+ total = obj.usage_logs.filter(created_at__gte=cutoff).aggregate(total=Sum("cost_usd"))["total"]
89
+ return f"${total:.4f}" if total else "$0"
90
+ total_cost_30d.short_description = "Cost (30d)"
91
+
92
+ def usage_charts(self, obj):
93
+ if not obj.pk:
94
+ return "Save the model first to see usage charts."
95
+ return _daily_usage_chart(obj.usage_logs.all(), f"{obj.display_name or obj.name} — Usage")
96
+ usage_charts.short_description = "Usage Statistics"
97
+
98
+ def get_fieldsets(self, request, obj=None):
99
+ base = [(None, {"fields": ("name", "display_name", "model_type", "provider",
100
+ "input_cost_per_1m", "output_cost_per_1m",
101
+ "cache_write_cost_per_1m", "cache_read_cost_per_1m",
102
+ "active", "allowed_for_free", "allowed_for_paid",
103
+ "context_window", "max_output_tokens")})]
104
+ if obj and obj.pk:
105
+ base.append(("Usage Statistics", {"fields": ("usage_charts",)}))
106
+ return base
107
+
108
+
109
+ @admin.register(SiteAIConfig)
110
+ class SiteAIConfigAdmin(admin.ModelAdmin):
111
+ list_display = ("__str__", "default_free_model", "default_paid_model", "monthly_free_credits")
112
+ def formfield_for_foreignkey(self, db_field, request, **kwargs):
113
+ if db_field.name in ("default_free_model", "default_paid_model"):
114
+ kwargs["queryset"] = AIModel.objects.filter(active=True, model_type="chat")
115
+ if db_field.name == "default_free_model":
116
+ kwargs["queryset"] = kwargs["queryset"].filter(allowed_for_free=True)
117
+ return super().formfield_for_foreignkey(db_field, request, **kwargs)
118
+ def has_add_permission(self, request):
119
+ return not SiteAIConfig.objects.exists()
120
+ def has_delete_permission(self, request, obj=None):
121
+ return False
122
+
123
+
124
+ @admin.register(UserAIProfile)
125
+ class UserAIProfileAdmin(admin.ModelAdmin):
126
+ list_display = ("user", "free_monthly_credits", "purchased_credits", "total_credits",
127
+ "model_override", "credits_reset_at")
128
+ list_filter = ("model_override",)
129
+ search_fields = ("user__email", "user__username")
130
+ readonly_fields = ("created_at", "updated_at", "usage_charts")
131
+
132
+ def total_credits(self, obj):
133
+ return f"${obj.total_credits:.6f}"
134
+ total_credits.short_description = "Total"
135
+
136
+ def usage_charts(self, obj):
137
+ if not obj.pk:
138
+ return "Save the profile first to see usage charts."
139
+ return _daily_usage_chart(AIUsageLog.objects.filter(user=obj.user), f"{obj.user} — Usage")
140
+ usage_charts.short_description = "Usage Statistics"
141
+
142
+ def get_fieldsets(self, request, obj=None):
143
+ base = [(None, {"fields": ("user", "free_monthly_credits", "purchased_credits",
144
+ "model_override", "credits_reset_at", "created_at", "updated_at")})]
145
+ if obj and obj.pk:
146
+ base.append(("Usage Statistics", {"fields": ("usage_charts",)}))
147
+ return base
148
+
149
+
150
+ @admin.register(AIUsageLog)
151
+ class AIUsageLogAdmin(admin.ModelAdmin):
152
+ list_display = ("created_at", "workflow", "node", "model_name", "user",
153
+ "prompt_tokens", "completion_tokens",
154
+ "cache_read_tokens", "cache_creation_tokens",
155
+ "cost_usd", "used_free_credits", "used_paid_credits",
156
+ "error_type", "duration_ms", "success")
157
+ list_filter = ("workflow", "model_name", "success", "error_type", "created_at")
158
+ readonly_fields = ("id", "idempotency_key", "created_at", "request_time", "response_time",
159
+ "provider_request_id",
160
+ "input_cost_per_1m_at_time", "output_cost_per_1m_at_time",
161
+ "cache_write_cost_per_1m_at_time", "cache_read_cost_per_1m_at_time")
162
+ search_fields = ("workflow", "node", "input_summary", "user__email", "provider_request_id")
163
+ date_hierarchy = "created_at"
@@ -0,0 +1,101 @@
1
+ """ModelAgent — abstract base for entity-aware AI agents.
2
+
3
+ Mirrors silverstripe-ai's DataObjectAgent. Consuming app subclasses this
4
+ and implements get_static_instructions(), get_dynamic_context(), get_tools().
5
+ django_ai handles provider, prompt caching, chat history, logging, credits, HITL.
6
+
7
+ Usage:
8
+ from django_agentic.agent import ModelAgent
9
+
10
+ class MyModelAgent(ModelAgent):
11
+ def get_static_instructions(self) -> str: ...
12
+ def get_dynamic_context(self) -> str: ...
13
+ def get_tools(self) -> list: ... # optional
14
+
15
+ Register in settings:
16
+ DJANGO_AGENTIC = {
17
+ "AGENT_MAPPINGS": {
18
+ "myapp.MyModel": "myapp.agents.MyModelAgent",
19
+ }
20
+ }
21
+ """
22
+
23
+ import abc
24
+
25
+ from django.db.models import Model
26
+
27
+
28
+ class ModelAgent(abc.ABC):
29
+
30
+ def __init__(self, user, model_config: "AIModel", entity: Model | None, thread_id: str):
31
+ self.user = user
32
+ self.model_config = model_config
33
+ self.entity = entity
34
+ self.thread_id = thread_id
35
+
36
+ @property
37
+ def is_collection_mode(self) -> bool:
38
+ return self.entity is None or not getattr(self.entity, "pk", None)
39
+
40
+ @abc.abstractmethod
41
+ def get_static_instructions(self) -> str:
42
+ """Cacheable system prompt. Cached by Anthropic for 5 min."""
43
+
44
+ @abc.abstractmethod
45
+ def get_dynamic_context(self) -> str:
46
+ """Ephemeral context — entity state, date. Rebuilt per request."""
47
+
48
+ def get_tools(self) -> list:
49
+ """LangChain tools the agent can call. Default: no tools (chat-only).
50
+
51
+ Return a list of LangChain tool instances (functions decorated with
52
+ @tool, or BaseTool subclasses). Tools are passed to create_agent
53
+ which binds them to the LLM automatically.
54
+ """
55
+ return []
56
+
57
+ def get_tools_requiring_approval(self) -> list[str]:
58
+ """Tool names requiring HITL approval before execution.
59
+
60
+ Return tool names (strings) that should trigger an interrupt before
61
+ the tool node runs. The frontend receives the pending tool calls and
62
+ must resume with an approval/rejection decision.
63
+
64
+ Maps to silverstripe-ai's ToolApproval middleware concept.
65
+ """
66
+ return []
67
+
68
+ def summarise_action(self, tool_name: str, tool_input: dict) -> str:
69
+ """Human-readable summary for a pending tool action (HITL card).
70
+
71
+ Override in subclasses for domain-specific summaries.
72
+ """
73
+ return f"Execute: {tool_name}"
74
+
75
+
76
+ class AgentRegistry:
77
+ """Maps Django model → agent class via DJANGO_AGENTIC['AGENT_MAPPINGS']."""
78
+
79
+ @classmethod
80
+ def get_agent_class(cls, entity_class_path: str) -> type[ModelAgent]:
81
+ from django.conf import settings
82
+ from django.utils.module_loading import import_string
83
+ mappings = getattr(settings, "DJANGO_AGENTIC", {}).get("AGENT_MAPPINGS", {})
84
+ if entity_class_path in mappings:
85
+ return import_string(mappings[entity_class_path])
86
+ try:
87
+ entity_class = import_string(entity_class_path)
88
+ for parent in entity_class.__mro__:
89
+ if hasattr(parent, "_meta"):
90
+ short = f"{parent._meta.app_label}.{parent.__name__}"
91
+ if short in mappings:
92
+ return import_string(mappings[short])
93
+ except (ImportError, AttributeError):
94
+ pass
95
+ raise ValueError(f"No agent mapping for '{entity_class_path}'.")
96
+
97
+ @classmethod
98
+ def create_for_entity(cls, user, model_config, entity, thread_id: str) -> ModelAgent:
99
+ path = f"{entity._meta.app_label}.{entity.__class__.__name__}"
100
+ agent_class = cls.get_agent_class(path)
101
+ return agent_class(user=user, model_config=model_config, entity=entity, thread_id=thread_id)
django_agentic/apps.py ADDED
@@ -0,0 +1,13 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class DjangoAIConfig(AppConfig):
5
+ """Django app configuration for django-agentic.
6
+
7
+ Add ``'django_agentic'`` to ``INSTALLED_APPS`` to enable AI model management,
8
+ credit tracking, usage logging, and agent chat endpoints.
9
+ """
10
+ default_auto_field = "django.db.models.BigAutoField"
11
+ name = "django_agentic"
12
+ label = "django_ai"
13
+ verbose_name = "AI"
@@ -0,0 +1,5 @@
1
+ """Context vars for passing user/model through LangGraph workflows."""
2
+ import contextvars
3
+
4
+ current_ai_user = contextvars.ContextVar("current_ai_user", default=None)
5
+ current_ai_model_name = contextvars.ContextVar("current_ai_model_name", default=None)
@@ -0,0 +1,89 @@
1
+ """Credit system — model selection and atomic deduction."""
2
+ import logging
3
+ from dataclasses import dataclass
4
+ from decimal import Decimal
5
+ from django.db import transaction
6
+ from .models import AIModel, SiteAIConfig, UserAIProfile
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class CreditLimitExceeded(Exception):
12
+ def __init__(self, available: Decimal, required: Decimal):
13
+ self.available = available
14
+ self.required = required
15
+ super().__init__(f"Insufficient credits. Required: ${required}, Available: ${available}.")
16
+
17
+
18
+ class AIServiceUnavailable(Exception):
19
+ pass
20
+
21
+
22
+ @dataclass
23
+ class ModelSelection:
24
+ model: AIModel
25
+ model_name: str
26
+ estimated_cost: Decimal
27
+ purchased_deduct: Decimal
28
+ free_deduct: Decimal
29
+ is_free_tier: bool
30
+
31
+
32
+ def get_or_create_profile(user) -> UserAIProfile:
33
+ profile, _ = UserAIProfile.objects.get_or_create(
34
+ user=user, defaults={"free_monthly_credits": SiteAIConfig.load().monthly_free_credits},
35
+ )
36
+ return profile
37
+
38
+
39
+ def resolve_model_for_user(user, input_tokens: int = 5000, output_tokens: int = 2000) -> ModelSelection:
40
+ config = SiteAIConfig.load()
41
+ profile = get_or_create_profile(user)
42
+ paid_model = profile.model_override or config.default_paid_model
43
+ free_model = config.default_free_model
44
+ if not paid_model:
45
+ raise AIServiceUnavailable("Paid AI model not configured. Contact admin.")
46
+ if not free_model:
47
+ raise AIServiceUnavailable("Free AI model not configured. Contact admin.")
48
+ if not free_model.allowed_for_free:
49
+ raise AIServiceUnavailable(f"Default free model '{free_model.display_name}' not allowed for free credits.")
50
+
51
+ paid_cost = paid_model.estimate_cost(input_tokens, output_tokens)
52
+ free_cost = free_model.estimate_cost(input_tokens, output_tokens)
53
+
54
+ if user.is_staff:
55
+ return ModelSelection(paid_model, paid_model.name, paid_cost, Decimal(0), Decimal(0), False)
56
+
57
+ if profile.purchased_credits >= paid_cost:
58
+ return ModelSelection(paid_model, paid_model.name, paid_cost, paid_cost, Decimal(0), False)
59
+
60
+ total = profile.purchased_credits + profile.free_monthly_credits
61
+ if total >= free_cost:
62
+ used_p = min(profile.purchased_credits, free_cost)
63
+ return ModelSelection(free_model, free_model.name, free_cost, used_p, free_cost - used_p, True)
64
+
65
+ raise CreditLimitExceeded(available=total, required=free_cost)
66
+
67
+
68
+ def deduct_credits(user, cost: Decimal, model: AIModel, idempotency_key: str | None = None) -> dict:
69
+ if user.is_staff:
70
+ return {"purchased_deduct": Decimal(0), "free_deduct": Decimal(0)}
71
+ if idempotency_key:
72
+ from .models import AIUsageLog
73
+ if AIUsageLog.objects.filter(idempotency_key=idempotency_key).exists():
74
+ return {"purchased_deduct": Decimal(0), "free_deduct": Decimal(0)}
75
+
76
+ profile = get_or_create_profile(user)
77
+ with transaction.atomic():
78
+ profile = UserAIProfile.objects.select_for_update().get(pk=profile.pk)
79
+ can_use_free = getattr(model, "allowed_for_free", True)
80
+ if can_use_free:
81
+ used_p = min(profile.purchased_credits, cost)
82
+ used_f = min(profile.free_monthly_credits, cost - used_p)
83
+ else:
84
+ used_p = min(profile.purchased_credits, cost)
85
+ used_f = Decimal(0)
86
+ profile.purchased_credits = max(Decimal(0), profile.purchased_credits - used_p)
87
+ profile.free_monthly_credits = max(Decimal(0), profile.free_monthly_credits - used_f)
88
+ profile.save(update_fields=["purchased_credits", "free_monthly_credits", "updated_at"])
89
+ return {"purchased_deduct": used_p, "free_deduct": used_f}
@@ -0,0 +1 @@
1
+ # management package
@@ -0,0 +1 @@
1
+ # commands package
@@ -0,0 +1,16 @@
1
+ """Monthly free credit reset. Run via cron on the 1st of each month."""
2
+ from django.core.management.base import BaseCommand
3
+ from django.utils import timezone
4
+ from django_agentic.models import SiteAIConfig, UserAIProfile
5
+
6
+
7
+ class Command(BaseCommand):
8
+ help = "Reset free monthly AI credits for all users"
9
+
10
+ def handle(self, *args, **options):
11
+ config = SiteAIConfig.load()
12
+ amount = config.monthly_free_credits
13
+ updated = UserAIProfile.objects.all().update(
14
+ free_monthly_credits=amount, credits_reset_at=timezone.now(),
15
+ )
16
+ self.stdout.write(self.style.SUCCESS(f"Reset {updated} profiles to ${amount} free credits."))
@@ -0,0 +1,116 @@
1
+ # Generated by Django 6.0.3 on 2026-04-11 22:33
2
+
3
+ import django.db.models.deletion
4
+ import uuid
5
+ from decimal import Decimal
6
+ from django.conf import settings
7
+ from django.db import migrations, models
8
+
9
+
10
+ class Migration(migrations.Migration):
11
+
12
+ initial = True
13
+
14
+ dependencies = [
15
+ ('contenttypes', '0002_remove_content_type_name'),
16
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
17
+ ]
18
+
19
+ operations = [
20
+ migrations.CreateModel(
21
+ name='AIModel',
22
+ fields=[
23
+ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
24
+ ('name', models.CharField(max_length=100, unique=True)),
25
+ ('display_name', models.CharField(blank=True, default='', max_length=200)),
26
+ ('provider', models.CharField(choices=[('anthropic', 'Anthropic'), ('openai', 'OpenAI'), ('google', 'Google'), ('bedrock', 'AWS Bedrock')], default='anthropic', max_length=50)),
27
+ ('input_cost_per_1m', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
28
+ ('output_cost_per_1m', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
29
+ ('cache_write_cost_per_1m', models.DecimalField(decimal_places=6, default=0, max_digits=10)),
30
+ ('cache_read_cost_per_1m', models.DecimalField(decimal_places=6, default=0, max_digits=10)),
31
+ ('active', models.BooleanField(default=True)),
32
+ ('allowed_for_free', models.BooleanField(default=True)),
33
+ ('allowed_for_paid', models.BooleanField(default=True)),
34
+ ('context_window', models.IntegerField(default=200000)),
35
+ ('max_output_tokens', models.IntegerField(default=8192)),
36
+ ('created_at', models.DateTimeField(auto_now_add=True)),
37
+ ],
38
+ options={
39
+ 'db_table': 'ailogs_aimodel',
40
+ 'ordering': ['input_cost_per_1m'],
41
+ },
42
+ ),
43
+ migrations.CreateModel(
44
+ name='SiteAIConfig',
45
+ fields=[
46
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
47
+ ('monthly_free_credits', models.DecimalField(decimal_places=2, default=Decimal('2.00'), max_digits=10)),
48
+ ('step_model_config', models.JSONField(blank=True, default=dict)),
49
+ ('default_free_model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='django_ai.aimodel')),
50
+ ('default_paid_model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='django_ai.aimodel')),
51
+ ],
52
+ options={
53
+ 'verbose_name': 'AI Configuration',
54
+ 'verbose_name_plural': 'AI Configuration',
55
+ 'db_table': 'ailogs_siteaiconfig',
56
+ },
57
+ ),
58
+ migrations.CreateModel(
59
+ name='UserAIProfile',
60
+ fields=[
61
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
62
+ ('free_monthly_credits', models.DecimalField(decimal_places=6, default=Decimal('2.000000'), max_digits=10)),
63
+ ('purchased_credits', models.DecimalField(decimal_places=6, default=Decimal('0.000000'), max_digits=10)),
64
+ ('credits_reset_at', models.DateTimeField(blank=True, null=True)),
65
+ ('created_at', models.DateTimeField(auto_now_add=True)),
66
+ ('updated_at', models.DateTimeField(auto_now=True)),
67
+ ('model_override', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='django_ai.aimodel')),
68
+ ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='ai_profile', to=settings.AUTH_USER_MODEL)),
69
+ ],
70
+ options={
71
+ 'verbose_name': 'User AI Profile',
72
+ 'db_table': 'ailogs_useraiprofile',
73
+ },
74
+ ),
75
+ migrations.CreateModel(
76
+ name='AIUsageLog',
77
+ fields=[
78
+ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
79
+ ('idempotency_key', models.CharField(blank=True, max_length=255, null=True, unique=True)),
80
+ ('model_name', models.CharField(db_index=True, max_length=100)),
81
+ ('workflow', models.CharField(db_index=True, max_length=100)),
82
+ ('node', models.CharField(max_length=100)),
83
+ ('entity_id', models.CharField(blank=True, default='', max_length=255)),
84
+ ('input_summary', models.TextField(blank=True, default='')),
85
+ ('output_json', models.JSONField(default=dict)),
86
+ ('prompt_tokens', models.IntegerField(blank=True, null=True)),
87
+ ('completion_tokens', models.IntegerField(blank=True, null=True)),
88
+ ('total_tokens', models.IntegerField(blank=True, null=True)),
89
+ ('cache_read_tokens', models.IntegerField(blank=True, null=True)),
90
+ ('cache_creation_tokens', models.IntegerField(blank=True, null=True)),
91
+ ('cost_usd', models.DecimalField(blank=True, decimal_places=6, max_digits=10, null=True)),
92
+ ('input_cost_per_1m_at_time', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
93
+ ('output_cost_per_1m_at_time', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
94
+ ('cache_write_cost_per_1m_at_time', models.DecimalField(blank=True, decimal_places=6, max_digits=10, null=True)),
95
+ ('cache_read_cost_per_1m_at_time', models.DecimalField(blank=True, decimal_places=6, max_digits=10, null=True)),
96
+ ('used_free_credits', models.DecimalField(decimal_places=6, default=0, max_digits=10)),
97
+ ('used_paid_credits', models.DecimalField(decimal_places=6, default=0, max_digits=10)),
98
+ ('provider_request_id', models.CharField(blank=True, default='', max_length=255)),
99
+ ('error_type', models.CharField(blank=True, choices=[('', 'None'), ('credit_limit', 'Credit Limit'), ('api_error', 'API Error'), ('validation', 'Validation'), ('context_overflow', 'Context Overflow'), ('unknown', 'Unknown')], default='', max_length=20)),
100
+ ('request_time', models.DateTimeField(blank=True, null=True)),
101
+ ('response_time', models.DateTimeField(blank=True, null=True)),
102
+ ('duration_ms', models.IntegerField(blank=True, null=True)),
103
+ ('success', models.BooleanField(default=True)),
104
+ ('error', models.TextField(blank=True, default='')),
105
+ ('created_at', models.DateTimeField(auto_now_add=True)),
106
+ ('ai_model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='usage_logs', to='django_ai.aimodel')),
107
+ ('entity_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='contenttypes.contenttype')),
108
+ ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ai_usage_logs', to=settings.AUTH_USER_MODEL)),
109
+ ],
110
+ options={
111
+ 'db_table': 'ailogs_aiusagelog',
112
+ 'ordering': ['-created_at'],
113
+ 'indexes': [models.Index(fields=['model_name', 'created_at'], name='ailogs_aius_model_n_434025_idx'), models.Index(fields=['workflow', 'created_at'], name='ailogs_aius_workflo_7e02c2_idx'), models.Index(fields=['user', 'created_at'], name='ailogs_aius_user_id_cd756c_idx')],
114
+ },
115
+ ),
116
+ ]
@@ -0,0 +1,18 @@
1
+ # Generated by Django 6.0.3 on 2026-04-11 22:56
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('django_ai', '0001_initial'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='aimodel',
15
+ name='model_type',
16
+ field=models.CharField(choices=[('chat', 'Chat / Generation'), ('embedding', 'Embedding'), ('transcription', 'Transcription'), ('image', 'Image Generation')], db_index=True, default='chat', max_length=20),
17
+ ),
18
+ ]
@@ -0,0 +1,44 @@
1
+ # Generated by Django 6.0.3 on 2026-04-13 00:36
2
+
3
+ from django.db import migrations
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('django_ai', '0002_add_model_type'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.RenameIndex(
14
+ model_name='aiusagelog',
15
+ new_name='django_ai_a_model_n_053a01_idx',
16
+ old_name='ailogs_aius_model_n_434025_idx',
17
+ ),
18
+ migrations.RenameIndex(
19
+ model_name='aiusagelog',
20
+ new_name='django_ai_a_workflo_9b3be0_idx',
21
+ old_name='ailogs_aius_workflo_7e02c2_idx',
22
+ ),
23
+ migrations.RenameIndex(
24
+ model_name='aiusagelog',
25
+ new_name='django_ai_a_user_id_6d10e0_idx',
26
+ old_name='ailogs_aius_user_id_cd756c_idx',
27
+ ),
28
+ migrations.AlterModelTable(
29
+ name='aimodel',
30
+ table=None,
31
+ ),
32
+ migrations.AlterModelTable(
33
+ name='aiusagelog',
34
+ table=None,
35
+ ),
36
+ migrations.AlterModelTable(
37
+ name='siteaiconfig',
38
+ table=None,
39
+ ),
40
+ migrations.AlterModelTable(
41
+ name='useraiprofile',
42
+ table=None,
43
+ ),
44
+ ]