Files

388 lines
15 KiB
Python

"""从可信已解决工单生成受治理的知识、排查方案和解决方案草稿。"""
from django.db import transaction
from django.utils import timezone
from apps.agents.models import AgentDefinition, AgentRun, AgentVersion
from apps.agents.services import queue_agent_run
from apps.audit.models import OperationAuditLog
from apps.knowledge.governance import ensure_governed_asset
from apps.knowledge.models import (
GovernedAssetType,
KnowledgeBase,
KnowledgeItem,
TicketKnowledgeDistillation,
TroubleshootingPlan,
)
from apps.solutions.models import Solution
from apps.tickets.models import Ticket, TicketActorSource
from apps.tickets.support_analysis import (
RETRIEVAL_POLICY_VERSION,
retrieve_support_references,
)
KNOWLEDGE_DISTILLATION_AGENT_CODE = "knowledge-distillation"
DISTILLATION_OUTPUT_FIELDS = {
"should_distill",
"reason",
"assessment",
"citations",
"knowledge_draft",
"troubleshooting_plan_draft",
"solution_draft",
}
ASSESSMENT_FIELDS = {
"novelty_score",
"reuse_value_score",
"completeness_score",
"duplicate_risk_score",
}
class KnowledgeDistillationError(ValueError):
pass
def validate_knowledge_distillation_contract(version: AgentVersion) -> None:
last_step = version.steps.select_related("skill_version").order_by("-order").first()
if last_step is None:
raise KnowledgeDistillationError("知识蒸馏 Agent 没有可执行步骤")
output_schema = last_step.skill_version.output_schema
if not isinstance(output_schema, dict) or output_schema.get("type") != "object":
raise KnowledgeDistillationError("知识蒸馏 Agent 的最终输出必须是对象")
missing = sorted(DISTILLATION_OUTPUT_FIELDS - set(output_schema.get("required", [])))
if missing:
raise KnowledgeDistillationError(
f"知识蒸馏 Agent 最终输出契约缺少字段:{', '.join(missing)}"
)
def _require_nonempty_fields(payload: dict, fields: set[str], label: str) -> None:
if not isinstance(payload, dict):
raise KnowledgeDistillationError(f"{label}必须是对象")
missing = sorted(field for field in fields if not str(payload.get(field) or "").strip())
if missing:
raise KnowledgeDistillationError(f"{label}缺少字段:{', '.join(missing)}")
def validate_knowledge_distillation_output(run: AgentRun, output) -> None:
if run.definition.code != KNOWLEDGE_DISTILLATION_AGENT_CODE:
return
if not isinstance(output, dict):
raise KnowledgeDistillationError("知识蒸馏结果必须是对象")
missing = sorted(DISTILLATION_OUTPUT_FIELDS - set(output))
if missing:
raise KnowledgeDistillationError(f"知识蒸馏结果缺少字段:{', '.join(missing)}")
try:
distillation = run.ticket_knowledge_distillation
except TicketKnowledgeDistillation.DoesNotExist as error:
raise KnowledgeDistillationError("知识蒸馏运行未关联真实工单") from error
citations = output.get("citations")
valid_ranks = {item.get("rank") for item in distillation.references}
if not isinstance(citations, list) or any(
not isinstance(rank, int) or rank not in valid_ranks for rank in citations
):
raise KnowledgeDistillationError("知识蒸馏结果包含无效引用")
assessment = output.get("assessment")
if not isinstance(assessment, dict) or set(assessment) != ASSESSMENT_FIELDS:
raise KnowledgeDistillationError("知识价值评分字段不完整")
if any(
isinstance(value, bool)
or not isinstance(value, (int, float))
or not 0 <= float(value) <= 1
for value in assessment.values()
):
raise KnowledgeDistillationError("知识价值评分必须在 0 到 1 之间")
if output.get("should_distill") is not True:
return
_require_nonempty_fields(
output.get("knowledge_draft"),
{"title", "problem", "solution"},
"知识草稿",
)
_require_nonempty_fields(
output.get("troubleshooting_plan_draft"),
{"title", "problem_pattern", "troubleshooting_steps"},
"排查方案草稿",
)
solution_draft = output.get("solution_draft")
if not isinstance(solution_draft, dict):
raise KnowledgeDistillationError("解决方案草稿必须是对象")
if solution_draft.get("create") is True:
_require_nonempty_fields(
solution_draft,
{"title", "problem_signature", "root_cause", "solution"},
"解决方案草稿",
)
def _ticket_snapshot(ticket: Ticket) -> dict:
resolution = ticket.resolution
return {
"id": ticket.id,
"ticket_no": ticket.ticket_no,
"title": ticket.title,
"description": ticket.description,
"problem_performance": ticket.problem_performance,
"category_code": ticket.category.code if ticket.category_id else "",
"related_system": ticket.related_system,
"environment": ticket.environment,
"product_name": ticket.product_name,
"module_name": ticket.module_name,
"attempted_actions": ticket.attempted_actions,
"error_information": ticket.error_information,
"resolution_type": resolution.resolution_type,
"root_cause": resolution.root_cause,
"solution": resolution.solution_text,
}
@transaction.atomic
def start_ticket_knowledge_distillation(
ticket: Ticket,
*,
created_by=None,
) -> TicketKnowledgeDistillation:
"""从可信解决结果启动幂等蒸馏运行,并冻结工单输入快照。"""
ticket = (
Ticket.objects.select_for_update(of=("self",))
.select_related("category", "resolution")
.get(id=ticket.id)
)
if ticket.archived_at is not None:
raise KnowledgeDistillationError("已归档工单不能启动知识蒸馏")
if ticket.status not in {"resolved", "closed"}:
raise KnowledgeDistillationError("只有已解决或已关闭工单可以沉淀知识")
if not hasattr(ticket, "resolution"):
raise KnowledgeDistillationError("工单缺少解决结论")
active = (
TicketKnowledgeDistillation.objects.select_related("run")
.filter(ticket=ticket, run__status__in=["queued", "running"])
.first()
)
if active is not None:
return active
completed = (
TicketKnowledgeDistillation.objects.select_related("run")
.filter(ticket=ticket, run__status="succeeded")
.first()
)
if completed is not None:
if completed.processed_at is None:
process_knowledge_distillation_result(completed.run)
completed.refresh_from_db()
return completed
if not KnowledgeBase.objects.filter(enabled=True).exists():
raise KnowledgeDistillationError("尚未配置启用的知识库")
definition = AgentDefinition.objects.select_related(
"active_version__model_deployment__provider"
).filter(code=KNOWLEDGE_DISTILLATION_AGENT_CODE).first()
if definition is None or definition.active_version_id is None:
raise KnowledgeDistillationError("尚未发布知识蒸馏 Agent")
version = definition.active_version
validate_knowledge_distillation_contract(version)
query, references = retrieve_support_references(ticket)
input_payload = {
"ticket": _ticket_snapshot(ticket),
"existing_matches": references,
}
run = queue_agent_run(
version,
input_payload,
created_by=created_by,
config_snapshot_extra={
"business_object": {"type": "ticket", "id": ticket.id},
"retrieval": {
"policy_version": RETRIEVAL_POLICY_VERSION,
"allowed_statuses": {
"knowledge": ["published"],
"solution": ["published"],
"historical_ticket": ["resolved", "closed"],
},
"reference_count": len(references),
},
"governance": {
"generated_status": "draft",
"self_review_allowed": False,
"publish_requires_review": True,
},
},
defer_until_commit=True,
)
distillation = TicketKnowledgeDistillation.objects.create(
ticket=ticket,
run=run,
references=references,
retrieval_policy=run.config_snapshot["retrieval"],
created_by=created_by,
)
ticket.events.create(
event_type="knowledge_distillation_started",
to_value={
"run_public_id": str(run.public_id),
"reference_count": len(references),
},
operator=created_by,
source=TicketActorSource.HUMAN,
agent_run_id=run.id,
)
OperationAuditLog.objects.create(
user=created_by,
module="knowledge",
action="knowledge_distillation_started",
target_type="ticket",
target_id=str(ticket.id),
request_snapshot={
"agent_run_public_id": str(run.public_id),
"reference_count": len(references),
},
result="success",
)
return distillation
def _value(payload: dict, field: str, fallback: str = "") -> str:
return str(payload.get(field) or fallback).strip()
@transaction.atomic
def process_knowledge_distillation_result(
run: AgentRun,
) -> TicketKnowledgeDistillation | None:
"""校验 Agent 产物并生成待独立审核的受治理资产草稿。"""
if run.definition.code != KNOWLEDGE_DISTILLATION_AGENT_CODE or run.status != "succeeded":
return None
distillation = (
TicketKnowledgeDistillation.objects.select_for_update(of=("self",))
.select_related("ticket__category", "ticket__resolution", "run__created_by")
.get(run=run)
)
if distillation.processed_at is not None:
return distillation
output = run.final_output if isinstance(run.final_output, dict) else {}
validate_knowledge_distillation_output(run, output)
distillation.should_distill = output.get("should_distill") is True
distillation.reason = _value(output, "reason")
distillation.assessment = output.get("assessment", {})
if distillation.should_distill:
knowledge_base = KnowledgeBase.objects.filter(enabled=True).order_by("id").first()
if knowledge_base is None:
raise KnowledgeDistillationError("尚未配置启用的知识库")
ticket = distillation.ticket
category_code = ticket.category.code if ticket.category_id else ""
knowledge_payload = output["knowledge_draft"]
knowledge_item = KnowledgeItem.objects.create(
knowledge_base=knowledge_base,
title=_value(knowledge_payload, "title"),
problem=_value(knowledge_payload, "problem"),
applicable_scope=_value(knowledge_payload, "applicable_scope"),
category_code=category_code,
related_system=ticket.related_system,
environment_scope=ticket.environment,
error_code=_value(knowledge_payload, "error_code"),
root_cause=_value(knowledge_payload, "root_cause"),
troubleshooting_steps=_value(knowledge_payload, "troubleshooting_steps"),
solution=_value(knowledge_payload, "solution"),
risk_note=_value(knowledge_payload, "risk_note"),
status="draft",
source_type="agent",
source_ticket_id=ticket.id,
created_by=run.created_by,
)
ensure_governed_asset(
GovernedAssetType.KNOWLEDGE_ITEM,
knowledge_item,
actor=run.created_by,
)
plan_payload = output["troubleshooting_plan_draft"]
troubleshooting_plan = TroubleshootingPlan.objects.create(
knowledge_base=knowledge_base,
title=_value(plan_payload, "title"),
problem_pattern=_value(plan_payload, "problem_pattern"),
applicable_scope=_value(plan_payload, "applicable_scope"),
category_code=category_code,
related_system=ticket.related_system,
environment_scope=ticket.environment,
decision_rules=_value(plan_payload, "decision_rules"),
troubleshooting_steps=_value(plan_payload, "troubleshooting_steps"),
risk_note=_value(plan_payload, "risk_note"),
status="draft",
source_ticket=ticket,
created_by=run.created_by,
)
ensure_governed_asset(
GovernedAssetType.TROUBLESHOOTING_PLAN,
troubleshooting_plan,
actor=run.created_by,
)
solution = None
solution_payload = output["solution_draft"]
if solution_payload.get("create") is True:
solution = Solution.objects.create(
title=_value(solution_payload, "title"),
problem_signature=_value(solution_payload, "problem_signature"),
related_system=ticket.related_system,
environment_scope=ticket.environment,
category=ticket.category,
root_cause=_value(solution_payload, "root_cause"),
solution=_value(solution_payload, "solution"),
risk_note=_value(solution_payload, "risk_note"),
status="draft",
allow_auto_resolve=False,
requires_human_action=True,
created_by=run.created_by,
)
ensure_governed_asset(
GovernedAssetType.SOLUTION,
solution,
actor=run.created_by,
)
distillation.knowledge_item = knowledge_item
distillation.troubleshooting_plan = troubleshooting_plan
distillation.solution = solution
distillation.processed_at = timezone.now()
distillation.save(
update_fields=[
"should_distill",
"reason",
"assessment",
"knowledge_item",
"troubleshooting_plan",
"solution",
"processed_at",
]
)
distillation.ticket.events.create(
event_type="knowledge_distillation_completed",
to_value={
"should_distill": distillation.should_distill,
"knowledge_item_id": distillation.knowledge_item_id,
"troubleshooting_plan_id": distillation.troubleshooting_plan_id,
"solution_id": distillation.solution_id,
},
operator=run.created_by,
source=TicketActorSource.AGENT,
agent_run_id=run.id,
metadata={"assessment": distillation.assessment},
)
OperationAuditLog.objects.create(
user=run.created_by,
module="knowledge",
action="knowledge_distillation_completed",
target_type="ticket",
target_id=str(distillation.ticket_id),
request_snapshot={
"agent_run_public_id": str(run.public_id),
"should_distill": distillation.should_distill,
"generated_asset_ids": {
"knowledge_item": distillation.knowledge_item_id,
"troubleshooting_plan": distillation.troubleshooting_plan_id,
"solution": distillation.solution_id,
},
},
result="success",
)
return distillation