refactor: 下沉会话执行编排到 chat 服务层

This commit is contained in:
2026-06-04 04:31:55 +08:00
parent de2bd2956f
commit 1b6c54fe78
3 changed files with 159 additions and 87 deletions

View File

@@ -1,3 +1,12 @@
from collections.abc import Callable
from django.utils import timezone
from agent_core.orchestrator import run_agent
from agent_core.results import AgentResult
from apps.audit.services import create_audit_log, create_notification_record
from apps.scenarios.services import get_scenario
from .models import Conversation from .models import Conversation
@@ -19,6 +28,48 @@ def create_conversation_for_batch(batch_id: str, product_name: str) -> Conversat
return conversation return conversation
def execute_conversation_agent(
*,
conversation: Conversation,
message: str,
document_ids: list[int],
detail_url_builder: Callable[[int], str] | None = None,
) -> tuple[AgentResult, object]:
"""
在服务层串起会话执行、审计留痕与通知落库。
View 只负责收集请求参数和渲染结果,不直接承载 Agent Core 编排。
"""
scenario = get_scenario("document_review")
try:
result = run_agent(
scenario,
message,
options={
"conversation_id": conversation.conversation_id,
"batch_id": conversation.batch_id,
"product_name": conversation.product_name,
"document_ids": document_ids,
},
)
except Exception as exc:
result = AgentResult(status="failed", error=str(exc), answer="")
audit_log = create_audit_log(
"document_review",
"注册审核智能体",
message,
result,
batch_id=conversation.batch_id,
conversation_id=conversation.conversation_id,
product_name=conversation.product_name,
)
_apply_agent_result_to_conversation(conversation, result)
detail_url = detail_url_builder(audit_log.id) if detail_url_builder else ""
_persist_notification_records(result, web_detail_url=detail_url)
return result, audit_log
def _generate_conversation_id() -> str: def _generate_conversation_id() -> str:
return f"conv-{Conversation.objects.count() + 1:03d}" return f"conv-{Conversation.objects.count() + 1:03d}"
@@ -34,3 +85,51 @@ def _build_initial_node_results() -> list[dict]:
{"code": "word_export", "label": "Word 回填导出", "status": "待处理"}, {"code": "word_export", "label": "Word 回填导出", "status": "待处理"},
{"code": "feishu_notify", "label": "飞书通知", "status": "待处理"}, {"code": "feishu_notify", "label": "飞书通知", "status": "待处理"},
] ]
def _persist_notification_records(result: AgentResult, *, web_detail_url: str = "") -> None:
payload = result.notification_payload or {}
owners = payload.get("owners") or []
if not owners:
return
resolved_detail_url = payload.get("web_detail_url") or web_detail_url
resolved_message_status = payload.get("message_status") or (
"sent" if result.status == "success" else "failed"
)
resolved_receipt = payload.get("receipt") or {"status": result.status}
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=resolved_message_status,
web_detail_url=resolved_detail_url,
receipt=resolved_receipt,
)
def _apply_agent_result_to_conversation(conversation: Conversation, result: AgentResult) -> None:
conversation.task_status = result.status
if result.node_results:
conversation.node_results = result.node_results
conversation.latest_summary = {
"answer": result.answer,
"status": result.status,
"error": result.error,
"structured_output": result.structured_output,
"notification_payload": result.notification_payload,
}
conversation.last_run_at = timezone.now()
conversation.save(
update_fields=[
"task_status",
"node_results",
"latest_summary",
"last_run_at",
"updated_at",
]
)

View File

