From ba3f5fc58475570eba09a08429e79935aca929e9 Mon Sep 17 00:00:00 2001 From: bruce Date: Sat, 30 May 2026 00:10:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(chat):=20=E6=89=93=E9=80=9A=E5=9C=BA?= =?UTF-8?q?=E6=99=AF=E5=AF=B9=E8=AF=9D=E4=B8=8E=E7=BB=93=E6=9E=9C=E5=B1=95?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/chat/__init__.py | 1 + apps/chat/apps.py | 6 ++++ apps/chat/forms.py | 30 +++++++++++++++++++ apps/chat/urls.py | 10 +++++++ apps/chat/views.py | 56 +++++++++++++++++++++++++++++++++++ templates/chat/index.html | 61 +++++++++++++++++++++++++++++++++++++++ tests/test_chat.py | 60 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 224 insertions(+) create mode 100644 apps/chat/__init__.py create mode 100644 apps/chat/apps.py create mode 100644 apps/chat/forms.py create mode 100644 apps/chat/urls.py create mode 100644 apps/chat/views.py create mode 100644 templates/chat/index.html create mode 100644 tests/test_chat.py diff --git a/apps/chat/__init__.py b/apps/chat/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/apps/chat/__init__.py @@ -0,0 +1 @@ + diff --git a/apps/chat/apps.py b/apps/chat/apps.py new file mode 100644 index 0000000..8bb7c3d --- /dev/null +++ b/apps/chat/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ChatConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.chat" diff --git a/apps/chat/forms.py b/apps/chat/forms.py new file mode 100644 index 0000000..90624ac --- /dev/null +++ b/apps/chat/forms.py @@ -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", [])] diff --git a/apps/chat/urls.py b/apps/chat/urls.py new file mode 100644 index 0000000..ba72e3c --- /dev/null +++ b/apps/chat/urls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from . import views + + +app_name = "chat" + +urlpatterns = [ + path("/", views.index, name="index"), +] diff --git a/apps/chat/views.py b/apps/chat/views.py new file mode 100644 index 0000000..38eeb53 --- /dev/null +++ b/apps/chat/views.py @@ -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, + }, + ) diff --git a/templates/chat/index.html b/templates/chat/index.html new file mode 100644 index 0000000..d9431bf --- /dev/null +++ b/templates/chat/index.html @@ -0,0 +1,61 @@ + + + + + Agent 对话 + + +

Agent 对话

+ + + {% if error %} +

{{ error }}

+ {% endif %} + + {% if scenario %} +
+

{{ scenario.name }}

+

{{ scenario.description }}

+

角色:{{ scenario.agent.role }}

+

目标:{{ scenario.agent.goal }}

+
+ +
+ {% csrf_token %} + {{ form.as_p }} + +
+ + {% if result %} +
+

回答

+

{{ result.answer }}

+

模型:{{ result.model_name }}

+

状态:{{ result.status }}

+

耗时:{{ result.latency_ms }} ms

+
+
+

结构化输出

+
{{ result.structured_output }}
+
+
+

引用来源

+
{{ result.references }}
+
+
+

工具调用

+
{{ result.tool_calls }}
+
+ {% if audit_log %} +

查看审计日志

+ {% endif %} + {% if result.error %} +
+

错误信息

+
{{ result.error }}
+
+ {% endif %} + {% endif %} + {% endif %} + + diff --git a/tests/test_chat.py b/tests/test_chat.py new file mode 100644 index 0000000..7cbe541 --- /dev/null +++ b/tests/test_chat.py @@ -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"]