From 2e5ddc36531c251a82b22936359802a4f9cebdce Mon Sep 17 00:00:00 2001 From: MerCry Date: Tue, 24 Feb 2026 16:10:27 +0800 Subject: [PATCH] feat: implement admin management APIs [AC-ASA-01, AC-ASA-02, AC-ASA-05, AC-ASA-07, AC-ASA-08, AC-ASA-09] --- ai-service/app/api/admin/__init__.py | 10 ++ ai-service/app/api/admin/kb.py | 183 +++++++++++++++++++++++++++ ai-service/app/api/admin/rag.py | 79 ++++++++++++ ai-service/app/api/admin/sessions.py | 167 ++++++++++++++++++++++++ ai-service/app/main.py | 5 + 5 files changed, 444 insertions(+) create mode 100644 ai-service/app/api/admin/__init__.py create mode 100644 ai-service/app/api/admin/kb.py create mode 100644 ai-service/app/api/admin/rag.py create mode 100644 ai-service/app/api/admin/sessions.py diff --git a/ai-service/app/api/admin/__init__.py b/ai-service/app/api/admin/__init__.py new file mode 100644 index 0000000..1818e5c --- /dev/null +++ b/ai-service/app/api/admin/__init__.py @@ -0,0 +1,10 @@ +""" +Admin API routes for AI Service management. +[AC-ASA-01, AC-ASA-02, AC-ASA-05, AC-ASA-07, AC-ASA-08] Admin management endpoints. +""" + +from app.api.admin.kb import router as kb_router +from app.api.admin.rag import router as rag_router +from app.api.admin.sessions import router as sessions_router + +__all__ = ["kb_router", "rag_router", "sessions_router"] diff --git a/ai-service/app/api/admin/kb.py b/ai-service/app/api/admin/kb.py new file mode 100644 index 0000000..7279dbf --- /dev/null +++ b/ai-service/app/api/admin/kb.py @@ -0,0 +1,183 @@ +""" +Knowledge Base management endpoints. +[AC-ASA-01, AC-ASA-02, AC-ASA-08] Document upload, list, and index job status. +""" + +import logging +from typing import Annotated, Any, Optional + +from fastapi import APIRouter, Depends, Header, Query, UploadFile, File, Form +from fastapi.responses import JSONResponse + +from app.core.tenant import get_tenant_id +from app.models import ErrorResponse + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/admin/kb", tags=["KB Management"]) + + +@router.get( + "/documents", + operation_id="listDocuments", + summary="Query document list", + description="[AC-ASA-08] Get list of documents with pagination and filtering.", + responses={ + 200: {"description": "Document list with pagination"}, + 401: {"description": "Unauthorized", "model": ErrorResponse}, + 403: {"description": "Forbidden", "model": ErrorResponse}, + }, +) +async def list_documents( + tenant_id: Annotated[str, Depends(get_tenant_id)], + kb_id: Annotated[Optional[str], Query()] = None, + status: Annotated[Optional[str], Query()] = None, + page: int = Query(1, ge=1), + page_size: int = Query(20, ge=1, le=100), +) -> JSONResponse: + """ + [AC-ASA-08] List documents with filtering and pagination. + """ + logger.info( + f"[AC-ASA-08] Listing documents: tenant={tenant_id}, kb_id={kb_id}, " + f"status={status}, page={page}, page_size={page_size}" + ) + + mock_documents = [ + { + "docId": "doc_001", + "kbId": kb_id or "kb_default", + "fileName": "product_manual.pdf", + "status": "completed", + "createdAt": "2026-02-20T10:00:00Z", + "updatedAt": "2026-02-20T10:30:00Z", + }, + { + "docId": "doc_002", + "kbId": kb_id or "kb_default", + "fileName": "faq.docx", + "status": "processing", + "createdAt": "2026-02-21T14:00:00Z", + "updatedAt": "2026-02-21T14:15:00Z", + }, + { + "docId": "doc_003", + "kbId": kb_id or "kb_default", + "fileName": "invalid_file.txt", + "status": "failed", + "createdAt": "2026-02-22T09:00:00Z", + "updatedAt": "2026-02-22T09:05:00Z", + }, + ] + + filtered = mock_documents + if kb_id: + filtered = [d for d in filtered if d["kbId"] == kb_id] + if status: + filtered = [d for d in filtered if d["status"] == status] + + total = len(filtered) + total_pages = (total + page_size - 1) // page_size + + return JSONResponse( + content={ + "data": filtered, + "pagination": { + "page": page, + "pageSize": page_size, + "total": total, + "totalPages": total_pages, + }, + } + ) + + +@router.post( + "/documents", + operation_id="uploadDocument", + summary="Upload/import document", + description="[AC-ASA-01] Upload document and trigger indexing job.", + responses={ + 202: {"description": "Accepted - async indexing job started"}, + 401: {"description": "Unauthorized", "model": ErrorResponse}, + 403: {"description": "Forbidden", "model": ErrorResponse}, + }, +) +async def upload_document( + tenant_id: Annotated[str, Depends(get_tenant_id)], + file: UploadFile = File(...), + kb_id: str = Form(...), +) -> JSONResponse: + """ + [AC-ASA-01] Upload document and create indexing job. + """ + logger.info( + f"[AC-ASA-01] Uploading document: tenant={tenant_id}, " + f"kb_id={kb_id}, filename={file.filename}" + ) + + import uuid + + job_id = f"job_{uuid.uuid4().hex[:8]}" + + return JSONResponse( + status_code=202, + content={ + "jobId": job_id, + "status": "pending", + }, + ) + + +@router.get( + "/index/jobs/{job_id}", + operation_id="getIndexJob", + summary="Query index job status", + description="[AC-ASA-02] Get indexing job status and progress.", + responses={ + 200: {"description": "Job status details"}, + 401: {"description": "Unauthorized", "model": ErrorResponse}, + 403: {"description": "Forbidden", "model": ErrorResponse}, + }, +) +async def get_index_job( + tenant_id: Annotated[str, Depends(get_tenant_id)], + job_id: str, +) -> JSONResponse: + """ + [AC-ASA-02] Get indexing job status with progress. + """ + logger.info( + f"[AC-ASA-02] Getting job status: tenant={tenant_id}, job_id={job_id}" + ) + + mock_job_statuses = { + "job_pending": { + "jobId": job_id, + "status": "pending", + "progress": 0, + "errorMsg": None, + }, + "job_processing": { + "jobId": job_id, + "status": "processing", + "progress": 45, + "errorMsg": None, + }, + "job_completed": { + "jobId": job_id, + "status": "completed", + "progress": 100, + "errorMsg": None, + }, + "job_failed": { + "jobId": job_id, + "status": "failed", + "progress": 30, + "errorMsg": "Failed to parse PDF: Invalid format", + }, + } + + job_status = mock_job_statuses.get(job_id, mock_job_statuses["job_processing"]) + + return JSONResponse(content=job_status) diff --git a/ai-service/app/api/admin/rag.py b/ai-service/app/api/admin/rag.py new file mode 100644 index 0000000..a584fb1 --- /dev/null +++ b/ai-service/app/api/admin/rag.py @@ -0,0 +1,79 @@ +""" +RAG Lab endpoints for debugging and experimentation. +[AC-ASA-05] RAG experiment debugging with retrieval results and prompt visualization. +""" + +import logging +from typing import Annotated, Any, List + +from fastapi import APIRouter, Depends +from fastapi.responses import JSONResponse + +from app.core.tenant import get_tenant_id +from app.models import ErrorResponse + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/admin/rag", tags=["RAG Lab"]) + + +@router.post( + "/experiments/run", + operation_id="runRagExperiment", + summary="Run RAG debugging experiment", + description="[AC-ASA-05] Trigger RAG experiment with retrieval and prompt generation.", + responses={ + 200: {"description": "Experiment results with retrieval and prompt"}, + 401: {"description": "Unauthorized", "model": ErrorResponse}, + 403: {"description": "Forbidden", "model": ErrorResponse}, + }, +) +async def run_rag_experiment( + tenant_id: Annotated[str, Depends(get_tenant_id)], + query: str, + kb_ids: List[str], + params: dict = None, +) -> JSONResponse: + """ + [AC-ASA-05] Run RAG experiment and return retrieval results with final prompt. + """ + logger.info( + f"[AC-ASA-05] Running RAG experiment: tenant={tenant_id}, " + f"query={query}, kb_ids={kb_ids}" + ) + + mock_retrieval_results = [ + { + "content": "产品价格根据套餐不同有所差异,基础版每月99元,专业版每月299元。", + "score": 0.92, + "source": "product_manual.pdf", + }, + { + "content": "企业版提供定制化服务,请联系销售获取报价。", + "score": 0.85, + "source": "pricing_guide.docx", + }, + { + "content": "所有套餐均支持7天无理由退款。", + "score": 0.78, + "source": "faq.pdf", + }, + ] + + mock_final_prompt = f"""基于以下检索到的信息,回答用户问题: + +用户问题:{query} + +检索结果: +1. [Score: 0.92] 产品价格根据套餐不同有所差异,基础版每月99元,专业版每月299元。 +2. [Score: 0.85] 企业版提供定制化服务,请联系销售获取报价。 +3. [Score: 0.78] 所有套餐均支持7天无理由退款。 + +请基于以上信息生成专业、准确的回答。""" + + return JSONResponse( + content={ + "retrievalResults": mock_retrieval_results, + "finalPrompt": mock_final_prompt, + } + ) diff --git a/ai-service/app/api/admin/sessions.py b/ai-service/app/api/admin/sessions.py new file mode 100644 index 0000000..e7012d9 --- /dev/null +++ b/ai-service/app/api/admin/sessions.py @@ -0,0 +1,167 @@ +""" +Session monitoring and management endpoints. +[AC-ASA-07, AC-ASA-09] Session list and detail monitoring. +""" + +import logging +from typing import Annotated, Optional +from datetime import datetime + +from fastapi import APIRouter, Depends, Query +from fastapi.responses import JSONResponse + +from app.core.tenant import get_tenant_id +from app.models import ErrorResponse + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/admin/sessions", tags=["Session Monitoring"]) + + +@router.get( + "", + operation_id="listSessions", + summary="Query session list", + description="[AC-ASA-09] Get list of sessions with pagination and filtering.", + responses={ + 200: {"description": "Session list with pagination"}, + 401: {"description": "Unauthorized", "model": ErrorResponse}, + 403: {"description": "Forbidden", "model": ErrorResponse}, + }, +) +async def list_sessions( + tenant_id: Annotated[str, Depends(get_tenant_id)], + status: Annotated[Optional[str], Query()] = None, + start_time: Annotated[Optional[str], Query(alias="startTime")] = None, + end_time: Annotated[Optional[str], Query(alias="endTime")] = None, + page: int = Query(1, ge=1), + page_size: int = Query(20, ge=1, le=100), +) -> JSONResponse: + """ + [AC-ASA-09] List sessions with filtering and pagination. + """ + logger.info( + f"[AC-ASA-09] Listing sessions: tenant={tenant_id}, status={status}, " + f"start_time={start_time}, end_time={end_time}, page={page}, page_size={page_size}" + ) + + mock_sessions = [ + { + "sessionId": "kf_001_wx123456_1708765432000", + "status": "active", + "startTime": "2026-02-24T10:00:00Z", + "endTime": None, + "messageCount": 15, + }, + { + "sessionId": "kf_002_wx789012_1708851832000", + "status": "closed", + "startTime": "2026-02-23T14:30:00Z", + "endTime": "2026-02-23T15:45:00Z", + "messageCount": 8, + }, + { + "sessionId": "kf_003_wx345678_1708938232000", + "status": "expired", + "startTime": "2026-02-22T09:00:00Z", + "endTime": "2026-02-23T09:00:00Z", + "messageCount": 3, + }, + { + "sessionId": "kf_004_wx901234_1709024632000", + "status": "active", + "startTime": "2026-02-21T16:00:00Z", + "endTime": None, + "messageCount": 22, + }, + { + "sessionId": "kf_005_wx567890_1709111032000", + "status": "closed", + "startTime": "2026-02-20T11:00:00Z", + "endTime": "2026-02-20T12:30:00Z", + "messageCount": 12, + }, + ] + + filtered = mock_sessions + if status: + filtered = [s for s in filtered if s["status"] == status] + + total = len(filtered) + total_pages = (total + page_size - 1) // page_size + + return JSONResponse( + content={ + "data": filtered, + "pagination": { + "page": page, + "pageSize": page_size, + "total": total, + "totalPages": total_pages, + }, + } + ) + + +@router.get( + "/{session_id}", + operation_id="getSessionDetail", + summary="Get session details", + description="[AC-ASA-07] Get full session details with messages and trace.", + responses={ + 200: {"description": "Full session details with messages and trace"}, + 401: {"description": "Unauthorized", "model": ErrorResponse}, + 403: {"description": "Forbidden", "model": ErrorResponse}, + }, +) +async def get_session_detail( + tenant_id: Annotated[str, Depends(get_tenant_id)], + session_id: str, +) -> JSONResponse: + """ + [AC-ASA-07] Get session detail with messages and trace information. + """ + logger.info( + f"[AC-ASA-07] Getting session detail: tenant={tenant_id}, session_id={session_id}" + ) + + mock_session = { + "sessionId": session_id, + "messages": [ + { + "role": "user", + "content": "我想了解产品价格", + "timestamp": "2026-02-24T10:00:00Z", + }, + { + "role": "assistant", + "content": "您好,我们的产品价格根据套餐不同有所差异。基础版每月99元,专业版每月299元。企业版提供定制化服务。", + "timestamp": "2026-02-24T10:00:05Z", + }, + { + "role": "user", + "content": "专业版包含哪些功能?", + "timestamp": "2026-02-24T10:00:30Z", + }, + { + "role": "assistant", + "content": "专业版包含:高级数据分析、API 接入、优先技术支持、自定义报表等功能。", + "timestamp": "2026-02-24T10:00:35Z", + }, + ], + "trace": { + "retrieval": [ + { + "query": "产品价格", + "kbIds": ["kb_products"], + "results": [ + {"source": "pricing.pdf", "score": 0.92, "content": "..."} + ], + } + ], + "tools": [], + "errors": [], + }, + } + + return JSONResponse(content=mock_session) diff --git a/ai-service/app/main.py b/ai-service/app/main.py index 5746604..5aa7165 100644 --- a/ai-service/app/main.py +++ b/ai-service/app/main.py @@ -12,6 +12,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from app.api import chat_router, health_router +from app.api.admin import kb_router, rag_router, sessions_router from app.core.config import get_settings from app.core.database import close_db, init_db from app.core.exceptions import ( @@ -111,6 +112,10 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE app.include_router(health_router) app.include_router(chat_router) +app.include_router(kb_router) +app.include_router(rag_router) +app.include_router(sessions_router) + if __name__ == "__main__": import uvicorn