feat: 重构处理历史与通知留痕追踪
This commit is contained in:
@@ -57,6 +57,14 @@ def run_agent(scenario_config: dict, user_input: str, options: dict | None = Non
|
||||
latency_ms=latency_ms,
|
||||
status="failed",
|
||||
error=str(llm_response.error or "未知模型错误"),
|
||||
conversation_id=str(options.get("conversation_id", "")),
|
||||
batch_id=str(options.get("batch_id", "")),
|
||||
product_name=str(options.get("product_name", "")),
|
||||
notification_payload=_build_notification_payload(
|
||||
{"notify_reason": "task_failed", "owner_roles": []},
|
||||
options=options,
|
||||
status="failed",
|
||||
),
|
||||
)
|
||||
|
||||
structured_output, _ = parse_structured_output(llm_response.content, output_type)
|
||||
@@ -70,6 +78,11 @@ def run_agent(scenario_config: dict, user_input: str, options: dict | None = Non
|
||||
model_name=llm_response.model_name or "unknown-model",
|
||||
latency_ms=latency_ms,
|
||||
status="success",
|
||||
conversation_id=str(options.get("conversation_id", "")),
|
||||
batch_id=str(options.get("batch_id", "")),
|
||||
product_name=str(options.get("product_name", "")),
|
||||
node_results=_build_node_results(output_type, structured_output),
|
||||
notification_payload=_build_notification_payload(structured_output, options=options, status="success"),
|
||||
)
|
||||
|
||||
|
||||
@@ -151,3 +164,29 @@ def _format_tool_calls(tool_calls: list[dict]) -> str:
|
||||
f"{index}. 工具={tool_call.get('tool_name')} 失败={tool_call.get('error', '未知错误')}"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _build_node_results(output_type: str, structured_output: dict) -> list[dict]:
|
||||
return [
|
||||
{
|
||||
"code": output_type,
|
||||
"label": output_type,
|
||||
"status": "已完成",
|
||||
"summary": structured_output.get("summary") or structured_output.get("answer", ""),
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def _build_notification_payload(structured_output: dict, options: dict, status: str) -> dict:
|
||||
notify_reason = structured_output.get("notify_reason") or (
|
||||
"task_completed" if status == "success" else "task_failed"
|
||||
)
|
||||
owners = structured_output.get("owner_roles") or []
|
||||
return {
|
||||
"batch_id": str(options.get("batch_id", "")),
|
||||
"conversation_id": str(options.get("conversation_id", "")),
|
||||
"product_name": str(options.get("product_name", "")),
|
||||
"notify_reason": notify_reason,
|
||||
"owners": owners,
|
||||
"status": status,
|
||||
}
|
||||
|
||||
@@ -20,3 +20,8 @@ class AgentResult:
|
||||
latency_ms: int = 0
|
||||
status: str = "success"
|
||||
error: str = ""
|
||||
conversation_id: str = ""
|
||||
batch_id: str = ""
|
||||
product_name: str = ""
|
||||
node_results: list = field(default_factory=list)
|
||||
notification_payload: dict = field(default_factory=dict)
|
||||
|
||||
@@ -6,6 +6,8 @@ SUPPORTED_OUTPUT_TYPES = {
|
||||
"registration_field_extraction_report",
|
||||
"registration_consistency_report",
|
||||
"registration_risk_report",
|
||||
"registration_word_export_report",
|
||||
"feishu_notification_report",
|
||||
"ticket_response",
|
||||
"quality_report",
|
||||
"risk_audit_report",
|
||||
|
||||
@@ -41,6 +41,60 @@ OUTPUT_FIELD_TEMPLATES = {
|
||||
"suggestions": [],
|
||||
"references": [],
|
||||
},
|
||||
"registration_overview_report": {
|
||||
"batch_id": "",
|
||||
"product_name": "",
|
||||
"file_count": 0,
|
||||
"total_page_count": 0,
|
||||
"chapter_summary": [],
|
||||
"documents": [],
|
||||
"warnings": [],
|
||||
},
|
||||
"registration_completeness_report": {
|
||||
"summary": "",
|
||||
"missing_items": [],
|
||||
"misplaced_items": [],
|
||||
"risk_level": "medium",
|
||||
"references": [],
|
||||
},
|
||||
"registration_field_extraction_report": {
|
||||
"summary": "",
|
||||
"field_items": [],
|
||||
"low_confidence_items": [],
|
||||
"references": [],
|
||||
},
|
||||
"registration_consistency_report": {
|
||||
"summary": "",
|
||||
"conflict_items": [],
|
||||
"mixed_document_risks": [],
|
||||
"risk_level": "medium",
|
||||
"references": [],
|
||||
},
|
||||
"registration_risk_report": {
|
||||
"summary": "",
|
||||
"risk_items": [],
|
||||
"highest_risk_level": "medium",
|
||||
"pass_status": "review_required",
|
||||
"manual_review_items": [],
|
||||
"owner_roles": [],
|
||||
"suggestions": [],
|
||||
"notify_reason": "task_completed",
|
||||
},
|
||||
"registration_word_export_report": {
|
||||
"summary": "",
|
||||
"export_status": "draft_only",
|
||||
"blocked_items": [],
|
||||
"download_url": "",
|
||||
},
|
||||
"feishu_notification_report": {
|
||||
"batch_id": "",
|
||||
"conversation_id": "",
|
||||
"notify_reason": "task_completed",
|
||||
"mentioned_users": [],
|
||||
"message_status": "pending",
|
||||
"web_detail_url": "",
|
||||
"receipt": {},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from agent_core.results import AgentResult
|
||||
|
||||
from .models import AgentAuditLog
|
||||
from .models import AgentAuditLog, NotificationRecord
|
||||
|
||||
|
||||
def create_audit_log(
|
||||
@@ -61,3 +61,36 @@ def _mask_token_after_marker(value: str, marker: str) -> str:
|
||||
secret, separator, rest = suffix.partition(" ")
|
||||
masked_secret = "sk-***" if secret.startswith("sk-") else "***"
|
||||
return f"{prefix}{marker}{masked_secret}{separator}{rest}"
|
||||
|
||||
|
||||
def create_notification_record(
|
||||
*,
|
||||
batch_id: str,
|
||||
conversation_id: str,
|
||||
product_name: str,
|
||||
trigger_source: str,
|
||||
notify_reason: str,
|
||||
owner_role: str,
|
||||
feishu_user_id: str,
|
||||
message_status: str,
|
||||
web_detail_url: str,
|
||||
receipt: dict,
|
||||
) -> NotificationRecord:
|
||||
"""
|
||||
保存通知留痕。
|
||||
|
||||
V1 先把通知载荷和结果状态稳定落库,
|
||||
真实飞书发送可在后续阶段接入。
|
||||
"""
|
||||
return NotificationRecord.objects.create(
|
||||
batch_id=batch_id,
|
||||
conversation_id=conversation_id,
|
||||
product_name=product_name,
|
||||
trigger_source=trigger_source,
|
||||
notify_reason=notify_reason,
|
||||
owner_role=owner_role,
|
||||
feishu_user_id=feishu_user_id,
|
||||
message_status=message_status,
|
||||
web_detail_url=web_detail_url,
|
||||
receipt=receipt,
|
||||
)
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
|
||||
from .models import AgentAuditLog
|
||||
from .models import AgentAuditLog, NotificationRecord
|
||||
|
||||
|
||||
def log_list(request):
|
||||
# 列表页支持按场景筛选,方便演示时快速定位同一类场景的执行记录。
|
||||
# 处理历史页支持按批次、产品和状态筛选。
|
||||
scenario_id = (request.GET.get("scenario_id") or "").strip()
|
||||
keyword = (request.GET.get("keyword") or "").strip()
|
||||
logs = AgentAuditLog.objects.all()
|
||||
if scenario_id:
|
||||
logs = logs.filter(scenario_id=scenario_id)
|
||||
if keyword:
|
||||
logs = logs.filter(product_name__icontains=keyword) | logs.filter(batch_id__icontains=keyword)
|
||||
return render(
|
||||
request,
|
||||
"audit/log_list.html",
|
||||
{
|
||||
"logs": logs,
|
||||
"selected_scenario_id": scenario_id,
|
||||
"keyword": keyword,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -23,4 +27,12 @@ def log_detail(request, log_id: int):
|
||||
# 详情页只负责按主键加载审计快照并渲染;
|
||||
# 所有脱敏和字段映射都应在服务层完成。
|
||||
audit_log = get_object_or_404(AgentAuditLog, pk=log_id)
|
||||
return render(request, "audit/log_detail.html", {"log": audit_log})
|
||||
notifications = NotificationRecord.objects.filter(
|
||||
conversation_id=audit_log.conversation_id,
|
||||
batch_id=audit_log.batch_id,
|
||||
)
|
||||
return render(
|
||||
request,
|
||||
"audit/log_detail.html",
|
||||
{"log": audit_log, "notifications": notifications},
|
||||
)
|
||||
|
||||
0
apps/chat/migrations/__init__.py
Normal file
0
apps/chat/migrations/__init__.py
Normal file
@@ -2,7 +2,7 @@ from django.shortcuts import get_object_or_404, redirect, render
|
||||
|
||||
from agent_core.orchestrator import run_agent
|
||||
from agent_core.results import AgentResult
|
||||
from apps.audit.services import create_audit_log
|
||||
from apps.audit.services import create_audit_log, create_notification_record
|
||||
from apps.documents.models import SubmissionBatch, UploadedDocument
|
||||
from apps.scenarios.services import get_scenario
|
||||
|
||||
@@ -70,6 +70,7 @@ def detail(request, conversation_id: str):
|
||||
conversation_id=conversation.conversation_id,
|
||||
product_name=conversation.product_name,
|
||||
)
|
||||
_persist_notification_records(result)
|
||||
active_node = "risk"
|
||||
|
||||
return render(
|
||||
@@ -89,3 +90,23 @@ def detail(request, conversation_id: str):
|
||||
"active_node": active_node,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _persist_notification_records(result: AgentResult) -> None:
|
||||
payload = result.notification_payload or {}
|
||||
owners = payload.get("owners") or []
|
||||
if not owners:
|
||||
return
|
||||
for owner in owners:
|
||||
create_notification_record(
|
||||
batch_id=payload.get("batch_id", ""),
|
||||
conversation_id=payload.get("conversation_id", ""),
|
||||
product_name=payload.get("product_name", ""),
|
||||
trigger_source="agent_execution",
|
||||
notify_reason=payload.get("notify_reason", "task_completed"),
|
||||
owner_role=owner.get("owner_role", ""),
|
||||
feishu_user_id=owner.get("feishu_user_id", ""),
|
||||
message_status="sent" if result.status == "success" else "failed",
|
||||
web_detail_url="",
|
||||
receipt={"status": result.status},
|
||||
)
|
||||
|
||||
@@ -1,73 +1,66 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}审计日志详情{% endblock %}
|
||||
{% block title %}处理历史详情{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-header">
|
||||
<span class="eyebrow">Audit Snapshot</span>
|
||||
<h1 class="page-title">审计日志 #{{ log.id }}</h1>
|
||||
<p class="page-lead">详情页集中展示当前请求的输入、结构化输出、引用来源、工具调用和原始输出,用来解释这一轮 Agent 执行到底做了什么。</p>
|
||||
<div class="button-row">
|
||||
<a class="button" href="{% url 'audit:list' %}">返回审计列表</a>
|
||||
<a class="button" href="{% url 'platform_ui:command-center' %}">返回工作台大屏</a>
|
||||
<span class="eyebrow">History Detail</span>
|
||||
<h1 class="page-title">处理历史 #{{ log.id }}</h1>
|
||||
<p class="page-lead">集中展示本次执行的业务上下文、结构化结果、引用来源与通知留痕。</p>
|
||||
<div class="badge-row">
|
||||
<span class="pill pill-accent">批次:{{ log.batch_id|default:"-" }}</span>
|
||||
<span class="pill">会话:{{ log.conversation_id|default:"-" }}</span>
|
||||
<span class="pill">产品:{{ log.product_name|default:"-" }}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="hero-metrics">
|
||||
<article class="metric-card">
|
||||
<div class="metric-label">场景</div>
|
||||
<div class="metric-value">{{ log.scenario_name }}</div>
|
||||
<section class="grid-2">
|
||||
<article class="panel">
|
||||
<h2 class="section-title">执行上下文</h2>
|
||||
<ul class="detail-list">
|
||||
<li class="detail-item"><strong>用户输入</strong><div>{{ log.user_input|linebreaksbr }}</div></li>
|
||||
<li class="detail-item"><strong>最终回答</strong><div>{{ log.final_answer|linebreaksbr }}</div></li>
|
||||
<li class="detail-item"><strong>结构化输出</strong><pre class="code-block">{{ log.structured_output }}</pre></li>
|
||||
</ul>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<div class="metric-label">状态</div>
|
||||
<div class="metric-value">{{ log.get_status_display_text }}</div>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<div class="metric-label">耗时</div>
|
||||
<div class="metric-value">{{ log.latency_ms }} ms</div>
|
||||
|
||||
<article class="panel">
|
||||
<h2 class="section-title">执行证据</h2>
|
||||
<ul class="detail-list">
|
||||
<li class="detail-item"><strong>引用来源</strong><pre class="code-block">{{ log.retrieved_chunks }}</pre></li>
|
||||
<li class="detail-item"><strong>工具调用</strong><pre class="code-block">{{ log.tool_calls }}</pre></li>
|
||||
<li class="detail-item"><strong>原始输出</strong><pre class="code-block">{{ log.raw_output }}</pre></li>
|
||||
</ul>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="layout-two-columns">
|
||||
<div class="stack">
|
||||
<article class="panel">
|
||||
<h2 class="section-title">用户输入</h2>
|
||||
<div class="detail-item">{{ log.user_input|linebreaksbr }}</div>
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
<h2 class="section-title">最终回答</h2>
|
||||
<div class="detail-item">{{ log.final_answer|linebreaksbr }}</div>
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
<h2 class="section-title">结构化输出</h2>
|
||||
<pre class="code-block">{{ log.structured_output }}</pre>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="stack">
|
||||
<article class="panel">
|
||||
<h2 class="section-title">引用来源</h2>
|
||||
<pre class="code-block">{{ log.retrieved_chunks }}</pre>
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
<h2 class="section-title">工具调用</h2>
|
||||
<pre class="code-block">{{ log.tool_calls }}</pre>
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
<h2 class="section-title">原始输出</h2>
|
||||
<pre class="code-block">{{ log.raw_output }}</pre>
|
||||
</article>
|
||||
|
||||
{% if log.error_message %}
|
||||
<article class="panel">
|
||||
<h2 class="section-title">错误信息</h2>
|
||||
<pre class="code-block">{{ log.error_message }}</pre>
|
||||
</article>
|
||||
{% endif %}
|
||||
<section class="panel">
|
||||
<h2 class="section-title">通知留痕</h2>
|
||||
<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>触发原因</th>
|
||||
<th>责任角色</th>
|
||||
<th>飞书用户</th>
|
||||
<th>消息状态</th>
|
||||
<th>详情链接</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in notifications %}
|
||||
<tr>
|
||||
<td>{{ item.notify_reason }}</td>
|
||||
<td>{{ item.owner_role }}</td>
|
||||
<td>{{ item.feishu_user_id }}</td>
|
||||
<td>{{ item.message_status }}</td>
|
||||
<td>{{ item.web_detail_url|default:"-" }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="5">当前执行尚无通知留痕。</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,43 +1,38 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}审计日志{% endblock %}
|
||||
{% block title %}处理历史{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-header">
|
||||
<span class="eyebrow">Audit Trail</span>
|
||||
<h1 class="page-title">审计日志与执行留痕中心</h1>
|
||||
<p class="page-lead">每次 Agent 执行都会保留输入、结构化结果、引用片段、工具调用和最终输出。这个页面用于说明系统为何可追溯、可复核、可解释。</p>
|
||||
{% if selected_scenario_id %}
|
||||
<div class="badge-row">
|
||||
<span class="pill pill-accent">当前筛选场景:{{ selected_scenario_id }}</span>
|
||||
<a class="button" href="{% url 'audit:list' %}">清空筛选</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<span class="eyebrow">Processing History</span>
|
||||
<h1 class="page-title">处理历史</h1>
|
||||
<p class="page-lead">按批次、产品和会话回看审核执行、结构化结论与通知留痕。</p>
|
||||
</section>
|
||||
|
||||
<section class="hero-metrics">
|
||||
<article class="metric-card">
|
||||
<div class="metric-label">日志总数</div>
|
||||
<div class="metric-value">{{ logs|length }}</div>
|
||||
<div class="metric-note">当前页面加载的执行快照数量。</div>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<div class="metric-label">最近状态</div>
|
||||
<div class="metric-value">{% if logs %}{{ logs.0.get_status_display_text }}{% else %}暂无{% endif %}</div>
|
||||
<div class="metric-note">默认按时间倒序展示最近一次 Agent 执行。</div>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<div class="metric-label">最近场景</div>
|
||||
<div class="metric-value">{% if logs %}{{ logs.0.scenario_name }}{% else %}暂无{% endif %}</div>
|
||||
<div class="metric-note">便于快速定位当前复试演示对应的执行记录。</div>
|
||||
</article>
|
||||
<section class="panel">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2 class="section-title">历史筛选</h2>
|
||||
<p class="section-copy">支持按产品名称或批次号搜索。</p>
|
||||
</div>
|
||||
</div>
|
||||
<form method="get" class="grid-2">
|
||||
<div>
|
||||
<label for="id_keyword">产品名称 / 批次号</label>
|
||||
<input id="id_keyword" type="text" name="keyword" value="{{ keyword }}" placeholder="例如:新型冠状病毒 或 SUB-20260604-001">
|
||||
</div>
|
||||
<div class="button-row" style="align-items: end;">
|
||||
<button type="submit">筛选历史</button>
|
||||
<a class="button" href="{% url 'audit:list' %}">清空</a>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2 class="section-title">执行快照列表</h2>
|
||||
<p class="section-copy">保留真实审计数据列表,同时把展示形式升级为与首页、大屏一致的分析板风格。</p>
|
||||
<p class="section-copy">围绕 `batch_id / conversation_id / product_name` 展示处理历史。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
@@ -46,11 +41,13 @@
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>场景</th>
|
||||
<th>产品名称</th>
|
||||
<th>批次号</th>
|
||||
<th>会话</th>
|
||||
<th>输入摘要</th>
|
||||
<th>状态</th>
|
||||
<th>模型</th>
|
||||
<th>耗时</th>
|
||||
<th>创建时间</th>
|
||||
<th>时间</th>
|
||||
<th>详情</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -59,17 +56,19 @@
|
||||
<tr>
|
||||
<td>{{ log.id }}</td>
|
||||
<td>{{ log.scenario_name }}</td>
|
||||
<td>{{ log.product_name|default:"-" }}</td>
|
||||
<td>{{ log.batch_id|default:"-" }}</td>
|
||||
<td>{{ log.conversation_id|default:"-" }}</td>
|
||||
<td>{{ log.get_user_input_summary }}</td>
|
||||
<td>
|
||||
<span class="pill {% if log.status == 'success' %}pill-success{% else %}pill-danger{% endif %}">{{ log.get_status_display_text }}</span>
|
||||
</td>
|
||||
<td>{{ log.model_name }}</td>
|
||||
<td>{{ log.latency_ms }} ms</td>
|
||||
<td>{{ log.created_at|date:"Y-m-d H:i" }}</td>
|
||||
<td><a class="button" href="{% url 'audit:detail' log.id %}">查看详情</a></td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="8">暂无审计日志,先去执行一次审核工作台任务。</td></tr>
|
||||
<tr><td colspan="10">暂无处理历史,先去执行一次审核任务。</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from agent_core.orchestrator import build_messages, run_agent
|
||||
from agent_core.rag.ingest import _split_text, ingest_document
|
||||
from agent_core.rag.retriever import retrieve
|
||||
from agent_core.schemas.outputs import SUPPORTED_OUTPUT_TYPES
|
||||
|
||||
|
||||
def test_run_agent_returns_structured_result_from_llm_output():
|
||||
@@ -248,3 +249,59 @@ def test_retrieve_returns_empty_when_query_has_no_overlap(tmp_path):
|
||||
)
|
||||
|
||||
assert chunks == []
|
||||
|
||||
|
||||
def test_registration_risk_result_includes_owner_fields_and_notification_payload():
|
||||
scenario = {
|
||||
"id": "document_review",
|
||||
"name": "注册审核智能体",
|
||||
"agent": {
|
||||
"role": "注册审核助手",
|
||||
"goal": "输出风险结果",
|
||||
"instructions": ["输出结构化风险结果"],
|
||||
},
|
||||
"rag": {"enabled": False},
|
||||
"tools": [],
|
||||
"output": {"type": "registration_risk_report"},
|
||||
}
|
||||
provider_response = """
|
||||
{
|
||||
"summary": "存在高风险项,需人工复核。",
|
||||
"highest_risk_level": "high",
|
||||
"pass_status": "blocked",
|
||||
"owner_roles": [
|
||||
{
|
||||
"owner_role": "注册资料负责人",
|
||||
"owner_name": "张三",
|
||||
"department": "注册事务部",
|
||||
"chapter_scope": "CH1",
|
||||
"risk_scope": "字段冲突",
|
||||
"feishu_user_id": "ou_demo_1",
|
||||
"feishu_open_id": "on_demo_1",
|
||||
"feishu_name": "张三",
|
||||
"notify_enabled": true
|
||||
}
|
||||
],
|
||||
"notify_reason": "task_completed"
|
||||
}
|
||||
"""
|
||||
|
||||
class FakeProvider:
|
||||
def generate(self, messages, response_format=None):
|
||||
from agent_core.llm_provider import LLMResponse
|
||||
|
||||
return LLMResponse(content=provider_response, model_name="demo-model", success=True)
|
||||
|
||||
result = run_agent(scenario, "请输出风险结果", options={"llm_provider": FakeProvider()})
|
||||
|
||||
owner = result.notification_payload["owners"][0]
|
||||
assert result.structured_output["output_type"] == "registration_risk_report"
|
||||
assert owner["owner_role"] == "注册资料负责人"
|
||||
assert owner["feishu_user_id"] == "ou_demo_1"
|
||||
assert owner["feishu_open_id"] == "on_demo_1"
|
||||
assert result.notification_payload["notify_reason"] == "task_completed"
|
||||
|
||||
|
||||
def test_supported_output_types_include_word_export_and_feishu_notification():
|
||||
assert "registration_word_export_report" in SUPPORTED_OUTPUT_TYPES
|
||||
assert "feishu_notification_report" in SUPPORTED_OUTPUT_TYPES
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from django.urls import reverse
|
||||
|
||||
from agent_core.results import AgentResult
|
||||
from apps.audit.models import AgentAuditLog, DemoBusinessRecord
|
||||
from apps.audit.services import create_audit_log
|
||||
from apps.audit.models import AgentAuditLog, DemoBusinessRecord, NotificationRecord
|
||||
from apps.audit.services import create_audit_log, create_notification_record
|
||||
from agent_core.tools.builtin_tools import query_demo_records
|
||||
|
||||
|
||||
@@ -117,3 +117,80 @@ def test_query_demo_records_reads_demo_business_record_table(db):
|
||||
|
||||
assert result["records"][0]["title"] == "A线缺陷"
|
||||
assert result["records"][0]["payload"] == {"rate": 0.12}
|
||||
|
||||
|
||||
def test_audit_log_records_batch_conversation_and_product_context(db):
|
||||
result = AgentResult(answer="回答", status="success")
|
||||
|
||||
log = create_audit_log(
|
||||
"document_review",
|
||||
"注册审核智能体",
|
||||
"开始审核",
|
||||
result,
|
||||
batch_id="SUB-20260604-001",
|
||||
conversation_id="conv-001",
|
||||
product_name="新型冠状病毒 2019-nCoV 核酸检测试剂盒",
|
||||
)
|
||||
|
||||
assert log.batch_id == "SUB-20260604-001"
|
||||
assert log.conversation_id == "conv-001"
|
||||
assert log.product_name == "新型冠状病毒 2019-nCoV 核酸检测试剂盒"
|
||||
|
||||
|
||||
def test_create_notification_record_persists_task_completed_and_task_failed(db):
|
||||
completed = create_notification_record(
|
||||
batch_id="SUB-20260604-001",
|
||||
conversation_id="conv-001",
|
||||
product_name="产品A",
|
||||
trigger_source="risk_report",
|
||||
notify_reason="task_completed",
|
||||
owner_role="注册资料负责人",
|
||||
feishu_user_id="ou_demo_1",
|
||||
message_status="sent",
|
||||
web_detail_url="https://example.com/detail/1",
|
||||
receipt={"message_id": "msg-1"},
|
||||
)
|
||||
failed = create_notification_record(
|
||||
batch_id="SUB-20260604-001",
|
||||
conversation_id="conv-001",
|
||||
product_name="产品A",
|
||||
trigger_source="risk_report",
|
||||
notify_reason="task_failed",
|
||||
owner_role="注册资料负责人",
|
||||
feishu_user_id="ou_demo_1",
|
||||
message_status="failed",
|
||||
web_detail_url="https://example.com/detail/1",
|
||||
receipt={"message_id": "msg-2"},
|
||||
)
|
||||
|
||||
assert NotificationRecord.objects.count() == 2
|
||||
assert completed.notify_reason == "task_completed"
|
||||
assert failed.notify_reason == "task_failed"
|
||||
|
||||
|
||||
def test_audit_list_supports_batch_and_product_filters(client, db):
|
||||
create_audit_log(
|
||||
"document_review",
|
||||
"注册审核智能体",
|
||||
"问题一",
|
||||
AgentResult(answer="回答一", status="success"),
|
||||
batch_id="SUB-20260604-001",
|
||||
conversation_id="conv-001",
|
||||
product_name="产品A",
|
||||
)
|
||||
create_audit_log(
|
||||
"document_review",
|
||||
"注册审核智能体",
|
||||
"问题二",
|
||||
AgentResult(answer="回答二", status="success"),
|
||||
batch_id="SUB-20260604-002",
|
||||
conversation_id="conv-002",
|
||||
product_name="产品B",
|
||||
)
|
||||
|
||||
response = client.get(reverse("audit:list"), {"keyword": "产品A"})
|
||||
|
||||
content = response.content.decode("utf-8")
|
||||
assert response.status_code == 200
|
||||
assert "产品A" in content
|
||||
assert "产品B" not in content
|
||||
|
||||
@@ -2,6 +2,7 @@ from django.urls import reverse
|
||||
|
||||
from agent_core.results import AgentResult
|
||||
from apps.audit.models import AgentAuditLog
|
||||
from apps.audit.models import NotificationRecord
|
||||
from apps.chat.models import Conversation
|
||||
from apps.documents.models import SubmissionBatch, UploadedDocument
|
||||
|
||||
@@ -126,3 +127,45 @@ def test_chat_renders_three_column_workspace_and_node_results(client, db):
|
||||
assert "上传区" in content
|
||||
assert "资料包导入 / 已完成" in content
|
||||
assert "目录汇总 / 处理中" in content
|
||||
|
||||
|
||||
def test_chat_execution_creates_notification_record_from_agent_result(client, db, monkeypatch):
|
||||
batch, conversation = _create_conversation_with_batch()
|
||||
UploadedDocument.objects.create(
|
||||
batch=batch,
|
||||
scenario_id="document_review",
|
||||
original_name="说明书.md",
|
||||
file_type="md",
|
||||
size=1,
|
||||
status=UploadedDocument.STATUS_INDEXED,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"apps.chat.views.run_agent",
|
||||
lambda *args, **kwargs: AgentResult(
|
||||
answer="执行完成",
|
||||
status="success",
|
||||
notification_payload={
|
||||
"batch_id": batch.batch_id,
|
||||
"conversation_id": conversation.conversation_id,
|
||||
"product_name": batch.product_name,
|
||||
"notify_reason": "task_completed",
|
||||
"owners": [
|
||||
{
|
||||
"owner_role": "注册资料负责人",
|
||||
"feishu_user_id": "ou_demo_1",
|
||||
}
|
||||
],
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
reverse("chat:detail", args=[conversation.conversation_id]),
|
||||
{"message": "执行审核"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
record = NotificationRecord.objects.get()
|
||||
assert record.notify_reason == "task_completed"
|
||||
assert record.batch_id == batch.batch_id
|
||||
|
||||
Reference in New Issue
Block a user