feat(frontend): 优化对话与管理页面展示体验
This commit is contained in:
@@ -2,6 +2,8 @@ from django import forms
|
|||||||
|
|
||||||
|
|
||||||
class ChatForm(forms.Form):
|
class ChatForm(forms.Form):
|
||||||
|
# 该表单只负责收集用户问题和可选文档范围,
|
||||||
|
# 不承载任何 Agent 业务逻辑,便于在 View 层保持轻量。
|
||||||
message = forms.CharField(
|
message = forms.CharField(
|
||||||
label="问题",
|
label="问题",
|
||||||
max_length=4000,
|
max_length=4000,
|
||||||
@@ -9,7 +11,12 @@ class ChatForm(forms.Form):
|
|||||||
"required": "请输入要咨询的问题。",
|
"required": "请输入要咨询的问题。",
|
||||||
"max_length": "问题过长,请控制在 4000 字以内。",
|
"max_length": "问题过长,请控制在 4000 字以内。",
|
||||||
},
|
},
|
||||||
widget=forms.Textarea(attrs={"rows": 6}),
|
widget=forms.Textarea(
|
||||||
|
attrs={
|
||||||
|
"rows": 8,
|
||||||
|
"placeholder": "例如:请结合已上传 SOP,分析当前异常的原因、风险等级和建议动作。",
|
||||||
|
}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
document_ids = forms.MultipleChoiceField(
|
document_ids = forms.MultipleChoiceField(
|
||||||
label="文档范围",
|
label="文档范围",
|
||||||
@@ -22,9 +29,12 @@ class ChatForm(forms.Form):
|
|||||||
def __init__(self, *args, documents=None, **kwargs):
|
def __init__(self, *args, documents=None, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
documents = documents or []
|
documents = documents or []
|
||||||
|
# 仅允许选择当前场景且已完成入库的文档,
|
||||||
|
# 避免前端把无效文件范围传入 Agent Core。
|
||||||
self.fields["document_ids"].choices = [
|
self.fields["document_ids"].choices = [
|
||||||
(str(document.id), document.original_name) for document in documents
|
(str(document.id), document.original_name) for document in documents
|
||||||
]
|
]
|
||||||
|
|
||||||
def clean_document_ids(self):
|
def clean_document_ids(self):
|
||||||
|
# View 与 Agent Core 都使用整型文档 ID,统一在表单层完成转换。
|
||||||
return [int(document_id) for document_id in self.cleaned_data.get("document_ids", [])]
|
return [int(document_id) for document_id in self.cleaned_data.get("document_ids", [])]
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ from .forms import ChatForm
|
|||||||
|
|
||||||
|
|
||||||
def index(request, scenario_id: str):
|
def index(request, scenario_id: str):
|
||||||
|
# View 只负责请求编排、表单校验和模板渲染。
|
||||||
|
# 具体 Agent 执行、审计写入和文档筛选规则分别交给独立模块处理。
|
||||||
try:
|
try:
|
||||||
scenario = get_scenario(scenario_id)
|
scenario = get_scenario(scenario_id)
|
||||||
except ScenarioNotFound:
|
except ScenarioNotFound:
|
||||||
@@ -34,6 +36,7 @@ def index(request, scenario_id: str):
|
|||||||
if request.method == "POST" and form.is_valid():
|
if request.method == "POST" and form.is_valid():
|
||||||
message = form.cleaned_data["message"]
|
message = form.cleaned_data["message"]
|
||||||
try:
|
try:
|
||||||
|
# 只把必要的运行选项传给 Agent Core,避免在 View 中散落模型细节。
|
||||||
result = run_agent(
|
result = run_agent(
|
||||||
scenario,
|
scenario,
|
||||||
message,
|
message,
|
||||||
@@ -50,6 +53,7 @@ def index(request, scenario_id: str):
|
|||||||
"scenario": scenario,
|
"scenario": scenario,
|
||||||
"form": form,
|
"form": form,
|
||||||
"documents": documents,
|
"documents": documents,
|
||||||
|
"document_count": documents.count(),
|
||||||
"result": result,
|
"result": result,
|
||||||
"audit_log": audit_log,
|
"audit_log": audit_log,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,37 +1,56 @@
|
|||||||
<!doctype html>
|
{% extends "base.html" %}
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
{% block title %}审计日志详情{% endblock %}
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>审计日志详情</title>
|
{% block content %}
|
||||||
</head>
|
<header class="page-header">
|
||||||
<body>
|
<span class="eyebrow">日志详情</span>
|
||||||
<h1>审计日志详情 #{{ log.id }}</h1>
|
<h1 class="page-title">审计日志 #{{ log.id }}</h1>
|
||||||
<nav><a href="{% url 'audit:list' %}">返回审计列表</a></nav>
|
<p class="page-lead">这里集中展示当前请求的输入、模型输出、知识库引用和工具调用记录。</p>
|
||||||
<section>
|
<p style="margin-top: 14px;"><a class="button" href="{% url 'audit:list' %}">返回审计列表</a></p>
|
||||||
<h2>用户输入</h2>
|
</header>
|
||||||
<p>{{ log.user_input }}</p>
|
|
||||||
|
<section class="stack">
|
||||||
|
<article class="panel">
|
||||||
|
<h2 class="section-title">基础信息</h2>
|
||||||
|
<ul class="meta-list">
|
||||||
|
<li class="meta-badge">场景:{{ log.scenario_name }}</li>
|
||||||
|
<li class="meta-badge">状态:{{ log.status }}</li>
|
||||||
|
<li class="meta-badge">模型:{{ log.model_name }}</li>
|
||||||
|
<li class="meta-badge">耗时:{{ log.latency_ms }} ms</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{% if log.error_message %}
|
||||||
|
<article class="panel">
|
||||||
|
<h2 class="section-title">错误信息</h2>
|
||||||
|
<pre class="code-block">{{ log.error_message }}</pre>
|
||||||
|
</article>
|
||||||
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
<section>
|
{% endblock %}
|
||||||
<h2>最终回答</h2>
|
|
||||||
<p>{{ log.final_answer }}</p>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<h2>结构化输出</h2>
|
|
||||||
<pre>{{ log.structured_output }}</pre>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<h2>引用来源</h2>
|
|
||||||
<pre>{{ log.retrieved_chunks }}</pre>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<h2>工具调用</h2>
|
|
||||||
<pre>{{ log.tool_calls }}</pre>
|
|
||||||
</section>
|
|
||||||
{% if log.error_message %}
|
|
||||||
<section>
|
|
||||||
<h2>错误信息</h2>
|
|
||||||
<pre>{{ log.error_message }}</pre>
|
|
||||||
</section>
|
|
||||||
{% endif %}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
@@ -1,37 +1,40 @@
|
|||||||
<!doctype html>
|
{% extends "base.html" %}
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
{% block title %}审计日志{% endblock %}
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>审计日志</title>
|
{% block content %}
|
||||||
</head>
|
<header class="page-header">
|
||||||
<body>
|
<span class="eyebrow">执行留痕</span>
|
||||||
<h1>审计日志</h1>
|
<h1 class="page-title">审计日志</h1>
|
||||||
<nav><a href="/">返回首页</a></nav>
|
<p class="page-lead">每次 Agent 执行都会记录模型、检索片段、工具调用和最终结果,方便演示链路可解释性。</p>
|
||||||
<table>
|
</header>
|
||||||
<thead>
|
|
||||||
<tr>
|
<article class="panel">
|
||||||
<th>ID</th>
|
<table class="kv-table">
|
||||||
<th>场景</th>
|
<thead>
|
||||||
<th>状态</th>
|
|
||||||
<th>模型</th>
|
|
||||||
<th>耗时</th>
|
|
||||||
<th>详情</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for log in logs %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ log.id }}</td>
|
<th>ID</th>
|
||||||
<td>{{ log.scenario_name }}</td>
|
<th>场景</th>
|
||||||
<td>{{ log.status }}</td>
|
<th>状态</th>
|
||||||
<td>{{ log.model_name }}</td>
|
<th>模型</th>
|
||||||
<td>{{ log.latency_ms }} ms</td>
|
<th>耗时</th>
|
||||||
<td><a href="{% url 'audit:detail' log.id %}">查看</a></td>
|
<th>详情</th>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
</thead>
|
||||||
<tr><td colspan="6">暂无审计日志。</td></tr>
|
<tbody>
|
||||||
{% endfor %}
|
{% for log in logs %}
|
||||||
</tbody>
|
<tr>
|
||||||
</table>
|
<td>{{ log.id }}</td>
|
||||||
</body>
|
<td>{{ log.scenario_name }}</td>
|
||||||
</html>
|
<td>{{ log.status }}</td>
|
||||||
|
<td>{{ log.model_name }}</td>
|
||||||
|
<td>{{ log.latency_ms }} ms</td>
|
||||||
|
<td><a class="button" href="{% url 'audit:detail' log.id %}">查看详情</a></td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr><td colspan="6">暂无审计日志,先去执行一次对话吧。</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
344
templates/base.html
Normal file
344
templates/base.html
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{% block title %}Universal Agent Demo Framework{% endblock %}</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #f4f1ea;
|
||||||
|
--surface: #fffdf8;
|
||||||
|
--surface-strong: #fff7ea;
|
||||||
|
--border: #d7c9b0;
|
||||||
|
--text: #2f261d;
|
||||||
|
--muted: #73614f;
|
||||||
|
--accent: #a54c2b;
|
||||||
|
--accent-soft: #f1d2b8;
|
||||||
|
--success: #2b6a4d;
|
||||||
|
--warning: #9a5a00;
|
||||||
|
--danger: #8d2f2f;
|
||||||
|
--shadow: 0 18px 40px rgba(91, 63, 36, 0.10);
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
|
||||||
|
color: var(--text);
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, #f7e2c8 0, transparent 30%),
|
||||||
|
linear-gradient(180deg, #f6efe2 0%, #f4f1ea 45%, #efe7da 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
a { color: var(--accent); text-decoration: none; }
|
||||||
|
a:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
.shell {
|
||||||
|
width: min(1180px, calc(100vw - 32px));
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px 0 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
border: 1px solid rgba(215, 201, 176, 0.7);
|
||||||
|
background: rgba(255, 253, 248, 0.88);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border-radius: 22px;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-note {
|
||||||
|
margin: 6px 0 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link,
|
||||||
|
.button,
|
||||||
|
button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text);
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-primary,
|
||||||
|
button {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: linear-gradient(135deg, #b9562f, #94452a);
|
||||||
|
color: #fffaf4;
|
||||||
|
box-shadow: 0 10px 22px rgba(165, 76, 43, 0.20);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover,
|
||||||
|
.button:hover,
|
||||||
|
button:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 12px 24px rgba(91, 63, 36, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eyebrow {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(241, 210, 184, 0.8);
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
font-size: clamp(2rem, 3vw, 3rem);
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-lead {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 1rem;
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel,
|
||||||
|
.card {
|
||||||
|
background: rgba(255, 253, 248, 0.94);
|
||||||
|
border: 1px solid rgba(215, 201, 176, 0.85);
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h2,
|
||||||
|
.panel h2,
|
||||||
|
.panel h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 12px 0 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--surface-strong);
|
||||||
|
border: 1px solid rgba(215, 201, 176, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-two-columns {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(320px, 420px) minmax(0, 1fr);
|
||||||
|
gap: 20px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea,
|
||||||
|
select,
|
||||||
|
input[type="text"],
|
||||||
|
input[type="file"] {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: #fff;
|
||||||
|
padding: 12px 14px;
|
||||||
|
font: inherit;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea { min-height: 150px; resize: vertical; }
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-text,
|
||||||
|
.muted {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(215, 201, 176, 0.85);
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 700;
|
||||||
|
background: var(--surface-strong);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-success { color: var(--success); }
|
||||||
|
.status-failed { color: var(--danger); }
|
||||||
|
|
||||||
|
.notice {
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: 18px;
|
||||||
|
border: 1px solid rgba(215, 201, 176, 0.85);
|
||||||
|
background: rgba(255, 247, 234, 0.92);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-error {
|
||||||
|
border-color: rgba(141, 47, 47, 0.25);
|
||||||
|
background: rgba(255, 238, 238, 0.95);
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kv-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kv-table th,
|
||||||
|
.kv-table td {
|
||||||
|
padding: 12px 10px;
|
||||||
|
border-bottom: 1px solid rgba(215, 201, 176, 0.6);
|
||||||
|
vertical-align: top;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kv-table th {
|
||||||
|
width: 150px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid rgba(215, 201, 176, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block {
|
||||||
|
margin: 0;
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: #2f261d;
|
||||||
|
color: #fdf8f1;
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.layout-two-columns { grid-template-columns: 1fr; }
|
||||||
|
.topbar { flex-direction: column; align-items: flex-start; }
|
||||||
|
.nav-links { justify-content: flex-start; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="shell">
|
||||||
|
<header class="topbar">
|
||||||
|
<div>
|
||||||
|
<p class="brand-title">Universal Agent Demo Framework</p>
|
||||||
|
<p class="brand-note">面向复试演示的可配置 AI Agent 单体系统</p>
|
||||||
|
</div>
|
||||||
|
<nav class="nav-links">
|
||||||
|
<a class="nav-link" href="{% url 'scenarios:index' %}">场景首页</a>
|
||||||
|
<a class="nav-link" href="{% url 'documents:list' %}">文档中心</a>
|
||||||
|
<a class="nav-link" href="{% url 'audit:list' %}">审计日志</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,61 +1,169 @@
|
|||||||
<!doctype html>
|
{% extends "base.html" %}
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Agent 对话</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Agent 对话</h1>
|
|
||||||
<nav><a href="/">返回首页</a></nav>
|
|
||||||
|
|
||||||
|
{% block title %}{{ scenario.name|default:"Agent 对话" }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
{% if error %}
|
{% if error %}
|
||||||
<p>{{ error }}</p>
|
<section class="notice notice-error">{{ error }}</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if scenario %}
|
{% if scenario %}
|
||||||
<section>
|
<header class="page-header">
|
||||||
<h2>{{ scenario.name }}</h2>
|
<span class="eyebrow">Agent 对话</span>
|
||||||
<p>{{ scenario.description }}</p>
|
<h1 class="page-title">{{ scenario.name }}</h1>
|
||||||
<p>角色:{{ scenario.agent.role }}</p>
|
<p class="page-lead">{{ scenario.description }}</p>
|
||||||
<p>目标:{{ scenario.agent.goal }}</p>
|
<ul class="meta-list">
|
||||||
|
<li class="meta-badge">角色:{{ scenario.agent.role }}</li>
|
||||||
|
<li class="meta-badge">目标:{{ scenario.agent.goal }}</li>
|
||||||
|
<li class="meta-badge">已入库文档:{{ document_count }}</li>
|
||||||
|
<li class="meta-badge">输出类型:{{ scenario.output.type }}</li>
|
||||||
|
</ul>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="layout-two-columns">
|
||||||
|
<div class="stack">
|
||||||
|
<article class="panel">
|
||||||
|
<h2 class="section-title">提问面板</h2>
|
||||||
|
<p class="muted">可以直接提问,也可以勾选部分已入库文档作为当前上下文范围。</p>
|
||||||
|
<form method="post" class="stack">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div>
|
||||||
|
{{ form.message.label_tag }}
|
||||||
|
{{ form.message }}
|
||||||
|
{% if form.message.errors %}
|
||||||
|
<p class="notice notice-error">{{ form.message.errors|join:" " }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ form.document_ids.label_tag }}
|
||||||
|
<p class="help-text">不勾选时默认检索当前场景全部已入库文档。</p>
|
||||||
|
<div class="checkbox-list">
|
||||||
|
{% for checkbox in form.document_ids %}
|
||||||
|
<label class="checkbox-item">
|
||||||
|
{{ checkbox.tag }}
|
||||||
|
<span>{{ checkbox.choice_label }}</span>
|
||||||
|
</label>
|
||||||
|
{% empty %}
|
||||||
|
<div class="notice">当前场景还没有已入库文档,系统将仅依赖工具和模型能力生成结果。</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% if form.document_ids.errors %}
|
||||||
|
<p class="notice notice-error">{{ form.document_ids.errors|join:" " }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button type="submit">提交问题并执行 Agent</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="panel">
|
||||||
|
<h2 class="section-title">执行说明</h2>
|
||||||
|
<ul class="detail-list">
|
||||||
|
<li class="detail-item">
|
||||||
|
<strong>1. 场景配置</strong>
|
||||||
|
系统会先读取当前 YAML 场景配置,确定角色、目标、工具和输出结构。
|
||||||
|
</li>
|
||||||
|
<li class="detail-item">
|
||||||
|
<strong>2. RAG 与工具</strong>
|
||||||
|
如果场景启用了知识库检索,系统会根据你的问题召回相关片段,并执行声明式工具。
|
||||||
|
</li>
|
||||||
|
<li class="detail-item">
|
||||||
|
<strong>3. 结构化结果</strong>
|
||||||
|
Agent Core 会优先解析 JSON 输出,解析失败时回退为稳定的展示结构。
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stack">
|
||||||
|
<article class="panel">
|
||||||
|
<h2 class="section-title">回答总览</h2>
|
||||||
|
{% if result %}
|
||||||
|
<ul class="meta-list">
|
||||||
|
<li class="meta-badge">模型:{{ result.model_name }}</li>
|
||||||
|
<li class="meta-badge {% if result.status == 'success' %}status-success{% else %}status-failed{% endif %}">状态:{{ result.status }}</li>
|
||||||
|
<li class="meta-badge">耗时:{{ result.latency_ms }} ms</li>
|
||||||
|
</ul>
|
||||||
|
<div class="detail-item" style="margin-top: 16px;">
|
||||||
|
<strong>主回答</strong>
|
||||||
|
<div>{{ result.answer|linebreaksbr }}</div>
|
||||||
|
</div>
|
||||||
|
{% if audit_log %}
|
||||||
|
<p style="margin-top: 14px;">
|
||||||
|
<a class="button" href="{% url 'audit:detail' audit_log.id %}">查看本次审计日志</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<div class="notice">提交问题后,这里会展示 Agent 的主回答、模型信息和执行状态。</div>
|
||||||
|
{% endif %}
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{% if result %}
|
||||||
|
<article class="panel">
|
||||||
|
<h2 class="section-title">结构化结果</h2>
|
||||||
|
<table class="kv-table">
|
||||||
|
<tbody>
|
||||||
|
{% for key, value in result.structured_output.items %}
|
||||||
|
<tr>
|
||||||
|
<th>{{ key }}</th>
|
||||||
|
<td>
|
||||||
|
{% if key == "answer" or key == "summary" or key == "reply" %}
|
||||||
|
{{ value|linebreaksbr }}
|
||||||
|
{% else %}
|
||||||
|
<pre class="code-block">{{ value }}</pre>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="panel">
|
||||||
|
<h2 class="section-title">引用片段</h2>
|
||||||
|
{% if result.references %}
|
||||||
|
<ul class="detail-list">
|
||||||
|
{% for reference in result.references %}
|
||||||
|
<li class="detail-item">
|
||||||
|
<strong>{{ reference.source }}</strong>
|
||||||
|
<div>{{ reference.content|default:"无正文内容"|linebreaksbr }}</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<div class="notice">当前回答没有引用知识库片段。</div>
|
||||||
|
{% endif %}
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="panel">
|
||||||
|
<h2 class="section-title">工具调用</h2>
|
||||||
|
{% if result.tool_calls %}
|
||||||
|
<ul class="detail-list">
|
||||||
|
{% for tool_call in result.tool_calls %}
|
||||||
|
<li class="detail-item">
|
||||||
|
<strong>{{ tool_call.tool_name }}</strong>
|
||||||
|
<p class="muted">执行状态:{{ tool_call.success }}</p>
|
||||||
|
{% if tool_call.error %}
|
||||||
|
<p class="notice notice-error">{{ tool_call.error }}</p>
|
||||||
|
{% endif %}
|
||||||
|
<pre class="code-block">{{ tool_call.result }}</pre>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<div class="notice">当前场景没有声明工具,或本次执行无需调用工具。</div>
|
||||||
|
{% endif %}
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{% if result.error %}
|
||||||
|
<article class="panel">
|
||||||
|
<h2 class="section-title">错误信息</h2>
|
||||||
|
<pre class="code-block">{{ result.error }}</pre>
|
||||||
|
</article>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<form method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ form.as_p }}
|
|
||||||
<button type="submit">提交</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{% if result %}
|
|
||||||
<section>
|
|
||||||
<h2>回答</h2>
|
|
||||||
<p>{{ result.answer }}</p>
|
|
||||||
<p>模型:{{ result.model_name }}</p>
|
|
||||||
<p>状态:{{ result.status }}</p>
|
|
||||||
<p>耗时:{{ result.latency_ms }} ms</p>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<h2>结构化输出</h2>
|
|
||||||
<pre>{{ result.structured_output }}</pre>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<h2>引用来源</h2>
|
|
||||||
<pre>{{ result.references }}</pre>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<h2>工具调用</h2>
|
|
||||||
<pre>{{ result.tool_calls }}</pre>
|
|
||||||
</section>
|
|
||||||
{% if audit_log %}
|
|
||||||
<p><a href="{% url 'audit:detail' audit_log.id %}">查看审计日志</a></p>
|
|
||||||
{% endif %}
|
|
||||||
{% if result.error %}
|
|
||||||
<section>
|
|
||||||
<h2>错误信息</h2>
|
|
||||||
<pre>{{ result.error }}</pre>
|
|
||||||
</section>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</body>
|
{% endblock %}
|
||||||
</html>
|
|
||||||
|
|||||||
@@ -1,50 +1,53 @@
|
|||||||
<!doctype html>
|
{% extends "base.html" %}
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
{% block title %}文档中心{% endblock %}
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>文件列表</title>
|
{% block content %}
|
||||||
</head>
|
<header class="page-header">
|
||||||
<body>
|
<span class="eyebrow">知识库资料</span>
|
||||||
<h1>文件列表</h1>
|
<h1 class="page-title">文档中心</h1>
|
||||||
<nav>
|
<p class="page-lead">上传题目材料后,可以在这里管理文件状态,并手动触发入库。</p>
|
||||||
<a href="/">返回首页</a>
|
<p style="margin-top: 14px;"><a class="button button-primary" href="{% url 'documents:upload' %}">上传新文件</a></p>
|
||||||
<a href="{% url 'documents:upload' %}">上传文件</a>
|
</header>
|
||||||
</nav>
|
|
||||||
<table>
|
<article class="panel">
|
||||||
<thead>
|
<table class="kv-table">
|
||||||
<tr>
|
<thead>
|
||||||
<th>文件名</th>
|
|
||||||
<th>场景</th>
|
|
||||||
<th>类型</th>
|
|
||||||
<th>大小</th>
|
|
||||||
<th>状态</th>
|
|
||||||
<th>操作</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for document in documents %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ document.original_name }}</td>
|
<th>文件名</th>
|
||||||
<td>{{ document.scenario_id }}</td>
|
<th>场景</th>
|
||||||
<td>{{ document.file_type }}</td>
|
<th>类型</th>
|
||||||
<td>{{ document.size }}</td>
|
<th>大小</th>
|
||||||
<td>{{ document.status }}</td>
|
<th>状态</th>
|
||||||
<td>
|
<th>操作</th>
|
||||||
{% if document.status != "indexed" %}
|
|
||||||
<form action="{% url 'documents:index' document.id %}" method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
<button type="submit">入库</button>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
{% if document.error_message %}
|
|
||||||
<pre>{{ document.error_message }}</pre>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
</thead>
|
||||||
<tr><td colspan="6">暂无文件。</td></tr>
|
<tbody>
|
||||||
{% endfor %}
|
{% for document in documents %}
|
||||||
</tbody>
|
<tr>
|
||||||
</table>
|
<td>{{ document.original_name }}</td>
|
||||||
</body>
|
<td>{{ document.scenario_id }}</td>
|
||||||
</html>
|
<td>{{ document.file_type }}</td>
|
||||||
|
<td>{{ document.size }}</td>
|
||||||
|
<td>{{ document.status }}</td>
|
||||||
|
<td>
|
||||||
|
{% if document.status != "indexed" %}
|
||||||
|
<form action="{% url 'documents:index' document.id %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit">执行入库</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<span class="status status-success">已可用于检索</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if document.error_message %}
|
||||||
|
<pre class="code-block" style="margin-top: 10px;">{{ document.error_message }}</pre>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr><td colspan="6">暂无文件,请先上传题目材料。</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,26 +1,39 @@
|
|||||||
<!doctype html>
|
{% extends "base.html" %}
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
{% block title %}上传文件{% endblock %}
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>上传文件</title>
|
{% block content %}
|
||||||
</head>
|
<header class="page-header">
|
||||||
<body>
|
<span class="eyebrow">文件上传</span>
|
||||||
<h1>上传文件</h1>
|
<h1 class="page-title">上传题目材料或知识库文档</h1>
|
||||||
<nav><a href="{% url 'documents:list' %}">返回文件列表</a></nav>
|
<p class="page-lead">支持 `.txt`、`.md`、`.pdf` 和 `.docx`。上传后可以在文档中心手动执行入库。</p>
|
||||||
<form method="post" enctype="multipart/form-data">
|
</header>
|
||||||
{% csrf_token %}
|
|
||||||
<p>
|
<article class="panel" style="max-width: 760px;">
|
||||||
<label for="id_scenario_id">场景</label>
|
<form method="post" enctype="multipart/form-data" class="stack">
|
||||||
<select name="scenario_id" id="id_scenario_id">
|
{% csrf_token %}
|
||||||
{% for scenario in scenarios %}
|
<div>
|
||||||
<option value="{{ scenario.id }}">{{ scenario.name }}</option>
|
<label for="id_scenario_id">关联场景</label>
|
||||||
{% endfor %}
|
<select name="scenario_id" id="id_scenario_id">
|
||||||
</select>
|
{% for scenario in scenarios %}
|
||||||
</p>
|
<option value="{{ scenario.id }}">{{ scenario.name }}</option>
|
||||||
<p>{{ form.file.label_tag }} {{ form.file }}</p>
|
{% endfor %}
|
||||||
{{ form.errors }}
|
</select>
|
||||||
<p>支持 .txt、.md、.pdf 和 .docx 文件。</p>
|
</div>
|
||||||
<button type="submit">上传</button>
|
<div>
|
||||||
</form>
|
{{ form.file.label_tag }}
|
||||||
</body>
|
{{ form.file }}
|
||||||
</html>
|
{% if form.file.errors %}
|
||||||
|
<p class="notice notice-error">{{ form.file.errors|join:" " }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if form.errors %}
|
||||||
|
<div class="notice notice-error">{{ form.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||||
|
<button type="submit">上传文件</button>
|
||||||
|
<a class="button" href="{% url 'documents:list' %}">返回文件列表</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,23 +1,30 @@
|
|||||||
<!doctype html>
|
{% extends "base.html" %}
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
{% block title %}场景首页{% endblock %}
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Universal Agent Demo Framework</title>
|
{% block content %}
|
||||||
</head>
|
<header class="page-header">
|
||||||
<body>
|
<span class="eyebrow">场景总览</span>
|
||||||
<h1>Universal Agent Demo Framework</h1>
|
<h1 class="page-title">用同一套底座快速切换不同业务 Agent</h1>
|
||||||
<p>用于复试展示的通用 AI Agent Demo 框架。</p>
|
<p class="page-lead">当前首页直接读取 YAML 场景配置。你可以从这里进入对话、上传资料,再用审计日志验证整条执行链路。</p>
|
||||||
<main>
|
</header>
|
||||||
|
|
||||||
|
<section class="card-grid">
|
||||||
{% for scenario in scenarios %}
|
{% for scenario in scenarios %}
|
||||||
<section>
|
<article class="card">
|
||||||
<h2>{{ scenario.name }}</h2>
|
<h2>{{ scenario.name }}</h2>
|
||||||
<p>{{ scenario.description }}</p>
|
<p>{{ scenario.description }}</p>
|
||||||
<p>场景 ID:{{ scenario.id }}</p>
|
<ul class="meta-list">
|
||||||
<p><a href="{% url 'chat:index' scenario.id %}">进入对话</a></p>
|
<li class="meta-badge">场景 ID:{{ scenario.id }}</li>
|
||||||
</section>
|
<li class="meta-badge">输出:{{ scenario.output.type }}</li>
|
||||||
|
<li class="meta-badge">工具数:{{ scenario.tools|length }}</li>
|
||||||
|
</ul>
|
||||||
|
<p style="margin-top: 16px;">
|
||||||
|
<a class="button button-primary" href="{% url 'chat:index' scenario.id %}">进入对话</a>
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<p>暂无可用场景,请检查 configs 目录。</p>
|
<div class="notice">暂无可用场景,请检查 `configs/` 目录和 YAML 配置内容。</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</main>
|
</section>
|
||||||
</body>
|
{% endblock %}
|
||||||
</html>
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from agent_core.results import AgentResult
|
||||||
from apps.audit.models import AgentAuditLog
|
from apps.audit.models import AgentAuditLog
|
||||||
from apps.documents.models import UploadedDocument
|
from apps.documents.models import UploadedDocument
|
||||||
|
|
||||||
@@ -58,3 +59,49 @@ def test_chat_passes_selected_document_ids_to_agent_core(client, db, monkeypatch
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert captured["options"]["document_ids"] == [selected.id]
|
assert captured["options"]["document_ids"] == [selected.id]
|
||||||
assert other.id not in captured["options"]["document_ids"]
|
assert other.id not in captured["options"]["document_ids"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_chat_renders_structured_output_references_and_tool_calls(client, db, monkeypatch):
|
||||||
|
def fake_run_agent(scenario_config, user_input, options=None):
|
||||||
|
return AgentResult(
|
||||||
|
answer="建议先隔离现场。",
|
||||||
|
structured_output={
|
||||||
|
"output_type": "quality_report",
|
||||||
|
"summary": "发现异常批次需要立即处置。",
|
||||||
|
"risk_level": "high",
|
||||||
|
"suggested_actions": ["隔离现场", "通知负责人"],
|
||||||
|
},
|
||||||
|
references=[
|
||||||
|
{
|
||||||
|
"source": "sop.md",
|
||||||
|
"content": "异常处理 SOP:先隔离现场,再通知负责人。",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
tool_calls=[
|
||||||
|
{
|
||||||
|
"tool_name": "query_demo_records",
|
||||||
|
"success": True,
|
||||||
|
"result": {"records": [{"title": "A线缺陷"}]},
|
||||||
|
"error": "",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
model_name="mock-model",
|
||||||
|
status="success",
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setattr("apps.chat.views.run_agent", fake_run_agent)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
reverse("chat:index", args=["quality_analysis"]),
|
||||||
|
{"message": "分析 A 线异常"},
|
||||||
|
)
|
||||||
|
|
||||||
|
content = response.content.decode("utf-8")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "结构化结果" in content
|
||||||
|
assert "发现异常批次需要立即处置" in content
|
||||||
|
assert "引用片段" in content
|
||||||
|
assert "sop.md" in content
|
||||||
|
assert "工具调用" in content
|
||||||
|
assert "query_demo_records" in content
|
||||||
|
assert "查看本次审计日志" in content
|
||||||
|
|||||||
Reference in New Issue
Block a user