388 lines
15 KiB
Python
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
|