fix: support vision prompt modes
This commit is contained in:
@@ -160,7 +160,8 @@ class Assistant(Base):
|
|||||||
ForeignKey("model_resources.id", ondelete="SET NULL"),
|
ForeignKey("model_resources.id", ondelete="SET NULL"),
|
||||||
nullable=True,
|
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),无默认兜底
|
# KB 引用:被引用时禁止删 KB(RESTRICT),无默认兜底
|
||||||
knowledge_base_id: Mapped[str | None] = mapped_column(
|
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
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
RuntimeMode = Literal["pipeline", "realtime"]
|
RuntimeMode = Literal["pipeline", "realtime"]
|
||||||
|
VisionPromptMode = Literal["fixed", "runtime", "guided"]
|
||||||
|
VISION_PROMPT_MODES = frozenset({"fixed", "runtime", "guided"})
|
||||||
|
|
||||||
|
|
||||||
class RuntimeMcpServer(BaseModel):
|
class RuntimeMcpServer(BaseModel):
|
||||||
@@ -92,7 +94,8 @@ class AssistantConfig(BaseModel):
|
|||||||
llm_support_image_input: bool = False
|
llm_support_image_input: bool = False
|
||||||
vision_enabled: bool = False
|
vision_enabled: bool = False
|
||||||
vision_model_resource_id: str | None = None
|
vision_model_resource_id: str | None = None
|
||||||
vision_focus_prompt: str = ""
|
vision_prompt_mode: VisionPromptMode = "runtime"
|
||||||
|
vision_prompt: str = ""
|
||||||
vision_model: str = ""
|
vision_model: str = ""
|
||||||
vision_llm_interface_type: str = "openai-llm"
|
vision_llm_interface_type: str = "openai-llm"
|
||||||
vision_llm_values: dict = {}
|
vision_llm_values: dict = {}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from models import RuntimeModelResource, RuntimeTool
|
from models import VISION_PROMPT_MODES, RuntimeTool
|
||||||
from db.models import (
|
from db.models import (
|
||||||
Assistant,
|
Assistant,
|
||||||
AssistantModelBinding,
|
AssistantModelBinding,
|
||||||
@@ -16,15 +16,12 @@ from fastapi import APIRouter, Depends, HTTPException
|
|||||||
from schemas import (
|
from schemas import (
|
||||||
AssistantOut,
|
AssistantOut,
|
||||||
AssistantUpsert,
|
AssistantUpsert,
|
||||||
VisionFocusPromptGenerateRequest,
|
|
||||||
VisionFocusPromptGenerateResult,
|
|
||||||
)
|
)
|
||||||
from services.auth import require_admin
|
from services.auth import require_admin
|
||||||
from services.masking import mask, resolve_incoming_key
|
from services.masking import mask, resolve_incoming_key
|
||||||
from services.node_specs import graph_references, normalize_graph, validate_graph
|
from services.node_specs import graph_references, normalize_graph, validate_graph
|
||||||
from services.system_tools import system_tool_kind
|
from services.system_tools import system_tool_kind
|
||||||
from services.tool_policy import policy_for_tool
|
from services.tool_policy import policy_for_tool
|
||||||
from services.vision import generate_vision_focus_prompt
|
|
||||||
from services.workflow_engine import WorkflowEngine
|
from services.workflow_engine import WorkflowEngine
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
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.
|
# capability summary used by clients to decide whether to open the camera.
|
||||||
body.vision_enabled = WorkflowEngine(body.graph).uses_vision()
|
body.vision_enabled = WorkflowEngine(body.graph).uses_vision()
|
||||||
body.vision_model_resource_id = None
|
body.vision_model_resource_id = None
|
||||||
body.vision_focus_prompt = ""
|
body.vision_prompt_mode = "runtime"
|
||||||
|
body.vision_prompt = ""
|
||||||
refs = graph_references(body.graph)
|
refs = graph_references(body.graph)
|
||||||
body.tool_ids = list(dict.fromkeys([*body.tool_ids, *sorted(refs["tools"])]))
|
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":
|
if body.type == "workflow":
|
||||||
return
|
return
|
||||||
if body.vision_enabled:
|
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:
|
if body.vision_model_resource_id:
|
||||||
resource = await session.get(ModelResource, body.vision_model_resource_id)
|
resource = await session.get(ModelResource, body.vision_model_resource_id)
|
||||||
else:
|
else:
|
||||||
@@ -396,7 +401,8 @@ async def _to_out(session: AsyncSession, assistant: Assistant) -> AssistantOut:
|
|||||||
startup=assistant.startup or {},
|
startup=assistant.startup or {},
|
||||||
vision_enabled=assistant.vision_enabled,
|
vision_enabled=assistant.vision_enabled,
|
||||||
vision_model_resource_id=assistant.vision_model_resource_id,
|
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),
|
model_resource_ids=await _resource_ids(session, assistant.id),
|
||||||
knowledge_base_id=assistant.knowledge_base_id,
|
knowledge_base_id=assistant.knowledge_base_id,
|
||||||
knowledge_retrieval_config=assistant.knowledge_retrieval_config or {},
|
knowledge_retrieval_config=assistant.knowledge_retrieval_config or {},
|
||||||
@@ -448,43 +454,6 @@ async def create_assistant(
|
|||||||
return await _to_out(session, 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)
|
@router.get("/{assistant_id}", response_model=AssistantOut)
|
||||||
async def get_assistant(
|
async def get_assistant(
|
||||||
assistant_id: str, session: AsyncSession = Depends(get_session)
|
assistant_id: str, session: AsyncSession = Depends(get_session)
|
||||||
@@ -513,7 +482,8 @@ async def duplicate_assistant(
|
|||||||
startup=dict(source.startup or {}),
|
startup=dict(source.startup or {}),
|
||||||
vision_enabled=source.vision_enabled,
|
vision_enabled=source.vision_enabled,
|
||||||
vision_model_resource_id=source.vision_model_resource_id,
|
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_base_id=source.knowledge_base_id,
|
||||||
knowledge_retrieval_config=dict(source.knowledge_retrieval_config or {}),
|
knowledge_retrieval_config=dict(source.knowledge_retrieval_config or {}),
|
||||||
prompt=source.prompt,
|
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
|
from pydantic.alias_generators import to_camel
|
||||||
|
|
||||||
RuntimeMode = Literal["pipeline", "realtime"]
|
RuntimeMode = Literal["pipeline", "realtime"]
|
||||||
|
VisionPromptMode = Literal["fixed", "runtime", "guided"]
|
||||||
ModelType = Literal["LLM", "ASR", "TTS", "Realtime", "Embedding", "Agent"]
|
ModelType = Literal["LLM", "ASR", "TTS", "Realtime", "Embedding", "Agent"]
|
||||||
AssistantType = Literal["prompt", "workflow", "dify", "fastgpt", "opencode"]
|
AssistantType = Literal["prompt", "workflow", "dify", "fastgpt", "opencode"]
|
||||||
TurnEndStrategy = Literal["silence", "smart_turn"]
|
TurnEndStrategy = Literal["silence", "smart_turn"]
|
||||||
@@ -198,7 +199,8 @@ class AssistantUpsert(CamelModel):
|
|||||||
startup: StartupConfig = Field(default_factory=StartupConfig)
|
startup: StartupConfig = Field(default_factory=StartupConfig)
|
||||||
vision_enabled: bool = False
|
vision_enabled: bool = False
|
||||||
vision_model_resource_id: str | None = None
|
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)
|
model_resource_ids: dict[ModelType, str] = Field(default_factory=dict)
|
||||||
knowledge_base_id: str | None = None
|
knowledge_base_id: str | None = None
|
||||||
knowledge_retrieval_config: KnowledgeRetrievalConfig = Field(
|
knowledge_retrieval_config: KnowledgeRetrievalConfig = Field(
|
||||||
@@ -264,15 +266,6 @@ class AssistantOut(AssistantUpsert):
|
|||||||
updated_at: str | None = None
|
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):
|
class DynamicVariableDefinition(CamelModel):
|
||||||
type: DynamicVariableType = "string"
|
type: DynamicVariableType = "string"
|
||||||
required: bool = False
|
required: bool = False
|
||||||
|
|||||||
@@ -670,7 +670,10 @@ class WorkflowBrain(BaseBrain):
|
|||||||
append_function(self._flow_tool(tool, node_id))
|
append_function(self._flow_tool(tool, node_id))
|
||||||
append_function(self._knowledge_function(node_id))
|
append_function(self._knowledge_function(node_id))
|
||||||
if stage.vision_enabled and self._require_runtime().vision_function:
|
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":
|
if self._engine.llm_routing_mode() == "edge_tool":
|
||||||
for edge in self._engine.edge_tool_edges(node_id):
|
for edge in self._engine.edge_tool_edges(node_id):
|
||||||
append_function(self._edge_tool(edge, 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_enabled=assistant.vision_enabled,
|
||||||
vision_model_resource_id=assistant.vision_model_resource_id,
|
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_model=str(_value(vision_resource, "modelId", "")),
|
||||||
vision_llm_interface_type=(
|
vision_llm_interface_type=(
|
||||||
vision_resource.interface_type if vision_resource else "openai-llm"
|
vision_resource.interface_type if vision_resource else "openai-llm"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from collections import defaultdict, deque
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from models import VISION_PROMPT_MODES
|
||||||
from services.message_policy import (
|
from services.message_policy import (
|
||||||
MESSAGE_COMPLETION_POLICIES,
|
MESSAGE_COMPLETION_POLICIES,
|
||||||
MESSAGE_CONFIRMATION,
|
MESSAGE_CONFIRMATION,
|
||||||
@@ -199,6 +200,11 @@ def _normalize_agent_data(data: dict[str, Any]) -> None:
|
|||||||
data["stateVariableNames"] = list(
|
data["stateVariableNames"] = list(
|
||||||
dict.fromkeys(str(name) for name in state_names or [] if str(name))
|
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:
|
if "inheritGlobalConfig" not in data:
|
||||||
has_node_overrides = any(
|
has_node_overrides = any(
|
||||||
(
|
(
|
||||||
@@ -207,7 +213,7 @@ def _normalize_agent_data(data: dict[str, Any]) -> None:
|
|||||||
data.get("ttsResourceId"),
|
data.get("ttsResourceId"),
|
||||||
data.get("visionEnabled"),
|
data.get("visionEnabled"),
|
||||||
data.get("visionModelResourceId"),
|
data.get("visionModelResourceId"),
|
||||||
data.get("visionFocusPrompt"),
|
data.get("visionPrompt"),
|
||||||
data.get("knowledgeBaseId"),
|
data.get("knowledgeBaseId"),
|
||||||
data.get("toolIds"),
|
data.get("toolIds"),
|
||||||
)
|
)
|
||||||
@@ -268,7 +274,10 @@ def _normalize_settings(settings: dict[str, Any], *, global_prompt: str = "") ->
|
|||||||
settings.setdefault("defaultTtsResourceId", "")
|
settings.setdefault("defaultTtsResourceId", "")
|
||||||
settings.setdefault("visionEnabled", False)
|
settings.setdefault("visionEnabled", False)
|
||||||
settings.setdefault("visionModelResourceId", "")
|
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("toolIds", [])
|
||||||
settings.setdefault("knowledgeBaseId", "")
|
settings.setdefault("knowledgeBaseId", "")
|
||||||
settings.setdefault("knowledgeMode", "automatic")
|
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
|
not isinstance(name, str) for name in state_names
|
||||||
):
|
):
|
||||||
errors.append(f"Agent 节点 {node_id} 的状态变量授权必须是列表")
|
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 runtime_mode == "realtime":
|
||||||
if data.get("contextPolicy", "inherit") != "inherit":
|
if data.get("contextPolicy", "inherit") != "inherit":
|
||||||
errors.append(
|
errors.append(
|
||||||
@@ -487,7 +510,6 @@ def validate_graph(graph: dict[str, Any]) -> list[str]:
|
|||||||
errors.append(
|
errors.append(
|
||||||
f"Realtime Agent 节点 {node_id} 不支持节点级模型、语音或交互策略覆盖"
|
f"Realtime Agent 节点 {node_id} 不支持节点级模型、语音或交互策略覆盖"
|
||||||
)
|
)
|
||||||
source = settings if data.get("inheritGlobalConfig", True) else data
|
|
||||||
if (
|
if (
|
||||||
source.get("knowledgeBaseId")
|
source.get("knowledgeBaseId")
|
||||||
and source.get("knowledgeMode") != "on_demand"
|
and source.get("knowledgeMode") != "on_demand"
|
||||||
|
|||||||
@@ -38,8 +38,9 @@ from services.vision import (
|
|||||||
image_data_uri,
|
image_data_uri,
|
||||||
image_frame_from_jpeg,
|
image_frame_from_jpeg,
|
||||||
image_jpeg_bytes,
|
image_jpeg_bytes,
|
||||||
|
resolve_vision_question,
|
||||||
vision_system_hint,
|
vision_system_hint,
|
||||||
vision_tool_description,
|
vision_tool_schema_parts,
|
||||||
)
|
)
|
||||||
from services.workflow_engine import WorkflowEngine
|
from services.workflow_engine import WorkflowEngine
|
||||||
|
|
||||||
@@ -349,7 +350,9 @@ async def run_pipeline(
|
|||||||
def with_vision_hint(text: str) -> str:
|
def with_vision_hint(text: str) -> str:
|
||||||
hints = []
|
hints = []
|
||||||
if vision_enabled and cfg.type != "workflow":
|
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:
|
if cfg.knowledge_base_id:
|
||||||
hints.append(
|
hints.append(
|
||||||
AUTOMATIC_KNOWLEDGE_SYSTEM_HINT
|
AUTOMATIC_KNOWLEDGE_SYSTEM_HINT
|
||||||
@@ -450,7 +453,8 @@ async def run_pipeline(
|
|||||||
workflow_vision_scope: dict[str, Any] = {
|
workflow_vision_scope: dict[str, Any] = {
|
||||||
"enabled": False,
|
"enabled": False,
|
||||||
"vision_model_resource_id": None,
|
"vision_model_resource_id": None,
|
||||||
"vision_focus_prompt": "",
|
"vision_prompt_mode": "runtime",
|
||||||
|
"vision_prompt": "",
|
||||||
"llm_resource_id": None,
|
"llm_resource_id": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,16 +462,15 @@ async def run_pipeline(
|
|||||||
if cfg.type == "workflow":
|
if cfg.type == "workflow":
|
||||||
return _workflow_vision_uses_main_llm(cfg, workflow_vision_scope)
|
return _workflow_vision_uses_main_llm(cfg, workflow_vision_scope)
|
||||||
return vision_native_mode
|
return vision_native_mode
|
||||||
|
vision_description, vision_properties, vision_required = vision_tool_schema_parts(
|
||||||
|
cfg.vision_prompt_mode,
|
||||||
|
cfg.vision_prompt,
|
||||||
|
)
|
||||||
vision_schema = FunctionSchema(
|
vision_schema = FunctionSchema(
|
||||||
name=VISION_TOOL_NAME,
|
name=VISION_TOOL_NAME,
|
||||||
description=vision_tool_description(cfg.vision_focus_prompt),
|
description=vision_description,
|
||||||
properties={
|
properties=vision_properties,
|
||||||
"question": {
|
required=vision_required,
|
||||||
"type": "string",
|
|
||||||
"description": "用户关于当前视频画面的具体问题。",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
required=["question"],
|
|
||||||
)
|
)
|
||||||
knowledge_schema = FunctionSchema(
|
knowledge_schema = FunctionSchema(
|
||||||
name=KNOWLEDGE_TOOL_NAME,
|
name=KNOWLEDGE_TOOL_NAME,
|
||||||
@@ -498,7 +501,15 @@ async def run_pipeline(
|
|||||||
await params.result_callback({"status": "error", "message": "知识库检索暂时不可用"})
|
await params.result_callback({"status": "error", "message": "知识库检索暂时不可用"})
|
||||||
|
|
||||||
async def fetch_user_image(params: FunctionCallParams):
|
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")
|
user_id = vision_state.get("client_id")
|
||||||
if not user_id:
|
if not user_id:
|
||||||
await params.result_callback(
|
await params.result_callback(
|
||||||
@@ -576,14 +587,26 @@ async def run_pipeline(
|
|||||||
result = config_with_main_llm_as_vision(cfg)
|
result = config_with_main_llm_as_vision(cfg)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"视觉模型资源未加载:{vision_resource_id}")
|
raise ValueError(f"视觉模型资源未加载:{vision_resource_id}")
|
||||||
result.vision_focus_prompt = str(
|
result.vision_prompt_mode = str(
|
||||||
workflow_vision_scope.get("vision_focus_prompt") or ""
|
workflow_vision_scope.get("vision_prompt_mode") or "runtime"
|
||||||
|
)
|
||||||
|
result.vision_prompt = str(
|
||||||
|
workflow_vision_scope.get("vision_prompt") or ""
|
||||||
).strip()
|
).strip()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
if cfg.type == "workflow" and vision_enabled:
|
if cfg.type == "workflow" and vision_enabled:
|
||||||
async def native_flow_fetch_user_image(params: FunctionCallParams) -> None:
|
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")
|
user_id = vision_state.get("client_id")
|
||||||
if not user_id:
|
if not user_id:
|
||||||
await params.result_callback(
|
await params.result_callback(
|
||||||
@@ -609,7 +632,15 @@ async def run_pipeline(
|
|||||||
"status": "disabled",
|
"status": "disabled",
|
||||||
"message": "当前 Agent 节点未开启视觉理解。",
|
"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")
|
user_id = vision_state.get("client_id")
|
||||||
if not user_id:
|
if not user_id:
|
||||||
return {
|
return {
|
||||||
@@ -625,7 +656,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(),
|
vision_cfg,
|
||||||
frame,
|
frame,
|
||||||
question,
|
question,
|
||||||
)
|
)
|
||||||
@@ -654,11 +685,16 @@ async def run_pipeline(
|
|||||||
active_vision_uses_main_llm,
|
active_vision_uses_main_llm,
|
||||||
)
|
)
|
||||||
|
|
||||||
workflow_vision_function = FlowsFunctionSchema(
|
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,
|
name=VISION_TOOL_NAME,
|
||||||
description=vision_schema.description,
|
description=description,
|
||||||
properties=vision_schema.properties,
|
properties=properties,
|
||||||
required=vision_schema.required,
|
required=required,
|
||||||
handler=flow_fetch_user_image,
|
handler=flow_fetch_user_image,
|
||||||
cancel_on_interruption=True,
|
cancel_on_interruption=True,
|
||||||
)
|
)
|
||||||
|
|||||||
+81
-70
@@ -5,8 +5,14 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import base64
|
import base64
|
||||||
from io import BytesIO
|
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 openai import AsyncOpenAI
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from pipecat.frames.frames import UserImageRawFrame
|
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:
|
def _require(value: str, label: str) -> str:
|
||||||
@@ -36,83 +36,96 @@ def _require(value: str, label: str) -> str:
|
|||||||
raise ValueError(f"缺少模型资源配置: {label}")
|
raise ValueError(f"缺少模型资源配置: {label}")
|
||||||
|
|
||||||
|
|
||||||
def vision_system_hint(focus_prompt: str = "") -> str:
|
def normalize_vision_prompt_mode(value: object) -> VisionPromptMode:
|
||||||
"""Describe when to use vision and keep a configured business focus visible."""
|
"""Return a supported mode while keeping older saved configs compatible."""
|
||||||
|
|
||||||
focus = focus_prompt.strip()
|
mode = str(value or "runtime")
|
||||||
if not focus:
|
if mode == "fixed":
|
||||||
return VISION_SYSTEM_HINT
|
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 (
|
return (
|
||||||
f"{VISION_SYSTEM_HINT}\n"
|
f"{VISION_SYSTEM_HINT}\n"
|
||||||
f"视觉关注目标(用于补充本次用户问题;冲突时以本次问题为准):{focus}"
|
f"该工具始终使用固定视觉问题“{prompt}”,调用时不需要生成视觉问题。"
|
||||||
)
|
)
|
||||||
|
if prompt_mode == "guided":
|
||||||
|
|
||||||
def vision_analysis_system_prompt(focus_prompt: str = "") -> str:
|
|
||||||
"""Keep the safety baseline fixed while appending an editable business focus."""
|
|
||||||
|
|
||||||
focus = focus_prompt.strip()
|
|
||||||
if not focus:
|
|
||||||
return VISION_ANALYSIS_SYSTEM_PROMPT
|
|
||||||
return (
|
return (
|
||||||
f"{VISION_ANALYSIS_SYSTEM_PROMPT}\n\n"
|
f"{VISION_SYSTEM_HINT}\n"
|
||||||
f"[业务视觉关注目标]\n{focus}\n"
|
"调用时请结合当前对话生成具体视觉问题,并遵守以下固定引导:"
|
||||||
"该目标用于补充当前用户问题;如果二者范围不同,优先回答当前用户问题。"
|
f"{prompt}"
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
f"{VISION_SYSTEM_HINT}\n"
|
||||||
|
"调用时请结合当前对话,生成一个具体、可由当前画面回答的视觉问题。"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def vision_tool_description(focus_prompt: str = "") -> str:
|
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."""
|
||||||
|
|
||||||
base = (
|
base = (
|
||||||
"获取用户当前摄像头画面。当用户询问当前画面、看到了什么、"
|
"获取用户当前摄像头画面。当用户询问当前画面、看到了什么、"
|
||||||
"人/物品/环境状态或需要视觉判断时调用。"
|
"人/物品/环境状态或需要视觉判断时调用。"
|
||||||
)
|
)
|
||||||
focus = " ".join(focus_prompt.split())
|
prompt_mode = normalize_vision_prompt_mode(mode)
|
||||||
return f"{base} 当前业务重点:{focus[:600]}" if focus else base
|
prompt = _compact_prompt(configured_prompt, limit=600)
|
||||||
|
if prompt_mode == "fixed":
|
||||||
|
return (
|
||||||
|
f"{base} 每次都检查同一个固定问题:{prompt}",
|
||||||
|
{},
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
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"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def generate_vision_focus_prompt(
|
def resolve_vision_question(
|
||||||
resource: RuntimeModelResource,
|
mode: VisionPromptMode,
|
||||||
source_prompt: str,
|
configured_prompt: str,
|
||||||
|
arguments: dict[str, Any] | None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Generate one editable focus draft with an existing LLM resource."""
|
"""Resolve the exact question sent with the captured image."""
|
||||||
|
|
||||||
if resource.interface_type not in {"openai-llm", "dashscope-llm"}:
|
prompt_mode = normalize_vision_prompt_mode(mode)
|
||||||
raise ValueError(f"当前模型接口不支持提示词生成: {resource.interface_type}")
|
if prompt_mode == "fixed":
|
||||||
source = source_prompt.strip()
|
question = configured_prompt.strip()
|
||||||
if not source:
|
else:
|
||||||
raise ValueError("助手提示词不能为空")
|
question = str((arguments or {}).get("question") or "").strip()
|
||||||
values = resource.values or {}
|
if not question:
|
||||||
secrets = resource.secrets or {}
|
raise ValueError(
|
||||||
extra_body = values.get("extraBody")
|
"固定视觉提示词为空"
|
||||||
extra = {"extra_body": extra_body} if isinstance(extra_body, dict) else {}
|
if prompt_mode == "fixed"
|
||||||
client = AsyncOpenAI(
|
else "视觉问题为空"
|
||||||
api_key=_require(str(secrets.get("apiKey") or ""), "LLM apiKey"),
|
|
||||||
base_url=_require(str(values.get("apiUrl") or ""), "LLM apiUrl"),
|
|
||||||
)
|
)
|
||||||
try:
|
return question
|
||||||
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,
|
|
||||||
)
|
|
||||||
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()
|
|
||||||
)
|
|
||||||
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 image_jpeg_bytes(frame: UserImageRawFrame) -> bytes:
|
def image_jpeg_bytes(frame: UserImageRawFrame) -> bytes:
|
||||||
@@ -173,9 +186,7 @@ async def analyze_image_with_vision_model(
|
|||||||
messages=[
|
messages=[
|
||||||
{
|
{
|
||||||
"role": "system",
|
"role": "system",
|
||||||
"content": vision_analysis_system_prompt(
|
"content": VISION_ANALYSIS_SYSTEM_PROMPT,
|
||||||
cfg.vision_focus_prompt
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"role": "user",
|
"role": "user",
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class WorkflowAgentStage:
|
|||||||
if stage.vision_enabled:
|
if stage.vision_enabled:
|
||||||
stage_prompt = (
|
stage_prompt = (
|
||||||
f"{stage_prompt}\n\n[视觉能力]\n"
|
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 = (
|
instruction = (
|
||||||
EDGE_TOOL_STAGE_INSTRUCTION
|
EDGE_TOOL_STAGE_INSTRUCTION
|
||||||
@@ -112,7 +112,8 @@ class WorkflowAgentStage:
|
|||||||
{
|
{
|
||||||
"enabled": stage.vision_enabled,
|
"enabled": stage.vision_enabled,
|
||||||
"vision_model_resource_id": stage.vision_model_resource_id,
|
"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,
|
"llm_resource_id": stage.llm_resource_id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -34,8 +34,9 @@ from services.vision import (
|
|||||||
VISION_TOOL_NAME,
|
VISION_TOOL_NAME,
|
||||||
analyze_image_with_vision_model,
|
analyze_image_with_vision_model,
|
||||||
config_with_vision_resource,
|
config_with_vision_resource,
|
||||||
|
resolve_vision_question,
|
||||||
vision_system_hint,
|
vision_system_hint,
|
||||||
vision_tool_description,
|
vision_tool_schema_parts,
|
||||||
)
|
)
|
||||||
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
|
||||||
@@ -414,7 +415,10 @@ class WorkflowRealtimeController:
|
|||||||
f"[工作流执行规则]\n{EDGE_TOOL_STAGE_INSTRUCTION}"
|
f"[工作流执行规则]\n{EDGE_TOOL_STAGE_INSTRUCTION}"
|
||||||
)
|
)
|
||||||
if stage.vision_enabled:
|
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
|
return prompt
|
||||||
|
|
||||||
def _build_agent_tools(self, node_id: str) -> list[RealtimeTool]:
|
def _build_agent_tools(self, node_id: str) -> list[RealtimeTool]:
|
||||||
@@ -573,10 +577,15 @@ class WorkflowRealtimeController:
|
|||||||
{"status": "stale", "message": "当前 Agent 已经切换。"},
|
{"status": "stale", "message": "当前 Agent 已经切换。"},
|
||||||
continue_response=False,
|
continue_response=False,
|
||||||
)
|
)
|
||||||
question = str(arguments.get("question") or "").strip()
|
try:
|
||||||
if not question:
|
question = resolve_vision_question(
|
||||||
|
stage.vision_prompt_mode,
|
||||||
|
stage.vision_prompt,
|
||||||
|
arguments,
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
return RealtimeToolResult(
|
return RealtimeToolResult(
|
||||||
{"status": "error", "message": "视觉问题为空"}
|
{"status": "error", "message": str(exc)}
|
||||||
)
|
)
|
||||||
if self._runtime.capture_image is None:
|
if self._runtime.capture_image is None:
|
||||||
return RealtimeToolResult(
|
return RealtimeToolResult(
|
||||||
@@ -605,7 +614,6 @@ class WorkflowRealtimeController:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if vision_cfg is not None:
|
if vision_cfg is not None:
|
||||||
vision_cfg.vision_focus_prompt = stage.vision_focus_prompt
|
|
||||||
observation = await analyze_image_with_vision_model(
|
observation = await analyze_image_with_vision_model(
|
||||||
vision_cfg,
|
vision_cfg,
|
||||||
frame,
|
frame,
|
||||||
@@ -634,17 +642,16 @@ class WorkflowRealtimeController:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
description, properties, required = vision_tool_schema_parts(
|
||||||
|
stage.vision_prompt_mode,
|
||||||
|
stage.vision_prompt,
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
RealtimeTool(
|
RealtimeTool(
|
||||||
name=VISION_TOOL_NAME,
|
name=VISION_TOOL_NAME,
|
||||||
description=vision_tool_description(stage.vision_focus_prompt),
|
description=description,
|
||||||
properties={
|
properties=properties,
|
||||||
"question": {
|
required=tuple(required),
|
||||||
"type": "string",
|
|
||||||
"description": "需要根据当前画面判断的具体问题",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
required=("question",),
|
|
||||||
),
|
),
|
||||||
handler,
|
handler,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ class AgentStageConfig:
|
|||||||
tts_resource_id: str
|
tts_resource_id: str
|
||||||
vision_enabled: bool
|
vision_enabled: bool
|
||||||
vision_model_resource_id: str | None
|
vision_model_resource_id: str | None
|
||||||
vision_focus_prompt: str
|
vision_prompt_mode: str
|
||||||
|
vision_prompt: str
|
||||||
tool_ids: tuple[str, ...]
|
tool_ids: tuple[str, ...]
|
||||||
state_variable_names: tuple[str, ...]
|
state_variable_names: tuple[str, ...]
|
||||||
knowledge_base_id: str | None
|
knowledge_base_id: str | None
|
||||||
@@ -152,7 +153,8 @@ class WorkflowEngine:
|
|||||||
vision_model_resource_id=(
|
vision_model_resource_id=(
|
||||||
str(source.get("visionModelResourceId") or "") or None
|
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 []),
|
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
|
# Variable write scope always belongs to the Agent node, even when
|
||||||
# its selected tool resources come from Workflow defaults.
|
# its selected tool resources come from Workflow defaults.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import unittest
|
import unittest
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
from models import AssistantConfig, RuntimeTool
|
from models import AssistantConfig, RuntimeTool
|
||||||
from pipecat.flows import FlowManager
|
from pipecat.flows import FlowManager
|
||||||
@@ -1782,7 +1782,7 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
scopes = []
|
scopes = []
|
||||||
vision_function = object()
|
vision_function = MagicMock(side_effect=lambda _stage: object())
|
||||||
|
|
||||||
async def queue_frame(_frame):
|
async def queue_frame(_frame):
|
||||||
pass
|
pass
|
||||||
@@ -1800,13 +1800,18 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
|
|
||||||
await brain._apply_agent_stage("agent")
|
await brain._apply_agent_stage("agent")
|
||||||
inherited_config = brain._agent_config("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.assertIn("fetch_user_image", inherited_config["role_message"])
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
scopes[-1],
|
scopes[-1],
|
||||||
{
|
{
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"vision_model_resource_id": "vision_global",
|
"vision_model_resource_id": "vision_global",
|
||||||
|
"vision_prompt_mode": "runtime",
|
||||||
|
"vision_prompt": "",
|
||||||
"llm_resource_id": "llm_global",
|
"llm_resource_id": "llm_global",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -1821,7 +1826,7 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
)
|
)
|
||||||
await brain._apply_agent_stage("agent")
|
await brain._apply_agent_stage("agent")
|
||||||
custom_config = brain._agent_config("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.assertNotIn("fetch_user_image", custom_config["role_message"])
|
||||||
self.assertFalse(scopes[-1]["enabled"])
|
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="开启后,助手会获得读取当前视频画面的工具。选择「模型自己」时,大语言模型必须支持图片输入。"
|
hint="开启后,助手会获得读取当前视频画面的工具。选择「模型自己」时,大语言模型必须支持图片输入。"
|
||||||
enabled={form.visionEnabled}
|
enabled={form.visionEnabled}
|
||||||
modelResourceId={form.visionModelResourceId}
|
modelResourceId={form.visionModelResourceId}
|
||||||
focusPrompt={form.visionFocusPrompt}
|
promptMode={form.visionPromptMode}
|
||||||
sourcePrompt={form.prompt}
|
prompt={form.visionPrompt}
|
||||||
generatorModelResourceId={form.model}
|
|
||||||
mainModelResourceId={form.model}
|
mainModelResourceId={form.model}
|
||||||
modelOptions={visionModelOptions}
|
modelOptions={visionModelOptions}
|
||||||
onEnabledChange={handlePromptVisionEnabledChange}
|
onEnabledChange={handlePromptVisionEnabledChange}
|
||||||
onModelResourceIdChange={(value) =>
|
onModelResourceIdChange={(value) =>
|
||||||
updateForm("visionModelResourceId", value)
|
updateForm("visionModelResourceId", value)
|
||||||
}
|
}
|
||||||
onFocusPromptChange={(value) =>
|
onPromptModeChange={(value) =>
|
||||||
updateForm("visionFocusPrompt", value)
|
updateForm("visionPromptMode", value)
|
||||||
|
}
|
||||||
|
onPromptChange={(value) =>
|
||||||
|
updateForm("visionPrompt", value)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type {
|
|||||||
KnowledgeRetrievalConfig,
|
KnowledgeRetrievalConfig,
|
||||||
StartupConfig,
|
StartupConfig,
|
||||||
TurnConfig,
|
TurnConfig,
|
||||||
|
VisionPromptMode,
|
||||||
} from "@/lib/api";
|
} from "@/lib/api";
|
||||||
|
|
||||||
export type RuntimeMode = "pipeline" | "realtime";
|
export type RuntimeMode = "pipeline" | "realtime";
|
||||||
@@ -26,6 +27,7 @@ export type AssistantForm = {
|
|||||||
startup: StartupConfig;
|
startup: StartupConfig;
|
||||||
visionEnabled: boolean;
|
visionEnabled: boolean;
|
||||||
visionModelResourceId: string;
|
visionModelResourceId: string;
|
||||||
visionFocusPrompt: string;
|
visionPromptMode: VisionPromptMode;
|
||||||
|
visionPrompt: string;
|
||||||
toolIds: string[];
|
toolIds: string[];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,53 +1,71 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { Camera, Check } from "lucide-react";
|
||||||
import { Camera, Loader2, Sparkles } from "lucide-react";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ResourceSelectField,
|
ResourceSelectField,
|
||||||
ToggleRow,
|
ToggleRow,
|
||||||
} from "@/components/assistant-editor/editor-controls";
|
} from "@/components/assistant-editor/editor-controls";
|
||||||
import { SectionCard } from "@/components/editor/section-card";
|
import { SectionCard } from "@/components/editor/section-card";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { assistantsApi } from "@/lib/api";
|
import type { VisionPromptMode } from "@/lib/api";
|
||||||
|
|
||||||
type VisionModelOption = {
|
type VisionModelOption = {
|
||||||
value: string;
|
value: string;
|
||||||
label: 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({
|
export function VisionConfigSection({
|
||||||
description,
|
description,
|
||||||
hint,
|
hint,
|
||||||
enabled,
|
enabled,
|
||||||
modelResourceId,
|
modelResourceId,
|
||||||
focusPrompt,
|
promptMode,
|
||||||
sourcePrompt,
|
prompt,
|
||||||
generatorModelResourceId,
|
|
||||||
mainModelResourceId,
|
mainModelResourceId,
|
||||||
mainModelSupportsImageInput,
|
mainModelSupportsImageInput,
|
||||||
modelOptions,
|
modelOptions,
|
||||||
onEnabledChange,
|
onEnabledChange,
|
||||||
onModelResourceIdChange,
|
onModelResourceIdChange,
|
||||||
onFocusPromptChange,
|
onPromptModeChange,
|
||||||
|
onPromptChange,
|
||||||
}: {
|
}: {
|
||||||
description: string;
|
description: string;
|
||||||
hint: string;
|
hint: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
modelResourceId: string;
|
modelResourceId: string;
|
||||||
focusPrompt: string;
|
promptMode: VisionPromptMode;
|
||||||
sourcePrompt: string;
|
prompt: string;
|
||||||
generatorModelResourceId: string;
|
|
||||||
mainModelResourceId: string;
|
mainModelResourceId: string;
|
||||||
mainModelSupportsImageInput?: boolean;
|
mainModelSupportsImageInput?: boolean;
|
||||||
modelOptions: VisionModelOption[];
|
modelOptions: VisionModelOption[];
|
||||||
onEnabledChange: (enabled: boolean) => void;
|
onEnabledChange: (enabled: boolean) => void;
|
||||||
onModelResourceIdChange: (modelResourceId: string) => 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 =
|
const mainModelSupportsVision =
|
||||||
mainModelSupportsImageInput ??
|
mainModelSupportsImageInput ??
|
||||||
modelOptions.some((option) => option.value === mainModelResourceId);
|
modelOptions.some((option) => option.value === mainModelResourceId);
|
||||||
@@ -55,25 +73,6 @@ export function VisionConfigSection({
|
|||||||
(option) => option.value !== mainModelResourceId,
|
(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 (
|
return (
|
||||||
<SectionCard
|
<SectionCard
|
||||||
icon={<Camera size={15} />}
|
icon={<Camera size={15} />}
|
||||||
@@ -100,56 +99,79 @@ export function VisionConfigSection({
|
|||||||
当前模型未标记支持图片输入,请选择独立视觉模型。
|
当前模型未标记支持图片输入,请选择独立视觉模型。
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between gap-3">
|
|
||||||
<label className="text-sm font-medium text-foreground">
|
<label className="text-sm font-medium text-foreground">
|
||||||
视觉关注目标
|
看图问题生成方式
|
||||||
</label>
|
</label>
|
||||||
<Button
|
<div role="radiogroup" aria-label="看图问题生成方式" className="space-y-2">
|
||||||
|
{promptModes.map((option) => {
|
||||||
|
const selected = promptMode === option.value;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={option.value}
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
role="radio"
|
||||||
size="sm"
|
aria-checked={selected}
|
||||||
className="h-7 gap-1.5 rounded-full px-2.5 text-xs"
|
onClick={() => onPromptModeChange(option.value)}
|
||||||
disabled={
|
className={[
|
||||||
generating || !sourcePrompt.trim() || !generatorModelResourceId
|
"flex w-full items-start gap-3 rounded-xl border px-3.5 py-3 text-left transition-colors",
|
||||||
}
|
selected
|
||||||
onClick={() => void generateFocusPrompt()}
|
? "border-primary bg-surface-strong/70 ring-1 ring-primary"
|
||||||
title={
|
: "border-hairline bg-background hover:border-hairline-strong",
|
||||||
!sourcePrompt.trim()
|
].join(" ")}
|
||||||
? "请先填写助手提示词"
|
|
||||||
: !generatorModelResourceId
|
|
||||||
? "请先选择大语言模型"
|
|
||||||
: "根据当前助手提示词生成可编辑草稿"
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{generating ? (
|
<span
|
||||||
<Loader2 size={12} className="animate-spin" />
|
className={[
|
||||||
) : (
|
"mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full border",
|
||||||
<Sparkles size={12} />
|
selected
|
||||||
)}
|
? "border-primary bg-primary text-primary-foreground"
|
||||||
{generating ? "生成中" : "根据提示词生成"}
|
: "border-hairline-strong text-transparent",
|
||||||
</Button>
|
].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>
|
</div>
|
||||||
|
</div>
|
||||||
|
{promptMode !== "runtime" && (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-sm font-medium text-foreground">
|
||||||
|
{promptMode === "fixed" ? "固定视觉提示词" : "运行时提示词引导"}
|
||||||
|
</label>
|
||||||
<Textarea
|
<Textarea
|
||||||
value={focusPrompt}
|
value={prompt}
|
||||||
maxLength={4000}
|
maxLength={4000}
|
||||||
rows={5}
|
rows={5}
|
||||||
onChange={(event) => {
|
onChange={(event) => onPromptChange(event.target.value)}
|
||||||
onFocusPromptChange(event.target.value);
|
placeholder={
|
||||||
if (generationError) setGenerationError("");
|
promptMode === "fixed"
|
||||||
}}
|
? "例如:请识别画面中的车辆数量、碰撞位置、明显损伤和人员状态。"
|
||||||
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"
|
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 className="text-[11px] leading-5 text-muted-soft">
|
||||||
可选。系统仍保留通用视觉安全规则;这里用于让每次看图稳定聚焦业务目标。
|
{promptMode === "fixed"
|
||||||
|
? "工具调用时不会让模型改写这段内容。"
|
||||||
|
: "模型会结合当前对话生成具体问题,并受到这段内容约束。"}
|
||||||
</p>
|
</p>
|
||||||
{generationError && (
|
{!prompt.trim() && (
|
||||||
<p className="text-xs leading-5 text-destructive">
|
<p className="text-xs leading-5 text-destructive">
|
||||||
{generationError}
|
当前模式需要填写提示词后才能保存。
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
|
|||||||
@@ -180,7 +180,8 @@ function blankPromptForm(name: string): AssistantForm {
|
|||||||
},
|
},
|
||||||
visionEnabled: false,
|
visionEnabled: false,
|
||||||
visionModelResourceId: "",
|
visionModelResourceId: "",
|
||||||
visionFocusPrompt: "",
|
visionPromptMode: "runtime",
|
||||||
|
visionPrompt: "",
|
||||||
toolIds: [],
|
toolIds: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -436,7 +437,8 @@ export function AssistantPage(props: AssistantPageProps) {
|
|||||||
},
|
},
|
||||||
visionEnabled: a.visionEnabled,
|
visionEnabled: a.visionEnabled,
|
||||||
visionModelResourceId: a.visionModelResourceId ?? "",
|
visionModelResourceId: a.visionModelResourceId ?? "",
|
||||||
visionFocusPrompt: a.visionFocusPrompt ?? "",
|
visionPromptMode: a.visionPromptMode ?? "runtime",
|
||||||
|
visionPrompt: a.visionPrompt ?? "",
|
||||||
toolIds: a.toolIds ?? [],
|
toolIds: a.toolIds ?? [],
|
||||||
};
|
};
|
||||||
setForm(next);
|
setForm(next);
|
||||||
@@ -487,7 +489,8 @@ export function AssistantPage(props: AssistantPageProps) {
|
|||||||
},
|
},
|
||||||
visionEnabled: false,
|
visionEnabled: false,
|
||||||
visionModelResourceId: null,
|
visionModelResourceId: null,
|
||||||
visionFocusPrompt: "",
|
visionPromptMode: "runtime",
|
||||||
|
visionPrompt: "",
|
||||||
modelResourceIds: {},
|
modelResourceIds: {},
|
||||||
knowledgeBaseId: null,
|
knowledgeBaseId: null,
|
||||||
knowledgeRetrievalConfig: defaultKnowledgeRetrievalConfig(),
|
knowledgeRetrievalConfig: defaultKnowledgeRetrievalConfig(),
|
||||||
@@ -552,7 +555,8 @@ export function AssistantPage(props: AssistantPageProps) {
|
|||||||
startup: form.startup,
|
startup: form.startup,
|
||||||
visionEnabled: form.visionEnabled,
|
visionEnabled: form.visionEnabled,
|
||||||
visionModelResourceId: form.visionModelResourceId || null,
|
visionModelResourceId: form.visionModelResourceId || null,
|
||||||
visionFocusPrompt: form.visionFocusPrompt,
|
visionPromptMode: form.visionPromptMode,
|
||||||
|
visionPrompt: form.visionPrompt,
|
||||||
modelResourceIds: {
|
modelResourceIds: {
|
||||||
...(form.model ? { LLM: form.model } : {}),
|
...(form.model ? { LLM: form.model } : {}),
|
||||||
...(form.asr ? { ASR: form.asr } : {}),
|
...(form.asr ? { ASR: form.asr } : {}),
|
||||||
@@ -680,7 +684,8 @@ export function AssistantPage(props: AssistantPageProps) {
|
|||||||
graph.settings?.visionModelResourceId ??
|
graph.settings?.visionModelResourceId ??
|
||||||
assistant.visionModelResourceId ??
|
assistant.visionModelResourceId ??
|
||||||
"",
|
"",
|
||||||
visionFocusPrompt: graph.settings?.visionFocusPrompt ?? "",
|
visionPromptMode: graph.settings?.visionPromptMode ?? "runtime",
|
||||||
|
visionPrompt: graph.settings?.visionPrompt ?? "",
|
||||||
toolIds: graph.settings?.toolIds ?? [],
|
toolIds: graph.settings?.toolIds ?? [],
|
||||||
knowledgeBaseId:
|
knowledgeBaseId:
|
||||||
graph.settings?.knowledgeBaseId || assistant.knowledgeBaseId || "",
|
graph.settings?.knowledgeBaseId || assistant.knowledgeBaseId || "",
|
||||||
|
|||||||
@@ -108,7 +108,8 @@ function baseUpsert(over: Partial<AssistantUpsert>): AssistantUpsert {
|
|||||||
},
|
},
|
||||||
visionEnabled: false,
|
visionEnabled: false,
|
||||||
visionModelResourceId: null,
|
visionModelResourceId: null,
|
||||||
visionFocusPrompt: "",
|
visionPromptMode: "runtime",
|
||||||
|
visionPrompt: "",
|
||||||
modelResourceIds: {},
|
modelResourceIds: {},
|
||||||
knowledgeBaseId: null,
|
knowledgeBaseId: null,
|
||||||
knowledgeRetrievalConfig: defaultKnowledgeRetrievalConfig(),
|
knowledgeRetrievalConfig: defaultKnowledgeRetrievalConfig(),
|
||||||
|
|||||||
@@ -96,7 +96,8 @@ function baseUpsertFromTemplate(
|
|||||||
},
|
},
|
||||||
visionEnabled: false,
|
visionEnabled: false,
|
||||||
visionModelResourceId: null,
|
visionModelResourceId: null,
|
||||||
visionFocusPrompt: "",
|
visionPromptMode: "runtime",
|
||||||
|
visionPrompt: "",
|
||||||
modelResourceIds: {},
|
modelResourceIds: {},
|
||||||
knowledgeBaseId: null,
|
knowledgeBaseId: null,
|
||||||
knowledgeRetrievalConfig: defaultKnowledgeRetrievalConfig(),
|
knowledgeRetrievalConfig: defaultKnowledgeRetrievalConfig(),
|
||||||
|
|||||||
@@ -129,7 +129,8 @@ function fromFlow(nodes: Node[], edges: Edge[]): WorkflowGraph {
|
|||||||
defaultTtsResourceId: "",
|
defaultTtsResourceId: "",
|
||||||
visionEnabled: false,
|
visionEnabled: false,
|
||||||
visionModelResourceId: "",
|
visionModelResourceId: "",
|
||||||
visionFocusPrompt: "",
|
visionPromptMode: "runtime",
|
||||||
|
visionPrompt: "",
|
||||||
toolIds: [],
|
toolIds: [],
|
||||||
knowledgeBaseId: "",
|
knowledgeBaseId: "",
|
||||||
knowledgeMode: "automatic",
|
knowledgeMode: "automatic",
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ export function settingsFromWorkflowGraph(
|
|||||||
tts: graph.settings.defaultTtsResourceId,
|
tts: graph.settings.defaultTtsResourceId,
|
||||||
visionEnabled: graph.settings.visionEnabled,
|
visionEnabled: graph.settings.visionEnabled,
|
||||||
visionModelResourceId: graph.settings.visionModelResourceId,
|
visionModelResourceId: graph.settings.visionModelResourceId,
|
||||||
visionFocusPrompt: graph.settings.visionFocusPrompt,
|
visionPromptMode: graph.settings.visionPromptMode,
|
||||||
|
visionPrompt: graph.settings.visionPrompt,
|
||||||
toolIds: graph.settings.toolIds,
|
toolIds: graph.settings.toolIds,
|
||||||
knowledgeBaseId: graph.settings.knowledgeBaseId,
|
knowledgeBaseId: graph.settings.knowledgeBaseId,
|
||||||
knowledgeRetrievalConfig: {
|
knowledgeRetrievalConfig: {
|
||||||
@@ -45,7 +46,8 @@ export function workflowGraphWithSettings(
|
|||||||
defaultTtsResourceId: settings.tts ?? "",
|
defaultTtsResourceId: settings.tts ?? "",
|
||||||
visionEnabled: settings.visionEnabled,
|
visionEnabled: settings.visionEnabled,
|
||||||
visionModelResourceId: settings.visionModelResourceId,
|
visionModelResourceId: settings.visionModelResourceId,
|
||||||
visionFocusPrompt: settings.visionFocusPrompt,
|
visionPromptMode: settings.visionPromptMode,
|
||||||
|
visionPrompt: settings.visionPrompt,
|
||||||
toolIds: settings.toolIds,
|
toolIds: settings.toolIds,
|
||||||
knowledgeBaseId: settings.knowledgeBaseId,
|
knowledgeBaseId: settings.knowledgeBaseId,
|
||||||
knowledgeMode: settings.knowledgeRetrievalConfig.mode,
|
knowledgeMode: settings.knowledgeRetrievalConfig.mode,
|
||||||
|
|||||||
@@ -78,8 +78,10 @@ export function AgentNodePanel({
|
|||||||
visionModelResourceId:
|
visionModelResourceId:
|
||||||
(draft.visionModelResourceId as string) ||
|
(draft.visionModelResourceId as string) ||
|
||||||
workflowSettings.visionModelResourceId,
|
workflowSettings.visionModelResourceId,
|
||||||
visionFocusPrompt:
|
visionPromptMode:
|
||||||
draft.visionFocusPrompt ?? workflowSettings.visionFocusPrompt,
|
draft.visionPromptMode ?? workflowSettings.visionPromptMode,
|
||||||
|
visionPrompt:
|
||||||
|
draft.visionPrompt ?? workflowSettings.visionPrompt,
|
||||||
...(!isRealtime
|
...(!isRealtime
|
||||||
? {
|
? {
|
||||||
llmResourceId:
|
llmResourceId:
|
||||||
@@ -308,13 +310,8 @@ export function AgentNodePanel({
|
|||||||
}
|
}
|
||||||
enabled={Boolean(draft.visionEnabled)}
|
enabled={Boolean(draft.visionEnabled)}
|
||||||
modelResourceId={draft.visionModelResourceId ?? ""}
|
modelResourceId={draft.visionModelResourceId ?? ""}
|
||||||
focusPrompt={draft.visionFocusPrompt ?? ""}
|
promptMode={draft.visionPromptMode ?? "runtime"}
|
||||||
sourcePrompt={draft.prompt ?? ""}
|
prompt={draft.visionPrompt ?? ""}
|
||||||
generatorModelResourceId={
|
|
||||||
isRealtime
|
|
||||||
? draft.visionModelResourceId ?? ""
|
|
||||||
: (draft.llmResourceId as string) || ""
|
|
||||||
}
|
|
||||||
mainModelResourceId={
|
mainModelResourceId={
|
||||||
isRealtime
|
isRealtime
|
||||||
? workflowSettings.realtime ?? ""
|
? workflowSettings.realtime ?? ""
|
||||||
@@ -339,8 +336,11 @@ export function AgentNodePanel({
|
|||||||
onModelResourceIdChange={(visionModelResourceId) =>
|
onModelResourceIdChange={(visionModelResourceId) =>
|
||||||
set("visionModelResourceId", visionModelResourceId)
|
set("visionModelResourceId", visionModelResourceId)
|
||||||
}
|
}
|
||||||
onFocusPromptChange={(visionFocusPrompt) =>
|
onPromptModeChange={(visionPromptMode) =>
|
||||||
set("visionFocusPrompt", visionFocusPrompt)
|
set("visionPromptMode", visionPromptMode)
|
||||||
|
}
|
||||||
|
onPromptChange={(visionPrompt) =>
|
||||||
|
set("visionPrompt", visionPrompt)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -184,13 +184,8 @@ export function GlobalSettingsPanel({
|
|||||||
}
|
}
|
||||||
enabled={settings.visionEnabled}
|
enabled={settings.visionEnabled}
|
||||||
modelResourceId={settings.visionModelResourceId}
|
modelResourceId={settings.visionModelResourceId}
|
||||||
focusPrompt={settings.visionFocusPrompt}
|
promptMode={settings.visionPromptMode}
|
||||||
sourcePrompt={settings.globalPrompt}
|
prompt={settings.visionPrompt}
|
||||||
generatorModelResourceId={
|
|
||||||
settings.runtimeMode === "realtime"
|
|
||||||
? settings.visionModelResourceId
|
|
||||||
: settings.llm ?? ""
|
|
||||||
}
|
|
||||||
mainModelResourceId={
|
mainModelResourceId={
|
||||||
settings.runtimeMode === "realtime"
|
settings.runtimeMode === "realtime"
|
||||||
? settings.realtime ?? ""
|
? settings.realtime ?? ""
|
||||||
@@ -216,8 +211,11 @@ export function GlobalSettingsPanel({
|
|||||||
onModelResourceIdChange={(visionModelResourceId) =>
|
onModelResourceIdChange={(visionModelResourceId) =>
|
||||||
onSettingsChange({ ...settings, visionModelResourceId })
|
onSettingsChange({ ...settings, visionModelResourceId })
|
||||||
}
|
}
|
||||||
onFocusPromptChange={(visionFocusPrompt) =>
|
onPromptModeChange={(visionPromptMode) =>
|
||||||
onSettingsChange({ ...settings, visionFocusPrompt })
|
onSettingsChange({ ...settings, visionPromptMode })
|
||||||
|
}
|
||||||
|
onPromptChange={(visionPrompt) =>
|
||||||
|
onSettingsChange({ ...settings, visionPrompt })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import * as LucideIcons from "lucide-react";
|
import * as LucideIcons from "lucide-react";
|
||||||
import { Circle, type LucideIcon } 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";
|
import { defaultTurnConfig } from "@/lib/turn-config";
|
||||||
|
|
||||||
export type WorkflowNodeType =
|
export type WorkflowNodeType =
|
||||||
@@ -54,7 +54,8 @@ export type WorkflowNodeData = {
|
|||||||
ttsResourceId?: string;
|
ttsResourceId?: string;
|
||||||
visionEnabled?: boolean;
|
visionEnabled?: boolean;
|
||||||
visionModelResourceId?: string;
|
visionModelResourceId?: string;
|
||||||
visionFocusPrompt?: string;
|
visionPromptMode?: VisionPromptMode;
|
||||||
|
visionPrompt?: string;
|
||||||
enableInterrupt?: boolean;
|
enableInterrupt?: boolean;
|
||||||
turnConfig?: TurnConfig;
|
turnConfig?: TurnConfig;
|
||||||
toolId?: string;
|
toolId?: string;
|
||||||
@@ -214,7 +215,8 @@ export type WorkflowGraph = {
|
|||||||
defaultTtsResourceId: string;
|
defaultTtsResourceId: string;
|
||||||
visionEnabled: boolean;
|
visionEnabled: boolean;
|
||||||
visionModelResourceId: string;
|
visionModelResourceId: string;
|
||||||
visionFocusPrompt: string;
|
visionPromptMode: VisionPromptMode;
|
||||||
|
visionPrompt: string;
|
||||||
toolIds: string[];
|
toolIds: string[];
|
||||||
knowledgeBaseId: string;
|
knowledgeBaseId: string;
|
||||||
knowledgeMode: "automatic" | "on_demand";
|
knowledgeMode: "automatic" | "on_demand";
|
||||||
@@ -252,7 +254,8 @@ export function defaultGraph(): WorkflowGraph {
|
|||||||
defaultTtsResourceId: "",
|
defaultTtsResourceId: "",
|
||||||
visionEnabled: false,
|
visionEnabled: false,
|
||||||
visionModelResourceId: "",
|
visionModelResourceId: "",
|
||||||
visionFocusPrompt: "",
|
visionPromptMode: "runtime",
|
||||||
|
visionPrompt: "",
|
||||||
toolIds: [],
|
toolIds: [],
|
||||||
knowledgeBaseId: "",
|
knowledgeBaseId: "",
|
||||||
knowledgeMode: "automatic",
|
knowledgeMode: "automatic",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
SystemToolKind,
|
SystemToolKind,
|
||||||
Tool,
|
Tool,
|
||||||
TurnConfig,
|
TurnConfig,
|
||||||
|
VisionPromptMode,
|
||||||
} from "@/lib/api";
|
} from "@/lib/api";
|
||||||
|
|
||||||
import type { WorkflowGraph } from "./specs";
|
import type { WorkflowGraph } from "./specs";
|
||||||
@@ -18,7 +19,8 @@ export type WorkflowSettings = {
|
|||||||
tts?: string;
|
tts?: string;
|
||||||
visionEnabled: boolean;
|
visionEnabled: boolean;
|
||||||
visionModelResourceId: string;
|
visionModelResourceId: string;
|
||||||
visionFocusPrompt: string;
|
visionPromptMode: VisionPromptMode;
|
||||||
|
visionPrompt: string;
|
||||||
toolIds: string[];
|
toolIds: string[];
|
||||||
knowledgeBaseId: string;
|
knowledgeBaseId: string;
|
||||||
knowledgeRetrievalConfig: KnowledgeRetrievalConfig;
|
knowledgeRetrievalConfig: KnowledgeRetrievalConfig;
|
||||||
|
|||||||
@@ -315,6 +315,8 @@ export type AnalysisConfig = {
|
|||||||
fields: AnalysisField[];
|
fields: AnalysisField[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type VisionPromptMode = "fixed" | "runtime" | "guided";
|
||||||
|
|
||||||
/** 后端 AssistantOut(宽表 STI:瘦字段平铺,workflow 用 graph)。apiKey 读时打码 */
|
/** 后端 AssistantOut(宽表 STI:瘦字段平铺,workflow 用 graph)。apiKey 读时打码 */
|
||||||
export type Assistant = {
|
export type Assistant = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -327,7 +329,8 @@ export type Assistant = {
|
|||||||
startup: StartupConfig;
|
startup: StartupConfig;
|
||||||
visionEnabled: boolean;
|
visionEnabled: boolean;
|
||||||
visionModelResourceId: string | null;
|
visionModelResourceId: string | null;
|
||||||
visionFocusPrompt: string;
|
visionPromptMode: VisionPromptMode;
|
||||||
|
visionPrompt: string;
|
||||||
modelResourceIds: Partial<Record<ModelType, string>>;
|
modelResourceIds: Partial<Record<ModelType, string>>;
|
||||||
knowledgeBaseId: string | null;
|
knowledgeBaseId: string | null;
|
||||||
knowledgeRetrievalConfig: KnowledgeRetrievalConfig;
|
knowledgeRetrievalConfig: KnowledgeRetrievalConfig;
|
||||||
@@ -372,14 +375,6 @@ export const assistantsApi = {
|
|||||||
// 服务端整行复制(含真 key,密钥不经浏览器)
|
// 服务端整行复制(含真 key,密钥不经浏览器)
|
||||||
duplicate: (id: string) =>
|
duplicate: (id: string) =>
|
||||||
request<Assistant>(`/api/assistants/${id}/duplicate`, { method: "POST" }),
|
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) =>
|
remove: (id: string) =>
|
||||||
request<{ ok: boolean }>(`/api/assistants/${id}`, { method: "DELETE" }),
|
request<{ ok: boolean }>(`/api/assistants/${id}`, { method: "DELETE" }),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user