feat: add governed knowledge lifecycle

This commit is contained in:
hailin
2026-07-27 19:01:19 +08:00
parent 1a49ce4eae
commit 07c9ea1cc4
32 changed files with 5893 additions and 178 deletions

View File

@@ -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),
]

View File

@@ -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),
),
]

View File

@@ -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),
]

View File

@@ -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'),
),
]

View File

@@ -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),
]

View File

@@ -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')],
},
),
]

View File

@@ -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"),
),
]