You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

402 lines
17 KiB

import uuid
import json
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from database import get_db
from models import FlowDefinition, FlowExecution, User
from schemas import FlowDefinitionCreate, FlowDefinitionUpdate, FlowDefinitionOut, FlowNode, FlowEdge
from modules.flow_engine.engine import FlowEngine
from agentscope.message import Msg
router = APIRouter(prefix="/api/flow", tags=["flow"])
@router.get("/definitions", response_model=list[FlowDefinitionOut])
async def list_flows(request: Request, db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(FlowDefinition).order_by(FlowDefinition.updated_at.desc())
)
flows = result.scalars().all()
return [FlowDefinitionOut(
id=f.id, name=f.name, description=f.description,
version=f.version, status=f.status,
definition_json=f.definition_json,
published_to_wecom=f.published_to_wecom,
created_at=f.created_at, updated_at=f.updated_at,
) for f in flows]
@router.get("/definitions/{flow_id}", response_model=FlowDefinitionOut)
async def get_flow(flow_id: uuid.UUID, request: Request, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(FlowDefinition).where(FlowDefinition.id == flow_id))
flow = result.scalar_one_or_none()
if not flow:
raise HTTPException(404, "流定义不存在")
return FlowDefinitionOut(
id=flow.id, name=flow.name, description=flow.description,
version=flow.version, status=flow.status,
definition_json=flow.definition_json,
published_to_wecom=flow.published_to_wecom,
created_at=flow.created_at, updated_at=flow.updated_at,
)
@router.post("/definitions", response_model=FlowDefinitionOut)
async def create_flow(req: FlowDefinitionCreate, request: Request, db: AsyncSession = Depends(get_db)):
user_ctx = request.state.user
definition_json = {
"nodes": [n.model_dump() for n in req.nodes],
"edges": [e.model_dump() for e in req.edges],
"trigger": req.trigger,
}
flow = FlowDefinition(
name=req.name,
description=req.description,
definition_json=definition_json,
creator_id=uuid.UUID(user_ctx["id"]),
)
db.add(flow)
await db.flush()
return FlowDefinitionOut(
id=flow.id, name=flow.name, description=flow.description,
version=flow.version, status=flow.status,
definition_json=flow.definition_json,
published_to_wecom=flow.published_to_wecom,
created_at=flow.created_at, updated_at=flow.updated_at,
)
@router.put("/definitions/{flow_id}", response_model=FlowDefinitionOut)
async def update_flow(
flow_id: uuid.UUID, req: FlowDefinitionUpdate,
request: Request, db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(FlowDefinition).where(FlowDefinition.id == flow_id))
flow = result.scalar_one_or_none()
if not flow:
raise HTTPException(404, "流定义不存在")
if req.name is not None:
flow.name = req.name
if req.description is not None:
flow.description = req.description
if req.nodes is not None and req.edges is not None:
flow.definition_json = {
"nodes": [n.model_dump() for n in req.nodes],
"edges": [e.model_dump() for e in req.edges],
"trigger": req.trigger or flow.definition_json.get("trigger", {}),
}
flow.version += 1
return FlowDefinitionOut(
id=flow.id, name=flow.name, description=flow.description,
version=flow.version, status=flow.status,
definition_json=flow.definition_json,
published_to_wecom=flow.published_to_wecom,
created_at=flow.created_at, updated_at=flow.updated_at,
)
@router.delete("/definitions/{flow_id}")
async def delete_flow(flow_id: uuid.UUID, request: Request, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(FlowDefinition).where(FlowDefinition.id == flow_id))
flow = result.scalar_one_or_none()
if not flow:
raise HTTPException(404, "流定义不存在")
await db.delete(flow)
return {"code": 200, "message": "已删除"}
@router.post("/definitions/{flow_id}/publish")
async def publish_flow(flow_id: uuid.UUID, request: Request, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(FlowDefinition).where(FlowDefinition.id == flow_id))
flow = result.scalar_one_or_none()
if not flow:
raise HTTPException(404, "流定义不存在")
nodes = flow.definition_json.get("nodes", [])
edges = flow.definition_json.get("edges", [])
if not nodes:
raise HTTPException(400, "流定义中没有节点")
flow.status = "published"
flow.published_to_wecom = True
return {"code": 200, "message": "流已上架到企微", "data": {"status": "published"}}
@router.post("/definitions/{flow_id}/unpublish")
async def unpublish_flow(flow_id: uuid.UUID, request: Request, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(FlowDefinition).where(FlowDefinition.id == flow_id))
flow = result.scalar_one_or_none()
if not flow:
raise HTTPException(404, "流定义不存在")
flow.status = "draft"
flow.published_to_wecom = False
return {"code": 200, "message": "流已下架"}
@router.post("/definitions/{flow_id}/execute")
async def execute_flow(flow_id: uuid.UUID, request: Request, payload: dict, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(FlowDefinition).where(FlowDefinition.id == flow_id))
flow = result.scalar_one_or_none()
if not flow:
raise HTTPException(404, "流定义不存在")
user_ctx = request.state.user
input_text = payload.get("input", payload.get("message", ""))
engine = FlowEngine(flow.definition_json)
input_msg = Msg(name="user", content=input_text, role="user")
context = {
"user_id": user_ctx["id"],
"username": user_ctx["username"],
"trigger_data": payload.get("trigger", {}),
"_node_results": {},
}
try:
result_msg = await engine.execute(input_msg, context)
output_text = result_msg.get_text_content() if hasattr(result_msg, 'get_text_content') else str(result_msg)
execution = FlowExecution(
flow_id=flow.id,
trigger_type=payload.get("trigger_type", "manual"),
trigger_user_id=uuid.UUID(user_ctx["id"]),
input_data={"input": input_text},
output_data={"output": output_text},
status="completed",
finished_at=datetime.utcnow(),
)
db.add(execution)
return {
"code": 200,
"data": {
"output": output_text,
"node_results": context.get("_node_results", {}),
"execution_id": str(execution.id),
},
}
except Exception as e:
execution = FlowExecution(
flow_id=flow.id,
trigger_type="manual",
trigger_user_id=uuid.UUID(user_ctx["id"]),
input_data={"input": input_text},
status="failed",
finished_at=datetime.utcnow(),
)
db.add(execution)
raise HTTPException(500, f"流执行失败: {str(e)}")
@router.post("/definitions/{flow_id}/test")
async def test_flow(flow_id: uuid.UUID, request: Request, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(FlowDefinition).where(FlowDefinition.id == flow_id))
flow = result.scalar_one_or_none()
if not flow:
raise HTTPException(404, "流定义不存在")
nodes = flow.definition_json.get("nodes", [])
edges = flow.definition_json.get("edges", [])
validation = {
"valid": True,
"node_count": len(nodes),
"edge_count": len(edges),
"node_types": list(set(n.get("type", "unknown") for n in nodes)),
"issues": [],
}
node_ids = {n["id"] for n in nodes}
for edge in edges:
source = edge.get("source") or edge.get("from")
target = edge.get("target") or edge.get("to")
if source and source not in node_ids:
validation["issues"].append(f"边源节点 {source} 不存在")
if target and target not in node_ids:
validation["issues"].append(f"边目标节点 {target} 不存在")
if validation["issues"]:
validation["valid"] = False
has_trigger = any(n.get("type") == "trigger" for n in nodes)
if not has_trigger:
validation["issues"].append("流缺少触发节点")
return {"code": 200, "data": validation}
FLOW_TEMPLATES = [
{
"id": "tpl_doc_process",
"name": "文档处理流",
"description": "自动解析文档内容,提取关键信息并生成摘要",
"icon": "Document",
"nodes": [
{"id": "n1", "type": "trigger", "label": "文档上传", "config": {"event_type": "document_upload"}, "position": {"x": 100, "y": 100}},
{"id": "n2", "type": "tool", "label": "解析文档", "config": {"tool_name": "parse_document"}, "position": {"x": 400, "y": 100}},
{"id": "n3", "type": "llm", "label": "生成摘要", "config": {"system_prompt": "请为以下文档内容生成简洁摘要", "model": "gpt-4o-mini", "temperature": 0.5}, "position": {"x": 700, "y": 100}},
{"id": "n4", "type": "output", "label": "输出结果", "config": {"format": "text"}, "position": {"x": 1000, "y": 100}},
],
"edges": [
{"source": "n1", "target": "n2", "sourceHandle": "source"},
{"source": "n2", "target": "n3", "sourceHandle": "source"},
{"source": "n3", "target": "n4", "sourceHandle": "source"},
],
},
{
"id": "tpl_wecom_notify",
"name": "企微通知流",
"description": "接收触发后查询数据并推送企微通知",
"icon": "Bell",
"nodes": [
{"id": "n1", "type": "trigger", "label": "定时触发", "config": {"event_type": "scheduled"}, "position": {"x": 100, "y": 100}},
{"id": "n2", "type": "tool", "label": "查询任务", "config": {"tool_name": "list_tasks"}, "position": {"x": 400, "y": 100}},
{"id": "n3", "type": "condition", "label": "有待办任务?", "config": {"condition": "tasks.length > 0"}, "position": {"x": 700, "y": 100}},
{"id": "n4", "type": "wecom_notify", "label": "推送通知", "config": {"message_template": "您有{{tasks.length}}条待办任务", "target": "@all"}, "position": {"x": 1000, "y": 50}},
{"id": "n5", "type": "output", "label": "无任务", "config": {"format": "text"}, "position": {"x": 1000, "y": 200}},
],
"edges": [
{"source": "n1", "target": "n2", "sourceHandle": "source"},
{"source": "n2", "target": "n3", "sourceHandle": "source"},
{"source": "n3", "target": "n4", "sourceHandle": "true"},
{"source": "n3", "target": "n5", "sourceHandle": "false"},
],
},
{
"id": "tpl_data_analysis",
"name": "数据分析流",
"description": "查询员工数据并生成效率分析报告",
"icon": "DataAnalysis",
"nodes": [
{"id": "n1", "type": "trigger", "label": "分析请求", "config": {"event_type": "button_click"}, "position": {"x": 100, "y": 100}},
{"id": "n2", "type": "tool", "label": "查询下属", "config": {"tool_name": "list_subordinates"}, "position": {"x": 400, "y": 100}},
{"id": "n3", "type": "tool", "label": "统计数据", "config": {"tool_name": "get_task_statistics"}, "position": {"x": 700, "y": 100}},
{"id": "n4", "type": "llm", "label": "生成报告", "config": {"system_prompt": "基于以下数据生成团队效率分析报告", "model": "gpt-4o", "temperature": 0.7}, "position": {"x": 1000, "y": 100}},
{"id": "n5", "type": "output", "label": "报告输出", "config": {"format": "json"}, "position": {"x": 1300, "y": 100}},
],
"edges": [
{"source": "n1", "target": "n2", "sourceHandle": "source"},
{"source": "n2", "target": "n3", "sourceHandle": "source"},
{"source": "n3", "target": "n4", "sourceHandle": "source"},
{"source": "n4", "target": "n5", "sourceHandle": "source"},
],
},
{
"id": "tpl_rag_qa",
"name": "知识库问答流",
"description": "从知识库检索信息后由LLM回答",
"icon": "Search",
"nodes": [
{"id": "n1", "type": "trigger", "label": "问题触发", "config": {"event_type": "text_message"}, "position": {"x": 100, "y": 100}},
{"id": "n2", "type": "rag", "label": "知识检索", "config": {"knowledge_base": "default", "top_k": 5}, "position": {"x": 400, "y": 100}},
{"id": "n3", "type": "llm", "label": "生成回答", "config": {"system_prompt": "基于知识库检索结果回答用户问题", "model": "gpt-4o-mini", "temperature": 0.3}, "position": {"x": 700, "y": 100}},
{"id": "n4", "type": "output", "label": "输出答案", "config": {"format": "text"}, "position": {"x": 1000, "y": 100}},
],
"edges": [
{"source": "n1", "target": "n2", "sourceHandle": "source"},
{"source": "n2", "target": "n3", "sourceHandle": "source"},
{"source": "n3", "target": "n4", "sourceHandle": "source"},
],
},
{
"id": "tpl_task_auto",
"name": "任务自动分配流",
"description": "根据描述自动创建任务并分派给合适人员",
"icon": "Tools",
"nodes": [
{"id": "n1", "type": "trigger", "label": "任务描述", "config": {"event_type": "text_message"}, "position": {"x": 100, "y": 100}},
{"id": "n2", "type": "llm", "label": "分析任务", "config": {"system_prompt": "分析以下任务描述,提取标题、优先级、负责人", "model": "gpt-4o-mini", "temperature": 0.5}, "position": {"x": 400, "y": 100}},
{"id": "n3", "type": "tool", "label": "创建任务", "config": {"tool_name": "create_task"}, "position": {"x": 700, "y": 100}},
{"id": "n4", "type": "wecom_notify", "label": "通知负责人", "config": {"message_template": "您有新任务: {{task_title}}", "target": "@all"}, "position": {"x": 1000, "y": 100}},
{"id": "n5", "type": "output", "label": "完成", "config": {"format": "text"}, "position": {"x": 1300, "y": 100}},
],
"edges": [
{"source": "n1", "target": "n2", "sourceHandle": "source"},
{"source": "n2", "target": "n3", "sourceHandle": "source"},
{"source": "n3", "target": "n4", "sourceHandle": "source"},
{"source": "n4", "target": "n5", "sourceHandle": "source"},
],
},
]
@router.get("/market", response_model=list[FlowDefinitionOut])
async def flow_market(request: Request, db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(FlowDefinition)
.where(FlowDefinition.status == "published")
.order_by(FlowDefinition.updated_at.desc())
)
flows = result.scalars().all()
return [FlowDefinitionOut(
id=f.id, name=f.name, description=f.description,
version=f.version, status=f.status,
definition_json=f.definition_json,
published_to_wecom=f.published_to_wecom,
created_at=f.created_at, updated_at=f.updated_at,
) for f in flows]
@router.get("/templates")
async def get_flow_templates(request: Request):
return {"code": 200, "data": FLOW_TEMPLATES}
@router.post("/templates/{template_id}/use")
async def use_flow_template(
template_id: str,
request: Request,
db: AsyncSession = Depends(get_db),
):
template = next((t for t in FLOW_TEMPLATES if t["id"] == template_id), None)
if not template:
raise HTTPException(404, "模板不存在")
user_ctx = request.state.user
flow = FlowDefinition(
name=template["name"] + " (副本)",
description=template["description"],
definition_json={
"nodes": template["nodes"],
"edges": template["edges"],
"trigger": {},
},
creator_id=uuid.UUID(user_ctx["id"]),
)
db.add(flow)
await db.flush()
return FlowDefinitionOut(
id=flow.id, name=flow.name, description=flow.description,
version=flow.version, status=flow.status,
definition_json=flow.definition_json,
published_to_wecom=flow.published_to_wecom,
created_at=flow.created_at, updated_at=flow.updated_at,
)
@router.get("/executions")
async def list_executions(request: Request, db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(FlowExecution).order_by(FlowExecution.started_at.desc()).limit(100)
)
executions = result.scalars().all()
return {
"code": 200,
"data": [{
"id": str(e.id),
"flow_id": str(e.flow_id),
"trigger_type": e.trigger_type,
"status": e.status,
"started_at": str(e.started_at),
"finished_at": str(e.finished_at) if e.finished_at else None,
} for e in executions],
}