feat: complete multimodal ticket intake

This commit is contained in:
hailin
2026-07-27 18:59:28 +08:00
parent 96452934a3
commit 93065c0a00
39 changed files with 6443 additions and 267 deletions

View File

@@ -0,0 +1,51 @@
from django.db import migrations, models
def migrate_intake_statuses(apps, schema_editor):
TicketDraft = apps.get_model("tickets", "TicketDraft")
TicketDraft.objects.filter(status="editing").update(status="collecting")
TicketDraft.objects.filter(status="ready").update(status="needs_input")
TicketDraft.objects.filter(status="confirmed").update(status="ticket_created")
def restore_draft_statuses(apps, schema_editor):
TicketDraft = apps.get_model("tickets", "TicketDraft")
TicketDraft.objects.filter(status="collecting").update(status="editing")
TicketDraft.objects.filter(status="needs_input").update(status="ready")
TicketDraft.objects.filter(status="ticket_created").update(status="confirmed")
class Migration(migrations.Migration):
dependencies = [
("tickets", "0005_ticketcollaborator_ticketfieldevidence_and_more"),
]
operations = [
migrations.AddField(
model_name="ticketdraft",
name="field_evidence",
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name="ticketdraft",
name="invalid_reason",
field=models.TextField(blank=True),
),
migrations.AddField(
model_name="ticketdraft",
name="message_timeline",
field=models.JSONField(default=list),
),
migrations.AddField(
model_name="ticketdraft",
name="problem_valid",
field=models.BooleanField(blank=True, null=True),
),
migrations.AlterField(
model_name="ticketdraft",
name="status",
field=models.CharField(default="collecting", max_length=24),
),
migrations.RunPython(migrate_intake_statuses, restore_draft_statuses),
]

View File

@@ -0,0 +1,100 @@
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("tickets", "0006_ticketdraft_intake_result"),
]
operations = [
migrations.AddField(
model_name="ticketattachment",
name="parse_error",
field=models.TextField(blank=True),
),
migrations.AddField(
model_name="ticketattachment",
name="parse_status",
field=models.CharField(default="pending", max_length=24),
),
migrations.AddField(
model_name="ticketattachment",
name="scan_result",
field=models.JSONField(blank=True, default=dict),
),
migrations.AddField(
model_name="ticketattachment",
name="scan_status",
field=models.CharField(default="pending", max_length=24),
),
migrations.AddField(
model_name="ticketattachment",
name="sha256",
field=models.CharField(blank=True, max_length=64),
),
migrations.RenameModel(
old_name="TicketDraftImage",
new_name="TicketDraftMaterial",
),
migrations.AlterField(
model_name="ticketdraftmaterial",
name="draft",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="materials",
to="tickets.ticketdraft",
),
),
migrations.AlterField(
model_name="ticketdraftmaterial",
name="confirmed_attachment",
field=models.OneToOneField(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="source_draft_material",
to="tickets.ticketattachment",
),
),
migrations.AddField(
model_name="ticketdraftmaterial",
name="claimed_mime_type",
field=models.CharField(blank=True, max_length=120),
),
migrations.AddField(
model_name="ticketdraftmaterial",
name="extension",
field=models.CharField(blank=True, max_length=16),
),
migrations.AddField(
model_name="ticketdraftmaterial",
name="material_type",
field=models.CharField(default="image", max_length=24),
),
migrations.AddField(
model_name="ticketdraftmaterial",
name="parse_error",
field=models.TextField(blank=True),
),
migrations.AddField(
model_name="ticketdraftmaterial",
name="parse_status",
field=models.CharField(default="pending", max_length=24),
),
migrations.AddField(
model_name="ticketdraftmaterial",
name="parsed_text",
field=models.TextField(blank=True),
),
migrations.AddField(
model_name="ticketdraftmaterial",
name="scan_result",
field=models.JSONField(blank=True, default=dict),
),
migrations.AddField(
model_name="ticketdraftmaterial",
name="scan_status",
field=models.CharField(default="pending", max_length=24),
),
]

View File

@@ -0,0 +1,38 @@
# Generated by Django 5.2.15 on 2026-07-27 02:44
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tickets', '0007_ticketdraftmaterial'),
]
operations = [
migrations.CreateModel(
name='TicketMessageTimelineItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('sequence', models.PositiveIntegerField()),
('speaker', models.CharField(blank=True, max_length=120)),
('role', models.CharField(default='unknown', max_length=24)),
('content', models.TextField()),
('occurred_at', models.DateTimeField(blank=True, null=True)),
('occurred_at_raw', models.CharField(blank=True, max_length=120)),
('source_ref', models.CharField(blank=True, max_length=160)),
('confidence', models.DecimalField(blank=True, decimal_places=4, max_digits=5, null=True)),
('conflict', models.TextField(blank=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('draft', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='timeline_items', to='tickets.ticketdraft')),
('source_material', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='message_timeline_items', to='tickets.ticketdraftmaterial')),
('ticket', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='message_timeline_items', to='tickets.ticket')),
],
options={
'ordering': ['sequence'],
'indexes': [models.Index(fields=['ticket', 'sequence'], name='tickets_tic_ticket__20b0c4_idx'), models.Index(fields=['draft', 'sequence'], name='tickets_tic_draft_i_0adfa1_idx')],
'constraints': [models.UniqueConstraint(fields=('draft', 'sequence'), name='unique_ticket_draft_timeline_sequence')],
},
),
]

