Files
ai-agent-admin/backend-fastapi/ai_platform/api/agent_api.py
T

1135 lines
43 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
智能体 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, 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)
.where(
LLMModel.is_deleted == False,
LLMModel.is_active == True,
LLMModel.model_type == "chat",
)
.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,
}