Skip to content

Commit e120962

Browse files
committed
added context model
1 parent 3038f32 commit e120962

File tree

8 files changed

+377
-107
lines changed

8 files changed

+377
-107
lines changed

backend/apps/ai/admin.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,21 @@
33
from django.contrib import admin
44

55
from apps.ai.models.chunk import Chunk
6+
from apps.ai.models.context import Context
7+
8+
9+
class ContextAdmin(admin.ModelAdmin):
10+
"""Admin for Context model."""
11+
12+
list_display = (
13+
"id",
14+
"generated_text",
15+
"content_type",
16+
"object_id",
17+
"source",
18+
)
19+
search_fields = ("generated_text", "source")
20+
list_filter = ("content_type", "source")
621

722

823
class ChunkAdmin(admin.ModelAdmin):
@@ -11,9 +26,11 @@ class ChunkAdmin(admin.ModelAdmin):
1126
list_display = (
1227
"id",
1328
"text",
14-
"content_type",
29+
"context",
1530
)
16-
search_fields = ("text", "object_id")
31+
search_fields = ("text",)
32+
list_filter = ("context__content_type",)
1733

1834

35+
admin.site.register(Context, ContextAdmin)
1936
admin.site.register(Chunk, ChunkAdmin)

backend/apps/ai/agent/tools/rag/retriever.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -223,29 +223,29 @@ def retrieve(
223223
queryset = queryset.filter(content_type_query)
224224

225225
chunks = (
226-
queryset.select_related("content_type")
227-
.prefetch_related("content_object")
226+
queryset.select_related("context__content_type")
227+
.prefetch_related("context__content_object")
228228
.order_by("-similarity")[:limit]
229229
)
230230

231231
results = []
232232
for chunk in chunks:
233-
if not chunk.content_object:
233+
if not chunk.context or not chunk.context.content_object:
234234
logger.warning("Content object is None for chunk %s. Skipping.", chunk.id)
235235
continue
236236

237-
source_name = self.get_source_name(chunk.content_object)
237+
source_name = self.get_source_name(chunk.context.content_object)
238238
additional_context = self.get_additional_context(
239-
chunk.content_object, chunk.content_type.model
239+
chunk.context.content_object, chunk.context.content_type.model
240240
)
241241

242242
results.append(
243243
{
244244
"text": chunk.text,
245245
"similarity": float(chunk.similarity),
246-
"source_type": chunk.content_type.model,
246+
"source_type": chunk.context.content_type.model,
247247
"source_name": source_name,
248-
"source_id": chunk.object_id,
248+
"source_id": chunk.context.object_id,
249249
"additional_context": additional_context,
250250
}
251251
)

backend/apps/ai/common/utils.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
MIN_REQUEST_INTERVAL_SECONDS,
1010
)
1111
from apps.ai.models.chunk import Chunk
12+
from apps.ai.models.context import Context
1213

1314
logger: logging.Logger = logging.getLogger(__name__)
1415

@@ -43,6 +44,12 @@ def create_chunks_and_embeddings(
4344
model="text-embedding-3-small",
4445
)
4546

