1138 lines
43 KiB
Python
1138 lines
43 KiB
Python
"""
|
||
智能体 API
|
||
"""
|
||
import json
|
||
import logging
|
||
from typing import List, Optional
|
||
from datetime import datetime
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||
from fastapi.responses import StreamingResponse
|
||
from sqlalchemy import select, func, or_, and_
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.database import get_db
|
||
from app.base_schema import PaginatedResponse, ResponseModel
|
||
from utils.context import get_current_user_id_from_context
|
||
from ai_platform.models import (
|
||
Agent, AgentConversation, AgentMessage,
|
||
LLMModel, LLMProvider, AIWorkflow,
|
||
)
|
||
from core.application.model import Application
|
||
from ai_platform.schemas.agent_schema import (
|
||
AgentCreate,
|
||
AgentUpdate,
|
||
AgentResponse,
|
||
AgentListResponse,
|
||
AgentImportCheckIn,
|
||
AgentImportCheckOut,
|
||
AgentImportIn,
|
||
AgentConversationCreate,
|
||
AgentConversationResponse,
|
||
AgentConversationListResponse,
|
||
AgentMessageResponse,
|
||
ChatInput,
|
||
MessageFeedback,
|
||
AgentPublishInput,
|
||
)
|
||
from ai_platform.services.agent_import_export import (
|
||
AgentImportExportException,
|
||
export_config as export_agent_config,
|
||
check_import as check_agent_import,
|
||
import_config as import_agent_config,
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
CODEX_AGENT_PROMPT = """
|
||
你是 ai-agent-admin 内置的 Codex 协作助手。你的目标是帮助用户拆解工程任务、解释代码、规划实现、生成可执行步骤,并在需要时提示用户进入智能体、流程编排、模型配置和执行历史等模块完成操作。
|
||
|
||
回答要求:
|
||
- 直接、具体、可执行。
|
||
- 优先给出最小可落地方案,再说明关键风险。
|
||
- 涉及代码或配置时,明确文件、接口、命令或字段。
|
||
- 对不确定事实要说明需要验证,不要编造。
|
||
""".strip()
|
||
|
||
|
||
async def _resolve_default_chat_model_id(db: AsyncSession) -> Optional[str]:
|
||
result = await db.execute(
|
||
select(LLMModel)
|
||
.join(LLMProvider, LLMProvider.id == LLMModel.provider_id)
|
||
.where(
|
||
LLMModel.is_deleted == False,
|
||
LLMModel.is_active == True,
|
||
LLMModel.model_type == "chat",
|
||
LLMProvider.is_deleted == False,
|
||
LLMProvider.is_active == True,
|
||
)
|
||
.order_by(LLMModel.sort.desc(), LLMModel.sys_create_datetime.desc())
|
||
)
|
||
model = result.scalars().first()
|
||
return str(model.id) if model else None
|
||
|
||
|
||
async def _ensure_codex_agent(db: AsyncSession) -> Agent:
|
||
model_id = await _resolve_default_chat_model_id(db)
|
||
if not model_id:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail="Codex 智能体需要先配置一个启用的 chat 模型",
|
||
)
|
||
|
||
agent = Agent(
|
||
name="Codex",
|
||
code="codex",
|
||
description="内置通用 AI 编程协作助手",
|
||
mode="autonomous",
|
||
status="published",
|
||
is_global=True,
|
||
is_public=True,
|
||
model_id=model_id,
|
||
temperature=0.3,
|
||
top_p=1.0,
|
||
max_tokens=4096,
|
||
max_iterations=6,
|
||
enable_memory=True,
|
||
memory_window=10,
|
||
enable_streaming=True,
|
||
persona={
|
||
"role": "AI 编程协作助手",
|
||
"skills": ["代码理解", "任务拆解", "方案设计", "问题排查"],
|
||
"constraints": ["不编造事实", "优先输出可执行步骤"],
|
||
},
|
||
system_prompt=CODEX_AGENT_PROMPT,
|
||
welcome_message="我是 Codex 协作助手,可以帮你拆解任务、解释代码、规划实现和排查问题。",
|
||
suggested_questions=[
|
||
"帮我分析当前 AI Agent Admin 的下一步实现重点",
|
||
"解释一下智能体和流程编排如何协作",
|
||
"帮我设计一个 OC-69 多智能体协作流程",
|
||
],
|
||
sort=100,
|
||
)
|
||
db.add(agent)
|
||
await db.commit()
|
||
await db.refresh(agent)
|
||
return agent
|
||
|
||
router = APIRouter(prefix="/agent", tags=["AI-智能体"])
|
||
|
||
|
||
# ==================== Agent API ====================
|
||
|
||
# 资源类型(用于数据权限配置)
|
||
AGENT_RESOURCE_TYPE = "ai_agent"
|
||
AGENT_RESOURCE_DISPLAY_NAME = "智能体管理"
|
||
|
||
|
||
@router.get("/list", response_model=PaginatedResponse[AgentListResponse], summary="智能体列表")
|
||
async def list_agents(
|
||
name: Optional[str] = Query(None, description="名称"),
|
||
mode: Optional[str] = Query(None, description="模式"),
|
||
status: Optional[str] = Query(None, description="状态"),
|
||
application_id: Optional[str] = Query(None, alias="applicationId", description="所属应用ID"),
|
||
page: int = Query(1, ge=1, description="页码"),
|
||
page_size: int = Query(20, ge=1, le=100, alias="pageSize", description="每页数量"),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""获取智能体列表(自动应用数据权限)"""
|
||
from app.data_scope_utils import get_data_scope_filter, apply_data_scope_to_conditions
|
||
|
||
conditions = [Agent.is_deleted == False]
|
||
|
||
if application_id:
|
||
conditions.append(or_(
|
||
Agent.application_id == application_id,
|
||
and_(Agent.application_id.is_(None), Agent.is_global == True)
|
||
))
|
||
if name:
|
||
conditions.append(Agent.name.ilike(f"%{name}%"))
|
||
if mode:
|
||
conditions.append(Agent.mode == mode)
|
||
if status:
|
||
conditions.append(Agent.status == status)
|
||
|
||
# 获取数据权限过滤条件并应用
|
||
data_scope_filter = await get_data_scope_filter(db, AGENT_RESOURCE_TYPE)
|
||
scope_conditions = apply_data_scope_to_conditions(Agent, data_scope_filter)
|
||
conditions.extend(scope_conditions)
|
||
|
||
# 获取总数
|
||
query = select(Agent).where(and_(*conditions))
|
||
count_result = await db.execute(select(func.count()).select_from(query.subquery()))
|
||
total = count_result.scalar() or 0
|
||
|
||
# 分页
|
||
offset = (page - 1) * page_size
|
||
query = query.order_by(Agent.sort.desc(), Agent.sys_create_datetime.desc())
|
||
query = query.offset(offset).limit(page_size)
|
||
|
||
result = await db.execute(query)
|
||
agents = result.scalars().all()
|
||
|
||
# 批量查询应用名称
|
||
app_ids = list({a.application_id for a in agents if a.application_id})
|
||
app_name_map = {}
|
||
if app_ids:
|
||
app_result = await db.execute(
|
||
select(Application.id, Application.name).where(Application.id.in_(app_ids))
|
||
)
|
||
app_name_map = {row.id: row.name for row in app_result}
|
||
|
||
# 批量查询菜单,判断哪些智能体已发布到菜单
|
||
from core.menu.model import Menu
|
||
agent_codes = [a.code for a in agents if a.code]
|
||
menu_paths = [f"/agent-chat/{code}" for code in agent_codes]
|
||
has_menu_set = set()
|
||
if menu_paths:
|
||
menu_result = await db.execute(
|
||
select(Menu.path).where(
|
||
Menu.path.in_(menu_paths),
|
||
Menu.is_deleted == False
|
||
)
|
||
)
|
||
has_menu_set = {row.path for row in menu_result}
|
||
|
||
items = []
|
||
for agent in agents:
|
||
model_name = ""
|
||
if agent.model_id:
|
||
model_result = await db.execute(
|
||
select(LLMModel).where(LLMModel.id == agent.model_id)
|
||
)
|
||
model = model_result.scalar_one_or_none()
|
||
model_name = model.display_name if model else ""
|
||
|
||
has_menu = f"/agent-chat/{agent.code}" in has_menu_set
|
||
items.append(_build_agent_list_response(agent, model_name, app_name_map.get(agent.application_id, ""), has_menu))
|
||
|
||
return PaginatedResponse(items=items, total=total)
|
||
|
||
|
||
@router.get("/published", response_model=List[AgentListResponse], summary="获取已发布智能体")
|
||
async def list_published_agents(db: AsyncSession = Depends(get_db)):
|
||
"""获取已发布的智能体列表(用于用户选择)"""
|
||
query = select(Agent).where(
|
||
Agent.is_deleted == False,
|
||
Agent.status == "published"
|
||
).order_by(Agent.sort.desc())
|
||
|
||
result = await db.execute(query)
|
||
agents = result.scalars().all()
|
||
|
||
items = []
|
||
for agent in agents:
|
||
model_name = ""
|
||
if agent.model_id:
|
||
model_result = await db.execute(
|
||
select(LLMModel).where(LLMModel.id == agent.model_id)
|
||
)
|
||
model = model_result.scalar_one_or_none()
|
||
model_name = model.display_name if model else ""
|
||
|
||
items.append(_build_agent_list_response(agent, model_name))
|
||
|
||
return items
|
||
|
||
|
||
@router.get("/code/{code}", response_model=AgentResponse, summary="根据编码获取智能体")
|
||
async def get_agent_by_code(code: str, db: AsyncSession = Depends(get_db)):
|
||
"""根据编码获取智能体"""
|
||
result = await db.execute(
|
||
select(Agent).where(Agent.code == code, Agent.is_deleted == False)
|
||
)
|
||
agent = result.scalar_one_or_none()
|
||
if not agent and code == "codex":
|
||
agent = await _ensure_codex_agent(db)
|
||
if not agent:
|
||
raise HTTPException(status_code=404, detail="智能体不存在")
|
||
|
||
return await _build_agent_response(agent, db)
|
||
|
||
|
||
@router.get("/{agent_id}", response_model=AgentResponse, summary="智能体详情")
|
||
async def get_agent(agent_id: str, db: AsyncSession = Depends(get_db)):
|
||
"""获取智能体详情"""
|
||
result = await db.execute(
|
||
select(Agent).where(Agent.id == agent_id, Agent.is_deleted == False)
|
||
)
|
||
agent = result.scalar_one_or_none()
|
||
if not agent:
|
||
raise HTTPException(status_code=404, detail="智能体不存在")
|
||
|
||
return await _build_agent_response(agent, db)
|
||
|
||
|
||
@router.post("", response_model=AgentResponse, summary="创建智能体")
|
||
async def create_agent(data: AgentCreate, db: AsyncSession = Depends(get_db)):
|
||
"""创建智能体"""
|
||
# 检查编码是否重复
|
||
exists_result = await db.execute(
|
||
select(Agent).where(Agent.code == data.code, Agent.is_deleted == False)
|
||
)
|
||
if exists_result.scalar_one_or_none():
|
||
raise HTTPException(status_code=400, detail=f"智能体编码 {data.code} 已存在")
|
||
|
||
# 验证模型
|
||
if data.model_id:
|
||
model_result = await db.execute(
|
||
select(LLMModel).where(LLMModel.id == data.model_id, LLMModel.is_deleted == False)
|
||
)
|
||
if not model_result.scalar_one_or_none():
|
||
raise HTTPException(status_code=400, detail="模型不存在")
|
||
|
||
# 验证工作流
|
||
if data.workflow_id:
|
||
workflow_result = await db.execute(
|
||
select(AIWorkflow).where(AIWorkflow.id == data.workflow_id, AIWorkflow.is_deleted == False)
|
||
)
|
||
if not workflow_result.scalar_one_or_none():
|
||
raise HTTPException(status_code=400, detail="工作流不存在")
|
||
|
||
# 创建智能体
|
||
agent_data = data.model_dump()
|
||
agent = Agent(**agent_data)
|
||
|
||
# 自动填充创建人和部门
|
||
from utils.context import get_current_user_info_from_context
|
||
user_info = get_current_user_info_from_context()
|
||
if user_info:
|
||
if not agent.sys_creator_id:
|
||
agent.sys_creator_id = user_info.get('user_id')
|
||
if not agent.sys_dept_id and user_info.get('dept_id'):
|
||
agent.sys_dept_id = user_info.get('dept_id')
|
||
|
||
db.add(agent)
|
||
await db.commit()
|
||
await db.refresh(agent)
|
||
|
||
return await _build_agent_response(agent, db)
|
||
|
||
|
||
@router.put("/{agent_id}", response_model=AgentResponse, summary="更新智能体")
|
||
async def update_agent(
|
||
agent_id: str,
|
||
data: AgentUpdate,
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""更新智能体"""
|
||
result = await db.execute(
|
||
select(Agent).where(Agent.id == agent_id, Agent.is_deleted == False)
|
||
)
|
||
agent = result.scalar_one_or_none()
|
||
if not agent:
|
||
raise HTTPException(status_code=404, detail="智能体不存在")
|
||
|
||
update_data = data.model_dump(exclude_unset=True)
|
||
|
||
# 验证模型
|
||
if "model_id" in update_data and update_data["model_id"]:
|
||
model_result = await db.execute(
|
||
select(LLMModel).where(LLMModel.id == update_data["model_id"], LLMModel.is_deleted == False)
|
||
)
|
||
if not model_result.scalar_one_or_none():
|
||
raise HTTPException(status_code=400, detail="模型不存在")
|
||
|
||
# 验证工作流
|
||
if "workflow_id" in update_data and update_data["workflow_id"]:
|
||
workflow_result = await db.execute(
|
||
select(AIWorkflow).where(AIWorkflow.id == update_data["workflow_id"], AIWorkflow.is_deleted == False)
|
||
)
|
||
if not workflow_result.scalar_one_or_none():
|
||
raise HTTPException(status_code=400, detail="工作流不存在")
|
||
|
||
for key, value in update_data.items():
|
||
setattr(agent, key, value)
|
||
|
||
await db.commit()
|
||
await db.refresh(agent)
|
||
|
||
return await _build_agent_response(agent, db)
|
||
|
||
|
||
@router.delete("/{agent_id}", response_model=ResponseModel, summary="删除智能体")
|
||
async def delete_agent(agent_id: str, db: AsyncSession = Depends(get_db)):
|
||
"""删除智能体"""
|
||
result = await db.execute(
|
||
select(Agent).where(Agent.id == agent_id, Agent.is_deleted == False)
|
||
)
|
||
agent = result.scalar_one_or_none()
|
||
if not agent:
|
||
raise HTTPException(status_code=404, detail="智能体不存在")
|
||
|
||
agent.is_deleted = True
|
||
await db.commit()
|
||
|
||
return ResponseModel(message="删除成功")
|
||
|
||
|
||
@router.get("/{agent_id}/export", summary="导出智能体配置")
|
||
async def export_agent(
|
||
agent_id: str,
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""导出智能体配置为 JSON"""
|
||
try:
|
||
config = await export_agent_config(db, agent_id)
|
||
content = json.dumps(config, ensure_ascii=False, indent=2)
|
||
return StreamingResponse(
|
||
iter([content]),
|
||
media_type="application/json",
|
||
headers={
|
||
"Content-Disposition": f'attachment; filename="{config["code"]}.json"'
|
||
},
|
||
)
|
||
except AgentImportExportException as e:
|
||
raise HTTPException(status_code=400, detail=str(e))
|
||
|
||
|
||
@router.post("/import/check", response_model=AgentImportCheckOut, summary="导入预检查")
|
||
async def check_import_agent(
|
||
data: AgentImportCheckIn,
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""导入预检查:检查智能体编码是否冲突"""
|
||
try:
|
||
return await check_agent_import(db, data.code)
|
||
except AgentImportExportException as e:
|
||
raise HTTPException(status_code=400, detail=str(e))
|
||
|
||
|
||
@router.post("/import", response_model=AgentResponse, summary="导入智能体配置")
|
||
async def import_agent(
|
||
data: AgentImportIn,
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""导入智能体配置"""
|
||
try:
|
||
agent = await import_agent_config(db, data.model_dump())
|
||
await db.commit()
|
||
return await _build_agent_response(agent, db)
|
||
except AgentImportExportException as e:
|
||
raise HTTPException(status_code=400, detail=str(e))
|
||
|
||
|
||
@router.post("/{agent_id}/publish", response_model=AgentResponse, summary="发布智能体")
|
||
async def publish_agent(agent_id: str, db: AsyncSession = Depends(get_db)):
|
||
"""发布智能体"""
|
||
result = await db.execute(
|
||
select(Agent).where(Agent.id == agent_id, Agent.is_deleted == False)
|
||
)
|
||
agent = result.scalar_one_or_none()
|
||
if not agent:
|
||
raise HTTPException(status_code=404, detail="智能体不存在")
|
||
|
||
# 验证必要配置
|
||
if agent.mode == "autonomous":
|
||
if agent.model_id:
|
||
model_result = await db.execute(
|
||
select(LLMModel).where(
|
||
LLMModel.id == agent.model_id,
|
||
LLMModel.is_deleted == False,
|
||
)
|
||
)
|
||
model = model_result.scalar_one_or_none()
|
||
if not model:
|
||
raise HTTPException(status_code=400, detail="模型不存在")
|
||
if not model.is_active:
|
||
raise HTTPException(status_code=400, detail="模型已禁用")
|
||
elif not await _resolve_default_chat_model_id(db):
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail="请先在模型配置中启用一个 chat 模型,或为智能体指定模型",
|
||
)
|
||
elif agent.mode == "dialog_flow":
|
||
if not agent.workflow_id:
|
||
raise HTTPException(status_code=400, detail="对话流模式需要配置工作流")
|
||
|
||
agent.status = "published"
|
||
await db.commit()
|
||
await db.refresh(agent)
|
||
|
||
return await _build_agent_response(agent, db)
|
||
|
||
|
||
@router.post("/{agent_id}/disable", response_model=AgentResponse, summary="停用智能体")
|
||
async def disable_agent(agent_id: str, db: AsyncSession = Depends(get_db)):
|
||
"""停用智能体"""
|
||
result = await db.execute(
|
||
select(Agent).where(Agent.id == agent_id, Agent.is_deleted == False)
|
||
)
|
||
agent = result.scalar_one_or_none()
|
||
if not agent:
|
||
raise HTTPException(status_code=404, detail="智能体不存在")
|
||
|
||
agent.status = "disabled"
|
||
await db.commit()
|
||
await db.refresh(agent)
|
||
|
||
return await _build_agent_response(agent, db)
|
||
|
||
|
||
@router.post("/{agent_id}/publish-to-menu", response_model=AgentResponse, summary="发布智能体到菜单")
|
||
async def publish_agent_to_menu(
|
||
agent_id: str,
|
||
data: AgentPublishInput,
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""发布智能体到菜单系统"""
|
||
from core.menu.model import Menu
|
||
from core.menu.service import MenuService
|
||
|
||
# 获取智能体
|
||
result = await db.execute(
|
||
select(Agent).where(Agent.id == agent_id, Agent.is_deleted == False)
|
||
)
|
||
agent = result.scalar_one_or_none()
|
||
if not agent:
|
||
raise HTTPException(status_code=404, detail="智能体不存在")
|
||
|
||
# 检查智能体是否已发布
|
||
if agent.status != "published":
|
||
raise HTTPException(status_code=400, detail="请先发布智能体")
|
||
|
||
# 检查是否已存在该智能体的菜单
|
||
menu_path = f"/agent-chat/{agent.code}"
|
||
existing_menu_result = await db.execute(
|
||
select(Menu).where(
|
||
Menu.path == menu_path,
|
||
Menu.is_deleted == False
|
||
)
|
||
)
|
||
existing_menu = existing_menu_result.scalar_one_or_none()
|
||
|
||
if existing_menu:
|
||
# 更新现有菜单
|
||
existing_menu.name = data.menu_name
|
||
existing_menu.title = data.menu_name
|
||
existing_menu.parent_id = data.menu_parent_id
|
||
existing_menu.icon = data.menu_icon
|
||
existing_menu.order = data.menu_order
|
||
existing_menu.application_id = agent.application_id
|
||
existing_menu.query = {"agentCode": agent.code}
|
||
logger.info(f"更新智能体菜单: {agent.code}")
|
||
else:
|
||
# 创建新菜单
|
||
new_menu = Menu(
|
||
name=data.menu_name,
|
||
title=data.menu_name,
|
||
path=menu_path,
|
||
component="_core/agent-chat/index",
|
||
type="agent",
|
||
parent_id=data.menu_parent_id,
|
||
icon=data.menu_icon,
|
||
order=data.menu_order,
|
||
application_id=agent.application_id,
|
||
query={"agentCode": agent.code},
|
||
)
|
||
db.add(new_menu)
|
||
logger.info(f"创建智能体菜单: {agent.code}")
|
||
|
||
await db.commit()
|
||
await db.refresh(agent)
|
||
|
||
# 清理菜单缓存
|
||
await MenuService.invalidate_cache()
|
||
|
||
return await _build_agent_response(agent, db)
|
||
|
||
|
||
@router.post("/{agent_id}/unpublish-menu", response_model=ResponseModel, summary="取消发布智能体菜单")
|
||
async def unpublish_agent_menu(
|
||
agent_id: str,
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""取消发布智能体菜单(物理删除菜单)"""
|
||
from core.menu.model import Menu
|
||
from core.menu.service import MenuService
|
||
|
||
# 获取智能体
|
||
result = await db.execute(
|
||
select(Agent).where(Agent.id == agent_id, Agent.is_deleted == False)
|
||
)
|
||
agent = result.scalar_one_or_none()
|
||
if not agent:
|
||
raise HTTPException(status_code=404, detail="智能体不存在")
|
||
|
||
# 查找并删除菜单
|
||
menu_path = f"/agent-chat/{agent.code}"
|
||
menu_result = await db.execute(
|
||
select(Menu).where(
|
||
Menu.path == menu_path,
|
||
Menu.is_deleted == False
|
||
)
|
||
)
|
||
menu = menu_result.scalar_one_or_none()
|
||
|
||
if menu:
|
||
await db.delete(menu)
|
||
await db.commit()
|
||
await MenuService.invalidate_cache()
|
||
logger.info(f"删除智能体菜单: {agent.code}")
|
||
|
||
return ResponseModel(message="取消发布成功")
|
||
|
||
|
||
# ==================== Conversation API ====================
|
||
|
||
@router.get("/{agent_id}/conversations", response_model=PaginatedResponse[AgentConversationListResponse], summary="对话列表")
|
||
async def list_agent_conversations(
|
||
agent_id: str,
|
||
page: int = Query(1, ge=1, description="页码"),
|
||
page_size: int = Query(20, ge=1, le=100, alias="pageSize", description="每页数量"),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""获取智能体的对话列表(仅返回当前用户的)"""
|
||
current_user_id = get_current_user_id_from_context()
|
||
query = select(AgentConversation).where(
|
||
AgentConversation.agent_id == agent_id,
|
||
AgentConversation.user_id == current_user_id,
|
||
AgentConversation.is_deleted == False
|
||
)
|
||
|
||
# 获取总数
|
||
count_result = await db.execute(select(func.count()).select_from(query.subquery()))
|
||
total = count_result.scalar() or 0
|
||
|
||
# 分页(按创建时间降序,最新的在前面)
|
||
offset = (page - 1) * page_size
|
||
query = query.order_by(AgentConversation.sys_create_datetime.desc())
|
||
query = query.offset(offset).limit(page_size)
|
||
|
||
result = await db.execute(query)
|
||
conversations = result.scalars().all()
|
||
|
||
# 获取智能体名称
|
||
agent_result = await db.execute(
|
||
select(Agent).where(Agent.id == agent_id)
|
||
)
|
||
agent = agent_result.scalar_one_or_none()
|
||
agent_name = agent.name if agent else ""
|
||
|
||
items = [
|
||
{
|
||
"id": c.id,
|
||
"agent_id": c.agent_id,
|
||
"agent_name": agent_name,
|
||
"title": c.title or "",
|
||
"message_count": c.message_count or 0,
|
||
"sys_create_datetime": c.sys_create_datetime,
|
||
}
|
||
for c in conversations
|
||
]
|
||
|
||
return PaginatedResponse(items=items, total=total)
|
||
|
||
|
||
@router.post("/{agent_id}/conversations", response_model=AgentConversationResponse, summary="创建对话")
|
||
async def create_agent_conversation(
|
||
agent_id: str,
|
||
data: AgentConversationCreate = None,
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""创建新对话"""
|
||
# 验证智能体
|
||
agent_result = await db.execute(
|
||
select(Agent).where(Agent.id == agent_id, Agent.is_deleted == False)
|
||
)
|
||
agent = agent_result.scalar_one_or_none()
|
||
if not agent:
|
||
raise HTTPException(status_code=404, detail="智能体不存在")
|
||
|
||
title = data.title if data and data.title else "新建对话"
|
||
|
||
current_user_id = get_current_user_id_from_context()
|
||
conversation = AgentConversation(
|
||
agent_id=agent_id,
|
||
user_id=current_user_id,
|
||
title=title,
|
||
)
|
||
db.add(conversation)
|
||
await db.commit()
|
||
await db.refresh(conversation)
|
||
|
||
return {
|
||
"id": conversation.id,
|
||
"agent_id": conversation.agent_id,
|
||
"agent_name": agent.name,
|
||
"title": conversation.title or "",
|
||
"summary": conversation.summary or "",
|
||
"message_count": conversation.message_count or 0,
|
||
"total_tokens": conversation.total_tokens or 0,
|
||
"sys_create_datetime": conversation.sys_create_datetime,
|
||
"sys_update_datetime": conversation.sys_update_datetime,
|
||
}
|
||
|
||
|
||
@router.get("/conversations/{conversation_id}", response_model=AgentConversationResponse, summary="对话详情")
|
||
async def get_agent_conversation(conversation_id: str, db: AsyncSession = Depends(get_db)):
|
||
"""获取对话详情"""
|
||
current_user_id = get_current_user_id_from_context()
|
||
result = await db.execute(
|
||
select(AgentConversation).where(
|
||
AgentConversation.id == conversation_id,
|
||
AgentConversation.user_id == current_user_id,
|
||
AgentConversation.is_deleted == False
|
||
)
|
||
)
|
||
conversation = result.scalar_one_or_none()
|
||
if not conversation:
|
||
raise HTTPException(status_code=404, detail="对话不存在")
|
||
|
||
# 获取智能体名称
|
||
agent_result = await db.execute(
|
||
select(Agent).where(Agent.id == conversation.agent_id)
|
||
)
|
||
agent = agent_result.scalar_one_or_none()
|
||
agent_name = agent.name if agent else ""
|
||
|
||
return {
|
||
"id": conversation.id,
|
||
"agent_id": conversation.agent_id,
|
||
"agent_name": agent_name,
|
||
"title": conversation.title or "",
|
||
"summary": conversation.summary or "",
|
||
"message_count": conversation.message_count or 0,
|
||
"total_tokens": conversation.total_tokens or 0,
|
||
"sys_create_datetime": conversation.sys_create_datetime,
|
||
"sys_update_datetime": conversation.sys_update_datetime,
|
||
}
|
||
|
||
|
||
@router.delete("/conversations/{conversation_id}", response_model=ResponseModel, summary="删除对话")
|
||
async def delete_agent_conversation(conversation_id: str, db: AsyncSession = Depends(get_db)):
|
||
"""删除对话"""
|
||
current_user_id = get_current_user_id_from_context()
|
||
result = await db.execute(
|
||
select(AgentConversation).where(
|
||
AgentConversation.id == conversation_id,
|
||
AgentConversation.user_id == current_user_id,
|
||
AgentConversation.is_deleted == False
|
||
)
|
||
)
|
||
conversation = result.scalar_one_or_none()
|
||
if not conversation:
|
||
raise HTTPException(status_code=404, detail="对话不存在")
|
||
|
||
conversation.is_deleted = True
|
||
await db.commit()
|
||
|
||
return ResponseModel(message="删除成功")
|
||
|
||
|
||
@router.get("/conversations/{conversation_id}/messages", response_model=List[AgentMessageResponse], summary="消息列表")
|
||
async def list_agent_messages(
|
||
conversation_id: str,
|
||
limit: int = Query(50, ge=1, le=200, description="限制数量"),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""获取对话的消息列表"""
|
||
current_user_id = get_current_user_id_from_context()
|
||
# 验证对话存在且属于当前用户
|
||
conv_result = await db.execute(
|
||
select(AgentConversation).where(
|
||
AgentConversation.id == conversation_id,
|
||
AgentConversation.user_id == current_user_id,
|
||
AgentConversation.is_deleted == False
|
||
)
|
||
)
|
||
if not conv_result.scalar_one_or_none():
|
||
raise HTTPException(status_code=404, detail="对话不存在")
|
||
|
||
query = select(AgentMessage).where(
|
||
AgentMessage.conversation_id == conversation_id,
|
||
AgentMessage.is_deleted == False
|
||
).order_by(AgentMessage.sys_create_datetime).limit(limit)
|
||
|
||
result = await db.execute(query)
|
||
messages = result.scalars().all()
|
||
|
||
return [
|
||
{
|
||
"id": m.id,
|
||
"role": m.role,
|
||
"content": m.content or "",
|
||
"attachments": m.attachments or [],
|
||
"status": m.status or "completed",
|
||
"reasoning_steps": m.reasoning_steps or [],
|
||
"tool_calls": m.tool_calls or [],
|
||
"prompt_tokens": m.prompt_tokens or 0,
|
||
"completion_tokens": m.completion_tokens or 0,
|
||
"total_tokens": m.total_tokens or 0,
|
||
"elapsed_time": m.elapsed_time or 0,
|
||
"error_message": m.error_message or "",
|
||
"feedback": m.feedback or "",
|
||
"sys_create_datetime": m.sys_create_datetime,
|
||
}
|
||
for m in messages
|
||
]
|
||
|
||
|
||
@router.post("/messages/{message_id}/feedback", response_model=ResponseModel, summary="消息反馈")
|
||
async def feedback_agent_message(
|
||
message_id: str,
|
||
data: MessageFeedback,
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""提交消息反馈"""
|
||
result = await db.execute(
|
||
select(AgentMessage).where(
|
||
AgentMessage.id == message_id,
|
||
AgentMessage.is_deleted == False
|
||
)
|
||
)
|
||
message = result.scalar_one_or_none()
|
||
if not message:
|
||
raise HTTPException(status_code=404, detail="消息不存在")
|
||
|
||
message.feedback = data.feedback
|
||
await db.commit()
|
||
|
||
return ResponseModel(message="反馈成功")
|
||
|
||
|
||
@router.post("/{agent_id}/chat", summary="智能体对话(流式,通过agent_id)")
|
||
async def chat_with_agent_by_id(
|
||
agent_id: str,
|
||
data: ChatInput,
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""通过agent_id与智能体对话,自动创建或使用现有对话"""
|
||
from ai_platform.services.agent_service import AgentService
|
||
|
||
current_user_id = get_current_user_id_from_context()
|
||
|
||
# 获取智能体
|
||
agent_result = await db.execute(
|
||
select(Agent).where(
|
||
Agent.id == agent_id,
|
||
Agent.is_deleted == False
|
||
)
|
||
)
|
||
agent = agent_result.scalar_one_or_none()
|
||
if not agent:
|
||
raise HTTPException(status_code=404, detail="智能体不存在")
|
||
|
||
# 如果提供了conversation_id,使用现有对话;否则创建新对话
|
||
conversation_id = data.conversation_id if data.conversation_id else None
|
||
|
||
if conversation_id:
|
||
# 验证对话存在且属于当前用户
|
||
conv_result = await db.execute(
|
||
select(AgentConversation).where(
|
||
AgentConversation.id == conversation_id,
|
||
AgentConversation.agent_id == agent_id,
|
||
AgentConversation.user_id == current_user_id,
|
||
AgentConversation.is_deleted == False
|
||
)
|
||
)
|
||
conversation = conv_result.scalar_one_or_none()
|
||
if not conversation:
|
||
raise HTTPException(status_code=404, detail="对话不存在")
|
||
else:
|
||
# 创建新对话
|
||
conversation = AgentConversation(
|
||
agent_id=agent_id,
|
||
user_id=current_user_id,
|
||
title="",
|
||
)
|
||
db.add(conversation)
|
||
await db.flush()
|
||
|
||
# 检查模型是否支持多模态(vision)
|
||
supports_vision = getattr(agent, 'supports_vision', False)
|
||
if agent.model_id:
|
||
from ai_platform.models.model import LLMModel
|
||
model_result = await db.execute(
|
||
select(LLMModel).where(LLMModel.id == agent.model_id)
|
||
)
|
||
model = model_result.scalar_one_or_none()
|
||
if model:
|
||
supports_vision = model.supports_vision or False
|
||
|
||
# 转换附件格式 - 从文件管理系统获取完整信息
|
||
# 对于图片类型:
|
||
# - 多模态模型:获取 base64 编码以便 LLM 能够处理(LLM 无法访问内部 URL)
|
||
# - 非多模态模型:转换为文本描述
|
||
# 对于文件类型,提取文本内容以便 LLM 能够理解文件内容
|
||
import logging
|
||
logging.info(f"Model supports_vision: {supports_vision}")
|
||
attachments = None
|
||
if data.attachments:
|
||
from core.file_manager.service import FileManagerService
|
||
attachments = []
|
||
for att in data.attachments:
|
||
logging.info(f"Processing attachment: file_id={att.file_id}, type={att.type}")
|
||
file_obj = await FileManagerService.get_by_id(db, att.file_id)
|
||
if file_obj:
|
||
logging.info(f"Found file: name={file_obj.name}, mime_type={file_obj.mime_type}, size={file_obj.size}")
|
||
attachment_data = {
|
||
'file_id': att.file_id,
|
||
'type': att.type,
|
||
'name': file_obj.name,
|
||
'url': file_obj.url or '',
|
||
'mime_type': file_obj.mime_type or '',
|
||
'size': file_obj.size or 0,
|
||
}
|
||
# 对于图片类型
|
||
if att.type == 'image' and file_obj.mime_type and file_obj.mime_type.startswith('image/'):
|
||
if supports_vision:
|
||
# 多模态模型:获取 base64 编码
|
||
base64_content = await FileManagerService.get_file_as_base64(db, att.file_id)
|
||
if base64_content:
|
||
attachment_data['base64'] = base64_content
|
||
logging.info(f"Got base64 for image (vision model), length={len(base64_content)}")
|
||
else:
|
||
# 非多模态模型:使用阿里云 qwen-vl-ocr 识别图片内容
|
||
logging.info(f"Using OCR to recognize image for non-vision model: {file_obj.name}")
|
||
ocr_text = await FileManagerService.recognize_image_with_ocr(db, att.file_id)
|
||
if ocr_text:
|
||
attachment_data['text_content'] = f"[图片 OCR 识别结果: {file_obj.name}]\n{ocr_text}"
|
||
logging.info(f"OCR recognized text, length={len(ocr_text)}")
|
||
else:
|
||
size_kb = file_obj.size / 1024 if file_obj.size else 0
|
||
attachment_data['text_content'] = f"[图片: {file_obj.name}, 类型: {file_obj.mime_type}, 大小: {size_kb:.1f}KB]\n注意:OCR 识别失败,无法提取图片中的文字内容。"
|
||
logging.warning(f"OCR failed for image: {file_obj.name}")
|
||
# 对于文件类型,提取文本内容
|
||
elif att.type == 'file':
|
||
logging.info(f"Extracting text content from file: {file_obj.name}")
|
||
text_content = await FileManagerService.get_file_text_content(db, att.file_id)
|
||
if text_content:
|
||
attachment_data['text_content'] = text_content
|
||
logging.info(f"Extracted text content, length={len(text_content)}")
|
||
else:
|
||
logging.warning(f"Failed to extract text content from file: {file_obj.name}")
|
||
attachments.append(attachment_data)
|
||
else:
|
||
logging.warning(f"File not found: {att.file_id}")
|
||
|
||
async def generate():
|
||
agent_service = AgentService(db)
|
||
async for event in agent_service.chat(
|
||
agent=agent,
|
||
conversation=conversation,
|
||
user_message=data.message,
|
||
application_id=data.application_id,
|
||
form_code=data.form_code,
|
||
attachments=attachments,
|
||
):
|
||
yield f"data: {json.dumps(event, ensure_ascii=False)}\n\n"
|
||
yield "data: [DONE]\n\n"
|
||
|
||
return StreamingResponse(
|
||
generate(),
|
||
media_type="text/event-stream",
|
||
headers={
|
||
"Cache-Control": "no-cache",
|
||
"Connection": "keep-alive",
|
||
}
|
||
)
|
||
|
||
|
||
@router.post("/conversations/{conversation_id}/chat", summary="智能体对话(流式)")
|
||
async def chat_with_agent(
|
||
conversation_id: str,
|
||
data: ChatInput,
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""与智能体对话,返回流式响应(SSE)"""
|
||
from ai_platform.services.agent_service import AgentService
|
||
|
||
current_user_id = get_current_user_id_from_context()
|
||
|
||
# 验证对话存在且属于当前用户
|
||
conv_result = await db.execute(
|
||
select(AgentConversation).where(
|
||
AgentConversation.id == conversation_id,
|
||
AgentConversation.user_id == current_user_id,
|
||
AgentConversation.is_deleted == False
|
||
)
|
||
)
|
||
conversation = conv_result.scalar_one_or_none()
|
||
if not conversation:
|
||
raise HTTPException(status_code=404, detail="对话不存在")
|
||
|
||
# 获取智能体
|
||
agent_result = await db.execute(
|
||
select(Agent).where(
|
||
Agent.id == conversation.agent_id,
|
||
Agent.is_deleted == False
|
||
)
|
||
)
|
||
agent = agent_result.scalar_one_or_none()
|
||
if not agent:
|
||
raise HTTPException(status_code=404, detail="智能体不存在")
|
||
|
||
# 检查模型是否支持多模态(vision)
|
||
supports_vision = getattr(agent, 'supports_vision', False)
|
||
if agent.model_id:
|
||
from ai_platform.models.model import LLMModel
|
||
model_result = await db.execute(
|
||
select(LLMModel).where(LLMModel.id == agent.model_id)
|
||
)
|
||
model = model_result.scalar_one_or_none()
|
||
if model:
|
||
supports_vision = model.supports_vision or False
|
||
|
||
# 转换附件格式 - 从文件管理系统获取完整信息
|
||
# 对于图片类型:
|
||
# - 多模态模型:获取 base64 编码以便 LLM 能够处理(LLM 无法访问内部 URL)
|
||
# - 非多模态模型:转换为文本描述
|
||
# 对于文件类型,提取文本内容以便 LLM 能够理解文件内容
|
||
import logging
|
||
logging.info(f"Model supports_vision: {supports_vision}")
|
||
attachments = None
|
||
if data.attachments:
|
||
from core.file_manager.service import FileManagerService
|
||
attachments = []
|
||
for att in data.attachments:
|
||
logging.info(f"Processing attachment: file_id={att.file_id}, type={att.type}")
|
||
file_obj = await FileManagerService.get_by_id(db, att.file_id)
|
||
if file_obj:
|
||
logging.info(f"Found file: name={file_obj.name}, mime_type={file_obj.mime_type}, size={file_obj.size}")
|
||
attachment_data = {
|
||
'file_id': att.file_id,
|
||
'type': att.type,
|
||
'name': file_obj.name,
|
||
'url': file_obj.url or '',
|
||
'mime_type': file_obj.mime_type or '',
|
||
'size': file_obj.size or 0,
|
||
}
|
||
# 对于图片类型
|
||
if att.type == 'image' and file_obj.mime_type and file_obj.mime_type.startswith('image/'):
|
||
if supports_vision:
|
||
# 多模态模型:获取 base64 编码
|
||
base64_content = await FileManagerService.get_file_as_base64(db, att.file_id)
|
||
if base64_content:
|
||
attachment_data['base64'] = base64_content
|
||
logging.info(f"Got base64 for image (vision model), length={len(base64_content)}")
|
||
else:
|
||
# 非多模态模型:使用阿里云 qwen-vl-ocr 识别图片内容
|
||
logging.info(f"Using OCR to recognize image for non-vision model: {file_obj.name}")
|
||
ocr_text = await FileManagerService.recognize_image_with_ocr(db, att.file_id)
|
||
if ocr_text:
|
||
attachment_data['text_content'] = f"[图片 OCR 识别结果: {file_obj.name}]\n{ocr_text}"
|
||
logging.info(f"OCR recognized text, length={len(ocr_text)}")
|
||
else:
|
||
size_kb = file_obj.size / 1024 if file_obj.size else 0
|
||
attachment_data['text_content'] = f"[图片: {file_obj.name}, 类型: {file_obj.mime_type}, 大小: {size_kb:.1f}KB]\n注意:OCR 识别失败,无法提取图片中的文字内容。"
|
||
logging.warning(f"OCR failed for image: {file_obj.name}")
|
||
# 对于文件类型,提取文本内容
|
||
elif att.type == 'file':
|
||
logging.info(f"Extracting text content from file: {file_obj.name}")
|
||
text_content = await FileManagerService.get_file_text_content(db, att.file_id)
|
||
if text_content:
|
||
attachment_data['text_content'] = text_content
|
||
logging.info(f"Extracted text content, length={len(text_content)}")
|
||
else:
|
||
logging.warning(f"Failed to extract text content from file: {file_obj.name}")
|
||
attachments.append(attachment_data)
|
||
else:
|
||
logging.warning(f"File not found: {att.file_id}")
|
||
|
||
async def generate():
|
||
agent_service = AgentService(db)
|
||
async for event in agent_service.chat(
|
||
agent=agent,
|
||
conversation=conversation,
|
||
user_message=data.message,
|
||
application_id=data.application_id,
|
||
form_code=data.form_code,
|
||
attachments=attachments,
|
||
):
|
||
yield f"data: {json.dumps(event, ensure_ascii=False)}\n\n"
|
||
yield "data: [DONE]\n\n"
|
||
|
||
return StreamingResponse(
|
||
generate(),
|
||
media_type="text/event-stream",
|
||
headers={
|
||
"Cache-Control": "no-cache",
|
||
"Connection": "keep-alive",
|
||
}
|
||
)
|
||
|
||
|
||
# ==================== Helper Functions ====================
|
||
|
||
async def _build_agent_response(agent: Agent, db: AsyncSession) -> dict:
|
||
"""构建智能体输出"""
|
||
# 获取模型名称
|
||
model_name = ""
|
||
if agent.model_id:
|
||
model_result = await db.execute(
|
||
select(LLMModel).where(LLMModel.id == agent.model_id)
|
||
)
|
||
model = model_result.scalar_one_or_none()
|
||
model_name = model.display_name if model else ""
|
||
|
||
# 获取工作流名称和类型
|
||
workflow_name = ""
|
||
workflow_type = "general"
|
||
if agent.workflow_id:
|
||
workflow_result = await db.execute(
|
||
select(AIWorkflow).where(AIWorkflow.id == agent.workflow_id)
|
||
)
|
||
workflow = workflow_result.scalar_one_or_none()
|
||
if workflow:
|
||
workflow_name = workflow.name
|
||
workflow_type = workflow.workflow_type or "general"
|
||
|
||
return {
|
||
"id": agent.id,
|
||
"name": agent.name,
|
||
"code": agent.code,
|
||
"description": agent.description or "",
|
||
"avatar": agent.avatar or "",
|
||
"mode": agent.mode or "autonomous",
|
||
"status": agent.status or "draft",
|
||
"persona": agent.persona or {},
|
||
"system_prompt": agent.system_prompt or "",
|
||
"model_id": agent.model_id,
|
||
"model_name": model_name,
|
||
"temperature": agent.temperature or 0.7,
|
||
"top_p": agent.top_p or 1.0,
|
||
"max_tokens": agent.max_tokens or 4096,
|
||
"max_iterations": agent.max_iterations or 10,
|
||
"welcome_message": agent.welcome_message or "",
|
||
"suggested_questions": agent.suggested_questions or [],
|
||
"workflow_id": agent.workflow_id,
|
||
"workflow_name": workflow_name,
|
||
"workflow_type": workflow_type,
|
||
"is_public": agent.is_public or False,
|
||
"enable_memory": agent.enable_memory or False,
|
||
"memory_window": agent.memory_window or 10,
|
||
"enable_streaming": agent.enable_streaming if agent.enable_streaming is not None else True,
|
||
"knowledge_base_ids": agent.knowledge_base_ids or [],
|
||
"knowledge_config": agent.knowledge_config or {},
|
||
"conversation_count": agent.conversation_count or 0,
|
||
"message_count": agent.message_count or 0,
|
||
"total_tokens": agent.total_tokens or 0,
|
||
"sort": agent.sort or 0,
|
||
"sys_create_datetime": agent.sys_create_datetime,
|
||
"sys_update_datetime": agent.sys_update_datetime,
|
||
}
|
||
|
||
|
||
def _build_agent_list_response(agent: Agent, model_name: str = "", application_name: str = "", has_menu: bool = False) -> dict:
|
||
"""构建智能体列表输出"""
|
||
return {
|
||
"id": agent.id,
|
||
"application_id": agent.application_id,
|
||
"application_name": application_name,
|
||
"is_global": agent.is_global or False,
|
||
"name": agent.name,
|
||
"code": agent.code,
|
||
"description": agent.description or "",
|
||
"avatar": agent.avatar or "",
|
||
"mode": agent.mode or "autonomous",
|
||
"status": agent.status or "draft",
|
||
"model_name": model_name,
|
||
"is_public": agent.is_public or False,
|
||
"conversation_count": agent.conversation_count or 0,
|
||
"message_count": agent.message_count or 0,
|
||
"has_menu": has_menu,
|
||
"sys_create_datetime": agent.sys_create_datetime,
|
||
}
|