Merge pull request #142 from SuanmoSuanyangTechnology/feature/workflow-release

Fix workflow release issues and enhance token metrics & loop node outputs
This commit is contained in:
Mark
2026-01-19 15:46:12 +08:00
committed by GitHub
15 changed files with 402 additions and 339 deletions

View File

@@ -8,9 +8,10 @@ from sqlalchemy.orm import Session
from app.core.logging_config import get_business_logger from app.core.logging_config import get_business_logger
from app.core.response_utils import success from app.core.response_utils import success
from app.db import get_db from app.db import get_db, get_db_read
from app.dependencies import get_share_user_id, ShareTokenData from app.dependencies import get_share_user_id, ShareTokenData
from app.repositories import knowledge_repository from app.repositories import knowledge_repository
from app.repositories.workflow_repository import WorkflowConfigRepository
from app.schemas import release_share_schema, conversation_schema from app.schemas import release_share_schema, conversation_schema
from app.schemas.response_schema import PageData, PageMeta from app.schemas.response_schema import PageData, PageMeta
from app.services import workspace_service from app.services import workspace_service
@@ -19,7 +20,8 @@ from app.services.conversation_service import ConversationService
from app.services.release_share_service import ReleaseShareService from app.services.release_share_service import ReleaseShareService
from app.services.shared_chat_service import SharedChatService from app.services.shared_chat_service import SharedChatService
from app.services.app_chat_service import AppChatService, get_app_chat_service from app.services.app_chat_service import AppChatService, get_app_chat_service
from app.utils.app_config_utils import dict_to_multi_agent_config, workflow_config_4_app_release, agent_config_4_app_release, multi_agent_config_4_app_release from app.utils.app_config_utils import dict_to_multi_agent_config, workflow_config_4_app_release, \
agent_config_4_app_release, multi_agent_config_4_app_release
router = APIRouter(prefix="/public/share", tags=["Public Share"]) router = APIRouter(prefix="/public/share", tags=["Public Share"])
logger = get_business_logger() logger = get_business_logger()
@@ -65,10 +67,10 @@ def get_or_generate_user_id(payload_user_id: str, request: Request) -> str:
summary="获取访问 token" summary="获取访问 token"
) )
def get_access_token( def get_access_token(
share_token: str, share_token: str,
payload: release_share_schema.TokenRequest, payload: release_share_schema.TokenRequest,
request: Request, request: Request,
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
"""获取访问 token """获取访问 token
@@ -113,9 +115,9 @@ def get_access_token(
response_model=None response_model=None
) )
def get_shared_release( def get_shared_release(
password: str = Query(None, description="访问密码(如果需要)"), password: str = Query(None, description="访问密码(如果需要)"),
share_data: ShareTokenData = Depends(get_share_user_id), share_data: ShareTokenData = Depends(get_share_user_id),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
"""获取公开分享的发布版本信息 """获取公开分享的发布版本信息
@@ -137,9 +139,9 @@ def get_shared_release(
summary="验证访问密码" summary="验证访问密码"
) )
def verify_password( def verify_password(
payload: release_share_schema.PasswordVerifyRequest, payload: release_share_schema.PasswordVerifyRequest,
share_data: ShareTokenData = Depends(get_share_user_id), share_data: ShareTokenData = Depends(get_share_user_id),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
"""验证分享的访问密码 """验证分享的访问密码
@@ -159,11 +161,11 @@ def verify_password(
summary="获取嵌入代码" summary="获取嵌入代码"
) )
def get_embed_code( def get_embed_code(
width: str = Query("100%", description="iframe 宽度"), width: str = Query("100%", description="iframe 宽度"),
height: str = Query("600px", description="iframe 高度"), height: str = Query("600px", description="iframe 高度"),
request: Request = None, request: Request = None,
share_data: ShareTokenData = Depends(get_share_user_id), share_data: ShareTokenData = Depends(get_share_user_id),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
"""获取嵌入代码 """获取嵌入代码
@@ -183,7 +185,6 @@ def get_embed_code(
return success(data=embed_code) return success(data=embed_code)
# ---------- 会话管理接口 ---------- # ---------- 会话管理接口 ----------
@router.get( @router.get(
@@ -191,11 +192,11 @@ def get_embed_code(
summary="获取会话列表" summary="获取会话列表"
) )
def list_conversations( def list_conversations(
password: str = Query(None, description="访问密码"), password: str = Query(None, description="访问密码"),
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
pagesize: int = Query(20, ge=1, le=100), pagesize: int = Query(20, ge=1, le=100),
share_data: ShareTokenData = Depends(get_share_user_id), share_data: ShareTokenData = Depends(get_share_user_id),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
"""获取分享应用的会话列表 """获取分享应用的会话列表
@@ -209,9 +210,9 @@ def list_conversations(
from app.repositories.end_user_repository import EndUserRepository from app.repositories.end_user_repository import EndUserRepository
end_user_repo = EndUserRepository(db) end_user_repo = EndUserRepository(db)
new_end_user = end_user_repo.get_or_create_end_user( new_end_user = end_user_repo.get_or_create_end_user(
app_id=share.app_id, app_id=share.app_id,
other_id=other_id other_id=other_id
) )
logger.debug(new_end_user.id) logger.debug(new_end_user.id)
service = SharedChatService(db) service = SharedChatService(db)
conversations, total = service.list_conversations( conversations, total = service.list_conversations(
@@ -233,10 +234,10 @@ def list_conversations(
summary="获取会话详情(含消息)" summary="获取会话详情(含消息)"
) )
def get_conversation( def get_conversation(
conversation_id: uuid.UUID, conversation_id: uuid.UUID,
password: str = Query(None, description="访问密码"), password: str = Query(None, description="访问密码"),
share_data: ShareTokenData = Depends(get_share_user_id), share_data: ShareTokenData = Depends(get_share_user_id),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
"""获取会话详情和消息历史""" """获取会话详情和消息历史"""
chat_service = SharedChatService(db) chat_service = SharedChatService(db)
@@ -266,10 +267,10 @@ def get_conversation(
summary="发送消息(支持流式和非流式)" summary="发送消息(支持流式和非流式)"
) )
async def chat( async def chat(
payload: conversation_schema.ChatRequest, payload: conversation_schema.ChatRequest,
share_data: ShareTokenData = Depends(get_share_user_id), share_data: ShareTokenData = Depends(get_share_user_id),
db: Session = Depends(get_db), db: Session = Depends(get_db),
app_chat_service: Annotated[AppChatService, Depends(get_app_chat_service)] = None, app_chat_service: Annotated[AppChatService, Depends(get_app_chat_service)] = None,
): ):
"""发送消息并获取回复 """发送消息并获取回复
@@ -313,7 +314,7 @@ async def chat(
) )
end_user_id = str(new_end_user.id) end_user_id = str(new_end_user.id)
appid=share.app_id appid = share.app_id
"""获取存储类型和工作空间的ID""" """获取存储类型和工作空间的ID"""
# 直接通过 SQLAlchemy 查询 app # 直接通过 SQLAlchemy 查询 app
@@ -425,16 +426,16 @@ async def chat(
# ) # )
async def event_generator(): async def event_generator():
async for event in app_chat_service.agnet_chat_stream( async for event in app_chat_service.agnet_chat_stream(
message=payload.message, message=payload.message,
conversation_id=conversation.id, # 使用已创建的会话 ID conversation_id=conversation.id, # 使用已创建的会话 ID
user_id= str(new_end_user.id), # 转换为字符串 user_id=str(new_end_user.id), # 转换为字符串
variables=payload.variables, variables=payload.variables,
web_search=payload.web_search, web_search=payload.web_search,
config=agent_config, config=agent_config,
memory=payload.memory, memory=payload.memory,
storage_type=storage_type, storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id, user_rag_memory_id=user_rag_memory_id,
workspace_id=workspace_id workspace_id=workspace_id
): ):
yield event yield event
@@ -481,15 +482,15 @@ async def chat(
async def event_generator(): async def event_generator():
async for event in app_chat_service.multi_agent_chat_stream( async for event in app_chat_service.multi_agent_chat_stream(
message=payload.message, message=payload.message,
conversation_id=conversation.id, # 使用已创建的会话 ID conversation_id=conversation.id, # 使用已创建的会话 ID
user_id=str(new_end_user.id), # 转换为字符串 user_id=str(new_end_user.id), # 转换为字符串
variables=payload.variables, variables=payload.variables,
config=config, config=config,
web_search=payload.web_search, web_search=payload.web_search,
memory=payload.memory, memory=payload.memory,
storage_type=storage_type, storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id user_rag_memory_id=user_rag_memory_id
): ):
yield event yield event
@@ -561,24 +562,27 @@ async def chat(
# return success(data=conversation_schema.ChatResponse(**result)) # return success(data=conversation_schema.ChatResponse(**result))
elif app_type == AppType.WORKFLOW: elif app_type == AppType.WORKFLOW:
config = workflow_config_4_app_release(release) config = workflow_config_4_app_release(release)
if not config.id:
with get_db_read() as db:
source_config = WorkflowConfigRepository(db).get_by_app_id(release.app_id)
config.id = source_config.id
config.id = uuid.UUID(config.id)
if payload.stream: if payload.stream:
async def event_generator(): async def event_generator():
async for event in app_chat_service.workflow_chat_stream( async for event in app_chat_service.workflow_chat_stream(
message=payload.message,
message=payload.message, conversation_id=conversation.id, # 使用已创建的会话 ID
conversation_id=conversation.id, # 使用已创建的会话 ID user_id=end_user_id, # 转换为字符串
user_id=end_user_id, # 转换为字符串 variables=payload.variables,
variables=payload.variables, config=config,
config=config, web_search=payload.web_search,
web_search=payload.web_search, memory=payload.memory,
memory=payload.memory, storage_type=storage_type,
storage_type=storage_type, user_rag_memory_id=user_rag_memory_id,
user_rag_memory_id=user_rag_memory_id, app_id=release.app_id,
app_id=release.app_id, workspace_id=workspace_id,
workspace_id=workspace_id release_id=release.id
): ):
event_type = event.get("event", "message") event_type = event.get("event", "message")
event_data = event.get("data", {}) event_data = event.get("data", {})
@@ -610,7 +614,8 @@ async def chat(
storage_type=storage_type, storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id, user_rag_memory_id=user_rag_memory_id,
app_id=release.app_id, app_id=release.app_id,
workspace_id=workspace_id workspace_id=workspace_id,
release_id=release.id
) )
logger.debug( logger.debug(
"工作流试运行返回结果", "工作流试运行返回结果",

View File

@@ -242,8 +242,9 @@ async def chat(
memory=payload.memory, memory=payload.memory,
storage_type=storage_type, storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id, user_rag_memory_id=user_rag_memory_id,
app_id=app.app_id, app_id=app.id,
workspace_id=workspace_id workspace_id=workspace_id,
release_id=app.current_release.id,
): ):
event_type = event.get("event", "message") event_type = event.get("event", "message")
event_data = event.get("data", {}) event_data = event.get("data", {})
@@ -274,8 +275,9 @@ async def chat(
memory=payload.memory, memory=payload.memory,
storage_type=storage_type, storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id, user_rag_memory_id=user_rag_memory_id,
app_id=app.app_id, app_id=app.id,
workspace_id=workspace_id workspace_id=workspace_id,
release_id=app.current_release.id
) )
logger.debug( logger.debug(
"工作流试运行返回结果", "工作流试运行返回结果",

View File

@@ -8,6 +8,7 @@ import logging
import uuid import uuid
from typing import Any from typing import Any
from langchain_core.runnables import RunnableConfig
from langgraph.graph.state import CompiledStateGraph from langgraph.graph.state import CompiledStateGraph
from app.core.workflow.graph_builder import GraphBuilder from app.core.workflow.graph_builder import GraphBuilder
@@ -53,11 +54,11 @@ class WorkflowExecutor:
self.edges = workflow_config.get("edges", []) self.edges = workflow_config.get("edges", [])
self.execution_config = workflow_config.get("execution_config", {}) self.execution_config = workflow_config.get("execution_config", {})
self.checkpoint_config = { self.checkpoint_config = RunnableConfig(
"configurable": { configurable={
"thread_id": uuid.uuid4(), "thread_id": uuid.uuid4(),
} }
} )
def _prepare_initial_state(self, input_data: dict[str, Any]) -> WorkflowState: def _prepare_initial_state(self, input_data: dict[str, Any]) -> WorkflowState:
"""准备初始状态(注入系统变量和会话变量) """准备初始状态(注入系统变量和会话变量)
@@ -214,13 +215,13 @@ class WorkflowExecutor:
return { return {
"status": "completed", "status": "completed",
"output": final_output, "output": final_output,
"variables": result.get("variables", {}),
"node_outputs": node_outputs, "node_outputs": node_outputs,
"messages": result.get("messages", []), "messages": result.get("messages", []),
"conversation_id": conversation_id, "conversation_id": conversation_id,
"elapsed_time": elapsed_time, "elapsed_time": elapsed_time,
"token_usage": token_usage, "token_usage": token_usage,
"error": result.get("error"), "error": result.get("error"),
"variables": result.get("variables", {}),
} }
def build_graph(self, stream=False) -> CompiledStateGraph: def build_graph(self, stream=False) -> CompiledStateGraph:
@@ -326,11 +327,10 @@ class WorkflowExecutor:
} }
# 1. 构建图 # 1. 构建图
graph = self.build_graph(True) graph = self.build_graph(stream=True)
# 2. 初始化状态(自动注入系统变量) # 2. 初始化状态(自动注入系统变量)
initial_state = self._prepare_initial_state(input_data) initial_state = self._prepare_initial_state(input_data)
# 3. Execute workflow # 3. Execute workflow
try: try:
chunk_count = 0 chunk_count = 0
@@ -346,14 +346,16 @@ class WorkflowExecutor:
mode, data = event mode, data = event
else: else:
# Unexpected format, log and skip # Unexpected format, log and skip
logger.warning(f"[STREAM] Unexpected event format: {type(event)}, value: {event}") logger.warning(f"[STREAM] Unexpected event format: {type(event)}, value: {event}"
f"- execution_id: {self.execution_id}")
continue continue
if mode == "custom": if mode == "custom":
# Handle custom streaming events (chunks from nodes via stream writer) # Handle custom streaming events (chunks from nodes via stream writer)
chunk_count += 1 chunk_count += 1
event_type = data.get("type", "node_chunk") # "message" or "node_chunk" event_type = data.get("type", "node_chunk") # "message" or "node_chunk"
logger.info(f"[CUSTOM] ✅ 收到 {event_type} #{chunk_count} from {data.get('node_id')}") logger.info(f"[CUSTOM] ✅ 收到 {event_type} #{chunk_count} from {data.get('node_id')}"
f"- execution_id: {self.execution_id}")
yield { yield {
"event": event_type, # "message" or "node_chunk" "event": event_type, # "message" or "node_chunk"
"data": { "data": {
@@ -380,7 +382,8 @@ class WorkflowExecutor:
variables_sys = variables.get("sys", {}) variables_sys = variables.get("sys", {})
conversation_id = input_data.get("conversation_id") conversation_id = input_data.get("conversation_id")
execution_id = variables_sys.get("execution_id") execution_id = variables_sys.get("execution_id")
logger.info(f"[DEBUG] Node starts execution: {node_name}") logger.info(f"[NODE-START] Node starts execution: {node_name} "
f"- execution_id: {self.execution_id}")
yield { yield {
"event": "node_start", "event": "node_start",
@@ -399,7 +402,8 @@ class WorkflowExecutor:
variables_sys = variables.get("sys", {}) variables_sys = variables.get("sys", {})
conversation_id = input_data.get("conversation_id") conversation_id = input_data.get("conversation_id")
execution_id = variables_sys.get("execution_id") execution_id = variables_sys.get("execution_id")
logger.info(f"[DEBUG] Node execution completed: {node_name}") logger.info(f"[NODE-END] Node execution completed: {node_name} "
f"- execution_id: {self.execution_id}")
yield { yield {
"event": "node_end", "event": "node_end",
@@ -407,13 +411,15 @@ class WorkflowExecutor:
"node_id": node_name, "node_id": node_name,
"conversation_id": conversation_id, "conversation_id": conversation_id,
"execution_id": execution_id, "execution_id": execution_id,
"timestamp": data.get("timestamp") "timestamp": data.get("timestamp"),
"state": result.get("node_outputs", {}).get(node_name),
} }
} }
elif mode == "updates": elif mode == "updates":
# Handle state updates - store final state # Handle state updates - store final state
logger.debug(f"[UPDATES] 收到 state 更新 from {list(data.keys())}") logger.debug(f"[UPDATES] 收到 state 更新 from {list(data.keys())} "
f"- execution_id: {self.execution_id}")
# 计算耗时 # 计算耗时
end_time = datetime.datetime.now() end_time = datetime.datetime.now()
@@ -421,7 +427,7 @@ class WorkflowExecutor:
result = graph.get_state(self.checkpoint_config).values result = graph.get_state(self.checkpoint_config).values
logger.info( logger.info(
f"Workflow execution completed (streaming), " f"Workflow execution completed (streaming), "
f"total chunks: {chunk_count}, elapsed: {elapsed_time:.2f}s" f"total chunks: {chunk_count}, elapsed: {elapsed_time:.2f}s, execution_id: {self.execution_id}"
) )
# 发送 workflow_end 事件 # 发送 workflow_end 事件
@@ -449,7 +455,8 @@ class WorkflowExecutor:
} }
} }
def _extract_final_output(self, node_outputs: dict[str, Any]) -> str | None: @staticmethod
def _extract_final_output(node_outputs: dict[str, Any]) -> str | None:
"""从节点输出中提取最终输出 """从节点输出中提取最终输出
优先级: 优先级:
@@ -473,7 +480,8 @@ class WorkflowExecutor:
return None return None
def _aggregate_token_usage(self, node_outputs: dict[str, Any]) -> dict[str, int] | None: @staticmethod
def _aggregate_token_usage(node_outputs: dict[str, Any]) -> dict[str, int] | None:
"""聚合所有节点的 token 使用情况 """聚合所有节点的 token 使用情况
Args: Args:

View File

@@ -25,7 +25,7 @@ class WorkflowState(TypedDict):
The state object passed between nodes in a workflow, containing messages, variables, node outputs, etc. The state object passed between nodes in a workflow, containing messages, variables, node outputs, etc.
""" """
# List of messages (append mode) # List of messages (append mode)
messages: list[dict[str, str]] messages: Annotated[list[dict[str, str]], lambda x, y: y]
# Set of loop node IDs, used for assigning values in loop nodes # Set of loop node IDs, used for assigning values in loop nodes
cycle_nodes: list cycle_nodes: list

View File

@@ -21,6 +21,7 @@ class IterationRuntime:
optional parallel execution, flattening of output, and loop control via optional parallel execution, flattening of output, and loop control via
the workflow state. the workflow state.
""" """
def __init__( def __init__(
self, self,
graph: CompiledStateGraph, graph: CompiledStateGraph,
@@ -87,6 +88,7 @@ class IterationRuntime:
self.result.append(output) self.result.append(output)
if not result["looping"]: if not result["looping"]:
self.looping = False self.looping = False
return result
def _create_iteration_tasks(self, array_obj, idx): def _create_iteration_tasks(self, array_obj, idx):
""" """
@@ -124,7 +126,7 @@ class IterationRuntime:
array_obj = VariablePool(self.state).get(input_expression) array_obj = VariablePool(self.state).get(input_expression)
if not isinstance(array_obj, list): if not isinstance(array_obj, list):
raise RuntimeError("Cannot iterate over a non-list variable") raise RuntimeError("Cannot iterate over a non-list variable")
child_state = []
idx = 0 idx = 0
if self.typed_config.parallel: if self.typed_config.parallel:
# Execute iterations in parallel batches # Execute iterations in parallel batches
@@ -132,15 +134,14 @@ class IterationRuntime:
tasks = self._create_iteration_tasks(array_obj, idx) tasks = self._create_iteration_tasks(array_obj, idx)
logger.info(f"Iteration node {self.node_id}: running, concurrency {len(tasks)}") logger.info(f"Iteration node {self.node_id}: running, concurrency {len(tasks)}")
idx += self.typed_config.parallel_count idx += self.typed_config.parallel_count
await asyncio.gather(*tasks) child_state.extend(await asyncio.gather(*tasks))
logger.info(f"Iteration node {self.node_id}: execution completed")
return self.result
else: else:
# Execute iterations sequentially # Execute iterations sequentially
while idx < len(array_obj) and self.looping: while idx < len(array_obj) and self.looping:
logger.info(f"Iteration node {self.node_id}: running") logger.info(f"Iteration node {self.node_id}: running")
item = array_obj[idx] item = array_obj[idx]
result = await self.graph.ainvoke(self._init_iteration_state(item, idx)) result = await self.graph.ainvoke(self._init_iteration_state(item, idx))
child_state.append(result)
output = VariablePool(result).get(self.output_value) output = VariablePool(result).get(self.output_value)
if isinstance(output, list) and self.typed_config.flatten: if isinstance(output, list) and self.typed_config.flatten:
self.result.extend(output) self.result.extend(output)
@@ -150,5 +151,8 @@ class IterationRuntime:
self.looping = False self.looping = False
idx += 1 idx += 1
logger.info(f"Iteration node {self.node_id}: execution completed") logger.info(f"Iteration node {self.node_id}: execution completed")
return self.result return {
"output": self.result,
"__child_state": child_state
}

View File

@@ -67,7 +67,9 @@ class LoopRuntime:
variables=pool.get_all_conversation_vars(), variables=pool.get_all_conversation_vars(),
node_outputs=pool.get_all_node_outputs(), node_outputs=pool.get_all_node_outputs(),
system_vars=pool.get_all_system_vars(), system_vars=pool.get_all_system_vars(),
) if variable.input_type == ValueInputType.VARIABLE else TypeTransformer.transform(variable.value, variable.type) )
if variable.input_type == ValueInputType.VARIABLE
else TypeTransformer.transform(variable.value, variable.type)
for variable in self.typed_config.cycle_vars for variable in self.typed_config.cycle_vars
} }
self.state["node_outputs"][self.node_id] = { self.state["node_outputs"][self.node_id] = {
@@ -76,7 +78,9 @@ class LoopRuntime:
variables=pool.get_all_conversation_vars(), variables=pool.get_all_conversation_vars(),
node_outputs=pool.get_all_node_outputs(), node_outputs=pool.get_all_node_outputs(),
system_vars=pool.get_all_system_vars(), system_vars=pool.get_all_system_vars(),
) if variable.input_type == ValueInputType.VARIABLE else TypeTransformer.transform(variable.value, variable.type) )
if variable.input_type == ValueInputType.VARIABLE
else TypeTransformer.transform(variable.value, variable.type)
for variable in self.typed_config.cycle_vars for variable in self.typed_config.cycle_vars
} }
loopstate = WorkflowState( loopstate = WorkflowState(
@@ -171,10 +175,11 @@ class LoopRuntime:
""" """
loopstate = self._init_loop_state() loopstate = self._init_loop_state()
loop_time = self.typed_config.max_loop loop_time = self.typed_config.max_loop
child_state = []
while self.evaluate_conditional(loopstate) and loopstate["looping"] and loop_time > 0: while self.evaluate_conditional(loopstate) and loopstate["looping"] and loop_time > 0:
logger.info(f"loop node {self.node_id}: running") logger.info(f"loop node {self.node_id}: running")
await self.graph.ainvoke(loopstate) child_state.append(await self.graph.ainvoke(loopstate))
loop_time -= 1 loop_time -= 1
logger.info(f"loop node {self.node_id}: execution completed") logger.info(f"loop node {self.node_id}: execution completed")
return loopstate["runtime_vars"][self.node_id] return loopstate["runtime_vars"][self.node_id] | {"__child_state": child_state}

View File

@@ -10,9 +10,8 @@ from app.core.workflow.nodes.base_node import BaseNode, WorkflowState
from app.core.workflow.nodes.knowledge import KnowledgeRetrievalNodeConfig from app.core.workflow.nodes.knowledge import KnowledgeRetrievalNodeConfig
from app.db import get_db_read from app.db import get_db_read
from app.models import knowledge_model, knowledgeshare_model, ModelType from app.models import knowledge_model, knowledgeshare_model, ModelType
from app.repositories import knowledge_repository from app.repositories import knowledge_repository, knowledgeshare_repository
from app.schemas.chunk_schema import RetrieveType from app.schemas.chunk_schema import RetrieveType
from app.services import knowledge_service, knowledgeshare_service
from app.services.model_service import ModelConfigService from app.services.model_service import ModelConfigService
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -96,7 +95,7 @@ class KnowledgeRetrievalNode(BaseNode):
filters = self._build_kb_filter(kb_ids, knowledge_model.PermissionType.Share) filters = self._build_kb_filter(kb_ids, knowledge_model.PermissionType.Share)
share_ids = knowledge_service.knowledge_repository.get_chunked_knowledgeids( share_ids = knowledge_repository.get_chunked_knowledgeids(
db=db, db=db,
filters=filters filters=filters
) )
@@ -105,7 +104,7 @@ class KnowledgeRetrievalNode(BaseNode):
filters = [ filters = [
knowledgeshare_model.KnowledgeShare.target_kb_id.in_(kb_ids) knowledgeshare_model.KnowledgeShare.target_kb_id.in_(kb_ids)
] ]
items = knowledgeshare_service.knowledgeshare_repository.get_source_kb_ids_by_target_kb_id( items = knowledgeshare_repository.get_source_kb_ids_by_target_kb_id(
db=db, db=db,
filters=filters filters=filters
) )

View File

@@ -66,7 +66,7 @@ class LLMNodeConfig(BaseNodeConfig):
) )
memory: MemoryWindowSetting = Field( memory: MemoryWindowSetting = Field(
..., default_factory=MemoryWindowSetting,
description="对话上下文窗口" description="对话上下文窗口"
) )

View File

@@ -85,6 +85,7 @@ class LLMNode(BaseNode):
""" """
# 1. 处理消息格式(优先使用 messages # 1. 处理消息格式(优先使用 messages
self.typed_config = LLMNodeConfig(**self.config)
messages_config = self.typed_config.messages messages_config = self.typed_config.messages
if messages_config: if messages_config:
@@ -167,7 +168,7 @@ class LLMNode(BaseNode):
Returns: Returns:
LLM 响应消息 LLM 响应消息
""" """
self.typed_config = LLMNodeConfig(**self.config) # self.typed_config = LLMNodeConfig(**self.config)
llm, prompt_or_messages = self._prepare_llm(state, True) llm, prompt_or_messages = self._prepare_llm(state, True)
logger.info(f"节点 {self.node_id} 开始执行 LLM 调用(非流式)") logger.info(f"节点 {self.node_id} 开始执行 LLM 调用(非流式)")
@@ -269,12 +270,16 @@ class LLMNode(BaseNode):
chunk_count = 0 chunk_count = 0
# 调用 LLM流式支持字符串或消息列表 # 调用 LLM流式支持字符串或消息列表
async for chunk in llm.astream(prompt_or_messages): last_meta_data = {}
async for chunk in llm.astream(prompt_or_messages, stream_usage=True):
# 提取内容 # 提取内容
if hasattr(chunk, 'content'): if hasattr(chunk, 'content'):
content = chunk.content content = chunk.content
else: else:
content = str(chunk) content = str(chunk)
if hasattr(chunk, 'response_metadata'):
if chunk.response_metadata:
last_meta_data = chunk.response_metadata
# 只有当内容不为空时才处理 # 只有当内容不为空时才处理
if content: if content:
@@ -288,13 +293,10 @@ class LLMNode(BaseNode):
logger.info(f"节点 {self.node_id} LLM 调用完成,输出长度: {len(full_response)}, 总 chunks: {chunk_count}") logger.info(f"节点 {self.node_id} LLM 调用完成,输出长度: {len(full_response)}, 总 chunks: {chunk_count}")
# 构建完整的 AIMessage包含元数据 # 构建完整的 AIMessage包含元数据
if isinstance(last_chunk, AIMessage): final_message = AIMessage(
final_message = AIMessage( content=full_response,
content=full_response, response_metadata=last_meta_data
response_metadata=last_chunk.response_metadata if hasattr(last_chunk, 'response_metadata') else {} )
)
else:
final_message = AIMessage(content=full_response)
# yield 完成标记 # yield 完成标记
yield {"__final__": True, "result": final_message} yield {"__final__": True, "result": final_message}

View File

@@ -75,6 +75,14 @@ class WorkflowExecution(Base):
nullable=False, nullable=False,
index=True index=True
) )
release_id = Column(
UUID(as_uuid=True),
ForeignKey("app_releases.id", ondelete="CASCADE"),
nullable=True,
index=True
)
app_id = Column( app_id = Column(
UUID(as_uuid=True), UUID(as_uuid=True),
ForeignKey("apps.id", ondelete="CASCADE"), ForeignKey("apps.id", ondelete="CASCADE"),

View File

@@ -527,6 +527,7 @@ class AppChatService:
conversation_id: uuid.UUID, conversation_id: uuid.UUID,
config: WorkflowConfig, config: WorkflowConfig,
app_id: uuid.UUID, app_id: uuid.UUID,
release_id: uuid.UUID,
workspace_id: uuid.UUID, workspace_id: uuid.UUID,
user_id: Optional[str] = None, user_id: Optional[str] = None,
variables: Optional[Dict[str, Any]] = None, variables: Optional[Dict[str, Any]] = None,
@@ -549,6 +550,7 @@ class AppChatService:
payload=payload, payload=payload,
config=config, config=config,
workspace_id=workspace_id, workspace_id=workspace_id,
release_id=release_id,
) )
async def workflow_chat_stream( async def workflow_chat_stream(
@@ -557,6 +559,7 @@ class AppChatService:
conversation_id: uuid.UUID, conversation_id: uuid.UUID,
config: WorkflowConfig, config: WorkflowConfig,
app_id: uuid.UUID, app_id: uuid.UUID,
release_id: uuid.UUID,
workspace_id: uuid.UUID, workspace_id: uuid.UUID,
user_id: str = None, user_id: str = None,
variables: Optional[Dict[str, Any]] = None, variables: Optional[Dict[str, Any]] = None,
@@ -565,7 +568,7 @@ class AppChatService:
storage_type: Optional[str] = None, storage_type: Optional[str] = None,
user_rag_memory_id: Optional[str] = None, user_rag_memory_id: Optional[str] = None,
) -> AsyncGenerator[str, None]: ) -> AsyncGenerator[dict, None]:
"""聊天(流式)""" """聊天(流式)"""
workflow_service = WorkflowService(self.db) workflow_service = WorkflowService(self.db)
payload = DraftRunRequest( payload = DraftRunRequest(
@@ -580,6 +583,7 @@ class AppChatService:
payload=payload, payload=payload,
config=config, config=config,
workspace_id=workspace_id, workspace_id=workspace_id,
release_id=release_id
): ):
yield event yield event

View File

@@ -129,7 +129,7 @@ class AppService:
Raises: Raises:
ResourceNotFoundException: 当应用不存在时 ResourceNotFoundException: 当应用不存在时
""" """
app = get_apps_by_id(self.db,app_id) app = get_apps_by_id(self.db, app_id)
if not app: if not app:
logger.warning("应用不存在", extra={"app_id": str(app_id)}) logger.warning("应用不存在", extra={"app_id": str(app_id)})
raise ResourceNotFoundException("应用", str(app_id)) raise ResourceNotFoundException("应用", str(app_id))
@@ -227,7 +227,6 @@ class AppService:
if not model_api_key: if not model_api_key:
raise ResourceNotFoundException("模型配置", str(multi_agent_config.default_model_config_id)) raise ResourceNotFoundException("模型配置", str(multi_agent_config.default_model_config_id))
# 3. 检查子 Agent 配置 # 3. 检查子 Agent 配置
if not multi_agent_config.sub_agents or len(multi_agent_config.sub_agents) == 0: if not multi_agent_config.sub_agents or len(multi_agent_config.sub_agents) == 0:
raise BusinessException( raise BusinessException(
@@ -281,10 +280,10 @@ class AppService:
) )
def _create_agent_config( def _create_agent_config(
self, self,
app_id: uuid.UUID, app_id: uuid.UUID,
config_data: app_schema.AgentConfigCreate, config_data: app_schema.AgentConfigCreate,
now: datetime.datetime now: datetime.datetime
) -> None: ) -> None:
"""创建 Agent 配置(内部方法) """创建 Agent 配置(内部方法)
@@ -313,10 +312,10 @@ class AppService:
logger.debug("Agent 配置已创建", extra={"app_id": str(app_id)}) logger.debug("Agent 配置已创建", extra={"app_id": str(app_id)})
def _create_multi_agent_config( def _create_multi_agent_config(
self, self,
app_id: uuid.UUID, app_id: uuid.UUID,
config_data: Dict[str, Any], config_data: Dict[str, Any],
now: datetime.datetime now: datetime.datetime
) -> None: ) -> None:
"""创建多 Agent 配置(内部方法) """创建多 Agent 配置(内部方法)
@@ -411,9 +410,9 @@ class AppService:
return 1 if max_ver is None else int(max_ver) + 1 return 1 if max_ver is None else int(max_ver) + 1
def _convert_to_schema( def _convert_to_schema(
self, self,
app: App, app: App,
current_workspace_id: uuid.UUID current_workspace_id: uuid.UUID
) -> app_schema.App: ) -> app_schema.App:
"""将 App 模型转换为 Schema并设置 is_shared 字段 """将 App 模型转换为 Schema并设置 is_shared 字段
@@ -447,9 +446,9 @@ class AppService:
# ==================== 应用管理 ==================== # ==================== 应用管理 ====================
def get_app( def get_app(
self, self,
app_id: uuid.UUID, app_id: uuid.UUID,
workspace_id: Optional[uuid.UUID] = None workspace_id: Optional[uuid.UUID] = None
) -> App: ) -> App:
"""获取应用详情 """获取应用详情
@@ -469,11 +468,11 @@ class AppService:
return app return app
def create_app( def create_app(
self, self,
*, *,
user_id: uuid.UUID, user_id: uuid.UUID,
workspace_id: uuid.UUID, workspace_id: uuid.UUID,
data: app_schema.AppCreate data: app_schema.AppCreate
) -> App: ) -> App:
"""创建应用 """创建应用
@@ -535,11 +534,11 @@ class AppService:
raise BusinessException(f"应用创建失败: {str(e)}", BizCode.INTERNAL_ERROR, cause=e) raise BusinessException(f"应用创建失败: {str(e)}", BizCode.INTERNAL_ERROR, cause=e)
def update_app( def update_app(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
data: app_schema.AppUpdate, data: app_schema.AppUpdate,
workspace_id: Optional[uuid.UUID] = None workspace_id: Optional[uuid.UUID] = None
) -> App: ) -> App:
"""更新应用基本信息 """更新应用基本信息
@@ -578,10 +577,10 @@ class AppService:
return app return app
def delete_app( def delete_app(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
workspace_id: Optional[uuid.UUID] = None workspace_id: Optional[uuid.UUID] = None
) -> None: ) -> None:
"""删除应用 """删除应用
@@ -612,12 +611,12 @@ class AppService:
) )
def copy_app( def copy_app(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
user_id: uuid.UUID, user_id: uuid.UUID,
workspace_id: Optional[uuid.UUID] = None, workspace_id: Optional[uuid.UUID] = None,
new_name: Optional[str] = None new_name: Optional[str] = None
) -> App: ) -> App:
"""复制应用(包括基础信息和配置) """复制应用(包括基础信息和配置)
@@ -716,16 +715,16 @@ class AppService:
raise BusinessException(f"应用复制失败: {str(e)}", BizCode.INTERNAL_ERROR, cause=e) raise BusinessException(f"应用复制失败: {str(e)}", BizCode.INTERNAL_ERROR, cause=e)
def list_apps( def list_apps(
self, self,
*, *,
workspace_id: uuid.UUID, workspace_id: uuid.UUID,
type: Optional[str] = None, type: Optional[str] = None,
visibility: Optional[str] = None, visibility: Optional[str] = None,
status: Optional[str] = None, status: Optional[str] = None,
search: Optional[str] = None, search: Optional[str] = None,
include_shared: bool = True, include_shared: bool = True,
page: int = 1, page: int = 1,
pagesize: int = 10, pagesize: int = 10,
) -> Tuple[List[App], int]: ) -> Tuple[List[App], int]:
"""列出工作空间中的应用(分页) """列出工作空间中的应用(分页)
@@ -759,8 +758,7 @@ class AppService:
) )
# 构建查询条件 # 构建查询条件
filters = [] filters = [App.is_active == True]
filters.append(App.is_active == True)
if type: if type:
filters.append(App.type == type) filters.append(App.type == type)
if visibility: if visibility:
@@ -813,9 +811,9 @@ class AppService:
return items, int(total) return items, int(total)
def get_apps_by_ids( def get_apps_by_ids(
self, self,
app_ids: List[str], app_ids: List[str],
workspace_id: uuid.UUID workspace_id: uuid.UUID
) -> List[App]: ) -> List[App]:
"""根据ID列表获取应用 """根据ID列表获取应用
@@ -846,11 +844,11 @@ class AppService:
# ==================== Agent 配置管理 ==================== # ==================== Agent 配置管理 ====================
def update_agent_config( def update_agent_config(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
data: app_schema.AgentConfigUpdate, data: app_schema.AgentConfigUpdate,
workspace_id: Optional[uuid.UUID] = None workspace_id: Optional[uuid.UUID] = None
) -> AgentConfig: ) -> AgentConfig:
"""更新 Agent 配置 """更新 Agent 配置
@@ -875,7 +873,8 @@ class AppService:
self._validate_workspace_access(app, workspace_id) self._validate_workspace_access(app, workspace_id)
stmt = select(AgentConfig).where(AgentConfig.app_id == app_id, AgentConfig.is_active==True).order_by(AgentConfig.updated_at.desc()) stmt = select(AgentConfig).where(AgentConfig.app_id == app_id, AgentConfig.is_active == True).order_by(
AgentConfig.updated_at.desc())
agent_cfg: Optional[AgentConfig] = self.db.scalars(stmt).first() agent_cfg: Optional[AgentConfig] = self.db.scalars(stmt).first()
now = datetime.datetime.now() now = datetime.datetime.now()
@@ -918,10 +917,10 @@ class AppService:
return agent_cfg return agent_cfg
def get_agent_config( def get_agent_config(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
workspace_id: Optional[uuid.UUID] = None workspace_id: Optional[uuid.UUID] = None
) -> AgentConfig: ) -> AgentConfig:
"""获取 Agent 配置 """获取 Agent 配置
@@ -948,7 +947,12 @@ class AppService:
# 只读操作,允许访问共享应用 # 只读操作,允许访问共享应用
self._validate_app_accessible(app, workspace_id) self._validate_app_accessible(app, workspace_id)
stmt = select(AgentConfig).where(AgentConfig.app_id == app_id, AgentConfig.is_active == True).order_by(AgentConfig.updated_at.desc()) stmt = select(AgentConfig).where(
AgentConfig.app_id == app_id,
AgentConfig.is_active.is_(True)
).order_by(
AgentConfig.updated_at.desc()
)
config = self.db.scalars(stmt).first() config = self.db.scalars(stmt).first()
if config: if config:
@@ -1166,13 +1170,13 @@ class AppService:
# ==================== 应用发布管理 ==================== # ==================== 应用发布管理 ====================
def publish( def publish(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
publisher_id: uuid.UUID, publisher_id: uuid.UUID,
version_name: str, version_name: str,
workspace_id: Optional[uuid.UUID] = None, workspace_id: Optional[uuid.UUID] = None,
release_notes: Optional[str] = None release_notes: Optional[str] = None
) -> AppRelease: ) -> AppRelease:
"""发布应用(创建不可变快照) """发布应用(创建不可变快照)
@@ -1200,7 +1204,8 @@ class AppService:
default_model_config_id = None default_model_config_id = None
if app.type == AppType.AGENT: if app.type == AppType.AGENT:
stmt = select(AgentConfig).where(AgentConfig.app_id == app_id, AgentConfig.is_active == True).order_by(AgentConfig.updated_at.desc()) stmt = select(AgentConfig).where(AgentConfig.app_id == app_id, AgentConfig.is_active == True).order_by(
AgentConfig.updated_at.desc())
agent_cfg = self.db.scalars(stmt).first() agent_cfg = self.db.scalars(stmt).first()
if not agent_cfg: if not agent_cfg:
raise BusinessException("Agent 应用缺少配置,无法发布", BizCode.AGENT_CONFIG_MISSING) raise BusinessException("Agent 应用缺少配置,无法发布", BizCode.AGENT_CONFIG_MISSING)
@@ -1236,8 +1241,7 @@ class AppService:
default_model_config_id = multi_agent_cfg.default_model_config_id default_model_config_id = multi_agent_cfg.default_model_config_id
# 4. 构建配置快照 # 4. 构建配置快照
config = { config = {
"model_parameters": model_parameters_to_dict(multi_agent_cfg.model_parameters), "model_parameters": model_parameters_to_dict(multi_agent_cfg.model_parameters),
"master_agent_id": str(multi_agent_cfg.master_agent_id), "master_agent_id": str(multi_agent_cfg.master_agent_id),
@@ -1264,6 +1268,7 @@ class AppService:
raise BusinessException("应用缺少有效配置,无法发布", BizCode.CONFIG_MISSING) raise BusinessException("应用缺少有效配置,无法发布", BizCode.CONFIG_MISSING)
config = { config = {
"id": str(workflow_cfg.id),
"nodes": workflow_cfg.nodes, "nodes": workflow_cfg.nodes,
"edges": workflow_cfg.edges, "edges": workflow_cfg.edges,
"variables": workflow_cfg.variables, "variables": workflow_cfg.variables,
@@ -1285,7 +1290,7 @@ class AppService:
id=uuid.uuid4(), id=uuid.uuid4(),
app_id=app_id, app_id=app_id,
version=version, version=version,
version_name = version_name, version_name=version_name,
release_notes=release_notes, release_notes=release_notes,
name=app.name, name=app.name,
description=app.description, description=app.description,
@@ -1319,10 +1324,10 @@ class AppService:
return release return release
def get_current_release( def get_current_release(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
workspace_id: Optional[uuid.UUID] = None workspace_id: Optional[uuid.UUID] = None
) -> Optional[AppRelease]: ) -> Optional[AppRelease]:
"""获取当前发布版本 """获取当前发布版本
@@ -1349,10 +1354,10 @@ class AppService:
return self.db.get(AppRelease, app.current_release_id) return self.db.get(AppRelease, app.current_release_id)
def list_releases( def list_releases(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
workspace_id: Optional[uuid.UUID] = None workspace_id: Optional[uuid.UUID] = None
) -> List[AppRelease]: ) -> List[AppRelease]:
"""列出应用的所有发布版本(倒序) """列出应用的所有发布版本(倒序)
@@ -1381,11 +1386,11 @@ class AppService:
return list(self.db.scalars(stmt).all()) return list(self.db.scalars(stmt).all())
def rollback( def rollback(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
version: int, version: int,
workspace_id: Optional[uuid.UUID] = None workspace_id: Optional[uuid.UUID] = None
) -> AppRelease: ) -> AppRelease:
"""回滚到指定版本 """回滚到指定版本
@@ -1434,12 +1439,12 @@ class AppService:
# ==================== 应用分享功能 ==================== # ==================== 应用分享功能 ====================
def share_app( def share_app(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
target_workspace_ids: List[uuid.UUID], target_workspace_ids: List[uuid.UUID],
user_id: uuid.UUID, user_id: uuid.UUID,
workspace_id: Optional[uuid.UUID] = None workspace_id: Optional[uuid.UUID] = None
) -> AppShare: ) -> AppShare:
"""分享应用到其他工作空间 """分享应用到其他工作空间
@@ -1457,7 +1462,6 @@ class AppService:
BusinessException: 当应用不在指定工作空间或目标工作空间无效时 BusinessException: 当应用不在指定工作空间或目标工作空间无效时
""" """
logger.info( logger.info(
"分享应用", "分享应用",
extra={ extra={
@@ -1536,11 +1540,11 @@ class AppService:
return shares return shares
def unshare_app( def unshare_app(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
target_workspace_id: uuid.UUID, target_workspace_id: uuid.UUID,
workspace_id: Optional[uuid.UUID] = None workspace_id: Optional[uuid.UUID] = None
) -> None: ) -> None:
"""取消应用分享 """取消应用分享
@@ -1594,10 +1598,10 @@ class AppService:
) )
def list_app_shares( def list_app_shares(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
workspace_id: Optional[uuid.UUID] = None workspace_id: Optional[uuid.UUID] = None
) -> List[AppShare]: ) -> List[AppShare]:
"""列出应用的所有分享记录 """列出应用的所有分享记录
@@ -1637,14 +1641,14 @@ class AppService:
# ==================== 试运行功能 ==================== # ==================== 试运行功能 ====================
async def draft_run( async def draft_run(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
message: str, message: str,
conversation_id: Optional[str] = None, conversation_id: Optional[str] = None,
user_id: Optional[str] = None, user_id: Optional[str] = None,
variables: Optional[Dict[str, Any]] = None, variables: Optional[Dict[str, Any]] = None,
workspace_id: Optional[uuid.UUID] = None workspace_id: Optional[uuid.UUID] = None
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""试运行 Agent使用当前草稿配置 """试运行 Agent使用当前草稿配置
@@ -1736,14 +1740,14 @@ class AppService:
return result return result
async def draft_run_stream( async def draft_run_stream(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
message: str, message: str,
conversation_id: Optional[str] = None, conversation_id: Optional[str] = None,
user_id: Optional[str] = None, user_id: Optional[str] = None,
variables: Optional[Dict[str, Any]] = None, variables: Optional[Dict[str, Any]] = None,
workspace_id: Optional[uuid.UUID] = None workspace_id: Optional[uuid.UUID] = None
): ):
"""试运行 Agent流式返回 """试运行 Agent流式返回
@@ -1794,30 +1798,30 @@ class AppService:
# 4. 调用流式试运行服务 # 4. 调用流式试运行服务
draft_service = DraftRunService(self.db) draft_service = DraftRunService(self.db)
async for event in draft_service.run_stream( async for event in draft_service.run_stream(
agent_config=agent_cfg, agent_config=agent_cfg,
model_config=model_config, model_config=model_config,
message=message, message=message,
workspace_id=workspace_id, workspace_id=workspace_id,
conversation_id=conversation_id, conversation_id=conversation_id,
user_id=user_id, user_id=user_id,
variables=variables variables=variables
): ):
yield event yield event
# ==================== 多模型对比试运行 ==================== # ==================== 多模型对比试运行 ====================
async def draft_run_compare( async def draft_run_compare(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
message: str, message: str,
models: List[app_schema.ModelCompareItem], models: List[app_schema.ModelCompareItem],
conversation_id: Optional[str] = None, conversation_id: Optional[str] = None,
user_id: Optional[str] = None, user_id: Optional[str] = None,
variables: Optional[Dict[str, Any]] = None, variables: Optional[Dict[str, Any]] = None,
workspace_id: Optional[uuid.UUID] = None, workspace_id: Optional[uuid.UUID] = None,
parallel: bool = True, parallel: bool = True,
timeout: int = 60 timeout: int = 60
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""多模型对比试运行 """多模型对比试运行
@@ -1907,17 +1911,17 @@ class AppService:
return result return result
async def draft_run_compare_stream( async def draft_run_compare_stream(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
message: str, message: str,
models: List[app_schema.ModelCompareItem], models: List[app_schema.ModelCompareItem],
conversation_id: Optional[str] = None, conversation_id: Optional[str] = None,
user_id: Optional[str] = None, user_id: Optional[str] = None,
variables: Optional[Dict[str, Any]] = None, variables: Optional[Dict[str, Any]] = None,
workspace_id: Optional[uuid.UUID] = None, workspace_id: Optional[uuid.UUID] = None,
parallel: bool = True, parallel: bool = True,
timeout: int = 60 timeout: int = 60
): ):
"""多模型对比试运行(流式返回) """多模型对比试运行(流式返回)
@@ -1982,15 +1986,15 @@ class AppService:
# 4. 调用 DraftRunService 的流式对比方法 # 4. 调用 DraftRunService 的流式对比方法
draft_service = DraftRunService(self.db) draft_service = DraftRunService(self.db)
async for event in draft_service.run_compare_stream( async for event in draft_service.run_compare_stream(
agent_config=agent_cfg, agent_config=agent_cfg,
models=model_configs, models=model_configs,
message=message, message=message,
workspace_id=workspace_id, workspace_id=workspace_id,
conversation_id=conversation_id, conversation_id=conversation_id,
user_id=user_id, user_id=user_id,
variables=variables, variables=variables,
parallel=parallel, parallel=parallel,
timeout=timeout timeout=timeout
): ):
yield event yield event
@@ -2009,7 +2013,8 @@ def create_app(db: Session, *, user_id: uuid.UUID, workspace_id: uuid.UUID, data
return service.create_app(user_id=user_id, workspace_id=workspace_id, data=data) return service.create_app(user_id=user_id, workspace_id=workspace_id, data=data)
def update_app(db: Session, *, app_id: uuid.UUID, data: app_schema.AppUpdate, workspace_id: uuid.UUID | None = None) -> App: def update_app(db: Session, *, app_id: uuid.UUID, data: app_schema.AppUpdate,
workspace_id: uuid.UUID | None = None) -> App:
"""更新应用(向后兼容接口)""" """更新应用(向后兼容接口)"""
service = AppService(db) service = AppService(db)
return service.update_app(app_id=app_id, data=data, workspace_id=workspace_id) return service.update_app(app_id=app_id, data=data, workspace_id=workspace_id)
@@ -2021,12 +2026,15 @@ def delete_app(db: Session, *, app_id: uuid.UUID, workspace_id: uuid.UUID | None
return service.delete_app(app_id=app_id, workspace_id=workspace_id) return service.delete_app(app_id=app_id, workspace_id=workspace_id)
def update_agent_config(db: Session, *, app_id: uuid.UUID, data: app_schema.AgentConfigUpdate, workspace_id: uuid.UUID | None = None) -> AgentConfig: def update_agent_config(db: Session, *, app_id: uuid.UUID, data: app_schema.AgentConfigUpdate,
workspace_id: uuid.UUID | None = None) -> AgentConfig:
"""更新 Agent 配置(向后兼容接口)""" """更新 Agent 配置(向后兼容接口)"""
service = AppService(db) service = AppService(db)
return service.update_agent_config(app_id=app_id, data=data, workspace_id=workspace_id) return service.update_agent_config(app_id=app_id, data=data, workspace_id=workspace_id)
def update_workflow_config(db: Session, *, app_id: uuid.UUID, data: WorkflowConfigUpdate, workspace_id: uuid.UUID | None = None) -> WorkflowConfig:
def update_workflow_config(db: Session, *, app_id: uuid.UUID, data: WorkflowConfigUpdate,
workspace_id: uuid.UUID | None = None) -> WorkflowConfig:
"""更新 Agent 配置(向后兼容接口)""" """更新 Agent 配置(向后兼容接口)"""
service = AppService(db) service = AppService(db)
return service.update_workflow_config(app_id=app_id, data=data, workspace_id=workspace_id) return service.update_workflow_config(app_id=app_id, data=data, workspace_id=workspace_id)
@@ -2040,6 +2048,7 @@ def get_agent_config(db: Session, *, app_id: uuid.UUID, workspace_id: uuid.UUID
service = AppService(db) service = AppService(db)
return service.get_agent_config(app_id=app_id, workspace_id=workspace_id) return service.get_agent_config(app_id=app_id, workspace_id=workspace_id)
def get_workflow_config(db: Session, *, app_id: uuid.UUID, workspace_id: uuid.UUID | None = None) -> WorkflowConfig: def get_workflow_config(db: Session, *, app_id: uuid.UUID, workspace_id: uuid.UUID | None = None) -> WorkflowConfig:
"""获取 Agent 配置(向后兼容接口) """获取 Agent 配置(向后兼容接口)
@@ -2049,13 +2058,20 @@ def get_workflow_config(db: Session, *, app_id: uuid.UUID, workspace_id: uuid.UU
return service.get_workflow_config(app_id=app_id, workspace_id=workspace_id) return service.get_workflow_config(app_id=app_id, workspace_id=workspace_id)
def publish(db: Session, *, app_id: uuid.UUID, publisher_id: uuid.UUID, workspace_id: uuid.UUID | None = None,version_name:str, release_notes: Optional[str] = None) -> AppRelease: def publish(db: Session, *, app_id: uuid.UUID, publisher_id: uuid.UUID, workspace_id: uuid.UUID | None = None,
version_name: str, release_notes: Optional[str] = None) -> AppRelease:
"""发布应用(向后兼容接口)""" """发布应用(向后兼容接口)"""
service = AppService(db) service = AppService(db)
return service.publish(app_id=app_id, publisher_id=publisher_id,version_name = version_name, workspace_id=workspace_id, release_notes=release_notes) return service.publish(app_id=app_id, publisher_id=publisher_id, version_name=version_name,
workspace_id=workspace_id, release_notes=release_notes)
def get_current_release(db: Session, *, app_id: uuid.UUID, workspace_id: uuid.UUID | None = None) -> Optional[AppRelease]: def get_current_release(
db: Session,
*,
app_id: uuid.UUID,
workspace_id: uuid.UUID | None = None
) -> Optional[AppRelease]:
"""获取当前发布版本(向后兼容接口)""" """获取当前发布版本(向后兼容接口)"""
service = AppService(db) service = AppService(db)
return service.get_current_release(app_id=app_id, workspace_id=workspace_id) return service.get_current_release(app_id=app_id, workspace_id=workspace_id)
@@ -2074,16 +2090,16 @@ def rollback(db: Session, *, app_id: uuid.UUID, version: int, workspace_id: uuid
def list_apps( def list_apps(
db: Session, db: Session,
*, *,
workspace_id: uuid.UUID, workspace_id: uuid.UUID,
type: Optional[str] = None, type: Optional[str] = None,
visibility: Optional[str] = None, visibility: Optional[str] = None,
status: Optional[str] = None, status: Optional[str] = None,
search: Optional[str] = None, search: Optional[str] = None,
include_shared: bool = True, include_shared: bool = True,
page: int = 1, page: int = 1,
pagesize: int = 10, pagesize: int = 10,
) -> Tuple[List[App], int]: ) -> Tuple[List[App], int]:
"""列出应用(向后兼容接口)""" """列出应用(向后兼容接口)"""
service = AppService(db) service = AppService(db)
@@ -2100,9 +2116,9 @@ def list_apps(
def get_apps_by_ids( def get_apps_by_ids(
db: Session, db: Session,
app_ids: List[str], app_ids: List[str],
workspace_id: uuid.UUID workspace_id: uuid.UUID
) -> List[App]: ) -> List[App]:
"""根据ID列表获取应用向后兼容接口""" """根据ID列表获取应用向后兼容接口"""
service = AppService(db) service = AppService(db)
@@ -2112,14 +2128,14 @@ def get_apps_by_ids(
# ==================== 向后兼容的函数接口 ==================== # ==================== 向后兼容的函数接口 ====================
async def draft_run( async def draft_run(
db: Session, db: Session,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
message: str, message: str,
conversation_id: Optional[str] = None, conversation_id: Optional[str] = None,
user_id: Optional[str] = None, user_id: Optional[str] = None,
variables: Optional[Dict[str, Any]] = None, variables: Optional[Dict[str, Any]] = None,
workspace_id: Optional[uuid.UUID] = None workspace_id: Optional[uuid.UUID] = None
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""试运行 Agent向后兼容接口""" """试运行 Agent向后兼容接口"""
service = AppService(db) service = AppService(db)
@@ -2134,30 +2150,28 @@ async def draft_run(
async def draft_run_stream( async def draft_run_stream(
db: Session, db: Session,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
message: str, message: str,
conversation_id: Optional[str] = None, conversation_id: Optional[str] = None,
user_id: Optional[str] = None, user_id: Optional[str] = None,
variables: Optional[Dict[str, Any]] = None, variables: Optional[Dict[str, Any]] = None,
workspace_id: Optional[uuid.UUID] = None workspace_id: Optional[uuid.UUID] = None
): ):
"""试运行 Agent 流式返回(向后兼容接口)""" """试运行 Agent 流式返回(向后兼容接口)"""
service = AppService(db) service = AppService(db)
async for event in service.draft_run_stream( async for event in service.draft_run_stream(
app_id=app_id, app_id=app_id,
message=message, message=message,
conversation_id=conversation_id, conversation_id=conversation_id,
user_id=user_id, user_id=user_id,
variables=variables, variables=variables,
workspace_id=workspace_id workspace_id=workspace_id
): ):
yield event yield event
# ==================== 依赖注入函数 ==================== # ==================== 依赖注入函数 ====================
def get_app_service( def get_app_service(

View File

@@ -4,7 +4,7 @@
import datetime import datetime
import logging import logging
import uuid import uuid
from typing import Any, Annotated, AsyncGenerator from typing import Any, Annotated, AsyncGenerator, Optional
from deprecated import deprecated from deprecated import deprecated
from fastapi import Depends from fastapi import Depends
@@ -14,15 +14,14 @@ from app.core.error_codes import BizCode
from app.core.exceptions import BusinessException from app.core.exceptions import BusinessException
from app.core.workflow.validator import validate_workflow_config from app.core.workflow.validator import validate_workflow_config
from app.db import get_db from app.db import get_db
from app.models.conversation_model import Message
from app.models.workflow_model import WorkflowConfig, WorkflowExecution from app.models.workflow_model import WorkflowConfig, WorkflowExecution
from app.repositories.conversation_repository import MessageRepository
from app.repositories.workflow_repository import ( from app.repositories.workflow_repository import (
WorkflowConfigRepository, WorkflowConfigRepository,
WorkflowExecutionRepository, WorkflowExecutionRepository,
WorkflowNodeExecutionRepository WorkflowNodeExecutionRepository
) )
from app.schemas import DraftRunRequest from app.schemas import DraftRunRequest
from app.services.conversation_service import ConversationService
from app.services.multi_agent_service import convert_uuids_to_str from app.services.multi_agent_service import convert_uuids_to_str
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -36,7 +35,7 @@ class WorkflowService:
self.config_repo = WorkflowConfigRepository(db) self.config_repo = WorkflowConfigRepository(db)
self.execution_repo = WorkflowExecutionRepository(db) self.execution_repo = WorkflowExecutionRepository(db)
self.node_execution_repo = WorkflowNodeExecutionRepository(db) self.node_execution_repo = WorkflowNodeExecutionRepository(db)
self.message_repo = MessageRepository(db) self.conversation_service = ConversationService(db)
# ==================== 配置管理 ==================== # ==================== 配置管理 ====================
@@ -266,6 +265,7 @@ class WorkflowService:
workflow_config_id: uuid.UUID, workflow_config_id: uuid.UUID,
app_id: uuid.UUID, app_id: uuid.UUID,
trigger_type: str, trigger_type: str,
release_id: uuid.UUID | None = None,
triggered_by: uuid.UUID | None = None, triggered_by: uuid.UUID | None = None,
conversation_id: uuid.UUID | None = None, conversation_id: uuid.UUID | None = None,
input_data: dict[str, Any] | None = None input_data: dict[str, Any] | None = None
@@ -273,6 +273,7 @@ class WorkflowService:
"""创建工作流执行记录 """创建工作流执行记录
Args: Args:
release_id: 应用发布 ID
workflow_config_id: 工作流配置 ID workflow_config_id: 工作流配置 ID
app_id: 应用 ID app_id: 应用 ID
trigger_type: 触发类型 trigger_type: 触发类型
@@ -289,6 +290,7 @@ class WorkflowService:
execution = WorkflowExecution( execution = WorkflowExecution(
workflow_config_id=workflow_config_id, workflow_config_id=workflow_config_id,
app_id=app_id, app_id=app_id,
release_id=release_id,
conversation_id=conversation_id, conversation_id=conversation_id,
execution_id=execution_id, execution_id=execution_id,
trigger_type=trigger_type, trigger_type=trigger_type,
@@ -337,6 +339,7 @@ class WorkflowService:
self, self,
execution_id: str, execution_id: str,
status: str, status: str,
token_usage: int | None = None,
output_data: dict[str, Any] | None = None, output_data: dict[str, Any] | None = None,
error_message: str | None = None, error_message: str | None = None,
error_node_id: str | None = None error_node_id: str | None = None
@@ -346,6 +349,7 @@ class WorkflowService:
Args: Args:
execution_id: 执行 ID execution_id: 执行 ID
status: 状态 status: 状态
token_usage: token消耗
output_data: 输出数据 output_data: 输出数据
error_message: 错误信息 error_message: 错误信息
error_node_id: 出错节点 ID error_node_id: 出错节点 ID
@@ -364,6 +368,8 @@ class WorkflowService:
) )
execution.status = status execution.status = status
if token_usage is not None:
execution.token_usage = token_usage
if output_data is not None: if output_data is not None:
execution.output_data = convert_uuids_to_str(output_data) execution.output_data = convert_uuids_to_str(output_data)
if error_message is not None: if error_message is not None:
@@ -414,12 +420,14 @@ class WorkflowService:
payload: DraftRunRequest, payload: DraftRunRequest,
config: WorkflowConfig, config: WorkflowConfig,
workspace_id: uuid.UUID, workspace_id: uuid.UUID,
release_id: uuid.UUID | None = None,
): ):
"""运行工作流 """运行工作流
Args: Args:
workspace_id: release_id: 发布 ID
config: workspace_id:工作空间 ID
config: 配置
payload: payload:
app_id: 应用 ID app_id: 应用 ID
@@ -463,7 +471,8 @@ class WorkflowService:
trigger_type="manual", trigger_type="manual",
triggered_by=None, triggered_by=None,
conversation_id=conversation_id_uuid, conversation_id=conversation_id_uuid,
input_data=input_data input_data=input_data,
release_id=release_id,
) )
# 3. 构建工作流配置字典 # 3. 构建工作流配置字典
@@ -507,20 +516,20 @@ class WorkflowService:
# 更新执行结果 # 更新执行结果
if result.get("status") == "completed": if result.get("status") == "completed":
token_usage = result.get("token_usage", {}) or {}
self.update_execution_status( self.update_execution_status(
execution.execution_id, execution.execution_id,
"completed", "completed",
output_data=result output_data=result,
token_usage=token_usage.get("total_tokens", None)
) )
final_messages = result.get("messages", [])[init_message_length:] final_messages = result.get("messages", [])[init_message_length:]
for message in final_messages: for message in final_messages:
message_obj = Message( self.conversation_service.add_message(
conversation_id=conversation_id_uuid, conversation_id=conversation_id_uuid,
role=message["role"], role=message["role"],
content=message["content"], content=message["content"]
) )
self.message_repo.add_message(message_obj)
self.db.commit()
logger.info(f"Workflow Run Success, " logger.info(f"Workflow Run Success, "
f"execution_id: {execution.execution_id}, message count: {len(final_messages)}") f"execution_id: {execution.execution_id}, message count: {len(final_messages)}")
else: else:
@@ -562,10 +571,12 @@ class WorkflowService:
payload: DraftRunRequest, payload: DraftRunRequest,
config: WorkflowConfig, config: WorkflowConfig,
workspace_id: uuid.UUID, workspace_id: uuid.UUID,
release_id: Optional[uuid.UUID] = None,
): ):
"""运行工作流(流式) """运行工作流(流式)
Args: Args:
release_id: 发布id
workspace_id: workspace_id:
app_id: 应用 ID app_id: 应用 ID
payload: 请求对象(包含 message, variables, conversation_id 等) payload: 请求对象(包含 message, variables, conversation_id 等)
@@ -611,7 +622,8 @@ class WorkflowService:
trigger_type="manual", trigger_type="manual",
triggered_by=None, triggered_by=None,
conversation_id=conversation_id_uuid, conversation_id=conversation_id_uuid,
input_data=input_data input_data=input_data,
release_id=release_id,
) )
# 3. 构建工作流配置字典 # 3. 构建工作流配置字典
@@ -653,21 +665,21 @@ class WorkflowService:
if event.get("event") == "workflow_end": if event.get("event") == "workflow_end":
status = event.get("data", {}).get("status") status = event.get("data", {}).get("status")
token_usage = event.get("data", {}).get("token_usage", {}) or {}
if status == "completed": if status == "completed":
self.update_execution_status( self.update_execution_status(
execution.execution_id, execution.execution_id,
"completed", "completed",
output_data=event.get("data") output_data=event.get("data"),
token_usage=token_usage.get("total_tokens", None)
) )
final_messages = event.get("data", {}).get("messages", [])[init_message_length:] final_messages = event.get("data", {}).get("messages", [])[init_message_length:]
for message in final_messages: for message in final_messages:
message_obj = Message( self.conversation_service.add_message(
conversation_id=conversation_id_uuid, conversation_id=conversation_id_uuid,
role=message["role"], role=message["role"],
content=message["content"], content=message["content"]
) )
self.message_repo.add_message(message_obj)
self.db.commit()
logger.info(f"Workflow Run Success, " logger.info(f"Workflow Run Success, "
f"execution_id: {execution.execution_id}, message count: {len(final_messages)}") f"execution_id: {execution.execution_id}, message count: {len(final_messages)}")
elif status == "failed": elif status == "failed":
@@ -784,10 +796,12 @@ class WorkflowService:
# 更新执行结果 # 更新执行结果
if result.get("status") == "completed": if result.get("status") == "completed":
token_usage = result.get("data").get("token_usage", {}) or {}
self.update_execution_status( self.update_execution_status(
execution.execution_id, execution.execution_id,
"completed", "completed",
output_data=result.get("node_outputs", {}) output_data=result.get("node_outputs", {}),
token_usage=token_usage.get("total_tokens", None)
) )
else: else:
self.update_execution_status( self.update_execution_status(
@@ -882,13 +896,14 @@ class WorkflowService:
): ):
# 直接转发事件executor 已经返回正确格式) # 直接转发事件executor 已经返回正确格式)
if event.get("event") == "workflow_end": if event.get("event") == "workflow_end":
token_usage = event.get("data").get("token_usage", {}) or {}
status = event.get("data", {}).get("status") status = event.get("data", {}).get("status")
if status == "completed": if status == "completed":
self.update_execution_status( self.update_execution_status(
execution_id, execution_id,
"completed", "completed",
output_data=event.get("data") output_data=event.get("data"),
token_usage=token_usage.get("total_tokens", None)
) )
elif status == "failed": elif status == "failed":
self.update_execution_status( self.update_execution_status(

View File

@@ -53,7 +53,7 @@ nodes:
type: end type: end
name: 结束 name: 结束
config: config:
output: "{{ llm_qa.output }}" output: "{{llm_qa.output}}"
position: position:
x: 900 x: 900
y: 100 y: 100

View File

@@ -120,12 +120,9 @@ def multi_agent_config_4_app_release(release: AppRelease) -> MultiAgentConfig:
def workflow_config_4_app_release(release: AppRelease) -> WorkflowConfig: def workflow_config_4_app_release(release: AppRelease) -> WorkflowConfig:
config_dict = release.config config_dict = release.config
with get_db_read() as db:
source_config = WorkflowConfigRepository(db).get_by_app_id(release.app_id)
source_config_id = source_config.id
config = WorkflowConfig( config = WorkflowConfig(
id=source_config_id, id=config_dict.get("id"),
app_id=release.app_id, app_id=release.app_id,
nodes=config_dict.get("nodes", []), nodes=config_dict.get("nodes", []),
edges=config_dict.get("edges", []), edges=config_dict.get("edges", []),