chore: update gitignore; feat: 新增customer_agent业务模块与api路由
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
"""Anonymous customer-service Agent HTTP endpoints."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
|
||||
from service.customer_agent.chat import QueryTooLongError
|
||||
from agent.customer_agent.session import SessionOwnershipError
|
||||
from utils.request_id import get_request_id, new_request_id
|
||||
from utils.response import fail, success
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _runtime(request: Request):
|
||||
runtime = getattr(request.app.state, "customer_agent_runtime", None)
|
||||
if runtime is None:
|
||||
raise RuntimeError("customer agent runtime is not configured")
|
||||
return runtime
|
||||
|
||||
|
||||
@router.post("/session/create")
|
||||
async def create_session(request: Request):
|
||||
runtime = _runtime(request)
|
||||
session_id = await runtime.session_service.create_session()
|
||||
return success({"session_id": session_id, "customer_id": None})
|
||||
|
||||
|
||||
@router.get("/chat")
|
||||
async def chat(request: Request, session_id: str, query: str):
|
||||
runtime = _runtime(request)
|
||||
trace_id = request.headers.get("X-Trace-Id") or get_request_id() or new_request_id()
|
||||
try:
|
||||
await runtime.session_service.verify_session_ownership(session_id)
|
||||
retry_after = await runtime.session_service.consume_chat_quota(session_id)
|
||||
if retry_after is not None:
|
||||
response = fail(429, "请求过于频繁", {"retry_after": retry_after})
|
||||
return JSONResponse(
|
||||
status_code=429,
|
||||
headers={"Retry-After": str(retry_after)},
|
||||
content=response.model_dump(),
|
||||
)
|
||||
result = await runtime.agent.handle(session_id, query, trace_id=trace_id)
|
||||
except SessionOwnershipError as exc:
|
||||
return JSONResponse(
|
||||
status_code=exc.code,
|
||||
content=fail(exc.code, exc.message).model_dump(),
|
||||
)
|
||||
except QueryTooLongError as exc:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content=fail(400, str(exc)).model_dump(),
|
||||
)
|
||||
|
||||
async def events():
|
||||
yield f"data: {json.dumps(result, ensure_ascii=False)}\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
events(), media_type="text/event-stream", headers={"X-Trace-Id": trace_id}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/session/end")
|
||||
async def end_session(request: Request, body: dict):
|
||||
runtime = _runtime(request)
|
||||
session_id = body.get("session_id", "")
|
||||
try:
|
||||
await runtime.session_service.verify_session_ownership(session_id)
|
||||
except SessionOwnershipError as exc:
|
||||
return JSONResponse(
|
||||
status_code=exc.code,
|
||||
content=fail(exc.code, exc.message).model_dump(),
|
||||
)
|
||||
if hasattr(runtime.redis, "delete"):
|
||||
await runtime.redis.delete(
|
||||
f"session:{session_id}", f"session:{session_id}:messages",
|
||||
f"rate:limit:anon:{session_id}:chat",
|
||||
)
|
||||
return success({"session_id": session_id, "archived": False})
|
||||
@@ -0,0 +1,108 @@
|
||||
"""Knowledge-base upload, preview, and confirmation endpoints."""
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from service.knowledge_base.upload import KnowledgeUploadService, UploadValidationError
|
||||
from api.deps import require_knowledge_operator
|
||||
from model.sys_user import SysUser
|
||||
from utils.response import fail, success
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _service(request: Request) -> KnowledgeUploadService:
|
||||
service = getattr(request.app.state, "knowledge_upload_service", None)
|
||||
if service is None:
|
||||
raise RuntimeError("knowledge upload service is not configured")
|
||||
return service
|
||||
|
||||
|
||||
def _optional_int(value):
|
||||
return None if value in (None, "") else int(value)
|
||||
|
||||
|
||||
@router.get("/documents")
|
||||
async def list_documents(
|
||||
request: Request,
|
||||
_: SysUser = Depends(require_knowledge_operator),
|
||||
):
|
||||
return success(await _service(request).list_documents())
|
||||
|
||||
|
||||
@router.get("/documents/{doc_id}")
|
||||
async def get_document(
|
||||
request: Request,
|
||||
doc_id: str,
|
||||
_: SysUser = Depends(require_knowledge_operator),
|
||||
):
|
||||
try:
|
||||
result = await _service(request).get_document(doc_id)
|
||||
if result is None:
|
||||
response = fail(404, "文档不存在")
|
||||
return JSONResponse(status_code=404, content=response.model_dump())
|
||||
return success(result)
|
||||
except (UploadValidationError, ValueError) as exc:
|
||||
response = fail(400, str(exc))
|
||||
return JSONResponse(status_code=400, content=response.model_dump())
|
||||
|
||||
|
||||
@router.post("/documents/preview")
|
||||
async def preview_document_upload(
|
||||
request: Request,
|
||||
_: SysUser = Depends(require_knowledge_operator),
|
||||
):
|
||||
try:
|
||||
form = await request.form()
|
||||
upload = form.get("file")
|
||||
if upload is None or not hasattr(upload, "read"):
|
||||
raise UploadValidationError("缺少上传文件")
|
||||
result = await _service(request).preview(
|
||||
upload.filename,
|
||||
await upload.read(),
|
||||
strategy=form.get("strategy", ""),
|
||||
chunk_size=_optional_int(form.get("chunk_size")),
|
||||
chunk_overlap=_optional_int(form.get("chunk_overlap")),
|
||||
)
|
||||
return success(result)
|
||||
except (UploadValidationError, ValueError) as exc:
|
||||
response = fail(400, str(exc))
|
||||
return JSONResponse(status_code=400, content=response.model_dump())
|
||||
|
||||
|
||||
@router.post("/documents/confirm")
|
||||
async def confirm_document_upload(
|
||||
request: Request,
|
||||
_: SysUser = Depends(require_knowledge_operator),
|
||||
):
|
||||
try:
|
||||
body = await request.json()
|
||||
result = await _service(request).confirm(
|
||||
upload_id=body.get("upload_id", ""),
|
||||
title=body.get("title", ""),
|
||||
doc_id=body.get("doc_id", ""),
|
||||
collection_name=body.get("collection_name", ""),
|
||||
strategy=body.get("strategy", ""),
|
||||
chunk_size=body.get("chunk_size"),
|
||||
chunk_overlap=body.get("chunk_overlap"),
|
||||
)
|
||||
return success(result)
|
||||
except (UploadValidationError, ValueError) as exc:
|
||||
response = fail(400, str(exc))
|
||||
return JSONResponse(status_code=400, content=response.model_dump())
|
||||
|
||||
|
||||
@router.delete("/documents/{doc_id}")
|
||||
async def delete_document(
|
||||
request: Request,
|
||||
doc_id: str,
|
||||
_: SysUser = Depends(require_knowledge_operator),
|
||||
):
|
||||
try:
|
||||
result = await _service(request).delete_document(doc_id)
|
||||
return success(result)
|
||||
except (UploadValidationError, ValueError) as exc:
|
||||
response = fail(400, str(exc))
|
||||
return JSONResponse(status_code=400, content=response.model_dump())
|
||||
Reference in New Issue
Block a user