feat(audit): 增加场景筛选与日志摘要展示
This commit is contained in:
@@ -2,6 +2,7 @@ from django.db import models
|
|||||||
|
|
||||||
|
|
||||||
class AgentAuditLog(models.Model):
|
class AgentAuditLog(models.Model):
|
||||||
|
# 审计状态需要同时服务数据库检索和前端展示。
|
||||||
STATUS_SUCCESS = "success"
|
STATUS_SUCCESS = "success"
|
||||||
STATUS_FAILED = "failed"
|
STATUS_FAILED = "failed"
|
||||||
|
|
||||||
@@ -25,6 +26,19 @@ class AgentAuditLog(models.Model):
|
|||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"{self.scenario_name or self.scenario_id} #{self.pk}"
|
return f"{self.scenario_name or self.scenario_id} #{self.pk}"
|
||||||
|
|
||||||
|
def get_status_display_text(self) -> str:
|
||||||
|
"""返回更适合页面展示的中文状态。"""
|
||||||
|
return {
|
||||||
|
self.STATUS_SUCCESS: "执行成功",
|
||||||
|
self.STATUS_FAILED: "执行失败",
|
||||||
|
}.get(self.status, self.status)
|
||||||
|
|
||||||
|
def get_user_input_summary(self, max_length: int = 28) -> str:
|
||||||
|
"""在列表页展示用户输入摘要,避免长文本撑破表格。"""
|
||||||
|
if len(self.user_input) <= max_length:
|
||||||
|
return self.user_input
|
||||||
|
return f"{self.user_input[:max_length]}..."
|
||||||
|
|
||||||
|
|
||||||
class DemoBusinessRecord(models.Model):
|
class DemoBusinessRecord(models.Model):
|
||||||
scenario_id = models.CharField(max_length=100, db_index=True)
|
scenario_id = models.CharField(max_length=100, db_index=True)
|
||||||
|
|||||||
@@ -4,8 +4,19 @@ from .models import AgentAuditLog
|
|||||||
|
|
||||||
|
|
||||||
def log_list(request):
|
def log_list(request):
|
||||||
|
# 列表页支持按场景筛选,方便演示时快速定位同一类场景的执行记录。
|
||||||
|
scenario_id = (request.GET.get("scenario_id") or "").strip()
|
||||||
logs = AgentAuditLog.objects.all()
|
logs = AgentAuditLog.objects.all()
|
||||||
return render(request, "audit/log_list.html", {"logs": logs})
|
if scenario_id:
|
||||||
|
logs = logs.filter(scenario_id=scenario_id)
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"audit/log_list.html",
|
||||||
|
{
|
||||||
|
"logs": logs,
|
||||||
|
"selected_scenario_id": scenario_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def log_detail(request, log_id: int):
|
def log_detail(request, log_id: int):
|
||||||
|
|||||||
@@ -7,6 +7,12 @@
|
|||||||
<span class="eyebrow">执行留痕</span>
|
<span class="eyebrow">执行留痕</span>
|
||||||
<h1 class="page-title">审计日志</h1>
|
<h1 class="page-title">审计日志</h1>
|
||||||
<p class="page-lead">每次 Agent 执行都会记录模型、检索片段、工具调用和最终结果,方便演示链路可解释性。</p>
|
<p class="page-lead">每次 Agent 执行都会记录模型、检索片段、工具调用和最终结果,方便演示链路可解释性。</p>
|
||||||
|
{% if selected_scenario_id %}
|
||||||
|
<p style="margin-top: 14px;">
|
||||||
|
<span class="meta-badge">当前筛选场景:{{ selected_scenario_id }}</span>
|
||||||
|
<a class="button" href="{% url 'audit:list' %}">清空筛选</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<article class="panel">
|
<article class="panel">
|
||||||
@@ -15,9 +21,11 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<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>
|
<th>详情</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -26,13 +34,15 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{{ log.id }}</td>
|
<td>{{ log.id }}</td>
|
||||||
<td>{{ log.scenario_name }}</td>
|
<td>{{ log.scenario_name }}</td>
|
||||||
<td>{{ log.status }}</td>
|
<td>{{ log.get_user_input_summary }}</td>
|
||||||
|
<td>{{ log.get_status_display_text }}</td>
|
||||||
<td>{{ log.model_name }}</td>
|
<td>{{ log.model_name }}</td>
|
||||||
<td>{{ log.latency_ms }} ms</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>
|
<td><a class="button" href="{% url 'audit:detail' log.id %}">查看详情</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr><td colspan="6">暂无审计日志,先去执行一次对话吧。</td></tr>
|
<tr><td colspan="8">暂无审计日志,先去执行一次对话吧。</td></tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -27,6 +27,41 @@ def test_audit_list_page_shows_log(client, db):
|
|||||||
assert "知识库问答助手" in response.content.decode("utf-8")
|
assert "知识库问答助手" in response.content.decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_audit_list_can_filter_by_scenario(client, db):
|
||||||
|
create_audit_log(
|
||||||
|
"knowledge_qa",
|
||||||
|
"知识库问答助手",
|
||||||
|
"制度问题",
|
||||||
|
AgentResult(answer="回答一", status="success"),
|
||||||
|
)
|
||||||
|
create_audit_log(
|
||||||
|
"quality_analysis",
|
||||||
|
"质量异常分析助手",
|
||||||
|
"质量问题",
|
||||||
|
AgentResult(answer="回答二", status="success"),
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get(reverse("audit:list"), {"scenario_id": "knowledge_qa"})
|
||||||
|
|
||||||
|
content = response.content.decode("utf-8")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "知识库问答助手" in content
|
||||||
|
assert "质量异常分析助手" not in content
|
||||||
|
|
||||||
|
|
||||||
|
def test_audit_list_page_shows_user_input_summary(client, db):
|
||||||
|
create_audit_log(
|
||||||
|
"knowledge_qa",
|
||||||
|
"知识库问答助手",
|
||||||
|
"这是一个比较长的用户输入,用于确认列表页会展示输入摘要。",
|
||||||
|
AgentResult(answer="回答", status="success"),
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get(reverse("audit:list"))
|
||||||
|
|
||||||
|
assert "这是一个比较长的用户输入" in response.content.decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
def test_create_audit_log_masks_api_keys_from_error_message(db):
|
def test_create_audit_log_masks_api_keys_from_error_message(db):
|
||||||
result = AgentResult(
|
result = AgentResult(
|
||||||
answer="",
|
answer="",
|
||||||
|
|||||||
Reference in New Issue
Block a user