feat(workflow): add per-agent vision configuration

This commit is contained in:
Xin Wang
2026-07-18 00:00:28 +08:00
parent bdf3d3dd9c
commit 28c6380d8a
23 changed files with 553 additions and 34 deletions

View File

@@ -16,6 +16,7 @@ from schemas import AssistantOut, AssistantUpsert
from services.auth import require_admin
from services.masking import mask, resolve_incoming_key
from services.node_specs import graph_references, normalize_graph, validate_graph
from services.workflow_engine import WorkflowEngine
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -35,6 +36,10 @@ def _validate_workflow(body: AssistantUpsert) -> None:
errors = validate_graph(body.graph)
if errors:
raise HTTPException(400, "工作流校验失败:" + ";".join(errors))
# Graph settings are the source of truth. The flat flag is only a session
# capability summary used by clients to decide whether to open the camera.
body.vision_enabled = WorkflowEngine(body.graph).uses_vision()
body.vision_model_resource_id = None
refs = graph_references(body.graph)
body.tool_ids = list(dict.fromkeys([*body.tool_ids, *sorted(refs["tools"])]))
@@ -45,8 +50,10 @@ async def _validate_workflow_references(
if body.type != "workflow" or not body.graph.get("nodes"):
return
graph = body.graph
engine = WorkflowEngine(graph)
settings = graph.get("settings") or {}
resource_expectations: dict[str, str] = {}
vision_resource_ids: set[str] = set()
for key, capability in (
("defaultLlmResourceId", "LLM"),
("defaultAsrResourceId", "ASR"),
@@ -71,10 +78,32 @@ async def _validate_workflow_references(
resource_expectations[str(data["ttsResourceId"])] = "TTS"
if data.get("knowledgeBaseId"):
knowledge_ids.add(str(data["knowledgeBaseId"]))
for node_id, node in engine.nodes.items():
if node.get("type") != "agent":
continue
stage = engine.agent_stage_config(node_id)
if not stage.vision_enabled:
continue
resource_id = (
stage.vision_model_resource_id or stage.llm_resource_id
)
if not resource_id:
raise HTTPException(
400,
f"Agent 节点 {node_id} 开启视觉理解时必须选择"
"支持图片输入的大语言模型或视觉模型",
)
resource_expectations[resource_id] = "LLM"
vision_resource_ids.add(resource_id)
for resource_id, capability in resource_expectations.items():
resource = await session.get(ModelResource, resource_id)
if not resource or not resource.enabled or resource.capability != capability:
raise HTTPException(400, f"Workflow 引用了无效的 {capability} 资源:{resource_id}")
if resource_id in vision_resource_ids and not resource.support_image_input:
raise HTTPException(
400,
f"Workflow 视觉模型必须支持图片输入:{resource_id}",
)
for knowledge_id in knowledge_ids:
knowledge = await session.get(KnowledgeBase, knowledge_id)
if not knowledge or knowledge.status != "active":
@@ -84,6 +113,9 @@ async def _validate_workflow_references(
async def _validate_vision_model(
session: AsyncSession, body: AssistantUpsert
) -> None:
# Workflow 的视觉模型属于图中的全局/Agent 配置,由上面的图引用校验处理。
if body.type == "workflow":
return
if body.vision_enabled:
if body.vision_model_resource_id:
resource = await session.get(ModelResource, body.vision_model_resource_id)

View File

@@ -163,8 +163,13 @@ async def _handle_offer_payload(payload, peers):
else:
cfg = await _resolve_config(offer) # 解析放在建连前,配置错就别建连
# 服务端助手配置是视觉理解的唯一授权来源;客户端 offer 只负责携带媒体轨。
vision_enabled = cfg.vision_enabled
if vision_enabled:
if cfg.type == "workflow":
from services.workflow_engine import WorkflowEngine
vision_enabled = WorkflowEngine(cfg.graph).uses_vision()
else:
vision_enabled = cfg.vision_enabled
if vision_enabled and cfg.type != "workflow":
has_native_vision = (
not cfg.vision_model_resource_id and cfg.llm_support_image_input
)