from django.contrib.auth.decorators import login_required from django.db.models import Count, Q, Sum import json from django.http import HttpRequest, HttpResponse, JsonResponse, StreamingHttpResponse from django.shortcuts import redirect, render from django.utils.http import urlencode 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 ApplicationFormFillBatch, Conversation, FileAttachment, FileSummaryBatch, RegulatoryReviewBatch, WorkflowNodeRun from .knowledge_base import build_knowledge_base_context, search_knowledge_base from .knowledge_base import ( build_knowledge_base_context_for_user, create_document_from_upload, delete_document, index_managed_document, list_documents_for_user, serialize_document, update_document, ) from .models import KnowledgeBaseDocument from .regulatory_review.services.info_extract import ensure_regulatory_condition_candidates @login_required @require_http_methods(["GET"]) def home_dashboard(request: HttpRequest) -> HttpResponse: """Renders the data-first home dashboard for the current user.""" if request.GET.get("conversation"): query = {"conversation": request.GET["conversation"]} search = (request.GET.get("q") or "").strip() if search: query["q"] = search return redirect(f"/chat/?{urlencode(query)}") context = build_home_dashboard_context(request.user) return render( request, "workbench.html", { "page_title": "首页", "dashboard": context, }, ) @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"/chat/?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"/chat/?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 [] condition_confirmation = build_condition_confirmation(workflow_cards) 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, "condition_confirmation": condition_confirmation, }, ) @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(["GET"]) def knowledge_base_manager(request: HttpRequest) -> HttpResponse: context = build_knowledge_base_context_for_user(request.user) return render( request, "knowledge_base.html", { "page_title": "知识库管理", "knowledge_base": context, }, ) @login_required @require_http_methods(["GET"]) def knowledge_base_status(request: HttpRequest) -> JsonResponse: return JsonResponse(build_knowledge_base_context_for_user(request.user)) @login_required @require_http_methods(["POST"]) def knowledge_base_search(request: HttpRequest) -> JsonResponse: if request.content_type == "application/json": try: payload = json.loads(request.body.decode("utf-8") or "{}") except json.JSONDecodeError: payload = {} query = payload.get("query", "") else: query = request.POST.get("query", "") return JsonResponse(search_knowledge_base(str(query))) @login_required @require_http_methods(["GET", "POST"]) def knowledge_base_documents(request: HttpRequest) -> JsonResponse: if request.method == "GET": return JsonResponse({"documents": list_documents_for_user(request.user)}) uploaded_file = request.FILES.get("file") if uploaded_file is None: return JsonResponse({"error": "请上传知识库材料。"}, status=400) is_active = str(request.POST.get("is_active", "true")).lower() not in {"0", "false", "off"} document = create_document_from_upload( user=request.user, uploaded_file=uploaded_file, display_name=request.POST.get("display_name", ""), description=request.POST.get("description", ""), is_active=is_active, ) return JsonResponse({"document": serialize_document(document)}) @login_required @require_http_methods(["GET", "PATCH", "DELETE"]) def knowledge_base_document_detail(request: HttpRequest, document_id: int) -> JsonResponse: try: document = KnowledgeBaseDocument.objects.get( pk=document_id, user=request.user, ) except KnowledgeBaseDocument.DoesNotExist: return JsonResponse({"error": "知识库材料不存在。"}, status=404) if document.status == KnowledgeBaseDocument.Status.DELETED: return JsonResponse({"error": "知识库材料不存在。"}, status=404) if request.method == "GET": return JsonResponse({"document": serialize_document(document)}) if request.method == "DELETE": delete_document(document) return JsonResponse({"document": serialize_document(document)}) try: payload = json.loads(request.body.decode("utf-8") or "{}") except json.JSONDecodeError: payload = {} update_document(document, payload) return JsonResponse({"document": serialize_document(document)}) @login_required @require_http_methods(["POST"]) def knowledge_base_document_index(request: HttpRequest, document_id: int) -> JsonResponse: try: document = KnowledgeBaseDocument.objects.get( pk=document_id, user=request.user, ) except KnowledgeBaseDocument.DoesNotExist: return JsonResponse({"error": "知识库材料不存在。"}, status=404) if document.status == KnowledgeBaseDocument.Status.DELETED: return JsonResponse({"error": "知识库材料不存在。"}, status=404) chunk_count = index_managed_document(document) document.refresh_from_db() return JsonResponse({"document": serialize_document(document), "chunk_count": chunk_count}) @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: condition_candidates = ensure_regulatory_condition_candidates(batch) 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": condition_candidates, "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") ), } ) form_fill_batches = ApplicationFormFillBatch.objects.filter(conversation=conversation, is_deleted=False) for batch in form_fill_batches: cards.append( { "id": batch.pk, "workflow_type": "application_form_fill", "batch_no": batch.batch_no, "status": batch.status, "error_message": batch.error_message, "risk_label": _format_form_fill_label(batch), "created_at": batch.created_at, "nodes": list( WorkflowNodeRun.objects.filter( workflow_type="application_form_fill", workflow_batch_id=batch.pk, ).order_by("id") ), } ) return sorted(cards, key=lambda item: item["created_at"], reverse=True)[:5] def build_condition_confirmation(workflow_cards: list[dict[str, object]]) -> dict[str, object] | None: for card in workflow_cards: if ( card.get("workflow_type") == "regulatory_review" and card.get("status") == RegulatoryReviewBatch.Status.WAITING_USER and card.get("condition_candidates") ): return { "id": card["id"], "batch_no": card["batch_no"], "candidates": card["condition_candidates"], } return None 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) def _format_form_fill_label(batch: ApplicationFormFillBatch) -> str: parts = [] if batch.selected_templates: parts.append("模板 " + "、".join(batch.selected_templates)) if batch.conflict_summary: parts.append(f"冲突字段 {len(batch.conflict_summary)}") if batch.risk_notes: parts.append(f"提示 {len(batch.risk_notes)}") return " · ".join(parts) def build_home_dashboard_context(user) -> dict[str, object]: conversations = Conversation.objects.filter(user=user) active_attachments = FileAttachment.objects.filter(user=user).exclude( upload_status=FileAttachment.UploadStatus.DELETED ) active_knowledge_documents = KnowledgeBaseDocument.objects.filter(user=user).exclude( status=KnowledgeBaseDocument.Status.DELETED ) knowledge_context = build_knowledge_base_context_for_user(user) builtin_source_count = int(knowledge_context.get("source_count") or 0) collection_chunk_count = int((knowledge_context.get("collection") or {}).get("count") or 0) managed_document_count = active_knowledge_documents.count() file_batches = FileSummaryBatch.objects.filter(user=user).select_related("conversation") regulatory_batches = RegulatoryReviewBatch.objects.filter(user=user).select_related("conversation") form_fill_batches = ApplicationFormFillBatch.objects.filter(user=user, is_deleted=False).select_related("conversation") batch_status_counts = _build_batch_status_counts(file_batches, regulatory_batches, form_fill_batches) total_batches = file_batches.count() + regulatory_batches.count() + form_fill_batches.count() successful_batches = batch_status_counts["success"] handled_batches = successful_batches + batch_status_counts["failed"] recent_records = _build_recent_dashboard_records( conversations.order_by("-updated_at", "-id")[:8], file_batches.order_by("-created_at", "-id")[:8], regulatory_batches.order_by("-created_at", "-id")[:8], form_fill_batches.order_by("-created_at", "-id")[:8], ) return { "metrics": { "conversation_count": conversations.count(), "recent_conversation_count": conversations.filter(messages__isnull=False).distinct().count(), "attachment_count": active_attachments.count(), "active_attachment_count": active_attachments.filter(is_active=True).count(), "knowledge_document_count": managed_document_count + builtin_source_count, "running_batch_count": batch_status_counts["running"], "handled_batch_count": handled_batches, "success_batch_count": successful_batches, "waiting_batch_count": batch_status_counts["waiting"], "failed_batch_count": batch_status_counts["failed"], "total_batch_count": total_batches, }, "knowledge": { "document_count": managed_document_count, "builtin_source_count": builtin_source_count, "total_material_count": managed_document_count + builtin_source_count, "active_document_count": active_knowledge_documents.filter(is_active=True).count(), "indexed_document_count": active_knowledge_documents.filter(indexed_chunk_count__gt=0).count(), "managed_chunk_count": active_knowledge_documents.aggregate(total=Sum("indexed_chunk_count"))["total"] or 0, "chunk_count": collection_chunk_count, }, "attachments": { "attachment_count": active_attachments.count(), "active_attachment_count": active_attachments.filter(is_active=True).count(), "recent_attachment_count": active_attachments.order_by("-created_at", "-id")[:5].count(), "conversation_count": active_attachments.values("conversation_id").distinct().count(), }, "workflow": { "file_summary_count": file_batches.count(), "regulatory_review_count": regulatory_batches.count(), "application_form_fill_count": form_fill_batches.count(), **batch_status_counts, }, "recent_records": recent_records, } def _build_batch_status_counts(file_batches, regulatory_batches, form_fill_batches) -> dict[str, int]: running_statuses = { FileSummaryBatch.Status.PENDING, FileSummaryBatch.Status.RUNNING, ApplicationFormFillBatch.Status.PENDING, ApplicationFormFillBatch.Status.RUNNING, RegulatoryReviewBatch.Status.PENDING, RegulatoryReviewBatch.Status.RUNNING, } waiting_statuses = { ApplicationFormFillBatch.Status.WAITING_USER, RegulatoryReviewBatch.Status.WAITING_USER, } success_statuses = { FileSummaryBatch.Status.SUCCESS, RegulatoryReviewBatch.Status.SUCCESS, ApplicationFormFillBatch.Status.SUCCESS, ApplicationFormFillBatch.Status.PARTIAL_SUCCESS, } failed_statuses = { FileSummaryBatch.Status.FAILED, RegulatoryReviewBatch.Status.FAILED, ApplicationFormFillBatch.Status.FAILED, } statuses = [ *file_batches.values_list("status", flat=True), *regulatory_batches.values_list("status", flat=True), *form_fill_batches.values_list("status", flat=True), ] return { "running": sum(1 for status in statuses if status in running_statuses), "waiting": sum(1 for status in statuses if status in waiting_statuses), "success": sum(1 for status in statuses if status in success_statuses), "failed": sum(1 for status in statuses if status in failed_statuses), } def _build_recent_dashboard_records(conversations, file_batches, regulatory_batches, form_fill_batches) -> list[dict[str, object]]: records = [] for conversation in conversations: records.append( { "type": "对话", "title": conversation.title or "新对话", "status": "已更新", "updated_at": conversation.updated_at, "url": f"/chat/?conversation={conversation.pk}", } ) for batch in file_batches: records.append(_batch_record(batch, "文件汇总")) for batch in regulatory_batches: status = batch.status risk_label = _format_risk_label(batch.risk_summary or {}) records.append(_batch_record(batch, "法规核查", status_label=risk_label or status)) for batch in form_fill_batches: records.append(_batch_record(batch, "申报填表")) return sorted(records, key=lambda item: item["updated_at"], reverse=True)[:8] def _batch_record(batch, record_type: str, status_label: str | None = None) -> dict[str, object]: return { "type": record_type, "title": batch.batch_no, "status": status_label or batch.status, "updated_at": batch.created_at, "url": f"/chat/?conversation={batch.conversation_id}", }