feat: add governed knowledge lifecycle
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
# Generated by Django 5.2.15 on 2026-07-27 04:24
|
||||
|
||||
import pgvector.django.vector
|
||||
from django.db import migrations
|
||||
from pgvector.django import VectorExtension
|
||||
|
||||
|
||||
def create_vector_index(apps, schema_editor):
|
||||
if schema_editor.connection.vendor != "postgresql":
|
||||
return
|
||||
schema_editor.execute(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS knowledge_chunk_embedding_hnsw
|
||||
ON knowledge_knowledgechunk
|
||||
USING hnsw (embedding_vector vector_cosine_ops)
|
||||
WHERE embedding_vector IS NOT NULL
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def drop_vector_index(apps, schema_editor):
|
||||
if schema_editor.connection.vendor == "postgresql":
|
||||
schema_editor.execute("DROP INDEX IF EXISTS knowledge_chunk_embedding_hnsw")
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('knowledge', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
VectorExtension(),
|
||||
migrations.AddField(
|
||||
model_name='knowledgechunk',
|
||||
name='embedding_vector',
|
||||
field=pgvector.django.vector.VectorField(blank=True, dimensions=1536, null=True),
|
||||
),
|
||||
migrations.RunPython(create_vector_index, drop_vector_index),
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.2.15 on 2026-07-27 04:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('knowledge', '0002_knowledgechunk_embedding_vector'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='knowledgeitem',
|
||||
name='category_code',
|
||||
field=models.CharField(blank=True, max_length=80),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='knowledgeitem',
|
||||
name='environment_scope',
|
||||
field=models.CharField(blank=True, max_length=120),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='knowledgeitem',
|
||||
name='related_system',
|
||||
field=models.CharField(blank=True, max_length=120),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,36 @@
|
||||
# Generated by Django 5.2.15 on 2026-07-27 04:40
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def backfill_historical_ticket(apps, schema_editor):
|
||||
KnowledgeChunk = apps.get_model("knowledge", "KnowledgeChunk")
|
||||
Ticket = apps.get_model("tickets", "Ticket")
|
||||
valid_ids = set(Ticket.objects.values_list("id", flat=True))
|
||||
for chunk in KnowledgeChunk.objects.filter(historical_ticket__isnull=True).iterator():
|
||||
ticket_id = (chunk.metadata or {}).get("historical_ticket_id")
|
||||
if ticket_id in valid_ids:
|
||||
chunk.historical_ticket_id = ticket_id
|
||||
chunk.save(update_fields=["historical_ticket"])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('knowledge', '0003_knowledgeitem_category_code_and_more'),
|
||||
('tickets', '0012_ticketsupportanalysis'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='knowledgechunk',
|
||||
options={'ordering': ['knowledge_base_id', 'document_id', 'item_id', 'historical_ticket_id', 'chunk_index']},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='knowledgechunk',
|
||||
name='historical_ticket',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='knowledge_chunks', to='tickets.ticket'),
|
||||
),
|
||||
migrations.RunPython(backfill_historical_ticket, migrations.RunPython.noop),
|
||||
]
|
||||
@@ -0,0 +1,122 @@
|
||||
# Generated by Django 5.2.15 on 2026-07-27 05:23
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('knowledge', '0004_alter_knowledgechunk_options_and_more'),
|
||||
('tickets', '0013_ticketsupportanalysis_applied_solution_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='knowledgechunk',
|
||||
options={'ordering': ['knowledge_base_id', 'document_id', 'item_id', 'troubleshooting_plan_id', 'historical_ticket_id', 'chunk_index']},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='GovernedAsset',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('public_id', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
||||
('asset_type', models.CharField(choices=[('knowledge_item', '知识'), ('troubleshooting_plan', '排查方案'), ('solution', '解决方案')], max_length=40)),
|
||||
('object_id', models.PositiveBigIntegerField()),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_governed_assets', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='GovernedAssetVersion',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('version', models.PositiveIntegerField()),
|
||||
('status', models.CharField(choices=[('draft', '草稿'), ('pending_review', '待审核'), ('rejected', '已驳回'), ('published', '已发布'), ('offline', '已下线')], default='draft', max_length=40)),
|
||||
('content', models.JSONField(default=dict)),
|
||||
('change_summary', models.CharField(blank=True, max_length=500)),
|
||||
('review_comment', models.TextField(blank=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('submitted_at', models.DateTimeField(blank=True, null=True)),
|
||||
('reviewed_at', models.DateTimeField(blank=True, null=True)),
|
||||
('published_at', models.DateTimeField(blank=True, null=True)),
|
||||
('offline_at', models.DateTimeField(blank=True, null=True)),
|
||||
('asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='versions', to='knowledge.governedasset')),
|
||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_asset_versions', to=settings.AUTH_USER_MODEL)),
|
||||
('reviewed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reviewed_asset_versions', to=settings.AUTH_USER_MODEL)),
|
||||
('source_version', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='derived_versions', to='knowledge.governedassetversion')),
|
||||
('submitted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='submitted_asset_versions', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-version'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='governedasset',
|
||||
name='current_published_version',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='knowledge.governedassetversion'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TroubleshootingPlan',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=200)),
|
||||
('problem_pattern', models.TextField()),
|
||||
('applicable_scope', models.TextField(blank=True)),
|
||||
('category_code', models.CharField(blank=True, max_length=80)),
|
||||
('related_system', models.CharField(blank=True, max_length=120)),
|
||||
('environment_scope', models.CharField(blank=True, max_length=120)),
|
||||
('decision_rules', models.TextField(blank=True)),
|
||||
('troubleshooting_steps', models.TextField()),
|
||||
('risk_note', models.TextField(blank=True)),
|
||||
('status', models.CharField(default='draft', max_length=40)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_troubleshooting_plans', to=settings.AUTH_USER_MODEL)),
|
||||
('knowledge_base', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='troubleshooting_plans', to='knowledge.knowledgebase')),
|
||||
('reviewed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reviewed_troubleshooting_plans', to=settings.AUTH_USER_MODEL)),
|
||||
('source_ticket', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='generated_troubleshooting_plans', to='tickets.ticket')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-updated_at'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='knowledgechunk',
|
||||
name='troubleshooting_plan',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='chunks', to='knowledge.troubleshootingplan'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='governedassetversion',
|
||||
index=models.Index(fields=['status', 'updated_at'], name='knowledge_g_status_be77c0_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='governedassetversion',
|
||||
index=models.Index(fields=['asset', 'status', 'version'], name='knowledge_g_asset_i_52c120_idx'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='governedassetversion',
|
||||
constraint=models.UniqueConstraint(fields=('asset', 'version'), name='unique_governed_asset_version'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='governedassetversion',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('status', 'draft')), fields=('asset',), name='unique_draft_governed_asset_version'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='governedassetversion',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('status', 'pending_review')), fields=('asset',), name='unique_pending_governed_asset_version'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='governedasset',
|
||||
index=models.Index(fields=['asset_type', 'object_id'], name='knowledge_g_asset_t_fd5a02_idx'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='governedasset',
|
||||
constraint=models.UniqueConstraint(fields=('asset_type', 'object_id'), name='unique_governed_asset_object'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,105 @@
|
||||
from django.db import migrations
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
KNOWLEDGE_FIELDS = (
|
||||
"knowledge_base_id",
|
||||
"title",
|
||||
"problem",
|
||||
"applicable_scope",
|
||||
"category_code",
|
||||
"related_system",
|
||||
"environment_scope",
|
||||
"error_code",
|
||||
"root_cause",
|
||||
"troubleshooting_steps",
|
||||
"solution",
|
||||
"risk_note",
|
||||
"source_type",
|
||||
"source_ticket_id",
|
||||
)
|
||||
|
||||
PLAN_FIELDS = (
|
||||
"knowledge_base_id",
|
||||
"title",
|
||||
"problem_pattern",
|
||||
"applicable_scope",
|
||||
"category_code",
|
||||
"related_system",
|
||||
"environment_scope",
|
||||
"decision_rules",
|
||||
"troubleshooting_steps",
|
||||
"risk_note",
|
||||
"source_ticket_id",
|
||||
)
|
||||
|
||||
SOLUTION_FIELDS = (
|
||||
"title",
|
||||
"problem_signature",
|
||||
"related_system",
|
||||
"environment_scope",
|
||||
"category_id",
|
||||
"root_cause",
|
||||
"solution",
|
||||
"risk_note",
|
||||
"allow_auto_resolve",
|
||||
"requires_human_action",
|
||||
"requires_external_confirmation",
|
||||
"requires_production_change",
|
||||
)
|
||||
|
||||
|
||||
def backfill_governed_assets(apps, schema_editor):
|
||||
GovernedAsset = apps.get_model("knowledge", "GovernedAsset")
|
||||
GovernedAssetVersion = apps.get_model("knowledge", "GovernedAssetVersion")
|
||||
KnowledgeItem = apps.get_model("knowledge", "KnowledgeItem")
|
||||
TroubleshootingPlan = apps.get_model("knowledge", "TroubleshootingPlan")
|
||||
Solution = apps.get_model("solutions", "Solution")
|
||||
now = timezone.now()
|
||||
definitions = (
|
||||
("knowledge_item", KnowledgeItem, KNOWLEDGE_FIELDS),
|
||||
("troubleshooting_plan", TroubleshootingPlan, PLAN_FIELDS),
|
||||
("solution", Solution, SOLUTION_FIELDS),
|
||||
)
|
||||
for asset_type, model, fields in definitions:
|
||||
for instance in model.objects.all().iterator():
|
||||
asset, _ = GovernedAsset.objects.get_or_create(
|
||||
asset_type=asset_type,
|
||||
object_id=instance.id,
|
||||
defaults={"created_by_id": instance.created_by_id},
|
||||
)
|
||||
if asset.versions.exists():
|
||||
continue
|
||||
original_status = instance.status
|
||||
if original_status == "published":
|
||||
version_status = "published"
|
||||
elif original_status in {"offline", "disabled"}:
|
||||
version_status = "offline"
|
||||
if original_status == "disabled":
|
||||
instance.status = "offline"
|
||||
instance.save(update_fields=["status"])
|
||||
else:
|
||||
version_status = "draft"
|
||||
version = GovernedAssetVersion.objects.create(
|
||||
asset=asset,
|
||||
version=1,
|
||||
status=version_status,
|
||||
content={field: getattr(instance, field) for field in fields},
|
||||
created_by_id=instance.created_by_id,
|
||||
published_at=now if version_status == "published" else None,
|
||||
offline_at=now if version_status == "offline" else None,
|
||||
)
|
||||
if version_status == "published":
|
||||
asset.current_published_version = version
|
||||
asset.save(update_fields=["current_published_version"])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("knowledge", "0005_alter_knowledgechunk_options_governedasset_and_more"),
|
||||
("solutions", "0003_solution_allow_auto_resolve_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(backfill_governed_assets, migrations.RunPython.noop),
|
||||
]
|
||||
@@ -0,0 +1,42 @@
|
||||
# Generated by Django 5.2.15 on 2026-07-27 05:35
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('agents', '0005_agentversion_allowed_actions'),
|
||||
('knowledge', '0006_backfill_governed_assets'),
|
||||
('solutions', '0003_solution_allow_auto_resolve_and_more'),
|
||||
('tickets', '0013_ticketsupportanalysis_applied_solution_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='TicketKnowledgeDistillation',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('references', models.JSONField(default=list)),
|
||||
('retrieval_policy', models.JSONField(default=dict)),
|
||||
('should_distill', models.BooleanField(blank=True, null=True)),
|
||||
('reason', models.TextField(blank=True)),
|
||||
('assessment', models.JSONField(blank=True, default=dict)),
|
||||
('processed_at', models.DateTimeField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_knowledge_distillations', to=settings.AUTH_USER_MODEL)),
|
||||
('knowledge_item', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='distillation_records', to='knowledge.knowledgeitem')),
|
||||
('run', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='ticket_knowledge_distillation', to='agents.agentrun')),
|
||||
('solution', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='distillation_records', to='solutions.solution')),
|
||||
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='knowledge_distillations', to='tickets.ticket')),
|
||||
('troubleshooting_plan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='distillation_records', to='knowledge.troubleshootingplan')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-created_at'],
|
||||
'indexes': [models.Index(fields=['ticket', 'created_at'], name='knowledge_t_ticket__fb3275_idx')],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,73 @@
|
||||
import uuid
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("knowledge", "0007_ticketknowledgedistillation"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="knowledgedocument",
|
||||
name="public_id",
|
||||
field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="knowledgedocument",
|
||||
name="extension",
|
||||
field=models.CharField(blank=True, max_length=16),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="knowledgedocument",
|
||||
name="mime_type",
|
||||
field=models.CharField(blank=True, max_length=160),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="knowledgedocument",
|
||||
name="file_size",
|
||||
field=models.PositiveBigIntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="knowledgedocument",
|
||||
name="sha256",
|
||||
field=models.CharField(blank=True, max_length=64),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="knowledgedocument",
|
||||
name="scan_status",
|
||||
field=models.CharField(default="pending", max_length=40),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="knowledgedocument",
|
||||
name="scan_result",
|
||||
field=models.JSONField(blank=True, default=dict),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="knowledgedocument",
|
||||
name="processing_attempts",
|
||||
field=models.PositiveSmallIntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="knowledgedocument",
|
||||
name="task_id",
|
||||
field=models.CharField(blank=True, max_length=120),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="knowledgedocument",
|
||||
name="indexed_at",
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="knowledgedocument",
|
||||
index=models.Index(
|
||||
fields=["knowledge_base", "parse_status", "created_at"],
|
||||
name="knowledge_k_knowled_df0102_idx",
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="knowledgedocument",
|
||||
index=models.Index(fields=["sha256"], name="knowledge_k_sha256_4df74c_idx"),
|
||||
),
|
||||
]
|
||||
Reference in New Issue
Block a user