feat: init commint

This commit is contained in:
2026-09-18 15:26:19 +08:00
parent 3b61a767a0
commit 9f47dbb5cc
95 changed files with 7712 additions and 1137 deletions

View File

@@ -18,25 +18,16 @@ from apps.knowledge.embeddings import KnowledgeEmbeddingError
from apps.knowledge.models import KnowledgeItem
from apps.knowledge.services import retrieve_knowledge
from apps.solutions.models import Solution
from apps.tickets.models import Ticket, TicketActorSource, TicketSupportAnalysis
from apps.tickets.models import Ticket, TicketActorSource, TicketStatus, TicketSupportAnalysis
from apps.tickets.replies import ensure_customer_reply_task
from apps.tickets.services import (
TicketDataError,
TicketStatusError,
adopt_solution_for_ticket,
get_allowed_transitions,
from apps.tickets.services import get_allowed_transitions
from apps.tickets.support_contract import (
SUPPORT_OUTPUT_FIELDS,
missing_support_output_fields,
)
SUPPORT_AGENT_CODE = "support-analysis"
RETRIEVAL_POLICY_VERSION = "published_scope_hybrid_rerank_v2"
SUPPORT_OUTPUT_FIELDS = {
"analysis_summary",
"troubleshooting_steps",
"candidate_solution_ids",
"customer_reply_draft",
"citations",
"auto_resolve_recommended",
}
class SupportAnalysisError(ValueError):
@@ -326,6 +317,10 @@ def retrieve_support_references(ticket: Ticket, *, limit: int = 8) -> tuple[str,
}
)
minimum_score = min(max(float(settings.SUPPORT_REFERENCE_MIN_SCORE), 0.0), 1.0)
candidates = [
item for item in candidates if float(item.get("score", 0)) >= minimum_score
]
candidates.sort(key=lambda item: (item["score"], item["source_version"]), reverse=True)
references = []
for rank, item in enumerate(candidates[:limit], start=1):
@@ -343,8 +338,7 @@ def validate_support_agent_contract(version: AgentVersion) -> None:
output_schema = last_step.skill_version.output_schema
if not isinstance(output_schema, dict) or output_schema.get("type") != "object":
raise SupportAnalysisError("支持分析 Agent 的最终输出必须是对象")
required = set(output_schema.get("required", []))
missing = sorted(SUPPORT_OUTPUT_FIELDS - required)
missing = missing_support_output_fields(output_schema)
if missing:
raise SupportAnalysisError(
f"支持分析 Agent 最终输出契约缺少字段:{', '.join(missing)}"
@@ -463,10 +457,24 @@ def _automation_decision(
and not solution.requires_production_change
),
"ticket_not_archived": ticket.archived_at is None,
"status_transition_allowed": "resolved"
in get_allowed_transitions(
ticket.status,
ticket.category.code if ticket.category_id else None,
"status_transition_allowed": (
"resolved"
in get_allowed_transitions(
ticket.status,
ticket.category.code if ticket.category_id else None,
)
or (
"pending_confirm"
in get_allowed_transitions(
ticket.status,
ticket.category.code if ticket.category_id else None,
)
and "resolved"
in get_allowed_transitions(
TicketStatus.PENDING_CONFIRM,
ticket.category.code if ticket.category_id else None,
)
)
),
"no_conflicting_resolution": existing_resolution is None
or existing_resolution.linked_solution_id in {None, solution.id if solution else None},
@@ -490,6 +498,7 @@ def _automation_decision(
decision = {
"policy_version": "support_auto_resolve_v1",
"status": "eligible" if not reasons else "manual_recommendation",
"eligible": not reasons,
"threshold": threshold,
"solution_id": solution.id if solution else None,
"score": round(float(reference.get("score", 0)), 4) if reference else 0.0,
@@ -520,32 +529,22 @@ def process_support_analysis_result(run: AgentRun) -> TicketSupportAnalysis | No
if analysis.processed_at is not None:
return analysis
output = run.final_output if isinstance(run.final_output, dict) else {}
ensure_customer_reply_task(
reply_task = ensure_customer_reply_task(
analysis,
str(output.get("customer_reply_draft") or ""),
)
if reply_task is None:
raise SupportAnalysisError("支持分析未生成可发送的客户回复")
decision, solution = _automation_decision(analysis, output)
analysis.result_mode = "trusted" if analysis.references else "manual_only"
if decision["status"] == "eligible" and solution is not None:
try:
adopt_solution_for_ticket(
analysis.ticket,
solution,
run.created_by,
remark="支持分析 Agent 通过统一策略自动采纳解决方案",
source=TicketActorSource.AGENT,
agent_run_id=run.id,
)
except (TicketDataError, TicketStatusError):
decision["status"] = "manual_recommendation"
decision["reasons"].append("domain_service_rejected")
else:
decision["status"] = "auto_resolved"
decision["applied"] = True
analysis.applied_solution = solution
# 方案只在技术支持执行“一次批准发送”后采纳,分析阶段不提前改变工单事实。
decision["status"] = "waiting_approval"
analysis.automation_decision = decision
analysis.processed_at = timezone.now()
analysis.save(
update_fields=[
"result_mode",
"automation_decision",
"applied_solution",
"processed_at",
@@ -580,7 +579,12 @@ def process_support_analysis_result(run: AgentRun) -> TicketSupportAnalysis | No
@transaction.atomic
def start_ticket_support_analysis(ticket: Ticket, *, created_by=None) -> TicketSupportAnalysis:
def start_ticket_support_analysis(
ticket: Ticket,
*,
created_by=None,
source: str = TicketActorSource.HUMAN,
) -> TicketSupportAnalysis:
"""为工单创建唯一活动分析并投递绑定正式 Agent 版本的运行。"""
ticket = (
@@ -680,7 +684,7 @@ def start_ticket_support_analysis(ticket: Ticket, *, created_by=None) -> TicketS
"reference_count": len(references),
},
operator=created_by,
source=TicketActorSource.HUMAN,
source=source,
agent_run_id=run.id,
metadata={"retrieval_policy_version": RETRIEVAL_POLICY_VERSION},
)