refactor: 下沉会话执行编排到 chat 服务层
This commit is contained in:
@@ -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",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|||||||
@@ -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",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
|||||||
Reference in New Issue
Block a user