feat(chat): 打通场景对话与结果展示
This commit is contained in:
1
apps/chat/__init__.py
Normal file
1
apps/chat/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
6
apps/chat/apps.py
Normal file
6
apps/chat/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ChatConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.chat"
|
||||
30
apps/chat/forms.py
Normal file
30
apps/chat/forms.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from django import forms
|
||||
|
||||
|
||||
class ChatForm(forms.Form):
|
||||
message = forms.CharField(
|
||||
label="问题",
|
||||
max_length=4000,
|
||||
error_messages={
|
||||
"required": "请输入要咨询的问题。",
|
||||
"max_length": "问题过长,请控制在 4000 字以内。",
|
||||
},
|
||||
widget=forms.Textarea(attrs={"rows": 6}),
|
||||
)
|
||||
document_ids = forms.MultipleChoiceField(
|
||||
label="文档范围",
|
||||
required=False,
|
||||
choices=(),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
error_messages={"invalid_choice": "请选择当前场景下已入库的文档。"},
|
||||
)
|
||||
|
||||
def __init__(self, *args, documents=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
documents = documents or []
|
||||
self.fields["document_ids"].choices = [
|
||||
(str(document.id), document.original_name) for document in documents
|
||||
]
|
||||
|
||||
def clean_document_ids(self):
|
||||
return [int(document_id) for document_id in self.cleaned_data.get("document_ids", [])]
|
||||
10
apps/chat/urls.py
Normal file
10
apps/chat/urls.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
app_name = "chat"
|
||||
|
||||
urlpatterns = [
|
||||
path("<str:scenario_id>/", views.index, name="index"),
|
||||
]
|
||||
56
apps/chat/views.py
Normal file
56
apps/chat/views.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
from agent_core.orchestrator import run_agent
|
||||
from agent_core.results import AgentResult
|
||||
from apps.audit.services import create_audit_log
|
||||
from apps.documents.models import UploadedDocument
|
||||
from apps.scenarios.services import ScenarioNotFound, get_scenario
|
||||
|
||||
from .forms import ChatForm
|
||||
|
||||
|
||||
def index(request, scenario_id: str):
|
||||
try:
|
||||
scenario = get_scenario(scenario_id)
|
||||
except ScenarioNotFound:
|
||||
return render(
|
||||
request,
|
||||
"chat/index.html",
|
||||
{
|
||||
"scenario": None,
|
||||
"form": ChatForm(),
|
||||
"error": "场景不存在,请返回首页检查配置。",
|
||||
},
|
||||
status=404,
|
||||
)
|
||||
|
||||
result = None
|
||||
audit_log = None
|
||||
documents = UploadedDocument.objects.filter(
|
||||
scenario_id=scenario["id"],
|
||||
status=UploadedDocument.STATUS_INDEXED,
|
||||
)
|
||||
form = ChatForm(request.POST or None, documents=documents)
|
||||
if request.method == "POST" and form.is_valid():
|
||||
message = form.cleaned_data["message"]
|
||||
try:
|
||||
result = run_agent(
|
||||
scenario,
|
||||
message,
|
||||
options={"document_ids": form.cleaned_data["document_ids"]},
|
||||
)
|
||||
except Exception as exc:
|
||||
result = AgentResult(status="failed", error=str(exc), answer="")
|
||||
audit_log = create_audit_log(scenario["id"], scenario["name"], message, result)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"chat/index.html",
|
||||
{
|
||||
"scenario": scenario,
|
||||
"form": form,
|
||||
"documents": documents,
|
||||
"result": result,
|
||||
"audit_log": audit_log,
|
||||
},
|
||||
)
|
||||
61
templates/chat/index.html
Normal file
61
templates/chat/index.html
Normal file
@@ -0,0 +1,61 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Agent 对话</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Agent 对话</h1>
|
||||
<nav><a href="/">返回首页</a></nav>
|
||||
|
||||
{% if error %}
|
||||
<p>{{ error }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if scenario %}
|
||||
<section>
|
||||
<h2>{{ scenario.name }}</h2>
|
||||
<p>{{ scenario.description }}</p>
|
||||
<p>角色:{{ scenario.agent.role }}</p>
|
||||
<p>目标:{{ scenario.agent.goal }}</p>
|
||||
</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 %}
|
||||
</body>
|
||||
</html>
|
||||
60
tests/test_chat.py
Normal file
60
tests/test_chat.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from django.urls import reverse
|
||||
|
||||
from apps.audit.models import AgentAuditLog
|
||||
from apps.documents.models import UploadedDocument
|
||||
|
||||
|
||||
def test_chat_post_returns_agent_result_and_audit_log(client, db):
|
||||
response = client.post(
|
||||
reverse("chat:index", args=["knowledge_qa"]),
|
||||
{"message": "如何处理异常?"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.content.decode("utf-8")
|
||||
assert "mock-model" in content
|
||||
assert "模拟回答" in content
|
||||
assert AgentAuditLog.objects.count() == 1
|
||||
|
||||
|
||||
def test_chat_rejects_empty_message(client, db):
|
||||
response = client.post(reverse("chat:index", args=["knowledge_qa"]), {"message": ""})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert AgentAuditLog.objects.count() == 0
|
||||
assert "请输入要咨询的问题" in response.content.decode("utf-8")
|
||||
|
||||
|
||||
def test_chat_passes_selected_document_ids_to_agent_core(client, db, monkeypatch):
|
||||
selected = UploadedDocument.objects.create(
|
||||
scenario_id="knowledge_qa",
|
||||
original_name="selected.md",
|
||||
file_type="md",
|
||||
size=1,
|
||||
status=UploadedDocument.STATUS_INDEXED,
|
||||
)
|
||||
other = UploadedDocument.objects.create(
|
||||
scenario_id="knowledge_qa",
|
||||
original_name="other.md",
|
||||
file_type="md",
|
||||
size=1,
|
||||
status=UploadedDocument.STATUS_INDEXED,
|
||||
)
|
||||
captured = {}
|
||||
|
||||
def fake_run_agent(scenario_config, user_input, options=None):
|
||||
captured["options"] = options or {}
|
||||
from agent_core.results import AgentResult
|
||||
|
||||
return AgentResult(answer="ok", status="success")
|
||||
|
||||
monkeypatch.setattr("apps.chat.views.run_agent", fake_run_agent)
|
||||
|
||||
response = client.post(
|
||||
reverse("chat:index", args=["knowledge_qa"]),
|
||||
{"message": "只查选中文档", "document_ids": [str(selected.id)]},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert captured["options"]["document_ids"] == [selected.id]
|
||||
assert other.id not in captured["options"]["document_ids"]
|
||||
Reference in New Issue
Block a user