feat: implement admin management APIs [AC-ASA-01, AC-ASA-02, AC-ASA-05, AC-ASA-07, AC-ASA-08, AC-ASA-09]

This commit is contained in:
MerCry 2026-02-24 16:10:27 +08:00
parent 1230b4005a
commit 2e5ddc3653
5 changed files with 444 additions and 0 deletions

View File

@ -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"]

View File

@ -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)

View File

@ -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,
}
)

View File

@ -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)

View File

@ -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