Skip to content

Commit 3120090

Browse files
author
kappa90
committed
feat(ph-ai): agent artifacts API endpoint
1 parent ccc4fe0 commit 3120090

File tree

4 files changed

+116
-3
lines changed

4 files changed

+116
-3
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Generated by Django 4.2.26 on 2025-11-21 16:00
2+
3+
import django.db.models.deletion
4+
from django.conf import settings
5+
from django.db import migrations, models
6+
7+
import posthog.models.utils
8+
9+
import ee.models.assistant
10+
11+
12+
class Migration(migrations.Migration):
13+
dependencies = [
14+
("posthog", "0914_alter_userproductlist_reason"),
15+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
16+
("ee", "0030_singlesessionsummary_distinct_id_and_more"),
17+
]
18+
19+
operations = [
20+
migrations.CreateModel(
21+
name="AgentArtifact",
22+
fields=[
23+
("created_at", models.DateTimeField(auto_now_add=True)),
24+
("updated_at", models.DateTimeField(auto_now=True, null=True)),
25+
("deleted", models.BooleanField(blank=True, default=False, null=True)),
26+
("deleted_at", models.DateTimeField(blank=True, null=True)),
27+
(
28+
"id",
29+
models.UUIDField(
30+
default=posthog.models.utils.uuid7, editable=False, primary_key=True, serialize=False
31+
),
32+
),
33+
("short_id", models.CharField(default=ee.models.assistant.generate_short_id, max_length=4)),
34+
("name", models.CharField(max_length=400)),
35+
(
36+
"type",
37+
models.CharField(
38+
choices=[("visualization", "Visualization"), ("notebook", "Notebook")], max_length=50
39+
),
40+
),
41+
("data", models.JSONField(help_text="Artifact content. Structure depends on artifact type.")),
42+
(
43+
"conversation",
44+
models.ForeignKey(
45+
on_delete=django.db.models.deletion.CASCADE, related_name="artifacts", to="ee.conversation"
46+
),
47+
),
48+
(
49+
"created_by",
50+
models.ForeignKey(
51+
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL
52+
),
53+
),
54+
("team", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="posthog.team")),
55+
],
56+
options={
57+
"indexes": [
58+
models.Index(fields=["team", "short_id"], name="ee_agentart_team_id_d05587_idx"),
59+
models.Index(fields=["team", "conversation", "created_at"], name="ee_agentart_team_id_e402e1_idx"),
60+
],
61+
},
62+
),
63+
migrations.AddConstraint(
64+
model_name="agentartifact",
65+
constraint=models.UniqueConstraint(fields=("team", "short_id"), name="unique_team_short_id"),
66+
),
67+
]

ee/migrations/max_migration.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0030_singlesessionsummary_distinct_id_and_more
1+
0031_agentartifact

ee/models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .assistant import (
2+
AgentArtifact,
23
Conversation,
34
ConversationCheckpoint,
45
ConversationCheckpointBlob,
@@ -16,6 +17,7 @@
1617

1718
__all__ = [
1819
"AccessControl",
20+
"AgentArtifact",
1921
"ConversationCheckpoint",
2022
"ConversationCheckpointBlob",
2123
"ConversationCheckpointWrite",

ee/models/assistant.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
1+
import string
2+
import secrets
13
from datetime import timedelta
24

3-
from django.db import models
5+
from django.db import IntegrityError, models
46
from django.utils import timezone
57

68
from posthog.models.team.team import Team
79
from posthog.models.user import User
8-
from posthog.models.utils import UUIDTModel
10+
from posthog.models.utils import CreatedMetaFields, DeletedMetaFields, UpdatedMetaFields, UUIDModel, UUIDTModel
11+
12+
13+
def generate_short_id():
14+
"""Generate securely random 4 characters long alphanumeric ID.
15+
16+
With team-scoped uniqueness, 4 characters (62^4 = 14.7M combinations)
17+
is sufficient to avoid collisions within a single team.
18+
"""
19+
return "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(4))
920

1021

1122
class Conversation(UUIDTModel):
@@ -183,3 +194,36 @@ def answers_left(self) -> int:
183194
if self.initial_text.endswith("\nAnswer:"):
184195
answers_given -= 1
185196
return MAX_ONBOARDING_QUESTIONS - answers_given
197+
198+
199+
class AgentArtifact(UUIDModel, CreatedMetaFields, UpdatedMetaFields, DeletedMetaFields):
200+
class Type(models.TextChoices):
201+
VISUALIZATION = "visualization", "Visualization"
202+
NOTEBOOK = "notebook", "Notebook"
203+
204+
short_id = models.CharField(max_length=4, default=generate_short_id)
205+
name = models.CharField(max_length=400)
206+
type = models.CharField(max_length=50, choices=Type.choices)
207+
data = models.JSONField(help_text="Artifact content. Structure depends on artifact type.")
208+
conversation = models.ForeignKey(Conversation, on_delete=models.CASCADE, related_name="artifacts")
209+
team = models.ForeignKey(Team, on_delete=models.CASCADE)
210+
211+
class Meta:
212+
indexes = [
213+
models.Index(fields=["team", "short_id"]),
214+
models.Index(fields=["team", "conversation", "created_at"]),
215+
]
216+
constraints = [
217+
models.UniqueConstraint(fields=["team", "short_id"], name="unique_team_short_id"),
218+
]
219+
220+
def save(self, *args, **kwargs):
221+
max_retries = 5
222+
for attempt in range(max_retries):
223+
try:
224+
return super().save(*args, **kwargs)
225+
except IntegrityError as e:
226+
if "short_id" in str(e) and attempt < max_retries - 1:
227+
self.short_id = generate_short_id()
228+
else:
229+
raise

0 commit comments

Comments
 (0)