47+
context = Context(
48+
generated_text="\n".join(all_chunk_texts),
49+
content_object=content_object,
50+
)
51+
context.save()
52+
4653
return [
4754
chunk
4855
for text, embedding in zip(
@@ -53,7 +60,7 @@ def create_chunks_and_embeddings(
5360
if (
5461
chunk := Chunk.update_data(
5562
text=text,
56-
content_object=content_object,
63+
context=context,
5764
embedding=embedding,
5865
save=False,
5966
)
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Generated by Django 5.2.4 on 2025-07-28 09:00
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
dependencies = [
9+
("ai", "0004_alter_chunk_unique_together_chunk_content_type_and_more"),
10+
("contenttypes", "0002_remove_content_type_name"),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name="Context",
16+
fields=[
17+
(
18+
"id",
19+
models.BigAutoField(
20+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
21+
),
22+
),
23+
("nest_created_at", models.DateTimeField(auto_now_add=True)),
24+
("nest_updated_at", models.DateTimeField(auto_now=True)),
25+
("generated_text", models.TextField(verbose_name="Generated Text")),
26+
("object_id", models.PositiveIntegerField(default=0)),
27+
("source", models.CharField(blank=True, default="", max_length=100)),
28+
(
29+
"content_type",
30+
models.ForeignKey(
31+
blank=True,
32+
null=True,
33+
on_delete=django.db.models.deletion.CASCADE,
34+
to="contenttypes.contenttype",
35+
),
36+
),
37+
],
38+
options={
39+
"verbose_name": "Context",
40+
"db_table": "ai_contexts",
41+
},
42+
),
43+
migrations.AlterUniqueTogether(
44+
name="chunk",
45+
unique_together=set(),
46+
),
47+
migrations.AddField(
48+
model_name="chunk",
49+
name="context",
50+
field=models.ForeignKey(
51+
blank=True,
52+
null=True,
53+
on_delete=django.db.models.deletion.CASCADE,
54+
related_name="chunks",
55+
to="ai.context",
56+
),
57+
),
58+
migrations.AlterUniqueTogether(
59+
name="chunk",
60+
unique_together={("context", "text")},
61+
),
62+
migrations.RemoveField(
63+
model_name="chunk",
64+
name="content_type",
65+
),
66+
migrations.RemoveField(
67+
model_name="chunk",
68+
name="object_id",
69+
),
70+
]

backend/apps/ai/models/chunk.py

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
"""AI app chunk model."""
22

3-
from django.contrib.contenttypes.fields import GenericForeignKey
4-
from django.contrib.contenttypes.models import ContentType
53
from django.db import models
64
from langchain.text_splitter import RecursiveCharacterTextSplitter
75
from pgvector.django import VectorField
86

7+
from apps.ai.models.context import Context
98
from apps.common.models import BulkSaveModel, TimestampedModel
109
from apps.common.utils import truncate
1110

@@ -16,25 +15,18 @@ class Chunk(TimestampedModel):
1615
class Meta:
1716
db_table = "ai_chunks"
1817
verbose_name = "Chunk"
19-
unique_together = ("content_type", "object_id", "text")
18+
unique_together = ("context", "text")
2019

21-
content_object = GenericForeignKey("content_type", "object_id")
22-
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, blank=True, null=True)
20+
context = models.ForeignKey(
21+
Context, on_delete=models.CASCADE, related_name="chunks", null=True, blank=True
22+
)
2323
embedding = VectorField(verbose_name="Embedding", dimensions=1536)
24-
object_id = models.PositiveIntegerField(default=0)
2524
text = models.TextField(verbose_name="Text")
2625

2726
def __str__(self):
2827
"""Human readable representation."""
29-
content_name = (
30-
getattr(self.content_object, "name", None)
31-
or getattr(self.content_object, "key", None)
32-
or str(self.content_object)
33-
)
34-
return (
35-
f"Chunk {self.id} for {self.content_type.model} {content_name}: "
36-
f"{truncate(self.text, 50)}"
37-
)
28+
context_str = str(self.context) if self.context else "No Context"
29+
return f"Chunk {self.id} for {context_str}: {truncate(self.text, 50)}"
3830

3931
@staticmethod
4032
def bulk_save(chunks, fields=None):
@@ -54,7 +46,7 @@ def split_text(text: str) -> list[str]:
5446
@staticmethod
5547
def update_data(
5648
text: str,
57-
content_object,
49+
context: Context,
5850
embedding,
5951
*,
6052
save: bool = True,
@@ -63,24 +55,18 @@ def update_data(
6355
6456
Args:
6557
text (str): The text content of the chunk.
66-
content_object: The object this chunk belongs to (Message, Chapter, etc.).
58+
context (Context): The context this chunk belongs to.
6759
embedding (list): The embedding vector for the chunk.
6860
save (bool): Whether to save the chunk to the database.
6961
7062
Returns:
7163
Chunk: The updated chunk instance or None if it already exists.
7264
7365
"""
74-
content_type = ContentType.objects.get_for_model(content_object)
75-
76-
if Chunk.objects.filter(
77-
content_type=content_type, object_id=content_object.id, text=text
78-
).exists():
66+
if Chunk.objects.filter(context=context, text=text).exists():
7967
return None
8068

81-
chunk = Chunk(
82-
content_type=content_type, object_id=content_object.id, text=text, embedding=embedding
83-
)
69+
chunk = Chunk(context=context, text=text, embedding=embedding)
8470

8571
if save:
8672
chunk.save()

backend/apps/ai/models/context.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""AI app context model."""
2+
3+
from django.contrib.contenttypes.fields import GenericForeignKey
4+
from django.contrib.contenttypes.models import ContentType
5+
from django.db import models
6+
7+
from apps.common.models import TimestampedModel
8+
9+
10+
class Context(TimestampedModel):
11+
"""Context model for storing generated text and optional relation to OWASP entities."""
12+
13+
generated_text = models.TextField(verbose_name="Generated Text")
14+
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, blank=True, null=True)
15+
object_id = models.PositiveIntegerField(default=0)
16+
content_object = GenericForeignKey("content_type", "object_id")
17+
source = models.CharField(max_length=100, blank=True, default="")
18+
19+
class Meta:
20+
db_table = "ai_contexts"
21+
verbose_name = "Context"
22+
23+
def __str__(self):
24+
"""Human readable representation."""
25+
entity = (
26+
getattr(self.content_object, "name", None)
27+
or getattr(self.content_object, "key", None)
28+
or str(self.content_object)
29+
)
30+
return (
31+
f"{self.content_type.model if self.content_type else 'None'} {entity}: "
32+
f"{self.generated_text[:50]}"
33+
)

0 commit comments

Comments
 (0)