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:
parent
1230b4005a
commit
2e5ddc3653
|
|
@ -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"]
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -12,6 +12,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
from app.api import chat_router, health_router
|
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.config import get_settings
|
||||||
from app.core.database import close_db, init_db
|
from app.core.database import close_db, init_db
|
||||||
from app.core.exceptions import (
|
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(health_router)
|
||||||
app.include_router(chat_router)
|
app.include_router(chat_router)
|
||||||
|
|
||||||
|
app.include_router(kb_router)
|
||||||
|
app.include_router(rag_router)
|
||||||
|
app.include_router(sessions_router)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue