171 lines
6.2 KiB
Python
171 lines
6.2 KiB
Python
from django.contrib.auth.decorators import login_required
|
|
from django.db.models import Count, Q
|
|
from django.http import HttpRequest, HttpResponse, JsonResponse, StreamingHttpResponse
|
|
from django.shortcuts import redirect, render
|
|
from django.views.decorators.http import require_http_methods
|
|
|
|
from .services import (
|
|
create_conversation,
|
|
get_conversation_for_user,
|
|
list_conversations,
|
|
send_message,
|
|
stream_message,
|
|
)
|
|
from .models import Conversation, FileAttachment, FileSummaryBatch, RegulatoryReviewBatch, WorkflowNodeRun
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["GET", "POST"])
|
|
def workspace(request: HttpRequest) -> HttpResponse:
|
|
"""Renders the review-agent workspace and handles conversation actions."""
|
|
|
|
if request.method == "POST":
|
|
action = request.POST.get("action")
|
|
conversation = get_conversation_for_user(request.user, request.POST.get("conversation_id"))
|
|
|
|
if action == "new_conversation":
|
|
conversation = create_conversation(request.user)
|
|
return redirect(f"/?conversation={conversation.pk}")
|
|
|
|
if action == "send_message":
|
|
content = (request.POST.get("prompt") or "").strip()
|
|
if not conversation:
|
|
conversation = create_conversation(request.user)
|
|
if content:
|
|
send_message(conversation, content)
|
|
return redirect(f"/?conversation={conversation.pk}")
|
|
|
|
search = (request.GET.get("q") or "").strip()
|
|
conversations = list_conversations(request.user, search)
|
|
current = get_conversation_for_user(request.user, request.GET.get("conversation"))
|
|
|
|
if current is None and conversations.exists():
|
|
current = conversations.first()
|
|
|
|
workflow_cards = build_workflow_cards(current) if current else []
|
|
|
|
return render(
|
|
request,
|
|
"home.html",
|
|
{
|
|
"page_title": "审核智能体",
|
|
"search_query": search,
|
|
"conversations": conversations,
|
|
"current_conversation": current,
|
|
"messages": current.messages.all() if current else [],
|
|
"attachments": FileAttachment.objects.filter(conversation=current).order_by("original_name", "-version_no") if current else [],
|
|
"workflow_cards": workflow_cards,
|
|
},
|
|
)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["GET"])
|
|
def attachment_manager(request: HttpRequest) -> HttpResponse:
|
|
conversations = (
|
|
Conversation.objects.filter(user=request.user)
|
|
.annotate(
|
|
attachment_count=Count(
|
|
"file_attachments",
|
|
filter=~Q(file_attachments__upload_status=FileAttachment.UploadStatus.DELETED),
|
|
)
|
|
)
|
|
.order_by("-updated_at", "-id")
|
|
)
|
|
selected = get_conversation_for_user(request.user, request.GET.get("conversation"))
|
|
attachments = (
|
|
FileAttachment.objects.filter(conversation=selected)
|
|
.order_by("original_name", "-version_no")
|
|
if selected
|
|
else []
|
|
)
|
|
return render(
|
|
request,
|
|
"attachment_manager.html",
|
|
{
|
|
"page_title": "附件管理",
|
|
"conversations": conversations,
|
|
"selected_conversation": selected,
|
|
"attachments": attachments,
|
|
},
|
|
)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def stream_chat(request: HttpRequest) -> HttpResponse:
|
|
"""Streams one assistant reply so the UI can render incremental output."""
|
|
|
|
content = (request.POST.get("prompt") or "").strip()
|
|
if not content:
|
|
return JsonResponse({"error": "消息内容不能为空。"}, status=400)
|
|
|
|
conversation = get_conversation_for_user(request.user, request.POST.get("conversation_id"))
|
|
if not conversation:
|
|
conversation = create_conversation(request.user)
|
|
|
|
response = StreamingHttpResponse(
|
|
streaming_content=stream_message(conversation, content),
|
|
content_type="text/event-stream",
|
|
)
|
|
response["Cache-Control"] = "no-cache"
|
|
response["X-Accel-Buffering"] = "no"
|
|
return response
|
|
|
|
|
|
def build_workflow_cards(conversation: Conversation) -> list[dict[str, object]]:
|
|
cards: list[dict[str, object]] = []
|
|
for batch in FileSummaryBatch.objects.filter(conversation=conversation).prefetch_related("node_runs"):
|
|
cards.append(
|
|
{
|
|
"id": batch.pk,
|
|
"workflow_type": "file_summary",
|
|
"batch_no": batch.batch_no,
|
|
"status": batch.status,
|
|
"error_message": batch.error_message,
|
|
"risk_label": "",
|
|
"created_at": batch.created_at,
|
|
"nodes": list(batch.node_runs.order_by("id")),
|
|
}
|
|
)
|
|
regulatory_batches = RegulatoryReviewBatch.objects.filter(conversation=conversation)
|
|
for batch in regulatory_batches:
|
|
cards.append(
|
|
{
|
|
"id": batch.pk,
|
|
"workflow_type": "regulatory_review",
|
|
"batch_no": batch.batch_no,
|
|
"status": batch.status,
|
|
"error_message": batch.error_message,
|
|
"risk_label": _format_risk_label(batch.risk_summary or {}),
|
|
"condition_json": batch.condition_json or {},
|
|
"condition_candidates": (batch.condition_json or {}).get("candidates") or {},
|
|
"notification_count": batch.notifications.count(),
|
|
"review_record_count": batch.artifacts.filter(metadata__artifact="review_record").count(),
|
|
"created_at": batch.created_at,
|
|
"nodes": list(
|
|
WorkflowNodeRun.objects.filter(
|
|
workflow_type="regulatory_review",
|
|
workflow_batch_id=batch.pk,
|
|
).order_by("id")
|
|
),
|
|
}
|
|
)
|
|
return sorted(cards, key=lambda item: item["created_at"], reverse=True)[:5]
|
|
|
|
|
|
def _format_risk_label(risk_summary: dict) -> str:
|
|
parts = []
|
|
labels = [
|
|
("blocking", "阻断项"),
|
|
("high", "高风险"),
|
|
("medium", "中风险"),
|
|
("low", "低风险"),
|
|
("info", "提示"),
|
|
]
|
|
for key, label in labels:
|
|
count = int(risk_summary.get(key) or 0)
|
|
if count:
|
|
parts.append(f"{label} {count}")
|
|
return " · ".join(parts)
|