Files
xiaozhi-esp32-server/main/manager-api-fastapi/app/routers/agent.py
T

387 lines
15 KiB
Python

from __future__ import annotations
from typing import Annotated
from fastapi import APIRouter, BackgroundTasks, Body, Depends, Query, Request
from sqlalchemy.ext.asyncio import AsyncSession
from starlette.responses import Response
from app.core.database import get_db
from app.core.errors import ErrorCode
from app.core.responses import JavaJSONResponse, error_response, ok
from app.core.security import AuthUser, require_normal, require_super_admin
from app.schemas.agent import (
AgentChatHistoryReport,
AgentCreate,
AgentMemory,
AgentSnapshotPage,
AgentSnapshotRestore,
AgentTagAssignment,
AgentTemplate,
AgentUpdate,
AgentVoicePrintSave,
AgentVoicePrintUpdate,
)
from app.services.agent import AgentService, run_chat_summary_task
router = APIRouter(tags=["agent"])
DbSession = Annotated[AsyncSession, Depends(get_db)]
NormalUser = Annotated[AuthUser, Depends(require_normal)]
SuperUser = Annotated[AuthUser, Depends(require_super_admin)]
def _service(session: AsyncSession, user: AuthUser | None, request: Request) -> AgentService:
return AgentService(session, user, language=request.headers.get("Accept-Language"))
@router.post("/agent/chat-history/report")
async def report_chat_history(report: AgentChatHistoryReport, request: Request, session: DbSession) -> JavaJSONResponse:
return ok(await _service(session, None, request).report_chat(report))
@router.post("/agent/chat-history/getDownloadUrl/{agentId}/{sessionId}")
async def issue_chat_history_download(
agentId: str, sessionId: str, request: Request, session: DbSession, user: NormalUser
) -> JavaJSONResponse:
service = _service(session, user, request)
if not await service.has_agent_permission(agentId):
return error_response(request, 10132)
return ok(await service.issue_history_token(agentId, sessionId))
@router.get("/agent/chat-history/download/{uuid}/current")
async def download_current_chat_history(uuid: str, request: Request, session: DbSession) -> Response:
content = await _service(session, None, request).consume_history_download(uuid, previous=False)
return Response(
content.encode("utf-8"),
media_type="text/plain;charset=UTF-8",
headers={"Content-Disposition": "attachment;filename=history.txt"},
)
@router.get("/agent/chat-history/download/{uuid}/previous")
async def download_previous_chat_history(uuid: str, request: Request, session: DbSession) -> Response:
content = await _service(session, None, request).consume_history_download(uuid, previous=True)
return Response(
content.encode("utf-8"),
media_type="text/plain;charset=UTF-8",
headers={"Content-Disposition": "attachment;filename=history.txt"},
)
# Static paths are deliberately registered before /agent/{id}; Starlette resolves in declaration order.
@router.get("/agent/template/page")
async def template_page(
request: Request,
session: DbSession,
user: SuperUser,
page: int = Query(default=1),
limit: int = Query(default=10),
agentName: str | None = Query(default=None),
) -> JavaJSONResponse:
return ok(await _service(session, user, request).template_page(page, limit, agentName))
@router.post("/agent/template/batch-remove")
async def batch_delete_templates(
ids: list[str], request: Request, session: DbSession, user: SuperUser
) -> JavaJSONResponse:
deleted = await _service(session, user, request).batch_delete_templates(ids)
return (
ok("批量删除成功") if deleted else error_response(request, ErrorCode.INTERNAL_SERVER_ERROR, "批量删除模板失败")
)
@router.get("/agent/template/{id}")
async def template_detail(id: str, request: Request, session: DbSession, user: SuperUser) -> JavaJSONResponse:
result = await _service(session, user, request).template_detail(id)
return ok(result) if result is not None else error_response(request, ErrorCode.INTERNAL_SERVER_ERROR, "模板不存在")
@router.post("/agent/template")
async def create_template(
template: AgentTemplate, request: Request, session: DbSession, user: SuperUser
) -> JavaJSONResponse:
return ok(await _service(session, user, request).create_template(template))
@router.put("/agent/template")
async def update_template(
template: AgentTemplate, request: Request, session: DbSession, user: SuperUser
) -> JavaJSONResponse:
# MyBatis-Plus raises before returning a boolean when updateById receives
# an entity without its @TableId. Keep Java's generic error envelope for
# that exact input; an unknown but non-empty id still returns the controller's
# explicit "更新模板失败" message below.
if template.id is None:
return error_response(request, ErrorCode.INTERNAL_SERVER_ERROR)
updated = await _service(session, user, request).update_template(template)
return ok(template) if updated else error_response(request, ErrorCode.INTERNAL_SERVER_ERROR, "更新模板失败")
@router.delete("/agent/template/{id}")
async def delete_template(id: str, request: Request, session: DbSession, user: SuperUser) -> JavaJSONResponse:
service = _service(session, user, request)
if await service.template_detail(id) is None:
return error_response(request, ErrorCode.INTERNAL_SERVER_ERROR, "模板不存在")
return (
ok("删除模板成功")
if await service.delete_template(id)
else error_response(request, ErrorCode.INTERNAL_SERVER_ERROR, "删除模板失败")
)
@router.post("/agent/voice-print")
async def create_voiceprint(
dto: AgentVoicePrintSave, request: Request, session: DbSession, user: NormalUser
) -> JavaJSONResponse:
created = await _service(session, user, request).create_voiceprint(dto)
return ok() if created else error_response(request, 10057)
@router.put("/agent/voice-print")
async def update_voiceprint(
dto: AgentVoicePrintUpdate, request: Request, session: DbSession, user: NormalUser
) -> JavaJSONResponse:
updated = await _service(session, user, request).update_voiceprint(dto)
return ok() if updated else error_response(request, 10058)
@router.delete("/agent/voice-print/{id}")
async def delete_voiceprint(id: str, request: Request, session: DbSession, user: NormalUser) -> JavaJSONResponse:
deleted = await _service(session, user, request).delete_voiceprint(id)
return ok() if deleted else error_response(request, 10059)
@router.get("/agent/voice-print/list/{id}")
async def list_voiceprints(id: str, request: Request, session: DbSession, user: NormalUser) -> JavaJSONResponse:
return ok(await _service(session, user, request).voiceprint_list(id))
@router.get("/agent/mcp/address/{agentId}")
async def mcp_address(agentId: str, request: Request, session: DbSession, user: NormalUser) -> JavaJSONResponse:
service = _service(session, user, request)
if not await service.has_agent_permission(agentId):
return error_response(request, 10200)
address = await service.mcp_address(agentId)
return ok(address) if address is not None else error_response(request, 10201)
@router.get("/agent/mcp/tools/{agentId}")
async def mcp_tools(agentId: str, request: Request, session: DbSession, user: NormalUser) -> JavaJSONResponse:
service = _service(session, user, request)
if not await service.has_agent_permission(agentId):
return error_response(request, 10202)
return ok(await service.mcp_tools(agentId))
@router.get("/agent/tag/list")
async def all_tags(request: Request, session: DbSession, user: NormalUser) -> JavaJSONResponse:
return ok(await _service(session, user, request).all_tags())
@router.post("/agent/tag")
async def create_tag(
request: Request,
session: DbSession,
user: NormalUser,
params: dict[str, str] = Body(...),
) -> JavaJSONResponse:
tag_name = params.get("tagName")
if tag_name is None or not tag_name.strip():
return error_response(request, ErrorCode.INTERNAL_SERVER_ERROR, "标签名称不能为空")
return ok(await _service(session, user, request).save_tag(tag_name))
@router.delete("/agent/tag/{id}")
async def delete_tag(id: str, request: Request, session: DbSession, user: NormalUser) -> JavaJSONResponse:
await _service(session, user, request).delete_tag(id)
return ok()
@router.post("/agent/audio/{audioId}")
async def issue_audio_token(audioId: str, request: Request, session: DbSession, user: NormalUser) -> JavaJSONResponse:
token = await _service(session, user, request).issue_audio_token(audioId)
return ok(token) if token is not None else error_response(request, ErrorCode.INTERNAL_SERVER_ERROR, "音频不存在")
@router.get("/agent/play/{uuid}")
async def play_agent_audio(uuid: str, request: Request, session: DbSession) -> Response:
audio = await _service(session, None, request).consume_audio_token(uuid)
if audio is None:
return Response(status_code=404)
return Response(
audio,
media_type="application/octet-stream",
headers={"Content-Disposition": 'attachment; filename="play.wav"'},
)
@router.put("/agent/saveMemory/{macAddress}")
async def update_memory(
macAddress: str, dto: AgentMemory, request: Request, session: DbSession, user: NormalUser
) -> JavaJSONResponse:
await _service(session, user, request).update_memory_by_mac(macAddress, dto)
return ok()
@router.post("/agent/chat-summary/{sessionId}/save")
async def save_chat_summary(
sessionId: str, background_tasks: BackgroundTasks, request: Request, session: DbSession
) -> JavaJSONResponse:
await _service(session, None, request).session_agent(sessionId)
background_tasks.add_task(run_chat_summary_task, sessionId)
return ok()
@router.post("/agent/chat-title/{sessionId}/generate")
async def generate_chat_title(sessionId: str, request: Request, session: DbSession) -> JavaJSONResponse:
service = _service(session, None, request)
await service.session_agent(sessionId)
await service.generate_chat_title(sessionId)
return ok()
@router.get("/agent/all")
async def admin_agent_list(
request: Request,
session: DbSession,
user: SuperUser,
page: int = Query(default=1),
limit: int = Query(default=10),
orderField: str | None = Query(default=None),
order: str | None = Query(default=None),
) -> JavaJSONResponse:
return ok(await _service(session, user, request).admin_agents(page, limit, orderField, order))
@router.get("/agent/list")
async def user_agent_list(
request: Request,
session: DbSession,
user: NormalUser,
keyword: str | None = Query(default=None),
searchType: str = Query(default="name"),
) -> JavaJSONResponse:
del searchType # Java accepts the parameter but the consolidated implementation ignores it.
return ok(await _service(session, user, request).user_agents(keyword))
@router.get("/agent/template")
async def template_list(request: Request, session: DbSession, user: NormalUser) -> JavaJSONResponse:
return ok(await _service(session, user, request).templates())
@router.get("/agent/{agentId}/snapshots")
async def snapshot_page(
agentId: str,
request: Request,
session: DbSession,
user: NormalUser,
page: int | None = Query(default=1),
limit: int | None = Query(default=10),
maxVersionNo: int | None = Query(default=None),
) -> JavaJSONResponse:
params = AgentSnapshotPage(page=page, limit=limit, max_version_no=maxVersionNo)
return ok(await _service(session, user, request).snapshot_page(agentId, params))
@router.get("/agent/{agentId}/snapshots/{snapshotId}")
async def snapshot_detail(
agentId: str, snapshotId: str, request: Request, session: DbSession, user: NormalUser
) -> JavaJSONResponse:
return ok(await _service(session, user, request).snapshot_detail(agentId, snapshotId))
@router.post("/agent/{agentId}/snapshots/{snapshotId}/restore")
async def restore_snapshot(
agentId: str,
snapshotId: str,
dto: AgentSnapshotRestore,
request: Request,
session: DbSession,
user: NormalUser,
) -> JavaJSONResponse:
await _service(session, user, request).restore_snapshot(agentId, snapshotId, dto.current_state_token)
return ok()
@router.delete("/agent/{agentId}/snapshots/{snapshotId}")
async def delete_snapshot(
agentId: str, snapshotId: str, request: Request, session: DbSession, user: NormalUser
) -> JavaJSONResponse:
await _service(session, user, request).delete_snapshot(agentId, snapshotId)
return ok()
@router.get("/agent/{id}/sessions")
async def agent_sessions(
id: str,
request: Request,
session: DbSession,
user: NormalUser,
page: str | None = Query(default=None),
limit: str | None = Query(default=None),
) -> JavaJSONResponse:
return ok(await _service(session, user, request).sessions(id, page, limit))
@router.get("/agent/{id}/chat-history/user")
async def recent_agent_history(id: str, request: Request, session: DbSession, user: NormalUser) -> JavaJSONResponse:
service = _service(session, user, request)
if not await service.has_agent_permission(id):
return error_response(request, ErrorCode.INTERNAL_SERVER_ERROR, "没有权限查看该智能体的聊天记录")
return ok(await service.recent_user_history(id))
@router.get("/agent/{id}/chat-history/audio")
async def agent_audio_content(id: str, request: Request, session: DbSession, user: NormalUser) -> JavaJSONResponse:
return ok(await _service(session, user, request).audio_content(id))
@router.get("/agent/{id}/chat-history/{sessionId}")
async def agent_history(
id: str, sessionId: str, request: Request, session: DbSession, user: NormalUser
) -> JavaJSONResponse:
service = _service(session, user, request)
if not await service.has_agent_permission(id):
return error_response(request, ErrorCode.INTERNAL_SERVER_ERROR, "没有权限查看该智能体的聊天记录")
return ok(await service.history(id, sessionId))
@router.get("/agent/{id}/tags")
async def agent_tags(id: str, request: Request, session: DbSession, user: NormalUser) -> JavaJSONResponse:
return ok(await _service(session, user, request).agent_tags(id))
@router.put("/agent/{id}/tags")
async def save_agent_tags(
id: str, dto: AgentTagAssignment, request: Request, session: DbSession, user: NormalUser
) -> JavaJSONResponse:
await _service(session, user, request).save_agent_tags(id, dto.tag_ids, dto.tag_names)
return ok()
@router.post("/agent")
async def create_agent(dto: AgentCreate, request: Request, session: DbSession, user: NormalUser) -> JavaJSONResponse:
return ok(await _service(session, user, request).create_agent(dto))
@router.put("/agent/{id}")
async def update_agent(
id: str, dto: AgentUpdate, request: Request, session: DbSession, user: NormalUser
) -> JavaJSONResponse:
await _service(session, user, request).update_agent(id, dto)
return ok()
@router.delete("/agent/{id}")
async def delete_agent(id: str, request: Request, session: DbSession, user: NormalUser) -> JavaJSONResponse:
await _service(session, user, request).delete_agent(id)
return ok()
@router.get("/agent/{id}")
async def agent_detail(id: str, request: Request, session: DbSession, user: NormalUser) -> JavaJSONResponse:
return ok(await _service(session, user, request).agent_detail(id))