fix: support vision prompt modes
This commit is contained in:
@@ -160,7 +160,8 @@ class Assistant(Base):
|
||||
ForeignKey("model_resources.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
)
|
||||
vision_focus_prompt: Mapped[str] = mapped_column(Text, default="")
|
||||
vision_prompt_mode: Mapped[str] = mapped_column(String(16), default="runtime")
|
||||
vision_prompt: Mapped[str] = mapped_column(Text, default="")
|
||||
|
||||
# KB 引用:被引用时禁止删 KB(RESTRICT),无默认兜底
|
||||
knowledge_base_id: Mapped[str | None] = mapped_column(
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
"""replace vision focus with explicit prompt modes
|
||||
|
||||
Revision ID: 20260812_0020
|
||||
Revises: 20260812_0019
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision: str = "20260812_0020"
|
||||
down_revision: str | Sequence[str] | None = "20260812_0019"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.alter_column(
|
||||
"assistants",
|
||||
"vision_focus_prompt",
|
||||
new_column_name="vision_prompt",
|
||||
)
|
||||
op.add_column(
|
||||
"assistants",
|
||||
sa.Column(
|
||||
"vision_prompt_mode",
|
||||
sa.String(length=16),
|
||||
server_default="runtime",
|
||||
nullable=False,
|
||||
),
|
||||
)
|
||||
# The previous single-field implementation behaved most like guided mode.
|
||||
op.execute(
|
||||
"UPDATE assistants SET vision_prompt_mode = 'guided' "
|
||||
"WHERE TRIM(vision_prompt) <> ''"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("assistants", "vision_prompt_mode")
|
||||
op.alter_column(
|
||||
"assistants",
|
||||
"vision_prompt",
|
||||
new_column_name="vision_focus_prompt",
|
||||
)
|
||||
+4
-1
@@ -13,6 +13,8 @@ from typing import Literal
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
RuntimeMode = Literal["pipeline", "realtime"]
|
||||
VisionPromptMode = Literal["fixed", "runtime", "guided"]
|
||||
VISION_PROMPT_MODES = frozenset({"fixed", "runtime", "guided"})
|
||||
|
||||
|
||||
class RuntimeMcpServer(BaseModel):
|
||||
@@ -92,7 +94,8 @@ class AssistantConfig(BaseModel):
|
||||
llm_support_image_input: bool = False
|
||||
vision_enabled: bool = False
|
||||
vision_model_resource_id: str | None = None
|
||||
vision_focus_prompt: str = ""
|
||||
vision_prompt_mode: VisionPromptMode = "runtime"
|
||||
vision_prompt: str = ""
|
||||
vision_model: str = ""
|
||||
vision_llm_interface_type: str = "openai-llm"
|
||||
vision_llm_values: dict = {}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import uuid
|
||||
|
||||
from models import RuntimeModelResource, RuntimeTool
|
||||
from models import VISION_PROMPT_MODES, RuntimeTool
|
||||
from db.models import (
|
||||
Assistant,
|
||||
AssistantModelBinding,
|
||||
@@ -16,15 +16,12 @@ from fastapi import APIRouter, Depends, HTTPException
|
||||
from schemas import (
|
||||
AssistantOut,
|
||||
AssistantUpsert,
|
||||
VisionFocusPromptGenerateRequest,
|
||||
VisionFocusPromptGenerateResult,
|
||||
)
|
||||
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.system_tools import system_tool_kind
|
||||
from services.tool_policy import policy_for_tool
|
||||
from services.vision import generate_vision_focus_prompt
|
||||
from services.workflow_engine import WorkflowEngine
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -79,7 +76,8 @@ def _validate_workflow(body: AssistantUpsert) -> None:
|
||||
# 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
|
||||
body.vision_focus_prompt = ""
|
||||
body.vision_prompt_mode = "runtime"
|
||||
body.vision_prompt = ""
|
||||
refs = graph_references(body.graph)
|
||||
body.tool_ids = list(dict.fromkeys([*body.tool_ids, *sorted(refs["tools"])]))
|
||||
|
||||
@@ -271,6 +269,13 @@ async def _validate_vision_model(
|
||||
if body.type == "workflow":
|
||||
return
|
||||
if body.vision_enabled:
|
||||
if body.vision_prompt_mode not in VISION_PROMPT_MODES:
|
||||
raise HTTPException(400, "视觉提示词模式无效")
|
||||
if (
|
||||
body.vision_prompt_mode in {"fixed", "guided"}
|
||||
and not body.vision_prompt.strip()
|
||||
):
|
||||
raise HTTPException(400, "固定或引导模式必须填写视觉提示词")
|
||||
if body.vision_model_resource_id:
|
||||
resource = await session.get(ModelResource, body.vision_model_resource_id)
|
||||
else:
|
||||
@@ -396,7 +401,8 @@ async def _to_out(session: AsyncSession, assistant: Assistant) -> AssistantOut:
|
||||
startup=assistant.startup or {},
|
||||
vision_enabled=assistant.vision_enabled,
|
||||
vision_model_resource_id=assistant.vision_model_resource_id,
|
||||
vision_focus_prompt=assistant.vision_focus_prompt or "",
|
||||
vision_prompt_mode=assistant.vision_prompt_mode or "runtime",
|
||||
vision_prompt=assistant.vision_prompt or "",
|
||||
model_resource_ids=await _resource_ids(session, assistant.id),
|
||||
knowledge_base_id=assistant.knowledge_base_id,
|
||||
knowledge_retrieval_config=assistant.knowledge_retrieval_config or {},
|
||||
@@ -448,43 +454,6 @@ async def create_assistant(
|
||||
return await _to_out(session, assistant)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/vision-focus/generate",
|
||||
response_model=VisionFocusPromptGenerateResult,
|
||||
)
|
||||
async def generate_assistant_vision_focus(
|
||||
body: VisionFocusPromptGenerateRequest,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
"""Generate an editable visual focus draft from an assistant prompt."""
|
||||
|
||||
resource = await session.get(ModelResource, body.model_resource_id)
|
||||
if (
|
||||
resource is None
|
||||
or resource.capability != "LLM"
|
||||
or not resource.enabled
|
||||
):
|
||||
raise HTTPException(400, "请选择可用的大语言模型生成视觉关注目标")
|
||||
try:
|
||||
focus_prompt = await generate_vision_focus_prompt(
|
||||
RuntimeModelResource(
|
||||
id=resource.id,
|
||||
name=resource.name,
|
||||
capability=resource.capability,
|
||||
interface_type=resource.interface_type,
|
||||
values=resource.values or {},
|
||||
secrets=resource.secrets or {},
|
||||
support_image_input=bool(resource.support_image_input),
|
||||
),
|
||||
body.source_prompt,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(400, str(exc)) from exc
|
||||
except Exception as exc:
|
||||
raise HTTPException(502, "生成视觉关注目标失败,请稍后重试") from exc
|
||||
return VisionFocusPromptGenerateResult(focus_prompt=focus_prompt)
|
||||
|
||||
|
||||
@router.get("/{assistant_id}", response_model=AssistantOut)
|
||||
async def get_assistant(
|
||||
assistant_id: str, session: AsyncSession = Depends(get_session)
|
||||
@@ -513,7 +482,8 @@ async def duplicate_assistant(
|
||||
startup=dict(source.startup or {}),
|
||||
vision_enabled=source.vision_enabled,
|
||||
vision_model_resource_id=source.vision_model_resource_id,
|
||||
vision_focus_prompt=source.vision_focus_prompt or "",
|
||||
vision_prompt_mode=source.vision_prompt_mode or "runtime",
|
||||
vision_prompt=source.vision_prompt or "",
|
||||
knowledge_base_id=source.knowledge_base_id,
|
||||
knowledge_retrieval_config=dict(source.knowledge_retrieval_config or {}),
|
||||
prompt=source.prompt,
|
||||
|
||||
+3
-10
@@ -15,6 +15,7 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_valida
|
||||
from pydantic.alias_generators import to_camel
|
||||
|
||||
RuntimeMode = Literal["pipeline", "realtime"]
|
||||
VisionPromptMode = Literal["fixed", "runtime", "guided"]
|
||||
ModelType = Literal["LLM", "ASR", "TTS", "Realtime", "Embedding", "Agent"]
|
||||
AssistantType = Literal["prompt", "workflow", "dify", "fastgpt", "opencode"]
|
||||
TurnEndStrategy = Literal["silence", "smart_turn"]
|
||||
@@ -198,7 +199,8 @@ class AssistantUpsert(CamelModel):
|
||||
startup: StartupConfig = Field(default_factory=StartupConfig)
|
||||
vision_enabled: bool = False
|
||||
vision_model_resource_id: str | None = None
|
||||
vision_focus_prompt: str = Field(default="", max_length=4000)
|
||||
vision_prompt_mode: VisionPromptMode = "runtime"
|
||||
vision_prompt: str = Field(default="", max_length=4000)
|
||||
model_resource_ids: dict[ModelType, str] = Field(default_factory=dict)
|
||||
knowledge_base_id: str | None = None
|
||||
knowledge_retrieval_config: KnowledgeRetrievalConfig = Field(
|
||||
@@ -264,15 +266,6 @@ class AssistantOut(AssistantUpsert):
|
||||
updated_at: str | None = None
|
||||
|
||||
|
||||
class VisionFocusPromptGenerateRequest(CamelModel):
|
||||
source_prompt: str = Field(min_length=1, max_length=20000)
|
||||
model_resource_id: str = Field(min_length=1, max_length=40)
|
||||
|
||||
|
||||
class VisionFocusPromptGenerateResult(CamelModel):
|
||||
focus_prompt: str
|
||||
|
||||
|
||||
class DynamicVariableDefinition(CamelModel):
|
||||
type: DynamicVariableType = "string"
|
||||
required: bool = False
|
||||
|
||||
@@ -670,7 +670,10 @@ class WorkflowBrain(BaseBrain):
|
||||
append_function(self._flow_tool(tool, node_id))
|
||||
append_function(self._knowledge_function(node_id))
|
||||
if stage.vision_enabled and self._require_runtime().vision_function:
|
||||
append_function(self._require_runtime().vision_function)
|
||||
vision_function = self._require_runtime().vision_function
|
||||
append_function(
|
||||
vision_function(stage) if callable(vision_function) else vision_function
|
||||
)
|
||||
if self._engine.llm_routing_mode() == "edge_tool":
|
||||
for edge in self._engine.edge_tool_edges(node_id):
|
||||
append_function(self._edge_tool(edge, node_id))
|
||||
|
||||
@@ -301,7 +301,8 @@ async def resolve_runtime_config(
|
||||
),
|
||||
vision_enabled=assistant.vision_enabled,
|
||||
vision_model_resource_id=assistant.vision_model_resource_id,
|
||||
vision_focus_prompt=assistant.vision_focus_prompt or "",
|
||||
vision_prompt_mode=assistant.vision_prompt_mode or "runtime",
|
||||
vision_prompt=assistant.vision_prompt or "",
|
||||
vision_model=str(_value(vision_resource, "modelId", "")),
|
||||
vision_llm_interface_type=(
|
||||
vision_resource.interface_type if vision_resource else "openai-llm"
|
||||
|
||||
@@ -6,6 +6,7 @@ from collections import defaultdict, deque
|
||||
from copy import deepcopy
|
||||
from typing import Any
|
||||
|
||||
from models import VISION_PROMPT_MODES
|
||||
from services.message_policy import (
|
||||
MESSAGE_COMPLETION_POLICIES,
|
||||
MESSAGE_CONFIRMATION,
|
||||
@@ -199,6 +200,11 @@ def _normalize_agent_data(data: dict[str, Any]) -> None:
|
||||
data["stateVariableNames"] = list(
|
||||
dict.fromkeys(str(name) for name in state_names or [] if str(name))
|
||||
)
|
||||
legacy_prompt = str(data.pop("visionFocusPrompt", "") or "").strip()
|
||||
if "visionPrompt" not in data:
|
||||
data["visionPrompt"] = legacy_prompt
|
||||
if "visionPromptMode" not in data:
|
||||
data["visionPromptMode"] = "guided" if legacy_prompt else "runtime"
|
||||
if "inheritGlobalConfig" not in data:
|
||||
has_node_overrides = any(
|
||||
(
|
||||
@@ -207,7 +213,7 @@ def _normalize_agent_data(data: dict[str, Any]) -> None:
|
||||
data.get("ttsResourceId"),
|
||||
data.get("visionEnabled"),
|
||||
data.get("visionModelResourceId"),
|
||||
data.get("visionFocusPrompt"),
|
||||
data.get("visionPrompt"),
|
||||
data.get("knowledgeBaseId"),
|
||||
data.get("toolIds"),
|
||||
)
|
||||
@@ -268,7 +274,10 @@ def _normalize_settings(settings: dict[str, Any], *, global_prompt: str = "") ->
|
||||
settings.setdefault("defaultTtsResourceId", "")
|
||||
settings.setdefault("visionEnabled", False)
|
||||
settings.setdefault("visionModelResourceId", "")
|
||||
settings.setdefault("visionFocusPrompt", "")
|
||||
legacy_prompt = str(settings.pop("visionFocusPrompt", "") or "").strip()
|
||||
settings.setdefault("visionPrompt", legacy_prompt)
|
||||
if "visionPromptMode" not in settings:
|
||||
settings["visionPromptMode"] = "guided" if legacy_prompt else "runtime"
|
||||
settings.setdefault("toolIds", [])
|
||||
settings.setdefault("knowledgeBaseId", "")
|
||||
settings.setdefault("knowledgeMode", "automatic")
|
||||
@@ -471,6 +480,20 @@ def validate_graph(graph: dict[str, Any]) -> list[str]:
|
||||
not isinstance(name, str) for name in state_names
|
||||
):
|
||||
errors.append(f"Agent 节点 {node_id} 的状态变量授权必须是列表")
|
||||
source = settings if data.get("inheritGlobalConfig", True) else data
|
||||
vision_prompt_mode = str(source.get("visionPromptMode") or "runtime")
|
||||
if vision_prompt_mode not in VISION_PROMPT_MODES:
|
||||
errors.append(
|
||||
f"Agent 节点 {node_id} 的视觉提示词模式无效:{vision_prompt_mode}"
|
||||
)
|
||||
if (
|
||||
source.get("visionEnabled")
|
||||
and vision_prompt_mode in {"fixed", "guided"}
|
||||
and not str(source.get("visionPrompt") or "").strip()
|
||||
):
|
||||
errors.append(
|
||||
f"Agent 节点 {node_id} 的固定或引导模式必须填写视觉提示词"
|
||||
)
|
||||
if runtime_mode == "realtime":
|
||||
if data.get("contextPolicy", "inherit") != "inherit":
|
||||
errors.append(
|
||||
@@ -487,7 +510,6 @@ def validate_graph(graph: dict[str, Any]) -> list[str]:
|
||||
errors.append(
|
||||
f"Realtime Agent 节点 {node_id} 不支持节点级模型、语音或交互策略覆盖"
|
||||
)
|
||||
source = settings if data.get("inheritGlobalConfig", True) else data
|
||||
if (
|
||||
source.get("knowledgeBaseId")
|
||||
and source.get("knowledgeMode") != "on_demand"
|
||||
|
||||
@@ -38,8 +38,9 @@ from services.vision import (
|
||||
image_data_uri,
|
||||
image_frame_from_jpeg,
|
||||
image_jpeg_bytes,
|
||||
resolve_vision_question,
|
||||
vision_system_hint,
|
||||
vision_tool_description,
|
||||
vision_tool_schema_parts,
|
||||
)
|
||||
from services.workflow_engine import WorkflowEngine
|
||||
|
||||
@@ -349,7 +350,9 @@ async def run_pipeline(
|
||||
def with_vision_hint(text: str) -> str:
|
||||
hints = []
|
||||
if vision_enabled and cfg.type != "workflow":
|
||||
hints.append(vision_system_hint(cfg.vision_focus_prompt))
|
||||
hints.append(
|
||||
vision_system_hint(cfg.vision_prompt_mode, cfg.vision_prompt)
|
||||
)
|
||||
if cfg.knowledge_base_id:
|
||||
hints.append(
|
||||
AUTOMATIC_KNOWLEDGE_SYSTEM_HINT
|
||||
@@ -450,7 +453,8 @@ async def run_pipeline(
|
||||
workflow_vision_scope: dict[str, Any] = {
|
||||
"enabled": False,
|
||||
"vision_model_resource_id": None,
|
||||
"vision_focus_prompt": "",
|
||||
"vision_prompt_mode": "runtime",
|
||||
"vision_prompt": "",
|
||||
"llm_resource_id": None,
|
||||
}
|
||||
|
||||
@@ -458,16 +462,15 @@ async def run_pipeline(
|
||||
if cfg.type == "workflow":
|
||||
return _workflow_vision_uses_main_llm(cfg, workflow_vision_scope)
|
||||
return vision_native_mode
|
||||
vision_description, vision_properties, vision_required = vision_tool_schema_parts(
|
||||
cfg.vision_prompt_mode,
|
||||
cfg.vision_prompt,
|
||||
)
|
||||
vision_schema = FunctionSchema(
|
||||
name=VISION_TOOL_NAME,
|
||||
description=vision_tool_description(cfg.vision_focus_prompt),
|
||||
properties={
|
||||
"question": {
|
||||
"type": "string",
|
||||
"description": "用户关于当前视频画面的具体问题。",
|
||||
}
|
||||
},
|
||||
required=["question"],
|
||||
description=vision_description,
|
||||
properties=vision_properties,
|
||||
required=vision_required,
|
||||
)
|
||||
knowledge_schema = FunctionSchema(
|
||||
name=KNOWLEDGE_TOOL_NAME,
|
||||
@@ -498,7 +501,15 @@ async def run_pipeline(
|
||||
await params.result_callback({"status": "error", "message": "知识库检索暂时不可用"})
|
||||
|
||||
async def fetch_user_image(params: FunctionCallParams):
|
||||
question = str(params.arguments.get("question") or "请描述当前画面。")
|
||||
try:
|
||||
question = resolve_vision_question(
|
||||
cfg.vision_prompt_mode,
|
||||
cfg.vision_prompt,
|
||||
params.arguments,
|
||||
)
|
||||
except ValueError as exc:
|
||||
await params.result_callback({"status": "error", "message": str(exc)})
|
||||
return
|
||||
user_id = vision_state.get("client_id")
|
||||
if not user_id:
|
||||
await params.result_callback(
|
||||
@@ -576,14 +587,26 @@ async def run_pipeline(
|
||||
result = config_with_main_llm_as_vision(cfg)
|
||||
else:
|
||||
raise ValueError(f"视觉模型资源未加载:{vision_resource_id}")
|
||||
result.vision_focus_prompt = str(
|
||||
workflow_vision_scope.get("vision_focus_prompt") or ""
|
||||
result.vision_prompt_mode = str(
|
||||
workflow_vision_scope.get("vision_prompt_mode") or "runtime"
|
||||
)
|
||||
result.vision_prompt = str(
|
||||
workflow_vision_scope.get("vision_prompt") or ""
|
||||
).strip()
|
||||
return result
|
||||
|
||||
if cfg.type == "workflow" and vision_enabled:
|
||||
async def native_flow_fetch_user_image(params: FunctionCallParams) -> None:
|
||||
question = str(params.arguments.get("question") or "请描述当前画面。")
|
||||
try:
|
||||
vision_cfg = active_vision_config()
|
||||
question = resolve_vision_question(
|
||||
vision_cfg.vision_prompt_mode,
|
||||
vision_cfg.vision_prompt,
|
||||
params.arguments,
|
||||
)
|
||||
except ValueError as exc:
|
||||
await params.result_callback({"status": "error", "message": str(exc)})
|
||||
return
|
||||
user_id = vision_state.get("client_id")
|
||||
if not user_id:
|
||||
await params.result_callback(
|
||||
@@ -609,7 +632,15 @@ async def run_pipeline(
|
||||
"status": "disabled",
|
||||
"message": "当前 Agent 节点未开启视觉理解。",
|
||||
}
|
||||
question = str((args or {}).get("question") or "请描述当前画面。")
|
||||
vision_cfg = active_vision_config()
|
||||
try:
|
||||
question = resolve_vision_question(
|
||||
vision_cfg.vision_prompt_mode,
|
||||
vision_cfg.vision_prompt,
|
||||
args,
|
||||
)
|
||||
except ValueError as exc:
|
||||
return {"status": "error", "message": str(exc)}
|
||||
user_id = vision_state.get("client_id")
|
||||
if not user_id:
|
||||
return {
|
||||
@@ -625,7 +656,7 @@ async def run_pipeline(
|
||||
try:
|
||||
frame = await vision_capture.request_image(llm, request)
|
||||
observation = await analyze_image_with_vision_model(
|
||||
active_vision_config(),
|
||||
vision_cfg,
|
||||
frame,
|
||||
question,
|
||||
)
|
||||
@@ -654,14 +685,19 @@ async def run_pipeline(
|
||||
active_vision_uses_main_llm,
|
||||
)
|
||||
|
||||
workflow_vision_function = FlowsFunctionSchema(
|
||||
name=VISION_TOOL_NAME,
|
||||
description=vision_schema.description,
|
||||
properties=vision_schema.properties,
|
||||
required=vision_schema.required,
|
||||
handler=flow_fetch_user_image,
|
||||
cancel_on_interruption=True,
|
||||
)
|
||||
def workflow_vision_function(stage) -> FlowsFunctionSchema:
|
||||
description, properties, required = vision_tool_schema_parts(
|
||||
stage.vision_prompt_mode,
|
||||
stage.vision_prompt,
|
||||
)
|
||||
return FlowsFunctionSchema(
|
||||
name=VISION_TOOL_NAME,
|
||||
description=description,
|
||||
properties=properties,
|
||||
required=required,
|
||||
handler=flow_fetch_user_image,
|
||||
cancel_on_interruption=True,
|
||||
)
|
||||
|
||||
if vision_enabled and cfg.type != "workflow":
|
||||
llm.register_function(VISION_TOOL_NAME, fetch_user_image)
|
||||
|
||||
+85
-74
@@ -5,8 +5,14 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import base64
|
||||
from io import BytesIO
|
||||
from typing import Any
|
||||
|
||||
from models import AssistantConfig, RuntimeModelResource
|
||||
from models import (
|
||||
VISION_PROMPT_MODES,
|
||||
AssistantConfig,
|
||||
RuntimeModelResource,
|
||||
VisionPromptMode,
|
||||
)
|
||||
from openai import AsyncOpenAI
|
||||
from PIL import Image
|
||||
from pipecat.frames.frames import UserImageRawFrame
|
||||
@@ -22,12 +28,6 @@ VISION_ANALYSIS_SYSTEM_PROMPT = (
|
||||
"你是一个视觉理解模型。请只根据图片内容和用户问题给出准确、简洁的中文观察结果。"
|
||||
"如果画面不足以判断,请明确说明不确定。"
|
||||
)
|
||||
VISION_FOCUS_GENERATION_SYSTEM_PROMPT = (
|
||||
"你负责把对话助手提示词整理成一段可编辑的视觉关注目标。"
|
||||
"只提取图片中能够直接观察的对象、属性、状态、输出要求和不确定性边界;"
|
||||
"不要复制对话流程、工具调用规则或结束条件,不要要求根据画面推断身份、责任、"
|
||||
"意图等不可直接观察的信息。使用简洁中文,控制在 100 至 500 字,只输出关注目标正文。"
|
||||
)
|
||||
|
||||
|
||||
def _require(value: str, label: str) -> str:
|
||||
@@ -36,83 +36,96 @@ def _require(value: str, label: str) -> str:
|
||||
raise ValueError(f"缺少模型资源配置: {label}")
|
||||
|
||||
|
||||
def vision_system_hint(focus_prompt: str = "") -> str:
|
||||
"""Describe when to use vision and keep a configured business focus visible."""
|
||||
def normalize_vision_prompt_mode(value: object) -> VisionPromptMode:
|
||||
"""Return a supported mode while keeping older saved configs compatible."""
|
||||
|
||||
focus = focus_prompt.strip()
|
||||
if not focus:
|
||||
return VISION_SYSTEM_HINT
|
||||
mode = str(value or "runtime")
|
||||
if mode == "fixed":
|
||||
return "fixed"
|
||||
if mode == "guided":
|
||||
return "guided"
|
||||
return "runtime"
|
||||
|
||||
|
||||
def _compact_prompt(prompt: str, *, limit: int = 4000) -> str:
|
||||
return " ".join(prompt.split())[:limit]
|
||||
|
||||
|
||||
def vision_system_hint(
|
||||
mode: VisionPromptMode = "runtime",
|
||||
configured_prompt: str = "",
|
||||
) -> str:
|
||||
"""Tell the conversation model how the camera tool forms its question."""
|
||||
|
||||
prompt_mode = normalize_vision_prompt_mode(mode)
|
||||
prompt = _compact_prompt(configured_prompt)
|
||||
if prompt_mode == "fixed":
|
||||
return (
|
||||
f"{VISION_SYSTEM_HINT}\n"
|
||||
f"该工具始终使用固定视觉问题“{prompt}”,调用时不需要生成视觉问题。"
|
||||
)
|
||||
if prompt_mode == "guided":
|
||||
return (
|
||||
f"{VISION_SYSTEM_HINT}\n"
|
||||
"调用时请结合当前对话生成具体视觉问题,并遵守以下固定引导:"
|
||||
f"{prompt}"
|
||||
)
|
||||
return (
|
||||
f"{VISION_SYSTEM_HINT}\n"
|
||||
f"视觉关注目标(用于补充本次用户问题;冲突时以本次问题为准):{focus}"
|
||||
"调用时请结合当前对话,生成一个具体、可由当前画面回答的视觉问题。"
|
||||
)
|
||||
|
||||
|
||||
def vision_analysis_system_prompt(focus_prompt: str = "") -> str:
|
||||
"""Keep the safety baseline fixed while appending an editable business focus."""
|
||||
def vision_tool_schema_parts(
|
||||
mode: VisionPromptMode = "runtime",
|
||||
configured_prompt: str = "",
|
||||
) -> tuple[str, dict[str, Any], list[str]]:
|
||||
"""Build the tool contract for one configured visual-question mode."""
|
||||
|
||||
focus = focus_prompt.strip()
|
||||
if not focus:
|
||||
return VISION_ANALYSIS_SYSTEM_PROMPT
|
||||
return (
|
||||
f"{VISION_ANALYSIS_SYSTEM_PROMPT}\n\n"
|
||||
f"[业务视觉关注目标]\n{focus}\n"
|
||||
"该目标用于补充当前用户问题;如果二者范围不同,优先回答当前用户问题。"
|
||||
)
|
||||
|
||||
|
||||
def vision_tool_description(focus_prompt: str = "") -> str:
|
||||
base = (
|
||||
"获取用户当前摄像头画面。当用户询问当前画面、看到了什么、"
|
||||
"人/物品/环境状态或需要视觉判断时调用。"
|
||||
)
|
||||
focus = " ".join(focus_prompt.split())
|
||||
return f"{base} 当前业务重点:{focus[:600]}" if focus else base
|
||||
|
||||
|
||||
async def generate_vision_focus_prompt(
|
||||
resource: RuntimeModelResource,
|
||||
source_prompt: str,
|
||||
) -> str:
|
||||
"""Generate one editable focus draft with an existing LLM resource."""
|
||||
|
||||
if resource.interface_type not in {"openai-llm", "dashscope-llm"}:
|
||||
raise ValueError(f"当前模型接口不支持提示词生成: {resource.interface_type}")
|
||||
source = source_prompt.strip()
|
||||
if not source:
|
||||
raise ValueError("助手提示词不能为空")
|
||||
values = resource.values or {}
|
||||
secrets = resource.secrets or {}
|
||||
extra_body = values.get("extraBody")
|
||||
extra = {"extra_body": extra_body} if isinstance(extra_body, dict) else {}
|
||||
client = AsyncOpenAI(
|
||||
api_key=_require(str(secrets.get("apiKey") or ""), "LLM apiKey"),
|
||||
base_url=_require(str(values.get("apiUrl") or ""), "LLM apiUrl"),
|
||||
)
|
||||
try:
|
||||
response = await client.chat.completions.create(
|
||||
model=_require(str(values.get("modelId") or ""), "LLM modelId"),
|
||||
messages=[
|
||||
{"role": "system", "content": VISION_FOCUS_GENERATION_SYSTEM_PROMPT},
|
||||
{"role": "user", "content": source},
|
||||
],
|
||||
**extra,
|
||||
prompt_mode = normalize_vision_prompt_mode(mode)
|
||||
prompt = _compact_prompt(configured_prompt, limit=600)
|
||||
if prompt_mode == "fixed":
|
||||
return (
|
||||
f"{base} 每次都检查同一个固定问题:{prompt}",
|
||||
{},
|
||||
[],
|
||||
)
|
||||
finally:
|
||||
await client.close()
|
||||
content = response.choices[0].message.content if response.choices else ""
|
||||
result = (
|
||||
content.strip()
|
||||
if isinstance(content, str)
|
||||
else str(content or "").strip()
|
||||
|
||||
question_description = "结合当前对话生成的、关于当前视频画面的具体问题。"
|
||||
description = base
|
||||
if prompt_mode == "guided":
|
||||
description = f"{base} 生成问题时必须遵守固定引导:{prompt}"
|
||||
question_description = f"结合当前对话生成具体视觉问题,并遵守固定引导:{prompt}"
|
||||
return (
|
||||
description,
|
||||
{"question": {"type": "string", "description": question_description}},
|
||||
["question"],
|
||||
)
|
||||
if not result:
|
||||
raise ValueError("模型没有生成有效的视觉关注目标")
|
||||
if result.startswith("```") and result.endswith("```"):
|
||||
result = result[3:-3].strip()
|
||||
if result.startswith("text"):
|
||||
result = result[4:].strip()
|
||||
return result[:4000]
|
||||
|
||||
|
||||
def resolve_vision_question(
|
||||
mode: VisionPromptMode,
|
||||
configured_prompt: str,
|
||||
arguments: dict[str, Any] | None,
|
||||
) -> str:
|
||||
"""Resolve the exact question sent with the captured image."""
|
||||
|
||||
prompt_mode = normalize_vision_prompt_mode(mode)
|
||||
if prompt_mode == "fixed":
|
||||
question = configured_prompt.strip()
|
||||
else:
|
||||
question = str((arguments or {}).get("question") or "").strip()
|
||||
if not question:
|
||||
raise ValueError(
|
||||
"固定视觉提示词为空"
|
||||
if prompt_mode == "fixed"
|
||||
else "视觉问题为空"
|
||||
)
|
||||
return question
|
||||
|
||||
|
||||
def image_jpeg_bytes(frame: UserImageRawFrame) -> bytes:
|
||||
@@ -173,9 +186,7 @@ async def analyze_image_with_vision_model(
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": vision_analysis_system_prompt(
|
||||
cfg.vision_focus_prompt
|
||||
),
|
||||
"content": VISION_ANALYSIS_SYSTEM_PROMPT,
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
|
||||
@@ -49,7 +49,7 @@ class WorkflowAgentStage:
|
||||
if stage.vision_enabled:
|
||||
stage_prompt = (
|
||||
f"{stage_prompt}\n\n[视觉能力]\n"
|
||||
f"{vision_system_hint(stage.vision_focus_prompt)}"
|
||||
f"{vision_system_hint(stage.vision_prompt_mode, stage.vision_prompt)}"
|
||||
)
|
||||
instruction = (
|
||||
EDGE_TOOL_STAGE_INSTRUCTION
|
||||
@@ -112,7 +112,8 @@ class WorkflowAgentStage:
|
||||
{
|
||||
"enabled": stage.vision_enabled,
|
||||
"vision_model_resource_id": stage.vision_model_resource_id,
|
||||
"vision_focus_prompt": stage.vision_focus_prompt,
|
||||
"vision_prompt_mode": stage.vision_prompt_mode,
|
||||
"vision_prompt": stage.vision_prompt,
|
||||
"llm_resource_id": stage.llm_resource_id,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -34,8 +34,9 @@ from services.vision import (
|
||||
VISION_TOOL_NAME,
|
||||
analyze_image_with_vision_model,
|
||||
config_with_vision_resource,
|
||||
resolve_vision_question,
|
||||
vision_system_hint,
|
||||
vision_tool_description,
|
||||
vision_tool_schema_parts,
|
||||
)
|
||||
from services.workflow.agent import EDGE_TOOL_STAGE_INSTRUCTION
|
||||
from services.workflow.models import WorkflowRuntimeState, WorkflowStatus
|
||||
@@ -414,7 +415,10 @@ class WorkflowRealtimeController:
|
||||
f"[工作流执行规则]\n{EDGE_TOOL_STAGE_INSTRUCTION}"
|
||||
)
|
||||
if stage.vision_enabled:
|
||||
prompt = f"{prompt}\n{vision_system_hint(stage.vision_focus_prompt)}"
|
||||
prompt = (
|
||||
f"{prompt}\n"
|
||||
f"{vision_system_hint(stage.vision_prompt_mode, stage.vision_prompt)}"
|
||||
)
|
||||
return prompt
|
||||
|
||||
def _build_agent_tools(self, node_id: str) -> list[RealtimeTool]:
|
||||
@@ -573,10 +577,15 @@ class WorkflowRealtimeController:
|
||||
{"status": "stale", "message": "当前 Agent 已经切换。"},
|
||||
continue_response=False,
|
||||
)
|
||||
question = str(arguments.get("question") or "").strip()
|
||||
if not question:
|
||||
try:
|
||||
question = resolve_vision_question(
|
||||
stage.vision_prompt_mode,
|
||||
stage.vision_prompt,
|
||||
arguments,
|
||||
)
|
||||
except ValueError as exc:
|
||||
return RealtimeToolResult(
|
||||
{"status": "error", "message": "视觉问题为空"}
|
||||
{"status": "error", "message": str(exc)}
|
||||
)
|
||||
if self._runtime.capture_image is None:
|
||||
return RealtimeToolResult(
|
||||
@@ -605,7 +614,6 @@ class WorkflowRealtimeController:
|
||||
|
||||
try:
|
||||
if vision_cfg is not None:
|
||||
vision_cfg.vision_focus_prompt = stage.vision_focus_prompt
|
||||
observation = await analyze_image_with_vision_model(
|
||||
vision_cfg,
|
||||
frame,
|
||||
@@ -634,17 +642,16 @@ class WorkflowRealtimeController:
|
||||
}
|
||||
)
|
||||
|
||||
description, properties, required = vision_tool_schema_parts(
|
||||
stage.vision_prompt_mode,
|
||||
stage.vision_prompt,
|
||||
)
|
||||
return (
|
||||
RealtimeTool(
|
||||
name=VISION_TOOL_NAME,
|
||||
description=vision_tool_description(stage.vision_focus_prompt),
|
||||
properties={
|
||||
"question": {
|
||||
"type": "string",
|
||||
"description": "需要根据当前画面判断的具体问题",
|
||||
}
|
||||
},
|
||||
required=("question",),
|
||||
description=description,
|
||||
properties=properties,
|
||||
required=tuple(required),
|
||||
),
|
||||
handler,
|
||||
)
|
||||
|
||||
@@ -23,7 +23,8 @@ class AgentStageConfig:
|
||||
tts_resource_id: str
|
||||
vision_enabled: bool
|
||||
vision_model_resource_id: str | None
|
||||
vision_focus_prompt: str
|
||||
vision_prompt_mode: str
|
||||
vision_prompt: str
|
||||
tool_ids: tuple[str, ...]
|
||||
state_variable_names: tuple[str, ...]
|
||||
knowledge_base_id: str | None
|
||||
@@ -152,7 +153,8 @@ class WorkflowEngine:
|
||||
vision_model_resource_id=(
|
||||
str(source.get("visionModelResourceId") or "") or None
|
||||
),
|
||||
vision_focus_prompt=str(source.get("visionFocusPrompt") or "").strip(),
|
||||
vision_prompt_mode=str(source.get("visionPromptMode") or "runtime"),
|
||||
vision_prompt=str(source.get("visionPrompt") or "").strip(),
|
||||
tool_ids=tuple(str(tool_id) for tool_id in source.get("toolIds") or []),
|
||||
# Variable write scope always belongs to the Agent node, even when
|
||||
# its selected tool resources come from Workflow defaults.
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from models import AssistantConfig, RuntimeTool
|
||||
from pipecat.flows import FlowManager
|
||||
@@ -1782,7 +1782,7 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
||||
}
|
||||
)
|
||||
scopes = []
|
||||
vision_function = object()
|
||||
vision_function = MagicMock(side_effect=lambda _stage: object())
|
||||
|
||||
async def queue_frame(_frame):
|
||||
pass
|
||||
@@ -1800,13 +1800,18 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
await brain._apply_agent_stage("agent")
|
||||
inherited_config = brain._agent_config("agent")
|
||||
self.assertIn(vision_function, inherited_config["functions"])
|
||||
self.assertEqual(len(inherited_config["functions"]), 1)
|
||||
vision_function.assert_called_once()
|
||||
selected_stage = vision_function.call_args.args[0]
|
||||
self.assertEqual(selected_stage.vision_prompt_mode, "runtime")
|
||||
self.assertIn("fetch_user_image", inherited_config["role_message"])
|
||||
self.assertEqual(
|
||||
scopes[-1],
|
||||
{
|
||||
"enabled": True,
|
||||
"vision_model_resource_id": "vision_global",
|
||||
"vision_prompt_mode": "runtime",
|
||||
"vision_prompt": "",
|
||||
"llm_resource_id": "llm_global",
|
||||
},
|
||||
)
|
||||
@@ -1821,7 +1826,7 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
||||
)
|
||||
await brain._apply_agent_stage("agent")
|
||||
custom_config = brain._agent_config("agent")
|
||||
self.assertNotIn(vision_function, custom_config["functions"])
|
||||
self.assertEqual(custom_config["functions"], [])
|
||||
self.assertNotIn("fetch_user_image", custom_config["role_message"])
|
||||
self.assertFalse(scopes[-1]["enabled"])
|
||||
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from models import RuntimeModelResource
|
||||
from services.vision import (
|
||||
VISION_ANALYSIS_SYSTEM_PROMPT,
|
||||
VISION_SYSTEM_HINT,
|
||||
generate_vision_focus_prompt,
|
||||
vision_analysis_system_prompt,
|
||||
vision_system_hint,
|
||||
vision_tool_description,
|
||||
)
|
||||
from services.workflow_engine import WorkflowEngine
|
||||
|
||||
|
||||
class VisionFocusPromptTest(unittest.TestCase):
|
||||
def test_focus_extends_but_does_not_replace_safety_prompts(self):
|
||||
focus = "优先识别车辆数量和人员状态,只报告可确认的信息。"
|
||||
|
||||
system_hint = vision_system_hint(focus)
|
||||
analysis_prompt = vision_analysis_system_prompt(focus)
|
||||
|
||||
self.assertIn(VISION_SYSTEM_HINT, system_hint)
|
||||
self.assertIn(focus, system_hint)
|
||||
self.assertIn(VISION_ANALYSIS_SYSTEM_PROMPT, analysis_prompt)
|
||||
self.assertIn(focus, analysis_prompt)
|
||||
self.assertIn("优先回答当前用户问题", analysis_prompt)
|
||||
|
||||
def test_empty_focus_preserves_previous_runtime_prompts(self):
|
||||
self.assertEqual(vision_system_hint(), VISION_SYSTEM_HINT)
|
||||
self.assertEqual(
|
||||
vision_analysis_system_prompt(),
|
||||
VISION_ANALYSIS_SYSTEM_PROMPT,
|
||||
)
|
||||
|
||||
def test_tool_description_compacts_and_limits_focus(self):
|
||||
description = vision_tool_description("车辆\n 数量 " * 300)
|
||||
|
||||
self.assertIn("当前业务重点", description)
|
||||
self.assertNotIn("\n", description)
|
||||
self.assertLess(len(description), 700)
|
||||
|
||||
def test_workflow_resolves_global_and_node_focus(self):
|
||||
graph = {
|
||||
"specVersion": 3,
|
||||
"settings": {
|
||||
"visionEnabled": True,
|
||||
"visionFocusPrompt": "全局关注目标",
|
||||
},
|
||||
"nodes": [
|
||||
{
|
||||
"id": "global-agent",
|
||||
"type": "agent",
|
||||
"data": {"name": "全局 Agent", "prompt": "处理任务"},
|
||||
},
|
||||
{
|
||||
"id": "custom-agent",
|
||||
"type": "agent",
|
||||
"data": {
|
||||
"name": "独立 Agent",
|
||||
"prompt": "检查车辆",
|
||||
"inheritGlobalConfig": False,
|
||||
"visionEnabled": True,
|
||||
"visionFocusPrompt": "节点关注目标",
|
||||
},
|
||||
},
|
||||
],
|
||||
"edges": [],
|
||||
}
|
||||
engine = WorkflowEngine(graph)
|
||||
|
||||
self.assertEqual(
|
||||
engine.agent_stage_config("global-agent").vision_focus_prompt,
|
||||
"全局关注目标",
|
||||
)
|
||||
self.assertEqual(
|
||||
engine.agent_stage_config("custom-agent").vision_focus_prompt,
|
||||
"节点关注目标",
|
||||
)
|
||||
|
||||
|
||||
class VisionFocusGenerationTest(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_generates_editable_focus_with_selected_resource(self):
|
||||
resource = RuntimeModelResource(
|
||||
id="llm_1",
|
||||
name="生成模型",
|
||||
capability="LLM",
|
||||
interface_type="openai-llm",
|
||||
values={
|
||||
"apiUrl": "https://llm.example/v1",
|
||||
"modelId": "focus-model",
|
||||
},
|
||||
secrets={"apiKey": "secret"},
|
||||
)
|
||||
client = MagicMock()
|
||||
client.chat.completions.create = AsyncMock(
|
||||
return_value=SimpleNamespace(
|
||||
choices=[
|
||||
SimpleNamespace(
|
||||
message=SimpleNamespace(
|
||||
content="优先识别车辆数量、损伤位置和人员状态。"
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
client.close = AsyncMock()
|
||||
|
||||
with patch("services.vision.AsyncOpenAI", return_value=client):
|
||||
result = await generate_vision_focus_prompt(
|
||||
resource,
|
||||
"你是交通事故处理助手,需要了解车辆和人员情况。",
|
||||
)
|
||||
|
||||
self.assertEqual(result, "优先识别车辆数量、损伤位置和人员状态。")
|
||||
request = client.chat.completions.create.await_args.kwargs
|
||||
self.assertEqual(request["model"], "focus-model")
|
||||
self.assertIn("交通事故处理助手", request["messages"][1]["content"])
|
||||
client.close.assert_awaited_once()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,227 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
from models import AssistantConfig
|
||||
from services.brains.base import RealtimeBrainRuntime
|
||||
from services.node_specs import normalize_graph, validate_graph
|
||||
from services.vision import (
|
||||
VISION_SYSTEM_HINT,
|
||||
resolve_vision_question,
|
||||
vision_system_hint,
|
||||
vision_tool_schema_parts,
|
||||
)
|
||||
from services.workflow_engine import WorkflowEngine
|
||||
from services.workflow.realtime import WorkflowRealtimeController
|
||||
|
||||
|
||||
class VisionPromptModeTest(unittest.TestCase):
|
||||
def test_fixed_mode_has_no_runtime_question_parameter(self):
|
||||
description, properties, required = vision_tool_schema_parts(
|
||||
"fixed",
|
||||
"识别车辆数量和人员状态",
|
||||
)
|
||||
|
||||
self.assertIn("识别车辆数量和人员状态", description)
|
||||
self.assertEqual(properties, {})
|
||||
self.assertEqual(required, [])
|
||||
self.assertEqual(
|
||||
resolve_vision_question("fixed", " 识别车辆数量 ", {}),
|
||||
"识别车辆数量",
|
||||
)
|
||||
|
||||
def test_runtime_mode_requires_model_generated_question(self):
|
||||
description, properties, required = vision_tool_schema_parts("runtime")
|
||||
|
||||
self.assertNotIn("固定引导", description)
|
||||
self.assertIn("question", properties)
|
||||
self.assertEqual(required, ["question"])
|
||||
self.assertEqual(
|
||||
resolve_vision_question(
|
||||
"runtime",
|
||||
"不会使用这个配置",
|
||||
{"question": "用户手里拿着什么?"},
|
||||
),
|
||||
"用户手里拿着什么?",
|
||||
)
|
||||
|
||||
def test_guided_mode_exposes_guide_but_uses_runtime_question(self):
|
||||
guide = "只询问画面中可直接确认的车辆损伤"
|
||||
description, properties, required = vision_tool_schema_parts(
|
||||
"guided",
|
||||
guide,
|
||||
)
|
||||
|
||||
self.assertIn(guide, description)
|
||||
self.assertIn(guide, properties["question"]["description"])
|
||||
self.assertEqual(required, ["question"])
|
||||
self.assertEqual(
|
||||
resolve_vision_question(
|
||||
"guided",
|
||||
guide,
|
||||
{"question": "右侧车门是否有明显凹陷?"},
|
||||
),
|
||||
"右侧车门是否有明显凹陷?",
|
||||
)
|
||||
|
||||
def test_mode_specific_system_hints(self):
|
||||
self.assertIn(VISION_SYSTEM_HINT, vision_system_hint("runtime"))
|
||||
self.assertIn("不需要生成", vision_system_hint("fixed", "固定问题"))
|
||||
self.assertIn("固定引导", vision_system_hint("guided", "业务引导"))
|
||||
|
||||
def test_missing_required_config_or_question_is_rejected(self):
|
||||
with self.assertRaisesRegex(ValueError, "固定视觉提示词为空"):
|
||||
resolve_vision_question("fixed", "", {})
|
||||
with self.assertRaisesRegex(ValueError, "视觉问题为空"):
|
||||
resolve_vision_question("runtime", "", {})
|
||||
|
||||
|
||||
class WorkflowVisionPromptModeTest(unittest.TestCase):
|
||||
def test_workflow_resolves_global_and_node_modes(self):
|
||||
graph = {
|
||||
"specVersion": 3,
|
||||
"settings": {
|
||||
"visionEnabled": True,
|
||||
"visionPromptMode": "fixed",
|
||||
"visionPrompt": "全局固定问题",
|
||||
},
|
||||
"nodes": [
|
||||
{
|
||||
"id": "global-agent",
|
||||
"type": "agent",
|
||||
"data": {"name": "全局 Agent", "prompt": "处理任务"},
|
||||
},
|
||||
{
|
||||
"id": "custom-agent",
|
||||
"type": "agent",
|
||||
"data": {
|
||||
"name": "独立 Agent",
|
||||
"prompt": "检查车辆",
|
||||
"inheritGlobalConfig": False,
|
||||
"visionEnabled": True,
|
||||
"visionPromptMode": "guided",
|
||||
"visionPrompt": "节点引导",
|
||||
},
|
||||
},
|
||||
],
|
||||
"edges": [],
|
||||
}
|
||||
engine = WorkflowEngine(graph)
|
||||
|
||||
global_stage = engine.agent_stage_config("global-agent")
|
||||
self.assertEqual(global_stage.vision_prompt_mode, "fixed")
|
||||
self.assertEqual(global_stage.vision_prompt, "全局固定问题")
|
||||
custom_stage = engine.agent_stage_config("custom-agent")
|
||||
self.assertEqual(custom_stage.vision_prompt_mode, "guided")
|
||||
self.assertEqual(custom_stage.vision_prompt, "节点引导")
|
||||
|
||||
def test_legacy_focus_is_migrated_to_guided_mode(self):
|
||||
normalized = normalize_graph(
|
||||
{
|
||||
"specVersion": 3,
|
||||
"settings": {"visionFocusPrompt": "旧全局关注目标"},
|
||||
"nodes": [],
|
||||
"edges": [],
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(normalized["settings"]["visionPromptMode"], "guided")
|
||||
self.assertEqual(normalized["settings"]["visionPrompt"], "旧全局关注目标")
|
||||
self.assertNotIn("visionFocusPrompt", normalized["settings"])
|
||||
|
||||
def test_fixed_or_guided_mode_requires_prompt_when_vision_is_enabled(self):
|
||||
graph = {
|
||||
"specVersion": 3,
|
||||
"settings": {
|
||||
"visionEnabled": True,
|
||||
"visionPromptMode": "fixed",
|
||||
"visionPrompt": "",
|
||||
},
|
||||
"nodes": [
|
||||
{
|
||||
"id": "agent",
|
||||
"type": "agent",
|
||||
"data": {"name": "Agent", "prompt": "处理任务"},
|
||||
}
|
||||
],
|
||||
"edges": [],
|
||||
}
|
||||
|
||||
self.assertTrue(
|
||||
any("必须填写视觉提示词" in error for error in validate_graph(graph))
|
||||
)
|
||||
|
||||
def test_invalid_mode_is_reported_instead_of_silently_rewritten(self):
|
||||
graph = {
|
||||
"specVersion": 3,
|
||||
"settings": {"visionPromptMode": "unexpected"},
|
||||
"nodes": [
|
||||
{
|
||||
"id": "agent",
|
||||
"type": "agent",
|
||||
"data": {"name": "Agent", "prompt": "处理任务"},
|
||||
}
|
||||
],
|
||||
"edges": [],
|
||||
}
|
||||
|
||||
self.assertTrue(
|
||||
any("视觉提示词模式无效" in error for error in validate_graph(graph))
|
||||
)
|
||||
|
||||
def test_realtime_tool_contract_changes_with_mode(self):
|
||||
graph = {
|
||||
"specVersion": 3,
|
||||
"settings": {},
|
||||
"nodes": [
|
||||
{
|
||||
"id": "fixed-agent",
|
||||
"type": "agent",
|
||||
"data": {
|
||||
"name": "固定",
|
||||
"prompt": "处理任务",
|
||||
"inheritGlobalConfig": False,
|
||||
"visionEnabled": True,
|
||||
"visionPromptMode": "fixed",
|
||||
"visionPrompt": "检查安全帽",
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "guided-agent",
|
||||
"type": "agent",
|
||||
"data": {
|
||||
"name": "引导",
|
||||
"prompt": "处理任务",
|
||||
"inheritGlobalConfig": False,
|
||||
"visionEnabled": True,
|
||||
"visionPromptMode": "guided",
|
||||
"visionPrompt": "只询问安全设备",
|
||||
},
|
||||
},
|
||||
],
|
||||
"edges": [],
|
||||
}
|
||||
engine = WorkflowEngine(graph)
|
||||
controller = WorkflowRealtimeController(
|
||||
cfg=AssistantConfig(type="workflow"),
|
||||
engine=engine,
|
||||
store=SimpleNamespace(),
|
||||
runtime=RealtimeBrainRuntime(
|
||||
realtime=SimpleNamespace(),
|
||||
queue_frame=lambda _frame: None,
|
||||
call_end=SimpleNamespace(ending=False),
|
||||
),
|
||||
)
|
||||
|
||||
fixed_tool, _fixed_handler = controller._vision_tool("fixed-agent", 0)
|
||||
guided_tool, _guided_handler = controller._vision_tool("guided-agent", 0)
|
||||
|
||||
self.assertEqual(fixed_tool.properties, {})
|
||||
self.assertEqual(fixed_tool.required, ())
|
||||
self.assertIn("只询问安全设备", guided_tool.description)
|
||||
self.assertEqual(guided_tool.required, ("question",))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -648,17 +648,19 @@ export function PromptEditor({
|
||||
hint="开启后,助手会获得读取当前视频画面的工具。选择「模型自己」时,大语言模型必须支持图片输入。"
|
||||
enabled={form.visionEnabled}
|
||||
modelResourceId={form.visionModelResourceId}
|
||||
focusPrompt={form.visionFocusPrompt}
|
||||
sourcePrompt={form.prompt}
|
||||
generatorModelResourceId={form.model}
|
||||
promptMode={form.visionPromptMode}
|
||||
prompt={form.visionPrompt}
|
||||
mainModelResourceId={form.model}
|
||||
modelOptions={visionModelOptions}
|
||||
onEnabledChange={handlePromptVisionEnabledChange}
|
||||
onModelResourceIdChange={(value) =>
|
||||
updateForm("visionModelResourceId", value)
|
||||
}
|
||||
onFocusPromptChange={(value) =>
|
||||
updateForm("visionFocusPrompt", value)
|
||||
onPromptModeChange={(value) =>
|
||||
updateForm("visionPromptMode", value)
|
||||
}
|
||||
onPromptChange={(value) =>
|
||||
updateForm("visionPrompt", value)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
KnowledgeRetrievalConfig,
|
||||
StartupConfig,
|
||||
TurnConfig,
|
||||
VisionPromptMode,
|
||||
} from "@/lib/api";
|
||||
|
||||
export type RuntimeMode = "pipeline" | "realtime";
|
||||
@@ -26,6 +27,7 @@ export type AssistantForm = {
|
||||
startup: StartupConfig;
|
||||
visionEnabled: boolean;
|
||||
visionModelResourceId: string;
|
||||
visionFocusPrompt: string;
|
||||
visionPromptMode: VisionPromptMode;
|
||||
visionPrompt: string;
|
||||
toolIds: string[];
|
||||
};
|
||||
|
||||
@@ -1,53 +1,71 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Camera, Loader2, Sparkles } from "lucide-react";
|
||||
import { Camera, Check } from "lucide-react";
|
||||
|
||||
import {
|
||||
ResourceSelectField,
|
||||
ToggleRow,
|
||||
} from "@/components/assistant-editor/editor-controls";
|
||||
import { SectionCard } from "@/components/editor/section-card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { assistantsApi } from "@/lib/api";
|
||||
import type { VisionPromptMode } from "@/lib/api";
|
||||
|
||||
type VisionModelOption = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
const promptModes: Array<{
|
||||
value: VisionPromptMode;
|
||||
title: string;
|
||||
description: string;
|
||||
}> = [
|
||||
{
|
||||
value: "fixed",
|
||||
title: "固定提示词",
|
||||
description: "每次获取画面都发送同一个问题。",
|
||||
},
|
||||
{
|
||||
value: "runtime",
|
||||
title: "运行时自动生成",
|
||||
description: "模型结合当前对话,临时生成看图问题。",
|
||||
},
|
||||
{
|
||||
value: "guided",
|
||||
title: "固定提示词引导",
|
||||
description: "模型动态生成问题,同时遵守固定引导。",
|
||||
},
|
||||
];
|
||||
|
||||
export function VisionConfigSection({
|
||||
description,
|
||||
hint,
|
||||
enabled,
|
||||
modelResourceId,
|
||||
focusPrompt,
|
||||
sourcePrompt,
|
||||
generatorModelResourceId,
|
||||
promptMode,
|
||||
prompt,
|
||||
mainModelResourceId,
|
||||
mainModelSupportsImageInput,
|
||||
modelOptions,
|
||||
onEnabledChange,
|
||||
onModelResourceIdChange,
|
||||
onFocusPromptChange,
|
||||
onPromptModeChange,
|
||||
onPromptChange,
|
||||
}: {
|
||||
description: string;
|
||||
hint: string;
|
||||
enabled: boolean;
|
||||
modelResourceId: string;
|
||||
focusPrompt: string;
|
||||
sourcePrompt: string;
|
||||
generatorModelResourceId: string;
|
||||
promptMode: VisionPromptMode;
|
||||
prompt: string;
|
||||
mainModelResourceId: string;
|
||||
mainModelSupportsImageInput?: boolean;
|
||||
modelOptions: VisionModelOption[];
|
||||
onEnabledChange: (enabled: boolean) => void;
|
||||
onModelResourceIdChange: (modelResourceId: string) => void;
|
||||
onFocusPromptChange: (focusPrompt: string) => void;
|
||||
onPromptModeChange: (promptMode: VisionPromptMode) => void;
|
||||
onPromptChange: (prompt: string) => void;
|
||||
}) {
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [generationError, setGenerationError] = useState("");
|
||||
const mainModelSupportsVision =
|
||||
mainModelSupportsImageInput ??
|
||||
modelOptions.some((option) => option.value === mainModelResourceId);
|
||||
@@ -55,25 +73,6 @@ export function VisionConfigSection({
|
||||
(option) => option.value !== mainModelResourceId,
|
||||
);
|
||||
|
||||
async function generateFocusPrompt() {
|
||||
if (!sourcePrompt.trim() || !generatorModelResourceId || generating) return;
|
||||
setGenerating(true);
|
||||
setGenerationError("");
|
||||
try {
|
||||
const result = await assistantsApi.generateVisionFocus({
|
||||
sourcePrompt,
|
||||
modelResourceId: generatorModelResourceId,
|
||||
});
|
||||
onFocusPromptChange(result.focusPrompt);
|
||||
} catch (error) {
|
||||
setGenerationError(
|
||||
error instanceof Error ? error.message : "生成视觉关注目标失败",
|
||||
);
|
||||
} finally {
|
||||
setGenerating(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionCard
|
||||
icon={<Camera size={15} />}
|
||||
@@ -100,56 +99,79 @@ export function VisionConfigSection({
|
||||
当前模型未标记支持图片输入,请选择独立视觉模型。
|
||||
</p>
|
||||
)}
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
视觉关注目标
|
||||
</label>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 gap-1.5 rounded-full px-2.5 text-xs"
|
||||
disabled={
|
||||
generating || !sourcePrompt.trim() || !generatorModelResourceId
|
||||
}
|
||||
onClick={() => void generateFocusPrompt()}
|
||||
title={
|
||||
!sourcePrompt.trim()
|
||||
? "请先填写助手提示词"
|
||||
: !generatorModelResourceId
|
||||
? "请先选择大语言模型"
|
||||
: "根据当前助手提示词生成可编辑草稿"
|
||||
}
|
||||
>
|
||||
{generating ? (
|
||||
<Loader2 size={12} className="animate-spin" />
|
||||
) : (
|
||||
<Sparkles size={12} />
|
||||
)}
|
||||
{generating ? "生成中" : "根据提示词生成"}
|
||||
</Button>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
看图问题生成方式
|
||||
</label>
|
||||
<div role="radiogroup" aria-label="看图问题生成方式" className="space-y-2">
|
||||
{promptModes.map((option) => {
|
||||
const selected = promptMode === option.value;
|
||||
return (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={selected}
|
||||
onClick={() => onPromptModeChange(option.value)}
|
||||
className={[
|
||||
"flex w-full items-start gap-3 rounded-xl border px-3.5 py-3 text-left transition-colors",
|
||||
selected
|
||||
? "border-primary bg-surface-strong/70 ring-1 ring-primary"
|
||||
: "border-hairline bg-background hover:border-hairline-strong",
|
||||
].join(" ")}
|
||||
>
|
||||
<span
|
||||
className={[
|
||||
"mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full border",
|
||||
selected
|
||||
? "border-primary bg-primary text-primary-foreground"
|
||||
: "border-hairline-strong text-transparent",
|
||||
].join(" ")}
|
||||
>
|
||||
<Check size={12} />
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block text-sm font-medium text-foreground">
|
||||
{option.title}
|
||||
</span>
|
||||
<span className="mt-0.5 block text-xs leading-5 text-muted-foreground">
|
||||
{option.description}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Textarea
|
||||
value={focusPrompt}
|
||||
maxLength={4000}
|
||||
rows={5}
|
||||
onChange={(event) => {
|
||||
onFocusPromptChange(event.target.value);
|
||||
if (generationError) setGenerationError("");
|
||||
}}
|
||||
placeholder="例如:优先识别车辆数量、碰撞位置、明显损伤和人员状态;只报告画面中可直接确认的信息。"
|
||||
className="field-sizing-fixed min-h-28 resize-y border-hairline-strong bg-background text-sm leading-6 text-foreground placeholder:text-muted-soft"
|
||||
/>
|
||||
<p className="text-[11px] leading-5 text-muted-soft">
|
||||
可选。系统仍保留通用视觉安全规则;这里用于让每次看图稳定聚焦业务目标。
|
||||
</p>
|
||||
{generationError && (
|
||||
<p className="text-xs leading-5 text-destructive">
|
||||
{generationError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{promptMode !== "runtime" && (
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{promptMode === "fixed" ? "固定视觉提示词" : "运行时提示词引导"}
|
||||
</label>
|
||||
<Textarea
|
||||
value={prompt}
|
||||
maxLength={4000}
|
||||
rows={5}
|
||||
onChange={(event) => onPromptChange(event.target.value)}
|
||||
placeholder={
|
||||
promptMode === "fixed"
|
||||
? "例如:请识别画面中的车辆数量、碰撞位置、明显损伤和人员状态。"
|
||||
: "例如:问题应聚焦车辆数量、碰撞位置和人员状态;只询问画面中可直接判断的信息。"
|
||||
}
|
||||
className="field-sizing-fixed min-h-28 resize-y border-hairline-strong bg-background text-sm leading-6 text-foreground placeholder:text-muted-soft"
|
||||
/>
|
||||
<p className="text-[11px] leading-5 text-muted-soft">
|
||||
{promptMode === "fixed"
|
||||
? "工具调用时不会让模型改写这段内容。"
|
||||
: "模型会结合当前对话生成具体问题,并受到这段内容约束。"}
|
||||
</p>
|
||||
{!prompt.trim() && (
|
||||
<p className="text-xs leading-5 text-destructive">
|
||||
当前模式需要填写提示词后才能保存。
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</SectionCard>
|
||||
|
||||
@@ -180,7 +180,8 @@ function blankPromptForm(name: string): AssistantForm {
|
||||
},
|
||||
visionEnabled: false,
|
||||
visionModelResourceId: "",
|
||||
visionFocusPrompt: "",
|
||||
visionPromptMode: "runtime",
|
||||
visionPrompt: "",
|
||||
toolIds: [],
|
||||
};
|
||||
}
|
||||
@@ -436,7 +437,8 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
},
|
||||
visionEnabled: a.visionEnabled,
|
||||
visionModelResourceId: a.visionModelResourceId ?? "",
|
||||
visionFocusPrompt: a.visionFocusPrompt ?? "",
|
||||
visionPromptMode: a.visionPromptMode ?? "runtime",
|
||||
visionPrompt: a.visionPrompt ?? "",
|
||||
toolIds: a.toolIds ?? [],
|
||||
};
|
||||
setForm(next);
|
||||
@@ -487,7 +489,8 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
},
|
||||
visionEnabled: false,
|
||||
visionModelResourceId: null,
|
||||
visionFocusPrompt: "",
|
||||
visionPromptMode: "runtime",
|
||||
visionPrompt: "",
|
||||
modelResourceIds: {},
|
||||
knowledgeBaseId: null,
|
||||
knowledgeRetrievalConfig: defaultKnowledgeRetrievalConfig(),
|
||||
@@ -552,7 +555,8 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
startup: form.startup,
|
||||
visionEnabled: form.visionEnabled,
|
||||
visionModelResourceId: form.visionModelResourceId || null,
|
||||
visionFocusPrompt: form.visionFocusPrompt,
|
||||
visionPromptMode: form.visionPromptMode,
|
||||
visionPrompt: form.visionPrompt,
|
||||
modelResourceIds: {
|
||||
...(form.model ? { LLM: form.model } : {}),
|
||||
...(form.asr ? { ASR: form.asr } : {}),
|
||||
@@ -680,7 +684,8 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
graph.settings?.visionModelResourceId ??
|
||||
assistant.visionModelResourceId ??
|
||||
"",
|
||||
visionFocusPrompt: graph.settings?.visionFocusPrompt ?? "",
|
||||
visionPromptMode: graph.settings?.visionPromptMode ?? "runtime",
|
||||
visionPrompt: graph.settings?.visionPrompt ?? "",
|
||||
toolIds: graph.settings?.toolIds ?? [],
|
||||
knowledgeBaseId:
|
||||
graph.settings?.knowledgeBaseId || assistant.knowledgeBaseId || "",
|
||||
|
||||
@@ -108,7 +108,8 @@ function baseUpsert(over: Partial<AssistantUpsert>): AssistantUpsert {
|
||||
},
|
||||
visionEnabled: false,
|
||||
visionModelResourceId: null,
|
||||
visionFocusPrompt: "",
|
||||
visionPromptMode: "runtime",
|
||||
visionPrompt: "",
|
||||
modelResourceIds: {},
|
||||
knowledgeBaseId: null,
|
||||
knowledgeRetrievalConfig: defaultKnowledgeRetrievalConfig(),
|
||||
|
||||
@@ -96,7 +96,8 @@ function baseUpsertFromTemplate(
|
||||
},
|
||||
visionEnabled: false,
|
||||
visionModelResourceId: null,
|
||||
visionFocusPrompt: "",
|
||||
visionPromptMode: "runtime",
|
||||
visionPrompt: "",
|
||||
modelResourceIds: {},
|
||||
knowledgeBaseId: null,
|
||||
knowledgeRetrievalConfig: defaultKnowledgeRetrievalConfig(),
|
||||
|
||||
@@ -129,7 +129,8 @@ function fromFlow(nodes: Node[], edges: Edge[]): WorkflowGraph {
|
||||
defaultTtsResourceId: "",
|
||||
visionEnabled: false,
|
||||
visionModelResourceId: "",
|
||||
visionFocusPrompt: "",
|
||||
visionPromptMode: "runtime",
|
||||
visionPrompt: "",
|
||||
toolIds: [],
|
||||
knowledgeBaseId: "",
|
||||
knowledgeMode: "automatic",
|
||||
|
||||
@@ -15,7 +15,8 @@ export function settingsFromWorkflowGraph(
|
||||
tts: graph.settings.defaultTtsResourceId,
|
||||
visionEnabled: graph.settings.visionEnabled,
|
||||
visionModelResourceId: graph.settings.visionModelResourceId,
|
||||
visionFocusPrompt: graph.settings.visionFocusPrompt,
|
||||
visionPromptMode: graph.settings.visionPromptMode,
|
||||
visionPrompt: graph.settings.visionPrompt,
|
||||
toolIds: graph.settings.toolIds,
|
||||
knowledgeBaseId: graph.settings.knowledgeBaseId,
|
||||
knowledgeRetrievalConfig: {
|
||||
@@ -45,7 +46,8 @@ export function workflowGraphWithSettings(
|
||||
defaultTtsResourceId: settings.tts ?? "",
|
||||
visionEnabled: settings.visionEnabled,
|
||||
visionModelResourceId: settings.visionModelResourceId,
|
||||
visionFocusPrompt: settings.visionFocusPrompt,
|
||||
visionPromptMode: settings.visionPromptMode,
|
||||
visionPrompt: settings.visionPrompt,
|
||||
toolIds: settings.toolIds,
|
||||
knowledgeBaseId: settings.knowledgeBaseId,
|
||||
knowledgeMode: settings.knowledgeRetrievalConfig.mode,
|
||||
|
||||
@@ -78,8 +78,10 @@ export function AgentNodePanel({
|
||||
visionModelResourceId:
|
||||
(draft.visionModelResourceId as string) ||
|
||||
workflowSettings.visionModelResourceId,
|
||||
visionFocusPrompt:
|
||||
draft.visionFocusPrompt ?? workflowSettings.visionFocusPrompt,
|
||||
visionPromptMode:
|
||||
draft.visionPromptMode ?? workflowSettings.visionPromptMode,
|
||||
visionPrompt:
|
||||
draft.visionPrompt ?? workflowSettings.visionPrompt,
|
||||
...(!isRealtime
|
||||
? {
|
||||
llmResourceId:
|
||||
@@ -308,13 +310,8 @@ export function AgentNodePanel({
|
||||
}
|
||||
enabled={Boolean(draft.visionEnabled)}
|
||||
modelResourceId={draft.visionModelResourceId ?? ""}
|
||||
focusPrompt={draft.visionFocusPrompt ?? ""}
|
||||
sourcePrompt={draft.prompt ?? ""}
|
||||
generatorModelResourceId={
|
||||
isRealtime
|
||||
? draft.visionModelResourceId ?? ""
|
||||
: (draft.llmResourceId as string) || ""
|
||||
}
|
||||
promptMode={draft.visionPromptMode ?? "runtime"}
|
||||
prompt={draft.visionPrompt ?? ""}
|
||||
mainModelResourceId={
|
||||
isRealtime
|
||||
? workflowSettings.realtime ?? ""
|
||||
@@ -339,8 +336,11 @@ export function AgentNodePanel({
|
||||
onModelResourceIdChange={(visionModelResourceId) =>
|
||||
set("visionModelResourceId", visionModelResourceId)
|
||||
}
|
||||
onFocusPromptChange={(visionFocusPrompt) =>
|
||||
set("visionFocusPrompt", visionFocusPrompt)
|
||||
onPromptModeChange={(visionPromptMode) =>
|
||||
set("visionPromptMode", visionPromptMode)
|
||||
}
|
||||
onPromptChange={(visionPrompt) =>
|
||||
set("visionPrompt", visionPrompt)
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
@@ -184,13 +184,8 @@ export function GlobalSettingsPanel({
|
||||
}
|
||||
enabled={settings.visionEnabled}
|
||||
modelResourceId={settings.visionModelResourceId}
|
||||
focusPrompt={settings.visionFocusPrompt}
|
||||
sourcePrompt={settings.globalPrompt}
|
||||
generatorModelResourceId={
|
||||
settings.runtimeMode === "realtime"
|
||||
? settings.visionModelResourceId
|
||||
: settings.llm ?? ""
|
||||
}
|
||||
promptMode={settings.visionPromptMode}
|
||||
prompt={settings.visionPrompt}
|
||||
mainModelResourceId={
|
||||
settings.runtimeMode === "realtime"
|
||||
? settings.realtime ?? ""
|
||||
@@ -216,8 +211,11 @@ export function GlobalSettingsPanel({
|
||||
onModelResourceIdChange={(visionModelResourceId) =>
|
||||
onSettingsChange({ ...settings, visionModelResourceId })
|
||||
}
|
||||
onFocusPromptChange={(visionFocusPrompt) =>
|
||||
onSettingsChange({ ...settings, visionFocusPrompt })
|
||||
onPromptModeChange={(visionPromptMode) =>
|
||||
onSettingsChange({ ...settings, visionPromptMode })
|
||||
}
|
||||
onPromptChange={(visionPrompt) =>
|
||||
onSettingsChange({ ...settings, visionPrompt })
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import * as LucideIcons from "lucide-react";
|
||||
import { Circle, type LucideIcon } from "lucide-react";
|
||||
|
||||
import type { NodeSpecDto, TurnConfig } from "@/lib/api";
|
||||
import type { NodeSpecDto, TurnConfig, VisionPromptMode } from "@/lib/api";
|
||||
import { defaultTurnConfig } from "@/lib/turn-config";
|
||||
|
||||
export type WorkflowNodeType =
|
||||
@@ -54,7 +54,8 @@ export type WorkflowNodeData = {
|
||||
ttsResourceId?: string;
|
||||
visionEnabled?: boolean;
|
||||
visionModelResourceId?: string;
|
||||
visionFocusPrompt?: string;
|
||||
visionPromptMode?: VisionPromptMode;
|
||||
visionPrompt?: string;
|
||||
enableInterrupt?: boolean;
|
||||
turnConfig?: TurnConfig;
|
||||
toolId?: string;
|
||||
@@ -214,7 +215,8 @@ export type WorkflowGraph = {
|
||||
defaultTtsResourceId: string;
|
||||
visionEnabled: boolean;
|
||||
visionModelResourceId: string;
|
||||
visionFocusPrompt: string;
|
||||
visionPromptMode: VisionPromptMode;
|
||||
visionPrompt: string;
|
||||
toolIds: string[];
|
||||
knowledgeBaseId: string;
|
||||
knowledgeMode: "automatic" | "on_demand";
|
||||
@@ -252,7 +254,8 @@ export function defaultGraph(): WorkflowGraph {
|
||||
defaultTtsResourceId: "",
|
||||
visionEnabled: false,
|
||||
visionModelResourceId: "",
|
||||
visionFocusPrompt: "",
|
||||
visionPromptMode: "runtime",
|
||||
visionPrompt: "",
|
||||
toolIds: [],
|
||||
knowledgeBaseId: "",
|
||||
knowledgeMode: "automatic",
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
SystemToolKind,
|
||||
Tool,
|
||||
TurnConfig,
|
||||
VisionPromptMode,
|
||||
} from "@/lib/api";
|
||||
|
||||
import type { WorkflowGraph } from "./specs";
|
||||
@@ -18,7 +19,8 @@ export type WorkflowSettings = {
|
||||
tts?: string;
|
||||
visionEnabled: boolean;
|
||||
visionModelResourceId: string;
|
||||
visionFocusPrompt: string;
|
||||
visionPromptMode: VisionPromptMode;
|
||||
visionPrompt: string;
|
||||
toolIds: string[];
|
||||
knowledgeBaseId: string;
|
||||
knowledgeRetrievalConfig: KnowledgeRetrievalConfig;
|
||||
|
||||
@@ -315,6 +315,8 @@ export type AnalysisConfig = {
|
||||
fields: AnalysisField[];
|
||||
};
|
||||
|
||||
export type VisionPromptMode = "fixed" | "runtime" | "guided";
|
||||
|
||||
/** 后端 AssistantOut(宽表 STI:瘦字段平铺,workflow 用 graph)。apiKey 读时打码 */
|
||||
export type Assistant = {
|
||||
id: string;
|
||||
@@ -327,7 +329,8 @@ export type Assistant = {
|
||||
startup: StartupConfig;
|
||||
visionEnabled: boolean;
|
||||
visionModelResourceId: string | null;
|
||||
visionFocusPrompt: string;
|
||||
visionPromptMode: VisionPromptMode;
|
||||
visionPrompt: string;
|
||||
modelResourceIds: Partial<Record<ModelType, string>>;
|
||||
knowledgeBaseId: string | null;
|
||||
knowledgeRetrievalConfig: KnowledgeRetrievalConfig;
|
||||
@@ -372,14 +375,6 @@ export const assistantsApi = {
|
||||
// 服务端整行复制(含真 key,密钥不经浏览器)
|
||||
duplicate: (id: string) =>
|
||||
request<Assistant>(`/api/assistants/${id}/duplicate`, { method: "POST" }),
|
||||
generateVisionFocus: (body: {
|
||||
sourcePrompt: string;
|
||||
modelResourceId: string;
|
||||
}) =>
|
||||
request<{ focusPrompt: string }>("/api/assistants/vision-focus/generate", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
remove: (id: string) =>
|
||||
request<{ ok: boolean }>(`/api/assistants/${id}`, { method: "DELETE" }),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user