feat: add realtime workflow vision tools
This commit is contained in:
@@ -85,6 +85,7 @@ async def _validate_workflow_references(
|
|||||||
graph = body.graph
|
graph = body.graph
|
||||||
engine = WorkflowEngine(graph)
|
engine = WorkflowEngine(graph)
|
||||||
settings = graph.get("settings") or {}
|
settings = graph.get("settings") or {}
|
||||||
|
runtime_mode = str(settings.get("runtimeMode") or "pipeline")
|
||||||
resource_expectations: dict[str, str] = {}
|
resource_expectations: dict[str, str] = {}
|
||||||
vision_resource_ids: set[str] = set()
|
vision_resource_ids: set[str] = set()
|
||||||
for key, capability in (
|
for key, capability in (
|
||||||
@@ -118,16 +119,22 @@ async def _validate_workflow_references(
|
|||||||
stage = engine.agent_stage_config(node_id)
|
stage = engine.agent_stage_config(node_id)
|
||||||
if not stage.vision_enabled:
|
if not stage.vision_enabled:
|
||||||
continue
|
continue
|
||||||
resource_id = (
|
if stage.vision_model_resource_id:
|
||||||
stage.vision_model_resource_id or stage.llm_resource_id
|
resource_id = stage.vision_model_resource_id
|
||||||
)
|
capability = "LLM"
|
||||||
|
elif runtime_mode == "realtime":
|
||||||
|
resource_id = str(settings.get("defaultRealtimeResourceId") or "")
|
||||||
|
capability = "Realtime"
|
||||||
|
else:
|
||||||
|
resource_id = stage.llm_resource_id
|
||||||
|
capability = "LLM"
|
||||||
if not resource_id:
|
if not resource_id:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
400,
|
400,
|
||||||
f"Agent 节点 {node_id} 开启视觉理解时必须选择"
|
f"Agent 节点 {node_id} 开启视觉理解时必须选择"
|
||||||
"支持图片输入的大语言模型或视觉模型",
|
"支持图片输入的当前模型或独立视觉模型",
|
||||||
)
|
)
|
||||||
resource_expectations[resource_id] = "LLM"
|
resource_expectations[resource_id] = capability
|
||||||
vision_resource_ids.add(resource_id)
|
vision_resource_ids.add(resource_id)
|
||||||
for resource_id, capability in resource_expectations.items():
|
for resource_id, capability in resource_expectations.items():
|
||||||
resource = await session.get(ModelResource, resource_id)
|
resource = await session.get(ModelResource, resource_id)
|
||||||
|
|||||||
@@ -30,6 +30,19 @@ router = APIRouter(
|
|||||||
tags=["model-registry"],
|
tags=["model-registry"],
|
||||||
dependencies=[Depends(require_admin)],
|
dependencies=[Depends(require_admin)],
|
||||||
)
|
)
|
||||||
|
NATIVE_IMAGE_UNSUPPORTED_REALTIME_INTERFACES = frozenset(
|
||||||
|
{"qwen-audio-realtime"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _supports_configurable_image_input(definition: InterfaceDefinition) -> bool:
|
||||||
|
if definition.capability == "LLM":
|
||||||
|
return True
|
||||||
|
return (
|
||||||
|
definition.capability == "Realtime"
|
||||||
|
and definition.interface_type
|
||||||
|
not in NATIVE_IMAGE_UNSUPPORTED_REALTIME_INTERFACES
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _definition_dict(row: InterfaceDefinition) -> dict:
|
def _definition_dict(row: InterfaceDefinition) -> dict:
|
||||||
@@ -156,9 +169,11 @@ async def create_model_resource(
|
|||||||
interface_type=definition.interface_type,
|
interface_type=definition.interface_type,
|
||||||
values=body.values,
|
values=body.values,
|
||||||
secrets=secrets,
|
secrets=secrets,
|
||||||
support_image_input=body.support_image_input
|
support_image_input=(
|
||||||
if definition.capability == "LLM"
|
body.support_image_input
|
||||||
else False,
|
if _supports_configurable_image_input(definition)
|
||||||
|
else False
|
||||||
|
),
|
||||||
enabled=body.enabled,
|
enabled=body.enabled,
|
||||||
is_default=body.is_default,
|
is_default=body.is_default,
|
||||||
)
|
)
|
||||||
@@ -244,7 +259,9 @@ async def update_model_resource(
|
|||||||
row.values = body.values
|
row.values = body.values
|
||||||
row.secrets = secrets
|
row.secrets = secrets
|
||||||
row.support_image_input = (
|
row.support_image_input = (
|
||||||
body.support_image_input if definition.capability == "LLM" else False
|
body.support_image_input
|
||||||
|
if _supports_configurable_image_input(definition)
|
||||||
|
else False
|
||||||
)
|
)
|
||||||
row.enabled = body.enabled
|
row.enabled = body.enabled
|
||||||
row.is_default = body.is_default
|
row.is_default = body.is_default
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ class RealtimeBrainRuntime:
|
|||||||
session_id: str = ""
|
session_id: str = ""
|
||||||
client_tools: ClientToolPort | None = None
|
client_tools: ClientToolPort | None = None
|
||||||
set_input_enabled: Callable[[bool], None] | None = None
|
set_input_enabled: Callable[[bool], None] | None = None
|
||||||
|
capture_image: Callable[[str], Awaitable[Any | None]] | None = None
|
||||||
|
|
||||||
|
|
||||||
class BaseBrain:
|
class BaseBrain:
|
||||||
|
|||||||
@@ -438,8 +438,6 @@ def validate_graph(graph: dict[str, Any]) -> list[str]:
|
|||||||
"defaultRealtimeResourceId"
|
"defaultRealtimeResourceId"
|
||||||
):
|
):
|
||||||
errors.append("Realtime 工作流必须选择 Realtime 模型")
|
errors.append("Realtime 工作流必须选择 Realtime 模型")
|
||||||
if runtime_mode == "realtime" and settings.get("visionEnabled"):
|
|
||||||
errors.append("Realtime 工作流暂不支持视觉能力")
|
|
||||||
if (
|
if (
|
||||||
runtime_mode == "realtime"
|
runtime_mode == "realtime"
|
||||||
and settings.get("knowledgeBaseId")
|
and settings.get("knowledgeBaseId")
|
||||||
@@ -488,10 +486,6 @@ def validate_graph(graph: dict[str, Any]) -> list[str]:
|
|||||||
f"Realtime Agent 节点 {node_id} 不支持节点级模型、语音或交互策略覆盖"
|
f"Realtime Agent 节点 {node_id} 不支持节点级模型、语音或交互策略覆盖"
|
||||||
)
|
)
|
||||||
source = settings if data.get("inheritGlobalConfig", True) else data
|
source = settings if data.get("inheritGlobalConfig", True) else data
|
||||||
if source.get("visionEnabled"):
|
|
||||||
errors.append(
|
|
||||||
f"Realtime Agent 节点 {node_id} 暂不支持视觉能力"
|
|
||||||
)
|
|
||||||
if (
|
if (
|
||||||
source.get("knowledgeBaseId")
|
source.get("knowledgeBaseId")
|
||||||
and source.get("knowledgeMode") != "on_demand"
|
and source.get("knowledgeMode") != "on_demand"
|
||||||
|
|||||||
@@ -7,14 +7,10 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import base64
|
|
||||||
from io import BytesIO
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from models import AssistantConfig
|
from models import AssistantConfig
|
||||||
from openai import AsyncOpenAI
|
|
||||||
from PIL import Image
|
|
||||||
from services.brains import Brain, BrainRuntime, build_brain
|
from services.brains import Brain, BrainRuntime, build_brain
|
||||||
from services.brains.base import RealtimeBrainRuntime
|
from services.brains.base import RealtimeBrainRuntime
|
||||||
from services.conversation_history import ConversationRecorder
|
from services.conversation_history import ConversationRecorder
|
||||||
@@ -34,11 +30,12 @@ from services.knowledge import search as search_knowledge
|
|||||||
from services.client_tools import ClientToolBroker
|
from services.client_tools import ClientToolBroker
|
||||||
from services.tool_policy import policy_for_tool
|
from services.tool_policy import policy_for_tool
|
||||||
from services.vision import (
|
from services.vision import (
|
||||||
VISION_ANALYSIS_SYSTEM_PROMPT,
|
|
||||||
VISION_SYSTEM_HINT,
|
VISION_SYSTEM_HINT,
|
||||||
VISION_TOOL_NAME,
|
VISION_TOOL_NAME,
|
||||||
|
analyze_image_with_vision_model,
|
||||||
config_with_main_llm_as_vision,
|
config_with_main_llm_as_vision,
|
||||||
config_with_vision_resource,
|
config_with_vision_resource,
|
||||||
|
image_data_uri,
|
||||||
)
|
)
|
||||||
from services.workflow_engine import WorkflowEngine
|
from services.workflow_engine import WorkflowEngine
|
||||||
|
|
||||||
@@ -164,19 +161,6 @@ def _workflow_vision_uses_main_llm(
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _image_data_uri(frame: UserImageRawFrame) -> str:
|
|
||||||
if not frame.format:
|
|
||||||
raise ValueError("摄像头图片帧缺少 format,无法编码给视觉模型")
|
|
||||||
buffer = BytesIO()
|
|
||||||
Image.frombytes(frame.format, frame.size, frame.image).save(
|
|
||||||
buffer,
|
|
||||||
format="JPEG",
|
|
||||||
quality=85,
|
|
||||||
)
|
|
||||||
encoded = base64.b64encode(buffer.getvalue()).decode("utf-8")
|
|
||||||
return f"data:image/jpeg;base64,{encoded}"
|
|
||||||
|
|
||||||
|
|
||||||
def _multimodal_user_input_frame(
|
def _multimodal_user_input_frame(
|
||||||
image_frame: UserImageRawFrame,
|
image_frame: UserImageRawFrame,
|
||||||
prompt_text: str,
|
prompt_text: str,
|
||||||
@@ -197,7 +181,7 @@ def _multimodal_user_input_frame(
|
|||||||
{"type": "text", "text": prompt_text},
|
{"type": "text", "text": prompt_text},
|
||||||
{
|
{
|
||||||
"type": "image_url",
|
"type": "image_url",
|
||||||
"image_url": {"url": _image_data_uri(image_frame)},
|
"image_url": {"url": image_data_uri(image_frame)},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -206,45 +190,6 @@ def _multimodal_user_input_frame(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _analyze_image_with_vision_model(
|
|
||||||
cfg: AssistantConfig,
|
|
||||||
frame: UserImageRawFrame,
|
|
||||||
question: str,
|
|
||||||
) -> str:
|
|
||||||
if cfg.vision_llm_interface_type not in {"openai-llm", "dashscope-llm"}:
|
|
||||||
raise ValueError(f"不支持的视觉 LLM 接口类型: {cfg.vision_llm_interface_type}")
|
|
||||||
|
|
||||||
data_uri = await asyncio.to_thread(_image_data_uri, frame)
|
|
||||||
extra_body = cfg.vision_llm_values.get("extraBody")
|
|
||||||
extra = {"extra_body": extra_body} if isinstance(extra_body, dict) else {}
|
|
||||||
client = AsyncOpenAI(
|
|
||||||
api_key=_require(cfg.vision_llm_api_key, "Vision LLM apiKey"),
|
|
||||||
base_url=_require(cfg.vision_llm_base_url, "Vision LLM apiUrl"),
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
response = await client.chat.completions.create(
|
|
||||||
model=_require(cfg.vision_model, "Vision LLM modelId"),
|
|
||||||
messages=[
|
|
||||||
{"role": "system", "content": VISION_ANALYSIS_SYSTEM_PROMPT},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": [
|
|
||||||
{"type": "text", "text": question},
|
|
||||||
{"type": "image_url", "image_url": {"url": data_uri}},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
**extra,
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
await client.close()
|
|
||||||
|
|
||||||
content = response.choices[0].message.content if response.choices else ""
|
|
||||||
if isinstance(content, str):
|
|
||||||
return content.strip()
|
|
||||||
return str(content or "").strip()
|
|
||||||
|
|
||||||
|
|
||||||
async def run_pipeline(
|
async def run_pipeline(
|
||||||
transport,
|
transport,
|
||||||
cfg: AssistantConfig,
|
cfg: AssistantConfig,
|
||||||
@@ -278,12 +223,11 @@ async def run_pipeline(
|
|||||||
raise ValueError(f"类型 {cfg.type} 不支持 realtime 运行模式")
|
raise ValueError(f"类型 {cfg.type} 不支持 realtime 运行模式")
|
||||||
|
|
||||||
if cfg.runtimeMode == "realtime":
|
if cfg.runtimeMode == "realtime":
|
||||||
if vision_enabled:
|
|
||||||
logger.warning("Realtime 模式暂未接入视频帧工具,本次仅启用语音通话")
|
|
||||||
await run_realtime_pipeline(
|
await run_realtime_pipeline(
|
||||||
transport,
|
transport,
|
||||||
cfg,
|
cfg,
|
||||||
brain=brain,
|
brain=brain,
|
||||||
|
vision_enabled=vision_enabled,
|
||||||
assistant_id=assistant_id,
|
assistant_id=assistant_id,
|
||||||
channel=channel,
|
channel=channel,
|
||||||
)
|
)
|
||||||
@@ -524,7 +468,7 @@ async def run_pipeline(
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
frame = await vision_capture.request_image(params.llm, request)
|
frame = await vision_capture.request_image(params.llm, request)
|
||||||
observation = await _analyze_image_with_vision_model(cfg, frame, question)
|
observation = await analyze_image_with_vision_model(cfg, frame, question)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
await params.result_callback(
|
await params.result_callback(
|
||||||
{
|
{
|
||||||
@@ -614,7 +558,7 @@ async def run_pipeline(
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
frame = await vision_capture.request_image(llm, request)
|
frame = await vision_capture.request_image(llm, request)
|
||||||
observation = await _analyze_image_with_vision_model(
|
observation = await analyze_image_with_vision_model(
|
||||||
active_vision_config(),
|
active_vision_config(),
|
||||||
frame,
|
frame,
|
||||||
question,
|
question,
|
||||||
@@ -830,7 +774,7 @@ async def run_pipeline(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
assert analysis_cfg is not None
|
assert analysis_cfg is not None
|
||||||
observation = await _analyze_image_with_vision_model(
|
observation = await analyze_image_with_vision_model(
|
||||||
analysis_cfg,
|
analysis_cfg,
|
||||||
image_frame,
|
image_frame,
|
||||||
value.prompt_text,
|
value.prompt_text,
|
||||||
@@ -883,6 +827,7 @@ async def run_realtime_pipeline(
|
|||||||
cfg: AssistantConfig,
|
cfg: AssistantConfig,
|
||||||
*,
|
*,
|
||||||
brain: Brain,
|
brain: Brain,
|
||||||
|
vision_enabled: bool = False,
|
||||||
assistant_id: str | None = None,
|
assistant_id: str | None = None,
|
||||||
channel: str = "webrtc",
|
channel: str = "webrtc",
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -894,6 +839,8 @@ async def run_realtime_pipeline(
|
|||||||
input_sample_rate, output_sample_rate = realtime_audio_sample_rates(cfg)
|
input_sample_rate, output_sample_rate = realtime_audio_sample_rates(cfg)
|
||||||
worker_holder: dict[str, PipelineWorker] = {}
|
worker_holder: dict[str, PipelineWorker] = {}
|
||||||
input_state = {"enabled": True}
|
input_state = {"enabled": True}
|
||||||
|
vision_state: dict[str, str | None] = {"client_id": None}
|
||||||
|
vision_capture = VisionCaptureProcessor()
|
||||||
|
|
||||||
async def queue_call_end(reason: str) -> None:
|
async def queue_call_end(reason: str) -> None:
|
||||||
worker = worker_holder.get("worker")
|
worker = worker_holder.get("worker")
|
||||||
@@ -946,6 +893,18 @@ async def run_realtime_pipeline(
|
|||||||
)
|
)
|
||||||
greeting = await brain.greeting(cfg)
|
greeting = await brain.greeting(cfg)
|
||||||
|
|
||||||
|
async def capture_image(question: str):
|
||||||
|
user_id = vision_state["client_id"]
|
||||||
|
if not user_id:
|
||||||
|
return None
|
||||||
|
request = UserImageRequestFrame(
|
||||||
|
user_id=user_id,
|
||||||
|
text=question,
|
||||||
|
append_to_context=False,
|
||||||
|
function_name=VISION_TOOL_NAME,
|
||||||
|
)
|
||||||
|
return await vision_capture.request_image(realtime, request)
|
||||||
|
|
||||||
recorder = await ConversationRecorder.start(
|
recorder = await ConversationRecorder.start(
|
||||||
assistant_id=assistant_id,
|
assistant_id=assistant_id,
|
||||||
assistant_name=cfg.name,
|
assistant_name=cfg.name,
|
||||||
@@ -961,6 +920,7 @@ async def run_realtime_pipeline(
|
|||||||
pipeline = Pipeline(
|
pipeline = Pipeline(
|
||||||
[
|
[
|
||||||
transport.input(),
|
transport.input(),
|
||||||
|
vision_capture,
|
||||||
client_tools,
|
client_tools,
|
||||||
session_update,
|
session_update,
|
||||||
user_input,
|
user_input,
|
||||||
@@ -995,6 +955,7 @@ async def run_realtime_pipeline(
|
|||||||
session_id=cfg.conversation_id or "",
|
session_id=cfg.conversation_id or "",
|
||||||
client_tools=client_tools,
|
client_tools=client_tools,
|
||||||
set_input_enabled=set_input_enabled,
|
set_input_enabled=set_input_enabled,
|
||||||
|
capture_image=capture_image if vision_enabled else None,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1005,6 +966,8 @@ async def run_realtime_pipeline(
|
|||||||
brain=brain,
|
brain=brain,
|
||||||
text_input=user_input,
|
text_input=user_input,
|
||||||
greeting=greeting,
|
greeting=greeting,
|
||||||
|
vision_enabled=vision_enabled,
|
||||||
|
vision_state=vision_state,
|
||||||
)
|
)
|
||||||
runner = WorkerRunner(handle_sigint=False)
|
runner = WorkerRunner(handle_sigint=False)
|
||||||
run_status = "completed"
|
run_status = "completed"
|
||||||
|
|||||||
@@ -230,6 +230,8 @@ def bind_realtime_pipeline_events(
|
|||||||
brain,
|
brain,
|
||||||
text_input,
|
text_input,
|
||||||
greeting: str,
|
greeting: str,
|
||||||
|
vision_enabled: bool,
|
||||||
|
vision_state: dict[str, str | None],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Connect text and lifecycle events for a realtime model pipeline."""
|
"""Connect text and lifecycle events for a realtime model pipeline."""
|
||||||
|
|
||||||
@@ -274,6 +276,19 @@ def bind_realtime_pipeline_events(
|
|||||||
|
|
||||||
@transport.event_handler("on_client_connected")
|
@transport.event_handler("on_client_connected")
|
||||||
async def on_client_connected(_transport, _client):
|
async def on_client_connected(_transport, _client):
|
||||||
|
if vision_enabled:
|
||||||
|
try:
|
||||||
|
vision_state["client_id"] = get_transport_client_id(
|
||||||
|
_transport,
|
||||||
|
_client,
|
||||||
|
)
|
||||||
|
await maybe_capture_participant_camera(_transport, _client)
|
||||||
|
logger.info(
|
||||||
|
f"Realtime 视觉理解已接入视频客户端: "
|
||||||
|
f"{vision_state['client_id']}"
|
||||||
|
)
|
||||||
|
except Exception as exc: # noqa: BLE001 - video remains optional
|
||||||
|
logger.warning(f"Realtime 视觉理解摄像头捕获初始化失败: {exc}")
|
||||||
await brain.on_connected(greeting_pending=bool(greeting))
|
await brain.on_connected(greeting_pending=bool(greeting))
|
||||||
await brain.on_client_ready()
|
await brain.on_client_ready()
|
||||||
if greeting:
|
if greeting:
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
"""Shared vision capability metadata and Workflow model resolution."""
|
"""Shared camera-frame analysis for Pipeline and Realtime runtimes."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import base64
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
from models import AssistantConfig, RuntimeModelResource
|
from models import AssistantConfig, RuntimeModelResource
|
||||||
|
from openai import AsyncOpenAI
|
||||||
|
from PIL import Image
|
||||||
|
from pipecat.frames.frames import UserImageRawFrame
|
||||||
|
|
||||||
|
|
||||||
VISION_TOOL_NAME = "fetch_user_image"
|
VISION_TOOL_NAME = "fetch_user_image"
|
||||||
@@ -17,6 +24,68 @@ VISION_ANALYSIS_SYSTEM_PROMPT = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _require(value: str, label: str) -> str:
|
||||||
|
if value:
|
||||||
|
return value
|
||||||
|
raise ValueError(f"缺少模型资源配置: {label}")
|
||||||
|
|
||||||
|
|
||||||
|
def image_data_uri(frame: UserImageRawFrame) -> str:
|
||||||
|
"""Encode one Pipecat camera frame for a vision chat-completion request."""
|
||||||
|
if not frame.format:
|
||||||
|
raise ValueError("摄像头图片帧缺少 format,无法编码给视觉模型")
|
||||||
|
buffer = BytesIO()
|
||||||
|
Image.frombytes(frame.format, frame.size, frame.image).save(
|
||||||
|
buffer,
|
||||||
|
format="JPEG",
|
||||||
|
quality=85,
|
||||||
|
)
|
||||||
|
encoded = base64.b64encode(buffer.getvalue()).decode("utf-8")
|
||||||
|
return f"data:image/jpeg;base64,{encoded}"
|
||||||
|
|
||||||
|
|
||||||
|
async def analyze_image_with_vision_model(
|
||||||
|
cfg: AssistantConfig,
|
||||||
|
frame: UserImageRawFrame,
|
||||||
|
question: str,
|
||||||
|
) -> str:
|
||||||
|
"""Analyze one frame with the configured independent vision LLM."""
|
||||||
|
if cfg.vision_llm_interface_type not in {"openai-llm", "dashscope-llm"}:
|
||||||
|
raise ValueError(
|
||||||
|
f"不支持的视觉 LLM 接口类型: {cfg.vision_llm_interface_type}"
|
||||||
|
)
|
||||||
|
|
||||||
|
data_uri = await asyncio.to_thread(image_data_uri, frame)
|
||||||
|
extra_body = cfg.vision_llm_values.get("extraBody")
|
||||||
|
extra = {"extra_body": extra_body} if isinstance(extra_body, dict) else {}
|
||||||
|
client = AsyncOpenAI(
|
||||||
|
api_key=_require(cfg.vision_llm_api_key, "Vision LLM apiKey"),
|
||||||
|
base_url=_require(cfg.vision_llm_base_url, "Vision LLM apiUrl"),
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
response = await client.chat.completions.create(
|
||||||
|
model=_require(cfg.vision_model, "Vision LLM modelId"),
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": VISION_ANALYSIS_SYSTEM_PROMPT},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{"type": "text", "text": question},
|
||||||
|
{"type": "image_url", "image_url": {"url": data_uri}},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
**extra,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
await client.close()
|
||||||
|
|
||||||
|
content = response.choices[0].message.content if response.choices else ""
|
||||||
|
if isinstance(content, str):
|
||||||
|
return content.strip()
|
||||||
|
return str(content or "").strip()
|
||||||
|
|
||||||
|
|
||||||
def config_with_vision_resource(
|
def config_with_vision_resource(
|
||||||
cfg: AssistantConfig,
|
cfg: AssistantConfig,
|
||||||
resource: RuntimeModelResource,
|
resource: RuntimeModelResource,
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ from services.runtime_variables import DynamicVariableError, DynamicVariableStor
|
|||||||
from services.system_tools import state_update_properties, system_tool_kind
|
from services.system_tools import state_update_properties, system_tool_kind
|
||||||
from services.tool_executor import ToolExecutionError, ToolExecutor
|
from services.tool_executor import ToolExecutionError, ToolExecutor
|
||||||
from services.tool_policy import policy_for_tool
|
from services.tool_policy import policy_for_tool
|
||||||
|
from services.vision import (
|
||||||
|
VISION_SYSTEM_HINT,
|
||||||
|
VISION_TOOL_NAME,
|
||||||
|
analyze_image_with_vision_model,
|
||||||
|
config_with_vision_resource,
|
||||||
|
)
|
||||||
from services.workflow.agent import EDGE_TOOL_STAGE_INSTRUCTION
|
from services.workflow.agent import EDGE_TOOL_STAGE_INSTRUCTION
|
||||||
from services.workflow.models import WorkflowRuntimeState, WorkflowStatus
|
from services.workflow.models import WorkflowRuntimeState, WorkflowStatus
|
||||||
from services.workflow.output import WorkflowOutput
|
from services.workflow.output import WorkflowOutput
|
||||||
@@ -385,10 +391,13 @@ class WorkflowRealtimeController:
|
|||||||
return RealtimeActivation(continue_response=generate)
|
return RealtimeActivation(continue_response=generate)
|
||||||
|
|
||||||
def _agent_prompt(self, node_id: str) -> str:
|
def _agent_prompt(self, node_id: str) -> str:
|
||||||
return (
|
prompt = (
|
||||||
f"{self._engine.prompt_for(node_id, self._store)}\n\n"
|
f"{self._engine.prompt_for(node_id, self._store)}\n\n"
|
||||||
f"[工作流执行规则]\n{EDGE_TOOL_STAGE_INSTRUCTION}"
|
f"[工作流执行规则]\n{EDGE_TOOL_STAGE_INSTRUCTION}"
|
||||||
)
|
)
|
||||||
|
if self._engine.agent_stage_config(node_id).vision_enabled:
|
||||||
|
prompt = f"{prompt}\n{VISION_SYSTEM_HINT}"
|
||||||
|
return prompt
|
||||||
|
|
||||||
def _build_agent_tools(self, node_id: str) -> list[RealtimeTool]:
|
def _build_agent_tools(self, node_id: str) -> list[RealtimeTool]:
|
||||||
stage = self._engine.agent_stage_config(node_id)
|
stage = self._engine.agent_stage_config(node_id)
|
||||||
@@ -428,6 +437,9 @@ class WorkflowRealtimeController:
|
|||||||
knowledge = self._knowledge_tool(node_id, transition_id)
|
knowledge = self._knowledge_tool(node_id, transition_id)
|
||||||
if knowledge:
|
if knowledge:
|
||||||
add(*knowledge)
|
add(*knowledge)
|
||||||
|
vision = self._vision_tool(node_id, transition_id)
|
||||||
|
if vision:
|
||||||
|
add(*vision)
|
||||||
for edge in self._engine.edge_tool_edges(node_id):
|
for edge in self._engine.edge_tool_edges(node_id):
|
||||||
add(*self._transition_tool(edge, node_id, transition_id))
|
add(*self._transition_tool(edge, node_id, transition_id))
|
||||||
self._handlers = handlers
|
self._handlers = handlers
|
||||||
@@ -519,6 +531,105 @@ class WorkflowRealtimeController:
|
|||||||
handler,
|
handler,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _vision_tool(
|
||||||
|
self,
|
||||||
|
node_id: str,
|
||||||
|
transition_id: int,
|
||||||
|
) -> tuple[RealtimeTool, ToolHandler] | None:
|
||||||
|
"""Build the on-demand camera tool for the active Realtime Agent."""
|
||||||
|
stage = self._engine.agent_stage_config(node_id)
|
||||||
|
if not stage.vision_enabled:
|
||||||
|
return None
|
||||||
|
|
||||||
|
vision_cfg: AssistantConfig | None = None
|
||||||
|
if stage.vision_model_resource_id:
|
||||||
|
resource = self._cfg.workflow_model_resources.get(
|
||||||
|
stage.vision_model_resource_id
|
||||||
|
)
|
||||||
|
if resource is not None:
|
||||||
|
vision_cfg = config_with_vision_resource(self._cfg, resource)
|
||||||
|
|
||||||
|
async def handler(arguments: dict[str, Any]) -> RealtimeToolResult:
|
||||||
|
if not self._is_current(node_id, transition_id):
|
||||||
|
return RealtimeToolResult(
|
||||||
|
{"status": "stale", "message": "当前 Agent 已经切换。"},
|
||||||
|
continue_response=False,
|
||||||
|
)
|
||||||
|
question = str(arguments.get("question") or "").strip()
|
||||||
|
if not question:
|
||||||
|
return RealtimeToolResult(
|
||||||
|
{"status": "error", "message": "视觉问题为空"}
|
||||||
|
)
|
||||||
|
if self._runtime.capture_image is None:
|
||||||
|
return RealtimeToolResult(
|
||||||
|
{"status": "no_video", "message": "当前会话未启用视频输入"}
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
frame = await self._runtime.capture_image(question)
|
||||||
|
except TimeoutError:
|
||||||
|
return RealtimeToolResult(
|
||||||
|
{"status": "timeout", "message": "等待摄像头画面超时"}
|
||||||
|
)
|
||||||
|
except Exception as exc: # noqa: BLE001 - expose a stable tool error
|
||||||
|
logger.warning(f"Realtime Workflow 获取摄像头画面失败:{exc}")
|
||||||
|
return RealtimeToolResult(
|
||||||
|
{"status": "error", "message": "暂时无法获取摄像头画面"}
|
||||||
|
)
|
||||||
|
if frame is None:
|
||||||
|
return RealtimeToolResult(
|
||||||
|
{"status": "no_video", "message": "当前没有可用的视频客户端"}
|
||||||
|
)
|
||||||
|
if not self._is_current(node_id, transition_id):
|
||||||
|
return RealtimeToolResult(
|
||||||
|
{"status": "stale", "message": "获取画面时 Agent 已经切换。"},
|
||||||
|
continue_response=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if vision_cfg is not None:
|
||||||
|
observation = await analyze_image_with_vision_model(
|
||||||
|
vision_cfg,
|
||||||
|
frame,
|
||||||
|
question,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
analyze = getattr(self._runtime.realtime, "analyze_image", None)
|
||||||
|
if not callable(analyze):
|
||||||
|
raise ValueError("当前 Realtime 适配器未实现原生视觉理解")
|
||||||
|
observation = await analyze(frame, question)
|
||||||
|
except Exception as exc: # noqa: BLE001 - provider errors become tool output
|
||||||
|
logger.warning(f"Realtime Workflow 视觉模型调用失败:{exc}")
|
||||||
|
return RealtimeToolResult(
|
||||||
|
{"status": "error", "message": "视觉模型暂时不可用"}
|
||||||
|
)
|
||||||
|
if not self._is_current(node_id, transition_id):
|
||||||
|
return RealtimeToolResult(
|
||||||
|
{"status": "stale", "message": "分析画面时 Agent 已经切换。"},
|
||||||
|
continue_response=False,
|
||||||
|
)
|
||||||
|
return RealtimeToolResult(
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"question": question,
|
||||||
|
"observation": observation,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
RealtimeTool(
|
||||||
|
name=VISION_TOOL_NAME,
|
||||||
|
description="获取用户摄像头的当前画面,并回答一个视觉问题。",
|
||||||
|
properties={
|
||||||
|
"question": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "需要根据当前画面判断的具体问题",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required=("question",),
|
||||||
|
),
|
||||||
|
handler,
|
||||||
|
)
|
||||||
|
|
||||||
def _system_tool(
|
def _system_tool(
|
||||||
self,
|
self,
|
||||||
tool: RuntimeTool,
|
tool: RuntimeTool,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export function VisionConfigSection({
|
|||||||
enabled,
|
enabled,
|
||||||
modelResourceId,
|
modelResourceId,
|
||||||
mainModelResourceId,
|
mainModelResourceId,
|
||||||
|
mainModelSupportsImageInput,
|
||||||
modelOptions,
|
modelOptions,
|
||||||
onEnabledChange,
|
onEnabledChange,
|
||||||
onModelResourceIdChange,
|
onModelResourceIdChange,
|
||||||
@@ -28,13 +29,14 @@ export function VisionConfigSection({
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
modelResourceId: string;
|
modelResourceId: string;
|
||||||
mainModelResourceId: string;
|
mainModelResourceId: string;
|
||||||
|
mainModelSupportsImageInput?: boolean;
|
||||||
modelOptions: VisionModelOption[];
|
modelOptions: VisionModelOption[];
|
||||||
onEnabledChange: (enabled: boolean) => void;
|
onEnabledChange: (enabled: boolean) => void;
|
||||||
onModelResourceIdChange: (modelResourceId: string) => void;
|
onModelResourceIdChange: (modelResourceId: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const mainModelSupportsVision = modelOptions.some(
|
const mainModelSupportsVision =
|
||||||
(option) => option.value === mainModelResourceId,
|
mainModelSupportsImageInput ??
|
||||||
);
|
modelOptions.some((option) => option.value === mainModelResourceId);
|
||||||
const independentModelOptions = modelOptions.filter(
|
const independentModelOptions = modelOptions.filter(
|
||||||
(option) => option.value !== mainModelResourceId,
|
(option) => option.value !== mainModelResourceId,
|
||||||
);
|
);
|
||||||
@@ -62,7 +64,7 @@ export function VisionConfigSection({
|
|||||||
/>
|
/>
|
||||||
{!modelResourceId && !mainModelSupportsVision && (
|
{!modelResourceId && !mainModelSupportsVision && (
|
||||||
<p className="text-xs text-destructive">
|
<p className="text-xs text-destructive">
|
||||||
当前大语言模型未标记支持图片输入,请选择独立视觉模型。
|
当前模型未标记支持图片输入,请选择独立视觉模型。
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -411,7 +411,11 @@ export function AssistantPage(props: AssistantPageProps) {
|
|||||||
const credOptions = (type: ModelResource["capability"]) =>
|
const credOptions = (type: ModelResource["capability"]) =>
|
||||||
modelResources
|
modelResources
|
||||||
.filter((c) => c.capability === type)
|
.filter((c) => c.capability === type)
|
||||||
.map((c) => ({ value: c.id, label: c.name }));
|
.map((c) => ({
|
||||||
|
value: c.id,
|
||||||
|
label: c.name,
|
||||||
|
supportImageInput: c.supportImageInput,
|
||||||
|
}));
|
||||||
const agentOptions = (interfaceType: "dify" | "fastgpt" | "opencode") =>
|
const agentOptions = (interfaceType: "dify" | "fastgpt" | "opencode") =>
|
||||||
modelResources
|
modelResources
|
||||||
.filter(
|
.filter(
|
||||||
|
|||||||
@@ -66,6 +66,20 @@ const capabilities: ModelType[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const capabilityFilters = ["全部", ...capabilities] as const;
|
const capabilityFilters = ["全部", ...capabilities] as const;
|
||||||
|
const nativeImageUnsupportedRealtimeInterfaces = new Set([
|
||||||
|
"qwen-audio-realtime",
|
||||||
|
]);
|
||||||
|
|
||||||
|
function canConfigureImageInput(
|
||||||
|
capability: ModelType,
|
||||||
|
interfaceType: string,
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
capability === "LLM" ||
|
||||||
|
(capability === "Realtime" &&
|
||||||
|
!nativeImageUnsupportedRealtimeInterfaces.has(interfaceType))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type ResourceDraft = {
|
type ResourceDraft = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -294,6 +308,12 @@ export function ComponentsModelsPage() {
|
|||||||
interfaceType,
|
interfaceType,
|
||||||
values: definition ? defaults(definition) : {},
|
values: definition ? defaults(definition) : {},
|
||||||
secrets: {},
|
secrets: {},
|
||||||
|
supportImageInput: canConfigureImageInput(
|
||||||
|
previous.capability,
|
||||||
|
interfaceType,
|
||||||
|
)
|
||||||
|
? previous.supportImageInput
|
||||||
|
: false,
|
||||||
}));
|
}));
|
||||||
setStoredSecretMasks({});
|
setStoredSecretMasks({});
|
||||||
setExtraBodyDraft("");
|
setExtraBodyDraft("");
|
||||||
@@ -310,8 +330,7 @@ export function ComponentsModelsPage() {
|
|||||||
interfaceType: first?.interfaceType ?? "",
|
interfaceType: first?.interfaceType ?? "",
|
||||||
values: first ? defaults(first) : {},
|
values: first ? defaults(first) : {},
|
||||||
secrets: {},
|
secrets: {},
|
||||||
supportImageInput:
|
supportImageInput: false,
|
||||||
capability === "LLM" ? previous.supportImageInput : false,
|
|
||||||
}));
|
}));
|
||||||
setStoredSecretMasks({});
|
setStoredSecretMasks({});
|
||||||
setExtraBodyDraft("");
|
setExtraBodyDraft("");
|
||||||
@@ -392,7 +411,9 @@ export function ComponentsModelsPage() {
|
|||||||
? { ...storedSecretMasks, ...draft.secrets }
|
? { ...storedSecretMasks, ...draft.secrets }
|
||||||
: draft.secrets,
|
: draft.secrets,
|
||||||
supportImageInput:
|
supportImageInput:
|
||||||
draft.capability === "LLM" ? draft.supportImageInput : false,
|
canConfigureImageInput(draft.capability, draft.interfaceType)
|
||||||
|
? draft.supportImageInput
|
||||||
|
: false,
|
||||||
enabled: editingId
|
enabled: editingId
|
||||||
? resources.find((resource) => resource.id === editingId)?.enabled ?? true
|
? resources.find((resource) => resource.id === editingId)?.enabled ?? true
|
||||||
: true,
|
: true,
|
||||||
@@ -747,11 +768,25 @@ export function ComponentsModelsPage() {
|
|||||||
</Select>
|
</Select>
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
{draft.capability === "LLM" && (
|
{(draft.capability === "LLM" ||
|
||||||
|
draft.capability === "Realtime") && (
|
||||||
<div className="flex items-center justify-between rounded-xl border border-hairline p-4">
|
<div className="flex items-center justify-between rounded-xl border border-hairline p-4">
|
||||||
<span className="text-sm font-medium">支持图片输入</span>
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm font-medium">原生支持图片输入</p>
|
||||||
|
{draft.capability === "Realtime" && (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Qwen Audio Realtime 请保持关闭,并为助手选择独立视觉模型。
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={draft.supportImageInput}
|
checked={draft.supportImageInput}
|
||||||
|
disabled={
|
||||||
|
!canConfigureImageInput(
|
||||||
|
draft.capability,
|
||||||
|
draft.interfaceType,
|
||||||
|
)
|
||||||
|
}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
setDraft((previous) => ({
|
setDraft((previous) => ({
|
||||||
...previous,
|
...previous,
|
||||||
|
|||||||
@@ -798,6 +798,7 @@ export function WorkflowCanvas({
|
|||||||
llmOptions={modelOptions.llm}
|
llmOptions={modelOptions.llm}
|
||||||
asrOptions={modelOptions.asr}
|
asrOptions={modelOptions.asr}
|
||||||
ttsOptions={modelOptions.tts}
|
ttsOptions={modelOptions.tts}
|
||||||
|
realtimeOptions={modelOptions.realtime}
|
||||||
visionOptions={modelOptions.vision}
|
visionOptions={modelOptions.vision}
|
||||||
dynamicVariableOptions={dynamicVariableOptions}
|
dynamicVariableOptions={dynamicVariableOptions}
|
||||||
workflowSettings={settings}
|
workflowSettings={settings}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export function AgentNodePanel({
|
|||||||
llmOptions,
|
llmOptions,
|
||||||
asrOptions,
|
asrOptions,
|
||||||
ttsOptions,
|
ttsOptions,
|
||||||
|
realtimeOptions,
|
||||||
visionOptions,
|
visionOptions,
|
||||||
dynamicVariableOptions,
|
dynamicVariableOptions,
|
||||||
}: {
|
}: {
|
||||||
@@ -49,6 +50,7 @@ export function AgentNodePanel({
|
|||||||
llmOptions: ModelOption[];
|
llmOptions: ModelOption[];
|
||||||
asrOptions: ModelOption[];
|
asrOptions: ModelOption[];
|
||||||
ttsOptions: ModelOption[];
|
ttsOptions: ModelOption[];
|
||||||
|
realtimeOptions: ModelOption[];
|
||||||
visionOptions: ModelOption[];
|
visionOptions: ModelOption[];
|
||||||
dynamicVariableOptions: ModelOption[];
|
dynamicVariableOptions: ModelOption[];
|
||||||
}) {
|
}) {
|
||||||
@@ -71,6 +73,11 @@ export function AgentNodePanel({
|
|||||||
}
|
}
|
||||||
setPatch({
|
setPatch({
|
||||||
inheritGlobalConfig: false,
|
inheritGlobalConfig: false,
|
||||||
|
visionEnabled:
|
||||||
|
draft.visionEnabled ?? workflowSettings.visionEnabled,
|
||||||
|
visionModelResourceId:
|
||||||
|
(draft.visionModelResourceId as string) ||
|
||||||
|
workflowSettings.visionModelResourceId,
|
||||||
...(!isRealtime
|
...(!isRealtime
|
||||||
? {
|
? {
|
||||||
llmResourceId:
|
llmResourceId:
|
||||||
@@ -79,11 +86,6 @@ export function AgentNodePanel({
|
|||||||
(draft.asrResourceId as string) || workflowSettings.asr || "",
|
(draft.asrResourceId as string) || workflowSettings.asr || "",
|
||||||
ttsResourceId:
|
ttsResourceId:
|
||||||
(draft.ttsResourceId as string) || workflowSettings.tts || "",
|
(draft.ttsResourceId as string) || workflowSettings.tts || "",
|
||||||
visionEnabled:
|
|
||||||
draft.visionEnabled ?? workflowSettings.visionEnabled,
|
|
||||||
visionModelResourceId:
|
|
||||||
(draft.visionModelResourceId as string) ||
|
|
||||||
workflowSettings.visionModelResourceId,
|
|
||||||
enableInterrupt:
|
enableInterrupt:
|
||||||
draft.enableInterrupt ?? workflowSettings.allowInterrupt,
|
draft.enableInterrupt ?? workflowSettings.allowInterrupt,
|
||||||
turnConfig: agentTurnConfig,
|
turnConfig: agentTurnConfig,
|
||||||
@@ -295,12 +297,29 @@ export function AgentNodePanel({
|
|||||||
</PanelAnchor> : null}
|
</PanelAnchor> : null}
|
||||||
|
|
||||||
<PanelAnchor id="capabilities">
|
<PanelAnchor id="capabilities">
|
||||||
{!isRealtime ? <VisionConfigSection
|
<VisionConfigSection
|
||||||
description="配置当前 Agent 是否可以按需理解用户摄像头画面"
|
description="配置当前 Agent 是否可以按需理解用户摄像头画面"
|
||||||
hint="开启后,该 Agent 会获得读取当前视频画面的工具。选择「模型自己」时,当前大语言模型必须支持图片输入。"
|
hint={
|
||||||
|
isRealtime
|
||||||
|
? "开启后,该 Agent 会获得读取当前视频画面的工具。Qwen Audio Realtime 需要选择独立视觉模型。"
|
||||||
|
: "开启后,该 Agent 会获得读取当前视频画面的工具。选择「模型自己」时,当前大语言模型必须支持图片输入。"
|
||||||
|
}
|
||||||
enabled={Boolean(draft.visionEnabled)}
|
enabled={Boolean(draft.visionEnabled)}
|
||||||
modelResourceId={draft.visionModelResourceId ?? ""}
|
modelResourceId={draft.visionModelResourceId ?? ""}
|
||||||
mainModelResourceId={(draft.llmResourceId as string) || ""}
|
mainModelResourceId={
|
||||||
|
isRealtime
|
||||||
|
? workflowSettings.realtime ?? ""
|
||||||
|
: (draft.llmResourceId as string) || ""
|
||||||
|
}
|
||||||
|
mainModelSupportsImageInput={
|
||||||
|
isRealtime
|
||||||
|
? Boolean(
|
||||||
|
realtimeOptions.find(
|
||||||
|
(option) => option.value === workflowSettings.realtime,
|
||||||
|
)?.supportImageInput,
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
modelOptions={visionOptions}
|
modelOptions={visionOptions}
|
||||||
onEnabledChange={(visionEnabled) =>
|
onEnabledChange={(visionEnabled) =>
|
||||||
setPatch({
|
setPatch({
|
||||||
@@ -311,7 +330,7 @@ export function AgentNodePanel({
|
|||||||
onModelResourceIdChange={(visionModelResourceId) =>
|
onModelResourceIdChange={(visionModelResourceId) =>
|
||||||
set("visionModelResourceId", visionModelResourceId)
|
set("visionModelResourceId", visionModelResourceId)
|
||||||
}
|
}
|
||||||
/> : null}
|
/>
|
||||||
|
|
||||||
<SectionCard
|
<SectionCard
|
||||||
icon={<Database size={15} />}
|
icon={<Database size={15} />}
|
||||||
|
|||||||
@@ -175,12 +175,29 @@ export function GlobalSettingsPanel({
|
|||||||
</PanelAnchor>
|
</PanelAnchor>
|
||||||
|
|
||||||
<PanelAnchor id="capabilities">
|
<PanelAnchor id="capabilities">
|
||||||
{settings.runtimeMode === "pipeline" ? <VisionConfigSection
|
<VisionConfigSection
|
||||||
description="配置继承全局设置的 Agent 是否可以按需理解用户摄像头画面"
|
description="配置继承全局设置的 Agent 是否可以按需理解用户摄像头画面"
|
||||||
hint="开启后,继承全局配置的 Agent 会获得读取当前视频画面的工具。选择「模型自己」时,全局大语言模型必须支持图片输入。"
|
hint={
|
||||||
|
settings.runtimeMode === "realtime"
|
||||||
|
? "开启后,Agent 会获得读取当前视频画面的工具。Qwen Audio Realtime 需要选择独立视觉模型。"
|
||||||
|
: "开启后,继承全局配置的 Agent 会获得读取当前视频画面的工具。选择「模型自己」时,全局大语言模型必须支持图片输入。"
|
||||||
|
}
|
||||||
enabled={settings.visionEnabled}
|
enabled={settings.visionEnabled}
|
||||||
modelResourceId={settings.visionModelResourceId}
|
modelResourceId={settings.visionModelResourceId}
|
||||||
mainModelResourceId={settings.llm ?? ""}
|
mainModelResourceId={
|
||||||
|
settings.runtimeMode === "realtime"
|
||||||
|
? settings.realtime ?? ""
|
||||||
|
: settings.llm ?? ""
|
||||||
|
}
|
||||||
|
mainModelSupportsImageInput={
|
||||||
|
settings.runtimeMode === "realtime"
|
||||||
|
? Boolean(
|
||||||
|
modelOptions.realtime.find(
|
||||||
|
(option) => option.value === settings.realtime,
|
||||||
|
)?.supportImageInput,
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
modelOptions={modelOptions.vision}
|
modelOptions={modelOptions.vision}
|
||||||
onEnabledChange={(visionEnabled) =>
|
onEnabledChange={(visionEnabled) =>
|
||||||
onSettingsChange({
|
onSettingsChange({
|
||||||
@@ -192,7 +209,7 @@ export function GlobalSettingsPanel({
|
|||||||
onModelResourceIdChange={(visionModelResourceId) =>
|
onModelResourceIdChange={(visionModelResourceId) =>
|
||||||
onSettingsChange({ ...settings, visionModelResourceId })
|
onSettingsChange({ ...settings, visionModelResourceId })
|
||||||
}
|
}
|
||||||
/> : null}
|
/>
|
||||||
|
|
||||||
<SectionCard
|
<SectionCard
|
||||||
icon={<Database size={15} />}
|
icon={<Database size={15} />}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export function NodeSettingsPanel({
|
|||||||
llmOptions,
|
llmOptions,
|
||||||
asrOptions,
|
asrOptions,
|
||||||
ttsOptions,
|
ttsOptions,
|
||||||
|
realtimeOptions,
|
||||||
visionOptions,
|
visionOptions,
|
||||||
dynamicVariableOptions,
|
dynamicVariableOptions,
|
||||||
workflowSettings,
|
workflowSettings,
|
||||||
@@ -44,6 +45,7 @@ export function NodeSettingsPanel({
|
|||||||
llmOptions: ModelOption[];
|
llmOptions: ModelOption[];
|
||||||
asrOptions: ModelOption[];
|
asrOptions: ModelOption[];
|
||||||
ttsOptions: ModelOption[];
|
ttsOptions: ModelOption[];
|
||||||
|
realtimeOptions: ModelOption[];
|
||||||
visionOptions: ModelOption[];
|
visionOptions: ModelOption[];
|
||||||
dynamicVariableOptions: ModelOption[];
|
dynamicVariableOptions: ModelOption[];
|
||||||
workflowSettings: WorkflowSettings;
|
workflowSettings: WorkflowSettings;
|
||||||
@@ -136,6 +138,7 @@ export function NodeSettingsPanel({
|
|||||||
llmOptions={llmOptions}
|
llmOptions={llmOptions}
|
||||||
asrOptions={asrOptions}
|
asrOptions={asrOptions}
|
||||||
ttsOptions={ttsOptions}
|
ttsOptions={ttsOptions}
|
||||||
|
realtimeOptions={realtimeOptions}
|
||||||
visionOptions={visionOptions}
|
visionOptions={visionOptions}
|
||||||
dynamicVariableOptions={dynamicVariableOptions}
|
dynamicVariableOptions={dynamicVariableOptions}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export type ModelOption = {
|
|||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
supportImageInput?: boolean;
|
||||||
toolType?: Tool["type"];
|
toolType?: Tool["type"];
|
||||||
systemKind?: SystemToolKind;
|
systemKind?: SystemToolKind;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user