@@ -1,19 +1,17 @@
from django.contrib import messages from django.contrib import messages
from django.utils import timezone
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from agent_core.orchestrator import run_agent
from agent_core.results import AgentResult from agent_core.results import AgentResult
from apps.audit.services import create_audit_log, create_notification_record from apps.audit.services import create_audit_log
from apps.documents.models import SubmissionBatch, UploadedDocument from apps.documents.models import SubmissionBatch, UploadedDocument
from apps.documents.services import append_documents_to_batch from apps.documents.services import append_documents_to_batch
from apps.scenarios.services import get_scenario
from .export_service import generate_registration_export, update_conversation_with_export_report from .export_service import generate_registration_export, update_conversation_with_export_report
from .forms import ChatForm, ConversationUploadForm from .forms import ChatForm, ConversationUploadForm
from .models import Conversation from .models import Conversation
from .services import execute_conversation_agent
RISK_LEVEL_DISPLAY = { RISK_LEVEL_DISPLAY = {
"high": "", "high": "",
@@ -86,34 +84,12 @@ def detail(request, conversation_id: str):
{"name": "综合风险报告", "description": "形成高优先级问题、建议动作和责任人通知。"}, {"name": "综合风险报告", "description": "形成高优先级问题、建议动作和责任人通知。"},
] ]
if request.method == "POST" and form.is_valid(): if request.method == "POST" and form.is_valid():
scenario = get_scenario("document_review")
message = form.cleaned_data["message"] message = form.cleaned_data["message"]
try: result, audit_log = execute_conversation_agent(
result = run_agent( conversation=conversation,
scenario, message=message,
message, document_ids=form.cleaned_data["document_ids"],
options={ detail_url_builder=lambda log_id: reverse("audit:detail", args=[log_id]),
"conversation_id": conversation.conversation_id,
"batch_id": conversation.batch_id,
"product_name": conversation.product_name,
"document_ids": form.cleaned_data["document_ids"],
},
)
except Exception as exc:
result = AgentResult(status="failed", error=str(exc), answer="")
audit_log = create_audit_log(
"document_review",
"注册审核智能体",
message,
result,
batch_id=conversation.batch_id,
conversation_id=conversation.conversation_id,
product_name=conversation.product_name,
)
_apply_agent_result_to_conversation(conversation, result)
_persist_notification_records(
result,
web_detail_url=reverse("audit:detail", args=[audit_log.id]),
) )
active_node = "risk" active_node = "risk"
conversation.refresh_from_db() conversation.refresh_from_db()
@@ -233,32 +209,6 @@ def export_word(request, conversation_id: str):
messages.error(request, f"Word 导出失败:{exc}") messages.error(request, f"Word 导出失败:{exc}")
return redirect("chat:detail", conversation_id=conversation.conversation_id) return redirect("chat:detail", conversation_id=conversation.conversation_id)
def _persist_notification_records(result: AgentResult, *, web_detail_url: str = "") -> None:
payload = result.notification_payload or {}
owners = payload.get("owners") or []
if not owners:
return
resolved_detail_url = payload.get("web_detail_url") or web_detail_url
resolved_message_status = payload.get("message_status") or (
"sent" if result.status == "success" else "failed"
)
resolved_receipt = payload.get("receipt") or {"status": result.status}
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=resolved_message_status,
web_detail_url=resolved_detail_url,
receipt=resolved_receipt,
)
def _build_workspace_summary( def _build_workspace_summary(
conversation: Conversation, conversation: Conversation,
batch: SubmissionBatch | None, batch: SubmissionBatch | None,
@@ -495,26 +445,3 @@ def _get_export_status_display_text(status: str) -> str:
def _get_notify_message_status_display_text(status: str) -> str: def _get_notify_message_status_display_text(status: str) -> str:
return NOTIFY_MESSAGE_STATUS_DISPLAY.get(status, status) return NOTIFY_MESSAGE_STATUS_DISPLAY.get(status, status)
def _apply_agent_result_to_conversation(conversation: Conversation, result: AgentResult) -> None:
conversation.task_status = result.status
if result.node_results:
conversation.node_results = result.node_results
conversation.latest_summary = {
"answer": result.answer,
"status": result.status,
"error": result.error,
"structured_output": result.structured_output,
"notification_payload": result.notification_payload,
}
conversation.last_run_at = timezone.now()
conversation.save(
update_fields=[
"task_status",
"node_results",
"latest_summary",
"last_run_at",
"updated_at",
]
)

View File

@@ -8,7 +8,7 @@ from agent_core.results import AgentResult
from apps.audit.models import AgentAuditLog from apps.audit.models import AgentAuditLog
from apps.audit.models import NotificationRecord from apps.audit.models import NotificationRecord
from apps.chat.models import Conversation from apps.chat.models import Conversation
from apps.chat.services import create_conversation_for_batch from apps.chat.services import create_conversation_for_batch, execute_conversation_agent
from apps.documents.models import ExportedDocument, SubmissionBatch, UploadedDocument from apps.documents.models import ExportedDocument, SubmissionBatch, UploadedDocument
@@ -48,7 +48,7 @@ def test_chat_post_returns_agent_result_and_audit_log(client, db, monkeypatch):
) )
monkeypatch.setattr( monkeypatch.setattr(
"apps.chat.views.run_agent", "apps.chat.services.run_agent",
lambda *args, **kwargs: AgentResult(answer="模拟回答", status="success"), lambda *args, **kwargs: AgentResult(answer="模拟回答", status="success"),
) )
@@ -99,7 +99,7 @@ def test_chat_passes_selected_document_ids_to_agent_core(client, db, monkeypatch
captured["options"] = options or {} captured["options"] = options or {}
return AgentResult(answer="ok", status="success") return AgentResult(answer="ok", status="success")
monkeypatch.setattr("apps.chat.views.run_agent", fake_run_agent) monkeypatch.setattr("apps.chat.services.run_agent", fake_run_agent)
response = client.post( response = client.post(
reverse("chat:detail", args=[conversation.conversation_id]), reverse("chat:detail", args=[conversation.conversation_id]),
@@ -169,7 +169,7 @@ def test_chat_execution_creates_notification_record_from_agent_result(client, db
) )
monkeypatch.setattr( monkeypatch.setattr(
"apps.chat.views.run_agent", "apps.chat.services.run_agent",
lambda *args, **kwargs: AgentResult( lambda *args, **kwargs: AgentResult(
answer="执行完成", answer="执行完成",
status="success", status="success",
@@ -212,7 +212,7 @@ def test_chat_execution_uses_notification_payload_message_status_and_receipt(cli
) )
monkeypatch.setattr( monkeypatch.setattr(
"apps.chat.views.run_agent", "apps.chat.services.run_agent",
lambda *args, **kwargs: AgentResult( lambda *args, **kwargs: AgentResult(
answer="通知已发送", answer="通知已发送",
status="success", status="success",
@@ -257,7 +257,7 @@ def test_chat_execution_creates_failed_notification_record_and_updates_conversat
) )
monkeypatch.setattr( monkeypatch.setattr(
"apps.chat.views.run_agent", "apps.chat.services.run_agent",
lambda *args, **kwargs: AgentResult( lambda *args, **kwargs: AgentResult(
answer="执行失败", answer="执行失败",
status="failed", status="failed",
@@ -310,7 +310,7 @@ def test_chat_execution_persists_agent_node_results_to_conversation(client, db,
) )
monkeypatch.setattr( monkeypatch.setattr(
"apps.chat.views.run_agent", "apps.chat.services.run_agent",
lambda *args, **kwargs: AgentResult( lambda *args, **kwargs: AgentResult(
answer="已生成风险结论", answer="已生成风险结论",
status="success", status="success",
@@ -366,6 +366,52 @@ def test_create_conversation_for_batch_initializes_eight_workflow_nodes(db):
] ]
def test_execute_conversation_agent_runs_in_service_layer_and_persists_audit_and_notifications(db, monkeypatch):
batch, conversation = _create_conversation_with_batch()
captured = {}
def fake_run_agent(scenario_config, user_input, options=None):
captured["scenario_id"] = scenario_config["id"]
captured["user_input"] = user_input
captured["options"] = options or {}
return 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_service_1",
}
],
},
)
monkeypatch.setattr("apps.chat.services.run_agent", fake_run_agent)
result, audit_log = execute_conversation_agent(
conversation=conversation,
message="服务层执行审核",
document_ids=[101, 102],
detail_url_builder=lambda log_id: f"/audit/{log_id}/",
)
conversation.refresh_from_db()
assert result.answer == "服务层执行完成"
assert audit_log.batch_id == batch.batch_id
assert captured["scenario_id"] == "document_review"
assert captured["user_input"] == "服务层执行审核"
assert captured["options"]["document_ids"] == [101, 102]
assert conversation.latest_summary["answer"] == "服务层执行完成"
record = NotificationRecord.objects.get()
assert record.notify_reason == "task_completed"
assert record.web_detail_url == f"/audit/{audit_log.id}/"
def test_chat_page_shows_upload_entry_and_dynamic_context_cards(client, db): def test_chat_page_shows_upload_entry_and_dynamic_context_cards(client, db):
batch, conversation = _create_conversation_with_batch() batch, conversation = _create_conversation_with_batch()
conversation.node_results = [ conversation.node_results = [