View File

@@ -0,0 +1,83 @@
# Generated by Django 5.2.15 on 2026-07-27 03:03
import django.db.models.deletion
from django.conf import settings
from django.contrib.postgres.operations import TrigramExtension
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tickets', '0008_ticketmessagetimelineitem'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
TrigramExtension(),
migrations.CreateModel(
name='IntakeEvaluationCase',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('key', models.SlugField(max_length=120, unique=True)),
('name', models.CharField(max_length=200)),
('source_kind', models.CharField(default='synthetic', max_length=32)),
('valid_input', models.BooleanField(default=True)),
('input_manifest', models.JSONField(default=dict)),
('expected_fields', models.JSONField(default=dict)),
('expected_timeline', models.JSONField(default=list)),
('expected_auto_created', models.BooleanField(default=True)),
('expected_similar_ticket', models.BooleanField(default=False)),
('notes', models.TextField(blank=True)),
('enabled', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'ordering': ['key'],
'indexes': [models.Index(fields=['source_kind', 'enabled'], name='tickets_int_source__74e817_idx')],
},
),
migrations.CreateModel(
name='IntakeEvaluationResult',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('actual_fields', models.JSONField(default=dict)),
('actual_auto_created', models.BooleanField(default=False)),
('correct_field_count', models.PositiveIntegerField(default=0)),
('evaluated_field_count', models.PositiveIntegerField(default=0)),
('field_accuracy', models.DecimalField(blank=True, decimal_places=5, max_digits=6, null=True)),
('passed', models.BooleanField(default=False)),
('details', models.JSONField(blank=True, default=dict)),
('created_at', models.DateTimeField(auto_now_add=True)),
('case', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='results', to='tickets.intakeevaluationcase')),
('draft', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='evaluation_results', to='tickets.ticketdraft')),
('reviewed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reviewed_intake_evaluations', to=settings.AUTH_USER_MODEL)),
('ticket', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='intake_evaluation_results', to='tickets.ticket')),
],
options={
'ordering': ['-created_at'],
'indexes': [models.Index(fields=['case', 'created_at'], name='tickets_int_case_id_8c9c27_idx')],
'constraints': [models.UniqueConstraint(fields=('case', 'draft'), name='unique_intake_case_draft_result')],
},
),
migrations.CreateModel(
name='TicketSimilaritySuggestion',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('score', models.DecimalField(decimal_places=5, max_digits=6)),
('title_similarity', models.DecimalField(decimal_places=5, default=0, max_digits=6)),
('keyword_score', models.DecimalField(decimal_places=5, default=0, max_digits=6)),
('matched_excerpt', models.TextField(blank=True)),
('algorithm_version', models.CharField(default='fts_trigram_v1', max_length=40)),
('created_at', models.DateTimeField(auto_now_add=True)),
('candidate_ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='suggested_for_tickets', to='tickets.ticket')),
('source_ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='similarity_suggestions', to='tickets.ticket')),
],
options={
'ordering': ['-score', 'candidate_ticket_id'],
'indexes': [models.Index(fields=['source_ticket', 'algorithm_version', 'score'], name='tickets_tic_source__88b288_idx')],
'constraints': [models.UniqueConstraint(fields=('source_ticket', 'candidate_ticket', 'algorithm_version'), name='unique_ticket_similarity_suggestion'), models.CheckConstraint(condition=models.Q(('source_ticket', models.F('candidate_ticket')), _negated=True), name='ticket_similarity_not_self')],
},
),
]

View File

@@ -0,0 +1,58 @@
# Generated by Django 5.2.15 on 2026-07-27 03:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tickets', '0009_intakeevaluationcase_intakeevaluationresult_and_more'),
]
operations = [
migrations.AddField(
model_name='ticket',
name='attempted_actions',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='ticket',
name='current_result',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='ticket',
name='customer_request',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='ticket',
name='error_information',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='ticket',
name='module_name',
field=models.CharField(blank=True, max_length=120),
),
migrations.AddField(
model_name='ticket',
name='product_name',
field=models.CharField(blank=True, max_length=120),
),
migrations.AddField(
model_name='ticket',
name='tags',
field=models.JSONField(blank=True, default=list),
),
migrations.AddField(
model_name='ticketmessagetimelineitem',
name='timezone',
field=models.CharField(blank=True, max_length=80),
),
migrations.AlterField(
model_name='ticketsimilaritysuggestion',
name='algorithm_version',
field=models.CharField(default='fts_trigram_v2_context', max_length=40),
),
]

View File

@@ -0,0 +1,35 @@
# Generated by Django 5.2.15 on 2026-07-27 03:55
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tickets', '0010_ticket_attempted_actions_ticket_current_result_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterModelOptions(
name='intakeevaluationresult',
options={'ordering': ['-updated_at']},
),
migrations.RemoveIndex(
model_name='intakeevaluationresult',
name='tickets_int_case_id_8c9c27_idx',
),
migrations.AddField(
model_name='intakeevaluationresult',
name='updated_at',
field=models.DateTimeField(auto_now=True),
),
migrations.AddIndex(
model_name='intakeevaluationresult',
index=models.Index(
fields=['case', 'updated_at'],
name='tickets_int_case_id_79c7c4_idx',
),
),
]