Build lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,720 @@
|
||||
"""
|
||||
AI 工作流 API
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, 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 pydantic import BaseModel, Field
|
||||
|
||||
from app.database import get_db
|
||||
from app.base_schema import PaginatedResponse, ResponseModel
|
||||
from ai_platform.models import AIWorkflow, AIWorkflowVersion, AIWorkflowRun
|
||||
from core.application.model import Application
|
||||
from ai_platform.schemas.workflow_schema import (
|
||||
WorkflowCreate,
|
||||
WorkflowUpdate,
|
||||
WorkflowResponse,
|
||||
WorkflowListResponse,
|
||||
WorkflowRunInput,
|
||||
WorkflowRunResponse,
|
||||
WorkflowRunListResponse,
|
||||
WorkflowImportCheckIn,
|
||||
WorkflowImportCheckOut,
|
||||
WorkflowImportIn,
|
||||
NodeSchemaResponse,
|
||||
)
|
||||
from ai_platform.services.workflow_import_export import (
|
||||
WorkflowImportExportException,
|
||||
export_config as export_workflow_config,
|
||||
check_import as check_workflow_import,
|
||||
import_config as import_workflow_config,
|
||||
)
|
||||
from ai_platform.nodes.registry import NodeRegistry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/workflow", tags=["AI-工作流"])
|
||||
|
||||
|
||||
# ============ 节点 Schema API ============
|
||||
|
||||
@router.get("/nodes/schemas", response_model=List[NodeSchemaResponse], summary="获取节点 Schema 列表")
|
||||
async def get_node_schemas():
|
||||
"""获取所有已注册节点的 Schema"""
|
||||
return NodeRegistry.get_all_schemas()
|
||||
|
||||
|
||||
@router.get("/nodes/schemas/by-category", summary="按分类获取节点 Schema")
|
||||
async def get_node_schemas_by_category():
|
||||
"""按分类获取所有已注册节点的 Schema"""
|
||||
return NodeRegistry.get_schemas_by_category()
|
||||
|
||||
|
||||
# ============ 工作流 API ============
|
||||
|
||||
@router.get("/list", response_model=PaginatedResponse[WorkflowListResponse], summary="工作流列表")
|
||||
async def list_workflows(
|
||||
name: Optional[str] = Query(None, description="名称"),
|
||||
status: Optional[str] = Query(None, description="状态"),
|
||||
workflow_type: 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=1000, alias="pageSize", description="每页数量"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取工作流列表(自动应用数据权限)"""
|
||||
from ai_platform.services.workflow_service import AIWorkflowService
|
||||
from app.data_scope_utils import get_data_scope_filter, apply_data_scope_to_conditions
|
||||
|
||||
conditions = [AIWorkflow.is_deleted == False]
|
||||
|
||||
if application_id:
|
||||
conditions.append(or_(
|
||||
AIWorkflow.application_id == application_id,
|
||||
and_(AIWorkflow.application_id.is_(None), AIWorkflow.is_global == True)
|
||||
))
|
||||
if name:
|
||||
conditions.append(AIWorkflow.name.ilike(f"%{name}%"))
|
||||
if status:
|
||||
conditions.append(AIWorkflow.status == status)
|
||||
if workflow_type:
|
||||
conditions.append(AIWorkflow.workflow_type == workflow_type)
|
||||
|
||||
# 获取数据权限过滤条件并应用
|
||||
from ai_platform.services.workflow_service import RESOURCE_TYPE
|
||||
data_scope_filter = await get_data_scope_filter(db, RESOURCE_TYPE)
|
||||
scope_conditions = apply_data_scope_to_conditions(AIWorkflow, data_scope_filter)
|
||||
conditions.extend(scope_conditions)
|
||||
|
||||
# 获取总数
|
||||
query = select(AIWorkflow).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(AIWorkflow.sort.desc(), AIWorkflow.sys_create_datetime.desc())
|
||||
query = query.offset(offset).limit(page_size)
|
||||
|
||||
result = await db.execute(query)
|
||||
workflows = result.scalars().all()
|
||||
|
||||
# 批量查询应用名称
|
||||
app_ids = list({w.application_id for w in workflows if w.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}
|
||||
|
||||
items = [_build_workflow_list_response(w, app_name_map.get(w.application_id, "")) for w in workflows]
|
||||
|
||||
return PaginatedResponse(items=items, total=total)
|
||||
|
||||
|
||||
# 全局运行记录路由须注册在 /{workflow_id} 之前,避免 "runs" 被当作 workflow_id
|
||||
@router.get("/runs", response_model=PaginatedResponse[WorkflowRunListResponse], summary="全局工作流运行记录")
|
||||
async def list_all_workflow_runs(
|
||||
workflow_id: Optional[str] = Query(None, alias="workflowId", description="工作流ID"),
|
||||
status: Optional[str] = Query(None, description="运行状态"),
|
||||
trigger_type: Optional[str] = Query(None, alias="triggerType", description="触发来源"),
|
||||
page: int = Query(1, ge=1, description="页码"),
|
||||
page_size: int = Query(20, ge=1, le=100, alias="pageSize", description="每页数量"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取全局工作流运行记录(可按工作流、状态、触发来源筛选)"""
|
||||
conditions = [AIWorkflowRun.is_deleted == False]
|
||||
if workflow_id:
|
||||
conditions.append(AIWorkflowRun.workflow_id == workflow_id)
|
||||
if status:
|
||||
conditions.append(AIWorkflowRun.status == status)
|
||||
if trigger_type:
|
||||
conditions.append(AIWorkflowRun.trigger_type == trigger_type)
|
||||
|
||||
query = select(AIWorkflowRun).where(*conditions).order_by(
|
||||
AIWorkflowRun.sys_create_datetime.desc()
|
||||
)
|
||||
|
||||
count_result = await db.execute(select(func.count()).select_from(query.subquery()))
|
||||
total = count_result.scalar() or 0
|
||||
|
||||
offset = (page - 1) * page_size
|
||||
result = await db.execute(query.offset(offset).limit(page_size))
|
||||
runs = result.scalars().all()
|
||||
|
||||
workflow_ids = {r.workflow_id for r in runs if r.workflow_id}
|
||||
workflow_name_map: dict[str, str] = {}
|
||||
if workflow_ids:
|
||||
wf_result = await db.execute(
|
||||
select(AIWorkflow.id, AIWorkflow.name).where(AIWorkflow.id.in_(workflow_ids))
|
||||
)
|
||||
workflow_name_map = {row[0]: row[1] for row in wf_result.all()}
|
||||
|
||||
items = [
|
||||
_build_run_list_item(r, workflow_name_map.get(r.workflow_id, ""))
|
||||
for r in runs
|
||||
]
|
||||
return PaginatedResponse(items=items, total=total)
|
||||
|
||||
|
||||
@router.get("/runs/{run_id}", response_model=WorkflowRunResponse, summary="运行记录详情")
|
||||
async def get_workflow_run(run_id: str, db: AsyncSession = Depends(get_db)):
|
||||
"""获取运行记录详情"""
|
||||
result = await db.execute(
|
||||
select(AIWorkflowRun).where(AIWorkflowRun.id == run_id, AIWorkflowRun.is_deleted == False)
|
||||
)
|
||||
run = result.scalar_one_or_none()
|
||||
if not run:
|
||||
raise HTTPException(status_code=404, detail="运行记录不存在")
|
||||
|
||||
workflow_result = await db.execute(
|
||||
select(AIWorkflow).where(AIWorkflow.id == run.workflow_id)
|
||||
)
|
||||
workflow = workflow_result.scalar_one_or_none()
|
||||
workflow_name = workflow.name if workflow else ""
|
||||
|
||||
return _build_run_detail(run, workflow_name)
|
||||
|
||||
|
||||
@router.post("/runs/{run_id}/stop", response_model=ResponseModel, summary="停止运行")
|
||||
async def stop_workflow_run(run_id: str, db: AsyncSession = Depends(get_db)):
|
||||
"""停止工作流运行"""
|
||||
result = await db.execute(
|
||||
select(AIWorkflowRun).where(AIWorkflowRun.id == run_id, AIWorkflowRun.is_deleted == False)
|
||||
)
|
||||
run = result.scalar_one_or_none()
|
||||
if not run:
|
||||
raise HTTPException(status_code=404, detail="运行记录不存在")
|
||||
|
||||
if run.status not in ("pending", "running"):
|
||||
raise HTTPException(status_code=400, detail="工作流已完成,无法停止")
|
||||
|
||||
run.status = "stopped"
|
||||
await db.commit()
|
||||
|
||||
return ResponseModel(message="已停止")
|
||||
|
||||
|
||||
class ResumeWorkflowInput(BaseModel):
|
||||
"""恢复工作流输入"""
|
||||
user_input: Any = Field(..., description="用户输入(可以是字符串、布尔值、对象等)")
|
||||
|
||||
|
||||
@router.post("/runs/{run_id}/resume", summary="恢复工作流运行")
|
||||
async def resume_workflow_run(
|
||||
run_id: str,
|
||||
data: ResumeWorkflowInput,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""恢复等待中的工作流运行(SSE)"""
|
||||
import json
|
||||
from ai_platform.services.workflow_service import AIWorkflowService
|
||||
|
||||
async def generate():
|
||||
workflow_service = AIWorkflowService(db)
|
||||
async for event in workflow_service.resume_workflow_stream_async(
|
||||
run_id=run_id,
|
||||
user_input=data.user_input,
|
||||
):
|
||||
yield f"data: {json.dumps(event, ensure_ascii=False, default=str)}\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("/runs/{run_id}/resume/stream", summary="流式恢复工作流运行")
|
||||
async def resume_workflow_run_stream(
|
||||
run_id: str,
|
||||
data: ResumeWorkflowInput,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""流式恢复等待中的工作流运行(SSE)"""
|
||||
import json
|
||||
from ai_platform.services.workflow_service import AIWorkflowService
|
||||
|
||||
async def generate():
|
||||
workflow_service = AIWorkflowService(db)
|
||||
async for event in workflow_service.resume_workflow_stream_async(
|
||||
run_id=run_id,
|
||||
user_input=data.user_input,
|
||||
):
|
||||
yield f"data: {json.dumps(event, ensure_ascii=False, default=str)}\n\n"
|
||||
yield "data: [DONE]\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
generate(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/code/{code}", response_model=WorkflowResponse, summary="根据编码获取工作流")
|
||||
async def get_workflow_by_code(code: str, db: AsyncSession = Depends(get_db)):
|
||||
"""根据编码获取工作流"""
|
||||
result = await db.execute(
|
||||
select(AIWorkflow).where(AIWorkflow.code == code, AIWorkflow.is_deleted == False)
|
||||
)
|
||||
workflow = result.scalar_one_or_none()
|
||||
if not workflow:
|
||||
raise HTTPException(status_code=404, detail="工作流不存在")
|
||||
return _build_workflow_response(workflow)
|
||||
|
||||
|
||||
@router.get("/{workflow_id}", response_model=WorkflowResponse, summary="工作流详情")
|
||||
async def get_workflow(workflow_id: str, db: AsyncSession = Depends(get_db)):
|
||||
"""获取工作流详情"""
|
||||
result = await db.execute(
|
||||
select(AIWorkflow).where(AIWorkflow.id == workflow_id, AIWorkflow.is_deleted == False)
|
||||
)
|
||||
workflow = result.scalar_one_or_none()
|
||||
if not workflow:
|
||||
raise HTTPException(status_code=404, detail="工作流不存在")
|
||||
return _build_workflow_response(workflow)
|
||||
|
||||
|
||||
@router.post("", response_model=WorkflowResponse, summary="创建工作流")
|
||||
async def create_workflow(data: WorkflowCreate, db: AsyncSession = Depends(get_db)):
|
||||
"""创建工作流"""
|
||||
# 检查编码是否重复
|
||||
exists_result = await db.execute(
|
||||
select(AIWorkflow).where(AIWorkflow.code == data.code, AIWorkflow.is_deleted == False)
|
||||
)
|
||||
if exists_result.scalar_one_or_none():
|
||||
raise HTTPException(status_code=400, detail=f"工作流编码 {data.code} 已存在")
|
||||
|
||||
workflow = AIWorkflow(**data.model_dump())
|
||||
db.add(workflow)
|
||||
await db.commit()
|
||||
await db.refresh(workflow)
|
||||
|
||||
return _build_workflow_response(workflow)
|
||||
|
||||
|
||||
@router.put("/{workflow_id}", response_model=WorkflowResponse, summary="更新工作流")
|
||||
async def update_workflow(
|
||||
workflow_id: str,
|
||||
data: WorkflowUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""更新工作流"""
|
||||
result = await db.execute(
|
||||
select(AIWorkflow).where(AIWorkflow.id == workflow_id, AIWorkflow.is_deleted == False)
|
||||
)
|
||||
workflow = result.scalar_one_or_none()
|
||||
if not workflow:
|
||||
raise HTTPException(status_code=404, detail="工作流不存在")
|
||||
|
||||
# 如果更新了 code,检查编码是否重复(排除自身)
|
||||
if data.code and data.code != workflow.code:
|
||||
exists_result = await db.execute(
|
||||
select(AIWorkflow).where(
|
||||
AIWorkflow.code == data.code,
|
||||
AIWorkflow.id != workflow_id,
|
||||
AIWorkflow.is_deleted == False
|
||||
)
|
||||
)
|
||||
if exists_result.scalar_one_or_none():
|
||||
raise HTTPException(status_code=400, detail=f"工作流编码 {data.code} 已存在")
|
||||
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
setattr(workflow, key, value)
|
||||
|
||||
# 更新草稿版本号
|
||||
workflow.version = (workflow.version or 0) + 1
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(workflow)
|
||||
|
||||
return _build_workflow_response(workflow)
|
||||
|
||||
|
||||
@router.delete("/{workflow_id}", response_model=ResponseModel, summary="删除工作流")
|
||||
async def delete_workflow(workflow_id: str, db: AsyncSession = Depends(get_db)):
|
||||
"""删除工作流"""
|
||||
result = await db.execute(
|
||||
select(AIWorkflow).where(AIWorkflow.id == workflow_id, AIWorkflow.is_deleted == False)
|
||||
)
|
||||
workflow = result.scalar_one_or_none()
|
||||
if not workflow:
|
||||
raise HTTPException(status_code=404, detail="工作流不存在")
|
||||
|
||||
workflow.is_deleted = True
|
||||
await db.commit()
|
||||
|
||||
return ResponseModel(message="删除成功")
|
||||
|
||||
|
||||
@router.post("/{workflow_id}/copy", response_model=WorkflowResponse, summary="复制工作流")
|
||||
async def copy_workflow(workflow_id: str, db: AsyncSession = Depends(get_db)):
|
||||
"""复制工作流"""
|
||||
# 获取原工作流
|
||||
result = await db.execute(
|
||||
select(AIWorkflow).where(AIWorkflow.id == workflow_id, AIWorkflow.is_deleted == False)
|
||||
)
|
||||
workflow = result.scalar_one_or_none()
|
||||
if not workflow:
|
||||
raise HTTPException(status_code=404, detail="工作流不存在")
|
||||
|
||||
# 生成新的编码(原编码 + _copy + 时间戳)
|
||||
import time
|
||||
timestamp = int(time.time() * 1000)
|
||||
new_code = f"{workflow.code}_copy_{timestamp}"
|
||||
|
||||
# 检查编码是否重复(理论上不会,但保险起见)
|
||||
exists_result = await db.execute(
|
||||
select(AIWorkflow).where(AIWorkflow.code == new_code, AIWorkflow.is_deleted == False)
|
||||
)
|
||||
if exists_result.scalar_one_or_none():
|
||||
new_code = f"{workflow.code}_copy_{timestamp}_{int(time.time())}"
|
||||
|
||||
# 创建新工作流
|
||||
new_workflow = AIWorkflow(
|
||||
name=f"{workflow.name} (副本)",
|
||||
code=new_code,
|
||||
description=workflow.description,
|
||||
workflow_type=workflow.workflow_type,
|
||||
definition=workflow.definition, # 复制工作流定义
|
||||
input_variables=workflow.input_variables,
|
||||
output_variables=workflow.output_variables,
|
||||
status="draft", # 新工作流默认为草稿状态
|
||||
version=1,
|
||||
published_version=None,
|
||||
published_at=None,
|
||||
published_definition=None,
|
||||
)
|
||||
db.add(new_workflow)
|
||||
await db.commit()
|
||||
await db.refresh(new_workflow)
|
||||
|
||||
return _build_workflow_response(new_workflow)
|
||||
|
||||
|
||||
@router.get("/{workflow_id}/export", summary="导出工作流配置")
|
||||
async def export_workflow(
|
||||
workflow_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""导出工作流配置为 JSON(草稿 definition)"""
|
||||
try:
|
||||
config = await export_workflow_config(db, workflow_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 WorkflowImportExportException as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/import/check", response_model=WorkflowImportCheckOut, summary="导入预检查")
|
||||
async def check_import_workflow(
|
||||
data: WorkflowImportCheckIn,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""导入预检查:检查工作流编码是否冲突"""
|
||||
try:
|
||||
return await check_workflow_import(db, data.code)
|
||||
except WorkflowImportExportException as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/import", response_model=WorkflowResponse, summary="导入工作流配置")
|
||||
async def import_workflow(
|
||||
data: WorkflowImportIn,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""导入工作流配置"""
|
||||
try:
|
||||
workflow = await import_workflow_config(db, data.model_dump())
|
||||
await db.commit()
|
||||
await db.refresh(workflow)
|
||||
return _build_workflow_response(workflow)
|
||||
except WorkflowImportExportException as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/{workflow_id}/publish", response_model=WorkflowResponse, summary="发布工作流")
|
||||
async def publish_workflow(
|
||||
workflow_id: str,
|
||||
description: str = Query("", description="版本说明"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""发布工作流"""
|
||||
result = await db.execute(
|
||||
select(AIWorkflow).where(AIWorkflow.id == workflow_id, AIWorkflow.is_deleted == False)
|
||||
)
|
||||
workflow = result.scalar_one_or_none()
|
||||
if not workflow:
|
||||
raise HTTPException(status_code=404, detail="工作流不存在")
|
||||
|
||||
# 创建版本记录
|
||||
new_version = (workflow.published_version or 0) + 1
|
||||
version = AIWorkflowVersion(
|
||||
workflow_id=workflow_id,
|
||||
version=new_version,
|
||||
definition=workflow.definition or {},
|
||||
description=description,
|
||||
published_at=datetime.now(),
|
||||
)
|
||||
db.add(version)
|
||||
|
||||
# 更新工作流
|
||||
workflow.status = "published"
|
||||
workflow.published_version = new_version
|
||||
workflow.published_at = datetime.now()
|
||||
workflow.published_definition = workflow.definition
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(workflow)
|
||||
|
||||
return _build_workflow_response(workflow)
|
||||
|
||||
|
||||
@router.get("/{workflow_id}/versions", summary="获取版本历史")
|
||||
async def list_workflow_versions(
|
||||
workflow_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),
|
||||
):
|
||||
"""获取工作流版本历史"""
|
||||
query = select(AIWorkflowVersion).where(
|
||||
AIWorkflowVersion.workflow_id == workflow_id,
|
||||
AIWorkflowVersion.is_deleted == False
|
||||
).order_by(AIWorkflowVersion.version.desc())
|
||||
|
||||
# 获取总数
|
||||
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.offset(offset).limit(page_size)
|
||||
|
||||
result = await db.execute(query)
|
||||
versions = result.scalars().all()
|
||||
|
||||
items = [
|
||||
{
|
||||
"id": v.id,
|
||||
"version": v.version,
|
||||
"description": v.description or "",
|
||||
"published_at": v.published_at,
|
||||
"run_count": v.run_count or 0,
|
||||
"success_count": v.success_count or 0,
|
||||
}
|
||||
for v in versions
|
||||
]
|
||||
|
||||
return PaginatedResponse(items=items, total=total)
|
||||
|
||||
|
||||
@router.get("/{workflow_id}/runs", response_model=PaginatedResponse[WorkflowRunListResponse], summary="工作流运行记录")
|
||||
async def list_workflow_runs(
|
||||
workflow_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),
|
||||
):
|
||||
"""获取工作流运行记录"""
|
||||
query = select(AIWorkflowRun).where(
|
||||
AIWorkflowRun.workflow_id == workflow_id,
|
||||
AIWorkflowRun.is_deleted == False
|
||||
).order_by(AIWorkflowRun.sys_create_datetime.desc())
|
||||
|
||||
# 获取总数
|
||||
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.offset(offset).limit(page_size)
|
||||
|
||||
result = await db.execute(query)
|
||||
runs = result.scalars().all()
|
||||
|
||||
# 获取工作流名称
|
||||
workflow_result = await db.execute(
|
||||
select(AIWorkflow).where(AIWorkflow.id == workflow_id)
|
||||
)
|
||||
workflow = workflow_result.scalar_one_or_none()
|
||||
workflow_name = workflow.name if workflow else ""
|
||||
|
||||
items = [
|
||||
_build_run_list_item(r, workflow_name)
|
||||
for r in runs
|
||||
]
|
||||
|
||||
return PaginatedResponse(items=items, total=total)
|
||||
|
||||
|
||||
def _build_run_list_item(run: AIWorkflowRun, workflow_name: str = "") -> dict:
|
||||
return {
|
||||
"id": run.id,
|
||||
"workflow_id": run.workflow_id,
|
||||
"workflow_name": workflow_name,
|
||||
"status": run.status or "pending",
|
||||
"trigger_type": run.trigger_type or "api",
|
||||
"total_steps": run.total_steps or 0,
|
||||
"total_tokens": run.total_tokens or 0,
|
||||
"elapsed_time": run.elapsed_time or 0,
|
||||
"error_message": (run.error_message or "")[:200],
|
||||
"started_at": run.started_at,
|
||||
"completed_at": run.completed_at,
|
||||
}
|
||||
|
||||
|
||||
def _build_run_detail(run: AIWorkflowRun, workflow_name: str = "") -> dict:
|
||||
return {
|
||||
"id": run.id,
|
||||
"workflow_id": run.workflow_id,
|
||||
"workflow_name": workflow_name,
|
||||
"status": run.status or "pending",
|
||||
"trigger_type": run.trigger_type or "api",
|
||||
"use_draft": bool(run.use_draft),
|
||||
"workflow_version": run.workflow_version,
|
||||
"definition_snapshot": run.definition_snapshot or {},
|
||||
"inputs": run.inputs or {},
|
||||
"outputs": run.outputs or {},
|
||||
"execution_log": run.execution_log or [],
|
||||
"current_node_id": run.current_node_id or "",
|
||||
"waiting_config": run.waiting_config or {},
|
||||
"error_message": run.error_message or "",
|
||||
"total_tokens": run.total_tokens or 0,
|
||||
"total_steps": run.total_steps or 0,
|
||||
"elapsed_time": run.elapsed_time or 0,
|
||||
"started_at": run.started_at,
|
||||
"completed_at": run.completed_at,
|
||||
"sys_create_datetime": run.sys_create_datetime,
|
||||
}
|
||||
|
||||
|
||||
class WorkflowRunInput(BaseModel):
|
||||
"""工作流运行输入"""
|
||||
inputs: dict = Field(default_factory=dict, description="输入变量")
|
||||
use_draft: bool = Field(default=False, description="是否使用草稿版本")
|
||||
|
||||
|
||||
@router.post("/{workflow_id}/run", summary="运行工作流")
|
||||
async def run_workflow(
|
||||
workflow_id: str,
|
||||
data: WorkflowRunInput,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""运行工作流(非流式)"""
|
||||
from ai_platform.services.workflow_service import AIWorkflowService
|
||||
|
||||
workflow_service = AIWorkflowService(db)
|
||||
run = await workflow_service.run_workflow(
|
||||
workflow_id=workflow_id,
|
||||
inputs=data.inputs,
|
||||
use_draft=data.use_draft,
|
||||
trigger_type='api',
|
||||
)
|
||||
|
||||
return {
|
||||
"id": run.id,
|
||||
"workflow_id": run.workflow_id,
|
||||
"status": run.status,
|
||||
"outputs": run.outputs or {},
|
||||
"execution_log": run.execution_log or [],
|
||||
"total_tokens": run.total_tokens or 0,
|
||||
"total_steps": run.total_steps or 0,
|
||||
"elapsed_time": run.elapsed_time or 0,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{workflow_id}/run/stream", summary="流式运行工作流")
|
||||
async def run_workflow_stream(
|
||||
workflow_id: str,
|
||||
data: WorkflowRunInput,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""流式运行工作流(SSE)"""
|
||||
import json
|
||||
from ai_platform.services.workflow_service import AIWorkflowService
|
||||
|
||||
async def generate():
|
||||
workflow_service = AIWorkflowService(db)
|
||||
trigger_type = 'editor_draft' if data.use_draft else 'editor_published'
|
||||
async for event in workflow_service.run_workflow_stream_async(
|
||||
workflow_id=workflow_id,
|
||||
inputs=data.inputs,
|
||||
use_draft=data.use_draft,
|
||||
trigger_type=trigger_type,
|
||||
):
|
||||
yield f"data: {json.dumps(event, ensure_ascii=False, default=str)}\n\n"
|
||||
yield "data: [DONE]\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
generate(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _build_workflow_response(workflow: AIWorkflow) -> dict:
|
||||
"""构建工作流输出"""
|
||||
return {
|
||||
"id": workflow.id,
|
||||
"name": workflow.name,
|
||||
"code": workflow.code,
|
||||
"workflow_type": workflow.workflow_type or "general",
|
||||
"description": workflow.description or "",
|
||||
"status": workflow.status or "draft",
|
||||
"version": workflow.version or 1,
|
||||
"published_version": workflow.published_version,
|
||||
"published_at": workflow.published_at,
|
||||
"published_definition": workflow.published_definition,
|
||||
"definition": workflow.definition or {},
|
||||
"input_variables": workflow.input_variables or [],
|
||||
"output_variables": workflow.output_variables or [],
|
||||
"run_count": workflow.run_count or 0,
|
||||
"success_count": workflow.success_count or 0,
|
||||
"sort": workflow.sort or 0,
|
||||
"sys_create_datetime": workflow.sys_create_datetime,
|
||||
"sys_update_datetime": workflow.sys_update_datetime,
|
||||
}
|
||||
|
||||
|
||||
def _build_workflow_list_response(workflow: AIWorkflow, application_name: str = "") -> dict:
|
||||
"""构建工作流列表输出"""
|
||||
return {
|
||||
"id": workflow.id,
|
||||
"application_id": workflow.application_id,
|
||||
"application_name": application_name,
|
||||
"is_global": workflow.is_global or False,
|
||||
"name": workflow.name,
|
||||
"code": workflow.code,
|
||||
"workflow_type": workflow.workflow_type or "general",
|
||||
"description": workflow.description or "",
|
||||
"status": workflow.status or "draft",
|
||||
"version": workflow.version or 1,
|
||||
"run_count": workflow.run_count or 0,
|
||||
"success_count": workflow.success_count or 0,
|
||||
"sys_create_datetime": workflow.sys_create_datetime,
|
||||
}
|
||||
Reference in New Issue
Block a user