"""从可信已解决工单生成受治理的知识、排查方案和解决方案草稿。""" 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() .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() .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