refactor: unify system tools as resources
This commit is contained in:
@@ -154,8 +154,6 @@ class Assistant(Base):
|
|||||||
enable_interrupt: Mapped[bool] = mapped_column(Boolean, default=True)
|
enable_interrupt: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||||
turn_config: Mapped[dict] = mapped_column(JSON, default=dict)
|
turn_config: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||||
startup: Mapped[dict] = mapped_column(JSON, default=dict)
|
startup: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||||
# Prompt 助手级系统工具;Workflow 的权限保存在各 Agent 节点中。
|
|
||||||
system_tools: Mapped[list] = mapped_column(JSON, default=list)
|
|
||||||
vision_enabled: Mapped[bool] = mapped_column(Boolean, default=False)
|
vision_enabled: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
vision_model_resource_id: Mapped[str | None] = mapped_column(
|
vision_model_resource_id: Mapped[str | None] = mapped_column(
|
||||||
String(40),
|
String(40),
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import json
|
|||||||
|
|
||||||
import settings
|
import settings
|
||||||
from services.interface_catalog import INTERFACE_DEFINITIONS
|
from services.interface_catalog import INTERFACE_DEFINITIONS
|
||||||
|
from services.system_tools import SYSTEM_TOOL_SPECS
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
from sqlalchemy.ext.asyncio import (
|
from sqlalchemy.ext.asyncio import (
|
||||||
AsyncSession,
|
AsyncSession,
|
||||||
@@ -49,24 +50,31 @@ async def sync_interface_definitions() -> None:
|
|||||||
|
|
||||||
async def sync_default_tools() -> None:
|
async def sync_default_tools() -> None:
|
||||||
"""Ensure system-provided reusable tools exist without overwriting edits."""
|
"""Ensure system-provided reusable tools exist without overwriting edits."""
|
||||||
default_tools = [
|
default_system_tools = []
|
||||||
|
for kind, spec in SYSTEM_TOOL_SPECS.items():
|
||||||
|
config: dict[str, object] = {"kind": kind}
|
||||||
|
if kind == "end_conversation":
|
||||||
|
config.update(
|
||||||
{
|
{
|
||||||
"id": "tool_end_call_default",
|
|
||||||
"name": "结束对话",
|
|
||||||
"function_name": "end_call",
|
|
||||||
"type": "system",
|
|
||||||
"description": "当用户明确要求结束对话,或任务已完成时调用。",
|
|
||||||
"definition": {
|
|
||||||
"schema_version": 1,
|
|
||||||
"type": "system",
|
|
||||||
"config": {
|
|
||||||
"kind": "end_conversation",
|
|
||||||
"message_type": "none",
|
"message_type": "none",
|
||||||
"custom_message": "",
|
"custom_message": "",
|
||||||
"capture_reason": True,
|
"capture_reason": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
default_system_tools.append(
|
||||||
|
{
|
||||||
|
**spec,
|
||||||
|
"type": "system",
|
||||||
|
"definition": {
|
||||||
|
"schema_version": 1,
|
||||||
|
"type": "system",
|
||||||
|
"config": config,
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
},
|
)
|
||||||
|
|
||||||
|
default_tools = [
|
||||||
|
*default_system_tools,
|
||||||
{
|
{
|
||||||
"id": "tool_show_message_default",
|
"id": "tool_show_message_default",
|
||||||
"name": "显示确认消息",
|
"name": "显示确认消息",
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
"""unify system tools as reusable tool resources
|
||||||
|
|
||||||
|
Revision ID: 20260804_0012
|
||||||
|
Revises: 20260804_0011
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
import json
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
revision: str = "20260804_0012"
|
||||||
|
down_revision: str | Sequence[str] | None = "20260804_0011"
|
||||||
|
branch_labels: str | Sequence[str] | None = None
|
||||||
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
SYSTEM_TOOLS = {
|
||||||
|
"end_conversation": {
|
||||||
|
"id": "tool_end_call_default",
|
||||||
|
"name": "结束对话",
|
||||||
|
"function_name": "end_conversation",
|
||||||
|
"description": (
|
||||||
|
"礼貌地结束本次对话。当用户明确告别、表示任务已完成"
|
||||||
|
"或要求挂断时调用。"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"update_state": {
|
||||||
|
"id": "tool_update_state_default",
|
||||||
|
"name": "更新状态",
|
||||||
|
"function_name": "update_state",
|
||||||
|
"description": (
|
||||||
|
"静默更新本次对话中已经声明并明确授权的动态变量。"
|
||||||
|
"只提交本轮获得或确认的信息,更新后继续当前回答。"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"skip_turn": {
|
||||||
|
"id": "tool_skip_turn_default",
|
||||||
|
"name": "跳过本轮",
|
||||||
|
"function_name": "skip_turn",
|
||||||
|
"description": (
|
||||||
|
"跳过当前轮次,不生成任何语音回复。"
|
||||||
|
"仅当用户明确要求稍等、话还没说完,或输入可确认只是噪音时调用。"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"request_human_handoff": {
|
||||||
|
"id": "tool_request_human_handoff_default",
|
||||||
|
"name": "转接人工",
|
||||||
|
"function_name": "request_human_handoff",
|
||||||
|
"description": (
|
||||||
|
"提交人工接管请求。当用户明确要求人工服务、投诉升级或 AI 无法"
|
||||||
|
"解决时调用。该工具只提交请求,不代表人工已经接通;调用后继续"
|
||||||
|
"回复用户并说明正在等待人工响应。"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _definition(kind: str) -> dict:
|
||||||
|
config: dict[str, object] = {"kind": kind}
|
||||||
|
if kind == "end_conversation":
|
||||||
|
config.update(
|
||||||
|
{
|
||||||
|
"message_type": "none",
|
||||||
|
"custom_message": "",
|
||||||
|
"capture_reason": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return {"schema_version": 1, "type": "system", "config": config}
|
||||||
|
|
||||||
|
|
||||||
|
def _copy_global_agent_config(data: dict, settings: dict) -> None:
|
||||||
|
"""Freeze an inherited Agent before moving its node-local System tools."""
|
||||||
|
key_pairs = {
|
||||||
|
"defaultLlmResourceId": "llmResourceId",
|
||||||
|
"defaultAsrResourceId": "asrResourceId",
|
||||||
|
"defaultTtsResourceId": "ttsResourceId",
|
||||||
|
}
|
||||||
|
for source, target in key_pairs.items():
|
||||||
|
data[target] = settings.get(source)
|
||||||
|
for key in (
|
||||||
|
"visionEnabled",
|
||||||
|
"visionModelResourceId",
|
||||||
|
"knowledgeBaseId",
|
||||||
|
"knowledgeMode",
|
||||||
|
"knowledgeTopN",
|
||||||
|
"knowledgeScoreThreshold",
|
||||||
|
"enableInterrupt",
|
||||||
|
"turnConfig",
|
||||||
|
):
|
||||||
|
if key in settings:
|
||||||
|
data[key] = settings[key]
|
||||||
|
data["toolIds"] = list(settings.get("toolIds") or [])
|
||||||
|
data["inheritGlobalConfig"] = False
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
connection = op.get_bind()
|
||||||
|
for kind, spec in SYSTEM_TOOLS.items():
|
||||||
|
if kind == "end_conversation":
|
||||||
|
connection.execute(
|
||||||
|
sa.text(
|
||||||
|
"""
|
||||||
|
UPDATE tools
|
||||||
|
SET name = :name,
|
||||||
|
function_name = :function_name,
|
||||||
|
type = 'system',
|
||||||
|
description = :description,
|
||||||
|
definition = CAST(:definition AS jsonb),
|
||||||
|
status = 'active',
|
||||||
|
updated_at = now()
|
||||||
|
WHERE id = :id
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{**spec, "definition": json.dumps(_definition(kind))},
|
||||||
|
)
|
||||||
|
connection.execute(
|
||||||
|
sa.text(
|
||||||
|
"""
|
||||||
|
INSERT INTO tools
|
||||||
|
(id, name, function_name, type, description, definition, secrets, status)
|
||||||
|
VALUES
|
||||||
|
(:id, :name, :function_name, 'system', :description,
|
||||||
|
CAST(:definition AS jsonb), '{}'::jsonb, 'active')
|
||||||
|
ON CONFLICT (function_name) DO NOTHING
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{**spec, "definition": json.dumps(_definition(kind))},
|
||||||
|
)
|
||||||
|
|
||||||
|
assistants = connection.execute(
|
||||||
|
sa.text("SELECT id, system_tools, graph FROM assistants")
|
||||||
|
).mappings().all()
|
||||||
|
for assistant in assistants:
|
||||||
|
assistant_id = str(assistant["id"])
|
||||||
|
for kind in assistant["system_tools"] or []:
|
||||||
|
spec = SYSTEM_TOOLS.get(str(kind))
|
||||||
|
if not spec:
|
||||||
|
continue
|
||||||
|
connection.execute(
|
||||||
|
sa.text(
|
||||||
|
"""
|
||||||
|
INSERT INTO assistant_tool_bindings (assistant_id, tool_id)
|
||||||
|
VALUES (:assistant_id, :tool_id)
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{"assistant_id": assistant_id, "tool_id": spec["id"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
graph = assistant["graph"] or {}
|
||||||
|
if not isinstance(graph, dict):
|
||||||
|
continue
|
||||||
|
settings = graph.get("settings") or {}
|
||||||
|
changed = False
|
||||||
|
for node in graph.get("nodes") or []:
|
||||||
|
if node.get("type") != "agent":
|
||||||
|
continue
|
||||||
|
data = node.get("data") or {}
|
||||||
|
had_legacy_selection = "systemTools" in data
|
||||||
|
kinds = list(dict.fromkeys(data.pop("systemTools", []) or []))
|
||||||
|
if had_legacy_selection:
|
||||||
|
node["data"] = data
|
||||||
|
changed = True
|
||||||
|
if not kinds:
|
||||||
|
continue
|
||||||
|
if data.get("inheritGlobalConfig", True):
|
||||||
|
_copy_global_agent_config(data, settings)
|
||||||
|
tool_ids = list(data.get("toolIds") or [])
|
||||||
|
for kind in kinds:
|
||||||
|
spec = SYSTEM_TOOLS.get(str(kind))
|
||||||
|
if spec and spec["id"] not in tool_ids:
|
||||||
|
tool_ids.append(spec["id"])
|
||||||
|
connection.execute(
|
||||||
|
sa.text(
|
||||||
|
"""
|
||||||
|
INSERT INTO assistant_tool_bindings (assistant_id, tool_id)
|
||||||
|
VALUES (:assistant_id, :tool_id)
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{"assistant_id": assistant_id, "tool_id": spec["id"]},
|
||||||
|
)
|
||||||
|
data["toolIds"] = tool_ids
|
||||||
|
node["data"] = data
|
||||||
|
if changed:
|
||||||
|
connection.execute(
|
||||||
|
sa.text("UPDATE assistants SET graph = CAST(:graph AS json) WHERE id = :id"),
|
||||||
|
{"id": assistant_id, "graph": json.dumps(graph)},
|
||||||
|
)
|
||||||
|
|
||||||
|
op.drop_column("assistants", "system_tools")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.add_column(
|
||||||
|
"assistants",
|
||||||
|
sa.Column(
|
||||||
|
"system_tools",
|
||||||
|
sa.JSON(),
|
||||||
|
server_default=sa.text("'[]'"),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
DELETE FROM assistant_tool_bindings
|
||||||
|
WHERE tool_id IN (
|
||||||
|
'tool_update_state_default',
|
||||||
|
'tool_skip_turn_default',
|
||||||
|
'tool_request_human_handoff_default'
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
DELETE FROM tools
|
||||||
|
WHERE id IN (
|
||||||
|
'tool_update_state_default',
|
||||||
|
'tool_skip_turn_default',
|
||||||
|
'tool_request_human_handoff_default'
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
UPDATE tools
|
||||||
|
SET function_name = 'end_call'
|
||||||
|
WHERE id = 'tool_end_call_default'
|
||||||
|
"""
|
||||||
|
)
|
||||||
@@ -114,8 +114,6 @@ class AssistantConfig(BaseModel):
|
|||||||
# every item in ``tools``.
|
# every item in ``tools``.
|
||||||
tools: list[RuntimeTool] = Field(default_factory=list)
|
tools: list[RuntimeTool] = Field(default_factory=list)
|
||||||
llm_tool_ids: list[str] | None = None
|
llm_tool_ids: list[str] | None = None
|
||||||
# 助手级系统工具仅供 Prompt Pipeline;Workflow 在 Agent 节点中配置。
|
|
||||||
system_tools: list[str] = Field(default_factory=list)
|
|
||||||
knowledge_base_id: str | None = None
|
knowledge_base_id: str | None = None
|
||||||
knowledge_base_name: str = ""
|
knowledge_base_name: str = ""
|
||||||
knowledge_base_description: str = ""
|
knowledge_base_description: str = ""
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from schemas import AssistantOut, AssistantUpsert
|
|||||||
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.tool_policy import policy_for_tool
|
from services.tool_policy import policy_for_tool
|
||||||
from services.workflow_engine import WorkflowEngine
|
from services.workflow_engine import WorkflowEngine
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
@@ -58,13 +59,6 @@ def _validate_workflow(body: AssistantUpsert) -> None:
|
|||||||
f"Agent 节点 {node_id} 授权了未声明变量:"
|
f"Agent 节点 {node_id} 授权了未声明变量:"
|
||||||
+ ",".join(unknown)
|
+ ",".join(unknown)
|
||||||
)
|
)
|
||||||
if (
|
|
||||||
"update_state" in (data.get("systemTools") or [])
|
|
||||||
and not authorized
|
|
||||||
):
|
|
||||||
errors.append(
|
|
||||||
f"Agent 节点 {node_id} 启用更新状态工具时必须授权至少一个变量"
|
|
||||||
)
|
|
||||||
if errors:
|
if errors:
|
||||||
raise HTTPException(400, "工作流校验失败:" + ";".join(errors))
|
raise HTTPException(400, "工作流校验失败:" + ";".join(errors))
|
||||||
# Graph settings are the source of truth. The flat flag is only a session
|
# Graph settings are the source of truth. The flat flag is only a session
|
||||||
@@ -141,6 +135,60 @@ async def _validate_workflow_references(
|
|||||||
raise HTTPException(400, f"Workflow 引用了无效知识库:{knowledge_id}")
|
raise HTTPException(400, f"Workflow 引用了无效知识库:{knowledge_id}")
|
||||||
|
|
||||||
|
|
||||||
|
async def _validate_system_tool_selection(
|
||||||
|
session: AsyncSession,
|
||||||
|
body: AssistantUpsert,
|
||||||
|
) -> None:
|
||||||
|
"""Validate System resources in the same selection path as every other tool."""
|
||||||
|
selected_ids = list(dict.fromkeys(body.tool_ids))
|
||||||
|
if not selected_ids or body.type not in {"prompt", "workflow"}:
|
||||||
|
return
|
||||||
|
rows = (
|
||||||
|
await session.execute(select(Tool).where(Tool.id.in_(selected_ids)))
|
||||||
|
).scalars().all()
|
||||||
|
tools_by_id = {tool.id: tool for tool in rows if tool.status == "active"}
|
||||||
|
system_kinds = {
|
||||||
|
tool.id: system_tool_kind(tool.definition or {})
|
||||||
|
for tool in rows
|
||||||
|
if tool.status == "active" and tool.type == "system"
|
||||||
|
}
|
||||||
|
invalid = [tool_id for tool_id, kind in system_kinds.items() if kind is None]
|
||||||
|
if invalid:
|
||||||
|
raise HTTPException(400, "系统工具配置无效: " + ", ".join(invalid))
|
||||||
|
|
||||||
|
if body.type == "prompt":
|
||||||
|
if body.runtime_mode == "realtime" and system_kinds:
|
||||||
|
raise HTTPException(400, "Prompt Realtime 模式暂不支持系统工具")
|
||||||
|
if (
|
||||||
|
"update_state" in system_kinds.values()
|
||||||
|
and not body.dynamic_variable_definitions
|
||||||
|
):
|
||||||
|
raise HTTPException(400, "启用更新状态工具前必须声明至少一个动态变量")
|
||||||
|
return
|
||||||
|
|
||||||
|
engine = WorkflowEngine(body.graph)
|
||||||
|
for node_id, node in engine.nodes.items():
|
||||||
|
node_type = node.get("type")
|
||||||
|
data = node.get("data") or {}
|
||||||
|
if node_type == "agent":
|
||||||
|
stage = engine.agent_stage_config(node_id)
|
||||||
|
kinds = {
|
||||||
|
system_kinds[tool_id]
|
||||||
|
for tool_id in stage.tool_ids
|
||||||
|
if tool_id in system_kinds
|
||||||
|
}
|
||||||
|
if "update_state" in kinds and not stage.state_variable_names:
|
||||||
|
raise HTTPException(
|
||||||
|
400,
|
||||||
|
f"Agent 节点 {node_id} 启用更新状态工具时必须授权至少一个变量",
|
||||||
|
)
|
||||||
|
elif node_type == "action":
|
||||||
|
tool_id = str(data.get("toolId") or "")
|
||||||
|
tool = tools_by_id.get(tool_id)
|
||||||
|
if tool and tool.type == "system":
|
||||||
|
raise HTTPException(400, f"Action 节点 {node_id} 不能调用系统工具")
|
||||||
|
|
||||||
|
|
||||||
async def _validate_startup_actions(
|
async def _validate_startup_actions(
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
body: AssistantUpsert,
|
body: AssistantUpsert,
|
||||||
@@ -312,7 +360,6 @@ async def _to_out(session: AsyncSession, assistant: Assistant) -> AssistantOut:
|
|||||||
enable_interrupt=assistant.enable_interrupt,
|
enable_interrupt=assistant.enable_interrupt,
|
||||||
turn_config=assistant.turn_config or {},
|
turn_config=assistant.turn_config or {},
|
||||||
startup=assistant.startup or {},
|
startup=assistant.startup or {},
|
||||||
system_tools=assistant.system_tools 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,
|
||||||
model_resource_ids=await _resource_ids(session, assistant.id),
|
model_resource_ids=await _resource_ids(session, assistant.id),
|
||||||
@@ -347,6 +394,7 @@ async def create_assistant(
|
|||||||
):
|
):
|
||||||
_validate_workflow(body)
|
_validate_workflow(body)
|
||||||
await _validate_workflow_references(session, body)
|
await _validate_workflow_references(session, body)
|
||||||
|
await _validate_system_tool_selection(session, body)
|
||||||
await _validate_startup_actions(session, body)
|
await _validate_startup_actions(session, body)
|
||||||
await _validate_vision_model(session, body)
|
await _validate_vision_model(session, body)
|
||||||
await _validate_knowledge_base(session, body)
|
await _validate_knowledge_base(session, body)
|
||||||
@@ -389,7 +437,6 @@ async def duplicate_assistant(
|
|||||||
enable_interrupt=source.enable_interrupt,
|
enable_interrupt=source.enable_interrupt,
|
||||||
turn_config=dict(source.turn_config or {}),
|
turn_config=dict(source.turn_config or {}),
|
||||||
startup=dict(source.startup or {}),
|
startup=dict(source.startup or {}),
|
||||||
system_tools=list(source.system_tools 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,
|
||||||
knowledge_base_id=source.knowledge_base_id,
|
knowledge_base_id=source.knowledge_base_id,
|
||||||
@@ -423,6 +470,7 @@ async def update_assistant(
|
|||||||
raise HTTPException(404, "助手不存在")
|
raise HTTPException(404, "助手不存在")
|
||||||
_validate_workflow(body)
|
_validate_workflow(body)
|
||||||
await _validate_workflow_references(session, body)
|
await _validate_workflow_references(session, body)
|
||||||
|
await _validate_system_tool_selection(session, body)
|
||||||
await _validate_startup_actions(session, body)
|
await _validate_startup_actions(session, body)
|
||||||
await _validate_vision_model(session, body)
|
await _validate_vision_model(session, body)
|
||||||
await _validate_knowledge_base(session, body)
|
await _validate_knowledge_base(session, body)
|
||||||
|
|||||||
@@ -159,9 +159,6 @@ 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
|
||||||
# 内置系统工具开关(仅 prompt 类型可用,类似 ElevenLabs Agent 的系统工具)。
|
|
||||||
system_tools: list[SystemToolKind] = Field(default_factory=list, max_length=4)
|
|
||||||
|
|
||||||
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(
|
||||||
@@ -199,8 +196,6 @@ class AssistantUpsert(CamelModel):
|
|||||||
for field in ("prompt", "api_url", "api_key", "app_id"):
|
for field in ("prompt", "api_url", "api_key", "app_id"):
|
||||||
if field not in allowed:
|
if field not in allowed:
|
||||||
setattr(self, field, "")
|
setattr(self, field, "")
|
||||||
if self.type != "prompt":
|
|
||||||
self.system_tools = []
|
|
||||||
if "graph" not in allowed:
|
if "graph" not in allowed:
|
||||||
self.graph = {}
|
self.graph = {}
|
||||||
if self.type == "workflow":
|
if self.type == "workflow":
|
||||||
@@ -220,15 +215,6 @@ class AssistantUpsert(CamelModel):
|
|||||||
# 外部托管大脑只能 cascade,拦住不兼容的 realtime
|
# 外部托管大脑只能 cascade,拦住不兼容的 realtime
|
||||||
if self.runtime_mode == "realtime" and self.type not in REALTIME_CAPABLE_TYPES:
|
if self.runtime_mode == "realtime" and self.type not in REALTIME_CAPABLE_TYPES:
|
||||||
raise ValueError(f"类型 {self.type} 不支持 realtime 运行模式")
|
raise ValueError(f"类型 {self.type} 不支持 realtime 运行模式")
|
||||||
if self.type == "prompt":
|
|
||||||
self.system_tools = list(dict.fromkeys(self.system_tools))
|
|
||||||
if self.runtime_mode == "realtime" and self.system_tools:
|
|
||||||
raise ValueError("Prompt Realtime 模式暂不支持系统工具")
|
|
||||||
if (
|
|
||||||
"update_state" in self.system_tools
|
|
||||||
and not self.dynamic_variable_definitions
|
|
||||||
):
|
|
||||||
raise ValueError("启用更新状态工具前必须声明至少一个动态变量")
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
@@ -270,7 +256,7 @@ class ToolParameter(CamelModel):
|
|||||||
|
|
||||||
|
|
||||||
class SystemToolConfig(CamelModel):
|
class SystemToolConfig(CamelModel):
|
||||||
kind: Literal["end_conversation"] = "end_conversation"
|
kind: SystemToolKind = "end_conversation"
|
||||||
message_type: Literal["none", "custom"] = "none"
|
message_type: Literal["none", "custom"] = "none"
|
||||||
custom_message: str = ""
|
custom_message: str = ""
|
||||||
capture_reason: bool = True
|
capture_reason: bool = True
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ from services.message_stage import (
|
|||||||
MessageStageSpec,
|
MessageStageSpec,
|
||||||
)
|
)
|
||||||
from services.runtime_variables import DynamicVariableError, DynamicVariableStore
|
from services.runtime_variables import DynamicVariableError, DynamicVariableStore
|
||||||
from services.system_tools import SYSTEM_TOOL_KINDS, state_update_properties
|
from services.system_tools import state_update_properties, system_tool_kind
|
||||||
from services.tool_executor import ToolExecutionError, ToolExecutor
|
from services.tool_executor import ToolExecutionError, ToolExecutor
|
||||||
from services.tool_policy import policy_for_tool
|
from services.tool_policy import policy_for_tool
|
||||||
|
|
||||||
@@ -128,11 +128,16 @@ class PromptBrain(BaseBrain):
|
|||||||
if llm_tool_ids is not None and tool.id not in llm_tool_ids:
|
if llm_tool_ids is not None and tool.id not in llm_tool_ids:
|
||||||
continue
|
continue
|
||||||
if tool.type == "system":
|
if tool.type == "system":
|
||||||
schema, handler = self._make_end_call_tool(tool, runtime)
|
schema, handler = self._make_system_tool(tool, runtime)
|
||||||
elif tool.type in {"http", "mcp", "client"}:
|
elif tool.type in {"http", "mcp", "client"}:
|
||||||
schema, handler = self._make_remote_tool(tool, runtime)
|
schema, handler = self._make_remote_tool(tool, runtime)
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
if schema.name in registered_names:
|
||||||
|
logger.warning(
|
||||||
|
f"跳过工具 {tool.id}: 函数名 {schema.name} 已被占用"
|
||||||
|
)
|
||||||
|
continue
|
||||||
schemas.append(schema)
|
schemas.append(schema)
|
||||||
registered_names.add(schema.name)
|
registered_names.add(schema.name)
|
||||||
policy = policy_for_tool(tool)
|
policy = policy_for_tool(tool)
|
||||||
@@ -141,19 +146,6 @@ class PromptBrain(BaseBrain):
|
|||||||
handler,
|
handler,
|
||||||
cancel_on_interruption=policy.cancel_on_interruption,
|
cancel_on_interruption=policy.cancel_on_interruption,
|
||||||
)
|
)
|
||||||
for kind in cfg.system_tools or []:
|
|
||||||
if kind not in SYSTEM_TOOL_KINDS:
|
|
||||||
logger.warning(f"忽略未知系统工具: {kind}")
|
|
||||||
continue
|
|
||||||
schema, handler = self._make_system_tool(kind, runtime)
|
|
||||||
if schema.name in registered_names:
|
|
||||||
logger.warning(
|
|
||||||
f"跳过系统工具 {schema.name}: 与已绑定工具函数名冲突"
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
registered_names.add(schema.name)
|
|
||||||
schemas.append(schema)
|
|
||||||
runtime.llm.register_function(schema.name, handler)
|
|
||||||
runtime.set_tools(schemas)
|
runtime.set_tools(schemas)
|
||||||
|
|
||||||
async def run_preflight(self) -> None:
|
async def run_preflight(self) -> None:
|
||||||
@@ -598,50 +590,23 @@ class PromptBrain(BaseBrain):
|
|||||||
)
|
)
|
||||||
return schema, end_call
|
return schema, end_call
|
||||||
|
|
||||||
# ---------- 内置系统工具(助手配置 system_tools 开关) ----------
|
# ---------- System 工具资源 ----------
|
||||||
|
|
||||||
def _make_system_tool(self, kind: str, runtime: BrainRuntime):
|
def _make_system_tool(self, tool, runtime: BrainRuntime):
|
||||||
|
kind = system_tool_kind(tool.definition or {})
|
||||||
|
if not kind:
|
||||||
|
raise ValueError(f"系统工具 {tool.id} 缺少有效 kind")
|
||||||
if kind == "end_conversation":
|
if kind == "end_conversation":
|
||||||
return self._make_end_conversation_tool(runtime)
|
return self._make_end_call_tool(tool, runtime)
|
||||||
if kind == "update_state":
|
if kind == "update_state":
|
||||||
return self._make_update_state_tool(runtime)
|
return self._make_update_state_tool(tool, runtime)
|
||||||
if kind == "skip_turn":
|
if kind == "skip_turn":
|
||||||
return self._make_skip_turn_tool()
|
return self._make_skip_turn_tool(tool)
|
||||||
if kind == "request_human_handoff":
|
if kind == "request_human_handoff":
|
||||||
return self._make_handoff_tool(runtime)
|
return self._make_handoff_tool(tool, runtime)
|
||||||
raise ValueError(f"未知系统工具: {kind}")
|
raise ValueError(f"未知系统工具: {kind}")
|
||||||
|
|
||||||
def _make_end_conversation_tool(self, runtime: BrainRuntime):
|
def _make_update_state_tool(self, tool, runtime: BrainRuntime):
|
||||||
"""结束本次对话,等待模型已生成的告别语播完后再挂断。"""
|
|
||||||
|
|
||||||
async def end_conversation(params: FunctionCallParams) -> None:
|
|
||||||
reason = str(
|
|
||||||
params.arguments.get("reason") or "end_conversation"
|
|
||||||
).strip()
|
|
||||||
self._waiting_for_generated_end_speech = True
|
|
||||||
runtime.call_end.begin(reason)
|
|
||||||
await params.result_callback(
|
|
||||||
{"status": "success", "action": "ending_call"},
|
|
||||||
properties=FunctionCallResultProperties(run_llm=False),
|
|
||||||
)
|
|
||||||
|
|
||||||
schema = FunctionSchema(
|
|
||||||
name="end_conversation",
|
|
||||||
description=(
|
|
||||||
"礼貌地结束本次对话。当用户明确告别、表示任务已完成"
|
|
||||||
"或要求挂断时调用。"
|
|
||||||
),
|
|
||||||
properties={
|
|
||||||
"reason": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "结束对话的简短原因。",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
required=[],
|
|
||||||
)
|
|
||||||
return schema, end_conversation
|
|
||||||
|
|
||||||
def _make_update_state_tool(self, runtime: BrainRuntime):
|
|
||||||
"""更新已声明的动态变量(会话状态),并让模型继续当前回答。"""
|
"""更新已声明的动态变量(会话状态),并让模型继续当前回答。"""
|
||||||
|
|
||||||
writable = state_update_properties(self._cfg.dynamic_variable_definitions)
|
writable = state_update_properties(self._cfg.dynamic_variable_definitions)
|
||||||
@@ -676,8 +641,8 @@ class PromptBrain(BaseBrain):
|
|||||||
)
|
)
|
||||||
|
|
||||||
schema = FunctionSchema(
|
schema = FunctionSchema(
|
||||||
name="update_state",
|
name=tool.function_name,
|
||||||
description=(
|
description=tool.description or (
|
||||||
"静默更新本次对话中已经声明并明确列出的动态变量。"
|
"静默更新本次对话中已经声明并明确列出的动态变量。"
|
||||||
"只提交本轮获得或确认的信息,更新后继续当前回答。"
|
"只提交本轮获得或确认的信息,更新后继续当前回答。"
|
||||||
),
|
),
|
||||||
@@ -686,7 +651,7 @@ class PromptBrain(BaseBrain):
|
|||||||
)
|
)
|
||||||
return schema, update_state
|
return schema, update_state
|
||||||
|
|
||||||
def _make_skip_turn_tool(self):
|
def _make_skip_turn_tool(self, tool):
|
||||||
"""跳过当前轮次,不生成任何语音回复。"""
|
"""跳过当前轮次,不生成任何语音回复。"""
|
||||||
|
|
||||||
async def skip_turn(params: FunctionCallParams) -> None:
|
async def skip_turn(params: FunctionCallParams) -> None:
|
||||||
@@ -700,8 +665,8 @@ class PromptBrain(BaseBrain):
|
|||||||
)
|
)
|
||||||
|
|
||||||
schema = FunctionSchema(
|
schema = FunctionSchema(
|
||||||
name="skip_turn",
|
name=tool.function_name,
|
||||||
description=(
|
description=tool.description or (
|
||||||
"跳过当前轮次,不生成任何语音回复。"
|
"跳过当前轮次,不生成任何语音回复。"
|
||||||
"仅当用户明确要求稍等、话还没说完,或输入可确认只是噪音时调用。"
|
"仅当用户明确要求稍等、话还没说完,或输入可确认只是噪音时调用。"
|
||||||
),
|
),
|
||||||
@@ -715,7 +680,7 @@ class PromptBrain(BaseBrain):
|
|||||||
)
|
)
|
||||||
return schema, skip_turn
|
return schema, skip_turn
|
||||||
|
|
||||||
def _make_handoff_tool(self, runtime: BrainRuntime):
|
def _make_handoff_tool(self, tool, runtime: BrainRuntime):
|
||||||
"""提交人工接管请求;请求完成前保持当前 AI 会话可用。"""
|
"""提交人工接管请求;请求完成前保持当前 AI 会话可用。"""
|
||||||
|
|
||||||
async def request_human_handoff(params: FunctionCallParams) -> None:
|
async def request_human_handoff(params: FunctionCallParams) -> None:
|
||||||
@@ -741,8 +706,8 @@ class PromptBrain(BaseBrain):
|
|||||||
)
|
)
|
||||||
|
|
||||||
schema = FunctionSchema(
|
schema = FunctionSchema(
|
||||||
name="request_human_handoff",
|
name=tool.function_name,
|
||||||
description=(
|
description=tool.description or (
|
||||||
"提交人工接管请求。当用户明确要求人工服务、投诉升级或 AI 无法"
|
"提交人工接管请求。当用户明确要求人工服务、投诉升级或 AI 无法"
|
||||||
"解决时调用。该工具只提交请求,不代表人工已经接通;调用后继续"
|
"解决时调用。该工具只提交请求,不代表人工已经接通;调用后继续"
|
||||||
"回复用户并说明正在等待人工响应。"
|
"回复用户并说明正在等待人工响应。"
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ from services.message_stage import (
|
|||||||
MessageStageSpec,
|
MessageStageSpec,
|
||||||
)
|
)
|
||||||
from services.runtime_variables import DynamicVariableError, DynamicVariableStore
|
from services.runtime_variables import DynamicVariableError, DynamicVariableStore
|
||||||
from services.system_tools import SYSTEM_TOOL_KINDS, state_update_properties
|
from services.system_tools import state_update_properties, system_tool_kind
|
||||||
from services.tool_executor import ToolExecutionError, ToolExecutor
|
from services.tool_executor import ToolExecutionError, ToolExecutor
|
||||||
from services.tool_policy import policy_for_tool
|
from services.tool_policy import policy_for_tool
|
||||||
from services.workflow.agent import WorkflowAgentStage
|
from services.workflow.agent import WorkflowAgentStage
|
||||||
@@ -564,22 +564,21 @@ class WorkflowBrain(BaseBrain):
|
|||||||
|
|
||||||
for tool_id in stage.tool_ids:
|
for tool_id in stage.tool_ids:
|
||||||
tool = self._tool_by_id.get(str(tool_id))
|
tool = self._tool_by_id.get(str(tool_id))
|
||||||
if tool and tool.type in {"http", "mcp", "client"}:
|
if not tool:
|
||||||
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)
|
|
||||||
for kind in stage.system_tools:
|
|
||||||
if kind not in SYSTEM_TOOL_KINDS:
|
|
||||||
logger.warning(f"忽略 Agent {node_id} 的未知系统工具: {kind}")
|
|
||||||
continue
|
continue
|
||||||
|
if tool.type == "system":
|
||||||
append_function(
|
append_function(
|
||||||
self._workflow_system_tool(
|
self._workflow_system_tool(
|
||||||
kind,
|
tool,
|
||||||
node_id=node_id,
|
node_id=node_id,
|
||||||
state_variable_names=stage.state_variable_names,
|
state_variable_names=stage.state_variable_names,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
elif tool.type in {"http", "mcp", "client"}:
|
||||||
|
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)
|
||||||
return self._require_agent_stage().node_config(
|
return self._require_agent_stage().node_config(
|
||||||
node_id,
|
node_id,
|
||||||
functions=functions,
|
functions=functions,
|
||||||
@@ -738,27 +737,30 @@ class WorkflowBrain(BaseBrain):
|
|||||||
|
|
||||||
def _workflow_system_tool(
|
def _workflow_system_tool(
|
||||||
self,
|
self,
|
||||||
kind: str,
|
tool: RuntimeTool,
|
||||||
*,
|
*,
|
||||||
node_id: str,
|
node_id: str,
|
||||||
state_variable_names: tuple[str, ...],
|
state_variable_names: tuple[str, ...],
|
||||||
) -> FlowsFunctionSchema:
|
) -> FlowsFunctionSchema:
|
||||||
"""Build one platform-owned tool scoped to the active Agent node."""
|
"""Build one platform-owned tool scoped to the active Agent node."""
|
||||||
|
kind = system_tool_kind(tool.definition or {})
|
||||||
if kind == "update_state":
|
if kind == "update_state":
|
||||||
return self._workflow_update_state_tool(
|
return self._workflow_update_state_tool(
|
||||||
|
tool,
|
||||||
node_id,
|
node_id,
|
||||||
state_variable_names=state_variable_names,
|
state_variable_names=state_variable_names,
|
||||||
)
|
)
|
||||||
if kind == "skip_turn":
|
if kind == "skip_turn":
|
||||||
return self._workflow_skip_turn_tool()
|
return self._workflow_skip_turn_tool(tool)
|
||||||
if kind == "request_human_handoff":
|
if kind == "request_human_handoff":
|
||||||
return self._workflow_handoff_tool(node_id)
|
return self._workflow_handoff_tool(tool, node_id)
|
||||||
if kind == "end_conversation":
|
if kind == "end_conversation":
|
||||||
return self._workflow_end_conversation_tool()
|
return self._workflow_end_conversation_tool(tool, node_id)
|
||||||
raise ValueError(f"未知系统工具: {kind}")
|
raise ValueError(f"系统工具 {tool.id} 缺少有效 kind")
|
||||||
|
|
||||||
def _workflow_update_state_tool(
|
def _workflow_update_state_tool(
|
||||||
self,
|
self,
|
||||||
|
tool: RuntimeTool,
|
||||||
node_id: str,
|
node_id: str,
|
||||||
*,
|
*,
|
||||||
state_variable_names: tuple[str, ...],
|
state_variable_names: tuple[str, ...],
|
||||||
@@ -791,8 +793,8 @@ class WorkflowBrain(BaseBrain):
|
|||||||
}
|
}
|
||||||
|
|
||||||
return FlowsFunctionSchema(
|
return FlowsFunctionSchema(
|
||||||
name="update_state",
|
name=tool.function_name,
|
||||||
description=(
|
description=tool.description or (
|
||||||
"静默更新当前阶段明确授权的动态变量。"
|
"静默更新当前阶段明确授权的动态变量。"
|
||||||
"只提交本轮获得或确认的信息,更新后继续当前回答。"
|
"只提交本轮获得或确认的信息,更新后继续当前回答。"
|
||||||
),
|
),
|
||||||
@@ -805,7 +807,7 @@ class WorkflowBrain(BaseBrain):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _workflow_skip_turn_tool() -> FlowsFunctionSchema:
|
def _workflow_skip_turn_tool(tool: RuntimeTool) -> FlowsFunctionSchema:
|
||||||
async def handler(args, _flow_manager):
|
async def handler(args, _flow_manager):
|
||||||
reason = str((args or {}).get("reason") or "").strip()
|
reason = str((args or {}).get("reason") or "").strip()
|
||||||
result = {"status": "success", "action": "skip_turn"}
|
result = {"status": "success", "action": "skip_turn"}
|
||||||
@@ -815,8 +817,8 @@ class WorkflowBrain(BaseBrain):
|
|||||||
|
|
||||||
setattr(handler, "_suppress_followup_llm", True)
|
setattr(handler, "_suppress_followup_llm", True)
|
||||||
return FlowsFunctionSchema(
|
return FlowsFunctionSchema(
|
||||||
name="skip_turn",
|
name=tool.function_name,
|
||||||
description=(
|
description=tool.description or (
|
||||||
"跳过当前轮次,不生成任何语音回复。"
|
"跳过当前轮次,不生成任何语音回复。"
|
||||||
"仅当用户明确要求稍等、话还没说完,或输入可确认只是噪音时调用。"
|
"仅当用户明确要求稍等、话还没说完,或输入可确认只是噪音时调用。"
|
||||||
),
|
),
|
||||||
@@ -830,7 +832,9 @@ class WorkflowBrain(BaseBrain):
|
|||||||
handler=handler,
|
handler=handler,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _workflow_handoff_tool(self, node_id: str) -> FlowsFunctionSchema:
|
def _workflow_handoff_tool(
|
||||||
|
self, tool: RuntimeTool, node_id: str
|
||||||
|
) -> FlowsFunctionSchema:
|
||||||
async def handler(args, _flow_manager):
|
async def handler(args, _flow_manager):
|
||||||
reason = str((args or {}).get("reason") or "human_handoff").strip()
|
reason = str((args or {}).get("reason") or "human_handoff").strip()
|
||||||
await self._require_runtime().queue_frame(
|
await self._require_runtime().queue_frame(
|
||||||
@@ -852,8 +856,8 @@ class WorkflowBrain(BaseBrain):
|
|||||||
}
|
}
|
||||||
|
|
||||||
return FlowsFunctionSchema(
|
return FlowsFunctionSchema(
|
||||||
name="request_human_handoff",
|
name=tool.function_name,
|
||||||
description=(
|
description=tool.description or (
|
||||||
"提交人工接管请求。当用户明确要求人工服务、投诉升级或 AI 无法"
|
"提交人工接管请求。当用户明确要求人工服务、投诉升级或 AI 无法"
|
||||||
"解决时调用。该工具只提交请求,不代表人工已经接通;调用后继续"
|
"解决时调用。该工具只提交请求,不代表人工已经接通;调用后继续"
|
||||||
"回复用户并说明正在等待人工响应。"
|
"回复用户并说明正在等待人工响应。"
|
||||||
@@ -865,27 +869,55 @@ class WorkflowBrain(BaseBrain):
|
|||||||
handler=handler,
|
handler=handler,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _workflow_end_conversation_tool(self) -> FlowsFunctionSchema:
|
def _workflow_end_conversation_tool(
|
||||||
|
self, tool: RuntimeTool, node_id: str
|
||||||
|
) -> FlowsFunctionSchema:
|
||||||
|
config = (tool.definition or {}).get("config") or {}
|
||||||
|
message_type = str(config.get("message_type") or "none")
|
||||||
|
custom_message = str(config.get("custom_message") or "").strip()
|
||||||
|
capture_reason = bool(config.get("capture_reason", True))
|
||||||
|
|
||||||
async def handler(args, _flow_manager):
|
async def handler(args, _flow_manager):
|
||||||
reason = str((args or {}).get("reason") or "end_conversation").strip()
|
reason = str((args or {}).get("reason") or "end_conversation").strip()
|
||||||
self._waiting_for_generated_end_speech = True
|
uses_custom_message = message_type == "custom" and bool(custom_message)
|
||||||
self._require_runtime().call_end.begin(reason)
|
self._waiting_for_generated_end_speech = not uses_custom_message
|
||||||
|
runtime = self._require_runtime()
|
||||||
|
runtime.call_end.begin(reason)
|
||||||
|
if uses_custom_message:
|
||||||
|
await self._queue_visible_speech(
|
||||||
|
custom_message,
|
||||||
|
source="workflow-system-tool",
|
||||||
|
node_id=node_id,
|
||||||
|
)
|
||||||
|
arm_tracked = getattr(
|
||||||
|
runtime.call_end,
|
||||||
|
"arm_after_tracked_speech",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if callable(arm_tracked):
|
||||||
|
await arm_tracked()
|
||||||
|
else:
|
||||||
|
runtime.call_end.arm_after_speech()
|
||||||
return {"status": "success", "action": "ending_call"}
|
return {"status": "success", "action": "ending_call"}
|
||||||
|
|
||||||
setattr(handler, "_suppress_followup_llm", True)
|
setattr(handler, "_suppress_followup_llm", True)
|
||||||
return FlowsFunctionSchema(
|
return FlowsFunctionSchema(
|
||||||
name="end_conversation",
|
name=tool.function_name,
|
||||||
description=(
|
description=tool.description or (
|
||||||
"礼貌地结束本次对话。当用户明确告别、表示任务已完成"
|
"礼貌地结束本次对话。当用户明确告别、表示任务已完成"
|
||||||
"或要求挂断时调用。"
|
"或要求挂断时调用。"
|
||||||
),
|
),
|
||||||
properties={
|
properties=(
|
||||||
|
{
|
||||||
"reason": {
|
"reason": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "结束对话的简短原因。",
|
"description": "结束对话的简短原因。",
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
required=[],
|
if capture_reason
|
||||||
|
else {}
|
||||||
|
),
|
||||||
|
required=["reason"] if capture_reason else [],
|
||||||
handler=handler,
|
handler=handler,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -266,7 +266,6 @@ async def resolve_runtime_config(
|
|||||||
enableInterrupt=assistant.enable_interrupt,
|
enableInterrupt=assistant.enable_interrupt,
|
||||||
turnConfig=assistant.turn_config or {},
|
turnConfig=assistant.turn_config or {},
|
||||||
startup=assistant.startup or {},
|
startup=assistant.startup or {},
|
||||||
system_tools=assistant.system_tools or [],
|
|
||||||
tools=runtime_tools,
|
tools=runtime_tools,
|
||||||
llm_tool_ids=llm_tool_ids,
|
llm_tool_ids=llm_tool_ids,
|
||||||
knowledge_base_id=assistant.knowledge_base_id,
|
knowledge_base_id=assistant.knowledge_base_id,
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ from services.message_policy import (
|
|||||||
MESSAGE_CONFIRMATION,
|
MESSAGE_CONFIRMATION,
|
||||||
MESSAGE_PLAYBACK,
|
MESSAGE_PLAYBACK,
|
||||||
)
|
)
|
||||||
from services.system_tools import SYSTEM_TOOL_KINDS, normalize_system_tools
|
|
||||||
|
|
||||||
|
|
||||||
SPEC_VERSION = "3"
|
SPEC_VERSION = "3"
|
||||||
@@ -193,7 +192,7 @@ def _normalize_agent_data(data: dict[str, Any]) -> None:
|
|||||||
if data.get("entryMode") not in AGENT_ENTRY_MODES:
|
if data.get("entryMode") not in AGENT_ENTRY_MODES:
|
||||||
data["entryMode"] = "wait_user"
|
data["entryMode"] = "wait_user"
|
||||||
data.pop("entrySpeech", None)
|
data.pop("entrySpeech", None)
|
||||||
data["systemTools"] = list(normalize_system_tools(data.get("systemTools")))
|
data.pop("systemTools", None)
|
||||||
state_names = data.get("stateVariableNames")
|
state_names = data.get("stateVariableNames")
|
||||||
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))
|
||||||
@@ -439,11 +438,6 @@ def validate_graph(graph: dict[str, Any]) -> list[str]:
|
|||||||
entry_mode = data.get("entryMode", "wait_user")
|
entry_mode = data.get("entryMode", "wait_user")
|
||||||
if entry_mode not in AGENT_ENTRY_MODES:
|
if entry_mode not in AGENT_ENTRY_MODES:
|
||||||
errors.append(f"Agent 节点 {node_id} 的进入模式无效:{entry_mode}")
|
errors.append(f"Agent 节点 {node_id} 的进入模式无效:{entry_mode}")
|
||||||
system_tools = data.get("systemTools", [])
|
|
||||||
if not isinstance(system_tools, list) or any(
|
|
||||||
tool not in SYSTEM_TOOL_KINDS for tool in system_tools
|
|
||||||
):
|
|
||||||
errors.append(f"Agent 节点 {node_id} 的系统工具配置无效")
|
|
||||||
state_names = data.get("stateVariableNames", [])
|
state_names = data.get("stateVariableNames", [])
|
||||||
if not isinstance(state_names, list) or any(
|
if not isinstance(state_names, list) or any(
|
||||||
not isinstance(name, str) for name in state_names
|
not isinstance(name, str) for name in state_names
|
||||||
|
|||||||
@@ -6,25 +6,56 @@ from collections.abc import Iterable, Mapping
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
SYSTEM_TOOL_KINDS = frozenset(
|
SYSTEM_TOOL_SPECS: dict[str, dict[str, str]] = {
|
||||||
{
|
"end_conversation": {
|
||||||
"end_conversation",
|
"id": "tool_end_call_default",
|
||||||
"update_state",
|
"name": "结束对话",
|
||||||
"skip_turn",
|
"function_name": "end_conversation",
|
||||||
"request_human_handoff",
|
"description": (
|
||||||
|
"礼貌地结束本次对话。当用户明确告别、表示任务已完成"
|
||||||
|
"或要求挂断时调用。"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"update_state": {
|
||||||
|
"id": "tool_update_state_default",
|
||||||
|
"name": "更新状态",
|
||||||
|
"function_name": "update_state",
|
||||||
|
"description": (
|
||||||
|
"静默更新本次对话中已经声明并明确授权的动态变量。"
|
||||||
|
"只提交本轮获得或确认的信息,更新后继续当前回答。"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"skip_turn": {
|
||||||
|
"id": "tool_skip_turn_default",
|
||||||
|
"name": "跳过本轮",
|
||||||
|
"function_name": "skip_turn",
|
||||||
|
"description": (
|
||||||
|
"跳过当前轮次,不生成任何语音回复。"
|
||||||
|
"仅当用户明确要求稍等、话还没说完,或输入可确认只是噪音时调用。"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"request_human_handoff": {
|
||||||
|
"id": "tool_request_human_handoff_default",
|
||||||
|
"name": "转接人工",
|
||||||
|
"function_name": "request_human_handoff",
|
||||||
|
"description": (
|
||||||
|
"提交人工接管请求。当用户明确要求人工服务、投诉升级或 AI 无法"
|
||||||
|
"解决时调用。该工具只提交请求,不代表人工已经接通;调用后继续"
|
||||||
|
"回复用户并说明正在等待人工响应。"
|
||||||
|
),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
SYSTEM_TOOL_KINDS = frozenset(SYSTEM_TOOL_SPECS)
|
||||||
|
|
||||||
|
|
||||||
def normalize_system_tools(values: Iterable[Any] | None) -> tuple[str, ...]:
|
def system_tool_kind(definition: Mapping[str, Any] | None) -> str | None:
|
||||||
"""Return known tool names once each while preserving editor order."""
|
"""Return the supported platform behavior declared by a System resource."""
|
||||||
return tuple(
|
config = (definition or {}).get("config")
|
||||||
dict.fromkeys(
|
if not isinstance(config, Mapping):
|
||||||
str(value)
|
return None
|
||||||
for value in values or ()
|
kind = str(config.get("kind") or "")
|
||||||
if str(value) in SYSTEM_TOOL_KINDS
|
return kind if kind in SYSTEM_TOOL_KINDS else None
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def state_update_properties(
|
def state_update_properties(
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ from typing import Any
|
|||||||
|
|
||||||
from services.node_specs import normalize_graph
|
from services.node_specs import normalize_graph
|
||||||
from services.runtime_variables import DynamicVariableStore
|
from services.runtime_variables import DynamicVariableStore
|
||||||
from services.system_tools import normalize_system_tools
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -25,7 +24,6 @@ class AgentStageConfig:
|
|||||||
vision_enabled: bool
|
vision_enabled: bool
|
||||||
vision_model_resource_id: str | None
|
vision_model_resource_id: str | None
|
||||||
tool_ids: tuple[str, ...]
|
tool_ids: tuple[str, ...]
|
||||||
system_tools: tuple[str, ...]
|
|
||||||
state_variable_names: tuple[str, ...]
|
state_variable_names: tuple[str, ...]
|
||||||
knowledge_base_id: str | None
|
knowledge_base_id: str | None
|
||||||
knowledge_mode: str
|
knowledge_mode: str
|
||||||
@@ -148,9 +146,8 @@ class WorkflowEngine:
|
|||||||
str(source.get("visionModelResourceId") or "") or None
|
str(source.get("visionModelResourceId") or "") or None
|
||||||
),
|
),
|
||||||
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 []),
|
||||||
# System tools and their writable state scope always belong to the
|
# Variable write scope always belongs to the Agent node, even when
|
||||||
# Agent node. They are permissions, not inheritable model config.
|
# its selected tool resources come from Workflow defaults.
|
||||||
system_tools=normalize_system_tools(data.get("systemTools")),
|
|
||||||
state_variable_names=tuple(
|
state_variable_names=tuple(
|
||||||
dict.fromkeys(
|
dict.fromkeys(
|
||||||
str(name)
|
str(name)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from pipecat.frames.frames import (
|
|||||||
)
|
)
|
||||||
from pipecat.processors.aggregators.llm_context import LLMContext
|
from pipecat.processors.aggregators.llm_context import LLMContext
|
||||||
from pipecat.processors.frame_processor import FrameDirection
|
from pipecat.processors.frame_processor import FrameDirection
|
||||||
from schemas import AssistantUpsert, REALTIME_CAPABLE_TYPES
|
from schemas import AssistantUpsert, REALTIME_CAPABLE_TYPES, SystemToolConfig
|
||||||
from services.brains import BrainRuntime, SPECS, build_brain
|
from services.brains import BrainRuntime, SPECS, build_brain
|
||||||
from services.brains.base import GREETING_CONTEXT_MARKER
|
from services.brains.base import GREETING_CONTEXT_MARKER
|
||||||
from services.brains.dify_llm import (
|
from services.brains.dify_llm import (
|
||||||
@@ -104,6 +104,17 @@ async def noop_queue_frame(_frame):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def system_runtime_tool(kind: str) -> RuntimeTool:
|
||||||
|
return RuntimeTool(
|
||||||
|
id=f"tool_{kind}",
|
||||||
|
name=kind,
|
||||||
|
function_name=kind,
|
||||||
|
type="system",
|
||||||
|
description=f"{kind} system tool",
|
||||||
|
definition={"type": "system", "config": {"kind": kind}},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BrainRegistryTests(unittest.TestCase):
|
class BrainRegistryTests(unittest.TestCase):
|
||||||
def test_capability_matrix(self):
|
def test_capability_matrix(self):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -154,45 +165,18 @@ class BrainRegistryTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertIn("user_name", assistant.dynamic_variable_definitions)
|
self.assertIn("user_name", assistant.dynamic_variable_definitions)
|
||||||
|
|
||||||
def test_system_tools_are_prompt_pipeline_only(self):
|
def test_system_tool_config_accepts_all_platform_actions(self):
|
||||||
assistant = AssistantUpsert(
|
for kind in (
|
||||||
name="prompt",
|
"end_conversation",
|
||||||
type="prompt",
|
"update_state",
|
||||||
systemTools=["end_conversation", "skip_turn", "end_conversation"],
|
"skip_turn",
|
||||||
)
|
"request_human_handoff",
|
||||||
self.assertEqual(assistant.system_tools, ["end_conversation", "skip_turn"])
|
):
|
||||||
|
self.assertEqual(SystemToolConfig(kind=kind).kind, kind)
|
||||||
workflow = AssistantUpsert(
|
|
||||||
name="workflow",
|
|
||||||
type="workflow",
|
|
||||||
systemTools=["update_state"],
|
|
||||||
graph={},
|
|
||||||
)
|
|
||||||
self.assertEqual(workflow.system_tools, [])
|
|
||||||
|
|
||||||
|
def test_system_tool_config_rejects_unknown_kind(self):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
AssistantUpsert(
|
SystemToolConfig(kind="magic")
|
||||||
name="realtime prompt",
|
|
||||||
type="prompt",
|
|
||||||
runtimeMode="realtime",
|
|
||||||
systemTools=["skip_turn"],
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_system_tools_reject_unknown_kind(self):
|
|
||||||
with self.assertRaises(ValueError):
|
|
||||||
AssistantUpsert(
|
|
||||||
name="prompt",
|
|
||||||
type="prompt",
|
|
||||||
systemTools=["update_state", "magic"],
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_prompt_update_state_requires_a_declared_variable(self):
|
|
||||||
with self.assertRaisesRegex(ValueError, "必须声明至少一个动态变量"):
|
|
||||||
AssistantUpsert(
|
|
||||||
name="prompt",
|
|
||||||
type="prompt",
|
|
||||||
systemTools=["update_state"],
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_workflow_keeps_dynamic_variables_and_tool_bindings(self):
|
def test_workflow_keeps_dynamic_variables_and_tool_bindings(self):
|
||||||
assistant = AssistantUpsert(
|
assistant = AssistantUpsert(
|
||||||
@@ -916,11 +900,11 @@ class PromptBrainTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
"default": None,
|
"default": None,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
system_tools=[
|
tools=[
|
||||||
"end_conversation",
|
system_runtime_tool("end_conversation"),
|
||||||
"update_state",
|
system_runtime_tool("update_state"),
|
||||||
"skip_turn",
|
system_runtime_tool("skip_turn"),
|
||||||
"request_human_handoff",
|
system_runtime_tool("request_human_handoff"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
brain = build_brain(cfg)
|
brain = build_brain(cfg)
|
||||||
@@ -971,7 +955,7 @@ class PromptBrainTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
async def test_end_conversation_ends_call_after_generated_speech(self):
|
async def test_end_conversation_ends_call_after_generated_speech(self):
|
||||||
cfg = AssistantConfig(
|
cfg = AssistantConfig(
|
||||||
type="prompt",
|
type="prompt",
|
||||||
system_tools=["end_conversation"],
|
tools=[system_runtime_tool("end_conversation")],
|
||||||
)
|
)
|
||||||
brain = build_brain(cfg)
|
brain = build_brain(cfg)
|
||||||
llm = FakeLLM()
|
llm = FakeLLM()
|
||||||
@@ -1009,7 +993,7 @@ class PromptBrainTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
async def test_end_conversation_finishes_when_no_speech(self):
|
async def test_end_conversation_finishes_when_no_speech(self):
|
||||||
cfg = AssistantConfig(
|
cfg = AssistantConfig(
|
||||||
type="prompt",
|
type="prompt",
|
||||||
system_tools=["end_conversation"],
|
tools=[system_runtime_tool("end_conversation")],
|
||||||
)
|
)
|
||||||
brain = build_brain(cfg)
|
brain = build_brain(cfg)
|
||||||
llm = FakeLLM()
|
llm = FakeLLM()
|
||||||
@@ -1039,7 +1023,7 @@ class PromptBrainTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
dynamic_variable_definitions={
|
dynamic_variable_definitions={
|
||||||
"user_name": {"type": "string", "required": False, "default": None}
|
"user_name": {"type": "string", "required": False, "default": None}
|
||||||
},
|
},
|
||||||
system_tools=["update_state"],
|
tools=[system_runtime_tool("update_state")],
|
||||||
)
|
)
|
||||||
brain = build_brain(cfg)
|
brain = build_brain(cfg)
|
||||||
llm = FakeLLM()
|
llm = FakeLLM()
|
||||||
@@ -1083,7 +1067,7 @@ class PromptBrainTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
async def test_update_state_rejects_undeclared_variable(self):
|
async def test_update_state_rejects_undeclared_variable(self):
|
||||||
cfg = AssistantConfig(
|
cfg = AssistantConfig(
|
||||||
type="prompt",
|
type="prompt",
|
||||||
system_tools=["update_state"],
|
tools=[system_runtime_tool("update_state")],
|
||||||
)
|
)
|
||||||
brain = build_brain(cfg)
|
brain = build_brain(cfg)
|
||||||
llm = FakeLLM()
|
llm = FakeLLM()
|
||||||
@@ -1108,7 +1092,7 @@ class PromptBrainTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
async def test_skip_turn_suppresses_response(self):
|
async def test_skip_turn_suppresses_response(self):
|
||||||
cfg = AssistantConfig(
|
cfg = AssistantConfig(
|
||||||
type="prompt",
|
type="prompt",
|
||||||
system_tools=["skip_turn"],
|
tools=[system_runtime_tool("skip_turn")],
|
||||||
)
|
)
|
||||||
brain = build_brain(cfg)
|
brain = build_brain(cfg)
|
||||||
llm = FakeLLM()
|
llm = FakeLLM()
|
||||||
@@ -1134,7 +1118,7 @@ class PromptBrainTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
async def test_request_human_handoff_keeps_call_available(self):
|
async def test_request_human_handoff_keeps_call_available(self):
|
||||||
cfg = AssistantConfig(
|
cfg = AssistantConfig(
|
||||||
type="prompt",
|
type="prompt",
|
||||||
system_tools=["request_human_handoff"],
|
tools=[system_runtime_tool("request_human_handoff")],
|
||||||
)
|
)
|
||||||
brain = build_brain(cfg)
|
brain = build_brain(cfg)
|
||||||
llm = FakeLLM()
|
llm = FakeLLM()
|
||||||
@@ -1299,10 +1283,11 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
"id": "agent",
|
"id": "agent",
|
||||||
"type": "agent",
|
"type": "agent",
|
||||||
"data": {
|
"data": {
|
||||||
"systemTools": [
|
"inheritGlobalConfig": False,
|
||||||
"update_state",
|
"toolIds": [
|
||||||
"skip_turn",
|
"tool_update_state",
|
||||||
"request_human_handoff",
|
"tool_skip_turn",
|
||||||
|
"tool_request_human_handoff",
|
||||||
],
|
],
|
||||||
"stateVariableNames": ["customer_name"],
|
"stateVariableNames": ["customer_name"],
|
||||||
},
|
},
|
||||||
@@ -1322,6 +1307,11 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
"default": None,
|
"default": None,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
tools=[
|
||||||
|
system_runtime_tool("update_state"),
|
||||||
|
system_runtime_tool("skip_turn"),
|
||||||
|
system_runtime_tool("request_human_handoff"),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
{},
|
{},
|
||||||
assistant_id="asst_workflow_system_tools",
|
assistant_id="asst_workflow_system_tools",
|
||||||
@@ -1462,7 +1452,10 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
set_tools=lambda _tools: None,
|
set_tools=lambda _tools: None,
|
||||||
call_end=call_end,
|
call_end=call_end,
|
||||||
)
|
)
|
||||||
tool = brain._workflow_end_conversation_tool()
|
tool = brain._workflow_end_conversation_tool(
|
||||||
|
system_runtime_tool("end_conversation"),
|
||||||
|
"agent",
|
||||||
|
)
|
||||||
|
|
||||||
await brain.on_assistant_text_start("turn-1")
|
await brain.on_assistant_text_start("turn-1")
|
||||||
result = await tool.handler({"reason": "用户告别"}, None)
|
result = await tool.handler({"reason": "用户告别"}, None)
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import unittest
|
import unittest
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
from models import AssistantConfig, RuntimeModelResource
|
from models import AssistantConfig, RuntimeModelResource
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from routes.assistants import _validate_workflow, _validate_workflow_references
|
from routes.assistants import (
|
||||||
|
_validate_system_tool_selection,
|
||||||
|
_validate_workflow,
|
||||||
|
_validate_workflow_references,
|
||||||
|
)
|
||||||
from schemas import AssistantUpsert
|
from schemas import AssistantUpsert
|
||||||
from services.pipecat.service_factory import config_with_resource
|
from services.pipecat.service_factory import config_with_resource
|
||||||
from services.node_specs import graph_references, normalize_graph, validate_graph
|
from services.node_specs import graph_references, normalize_graph, validate_graph
|
||||||
@@ -363,7 +369,6 @@ class WorkflowGraphTests(unittest.TestCase):
|
|||||||
agent = next(node for node in graph["nodes"] if node["id"] == "agent")
|
agent = next(node for node in graph["nodes"] if node["id"] == "agent")
|
||||||
agent["data"].update(
|
agent["data"].update(
|
||||||
{
|
{
|
||||||
"systemTools": ["update_state", "skip_turn"],
|
|
||||||
"stateVariableNames": ["customer", "order_status"],
|
"stateVariableNames": ["customer", "order_status"],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -374,7 +379,7 @@ class WorkflowGraphTests(unittest.TestCase):
|
|||||||
"defaultTtsResourceId": "tts_global",
|
"defaultTtsResourceId": "tts_global",
|
||||||
"visionEnabled": True,
|
"visionEnabled": True,
|
||||||
"visionModelResourceId": "vision_global",
|
"visionModelResourceId": "vision_global",
|
||||||
"toolIds": ["tool_global"],
|
"toolIds": ["tool_global", "update_state", "skip_turn"],
|
||||||
"knowledgeBaseId": "kb_global",
|
"knowledgeBaseId": "kb_global",
|
||||||
"knowledgeMode": "on_demand",
|
"knowledgeMode": "on_demand",
|
||||||
"knowledgeTopN": 8,
|
"knowledgeTopN": 8,
|
||||||
@@ -395,8 +400,10 @@ class WorkflowGraphTests(unittest.TestCase):
|
|||||||
"vision_global",
|
"vision_global",
|
||||||
)
|
)
|
||||||
self.assertTrue(engine.uses_vision())
|
self.assertTrue(engine.uses_vision())
|
||||||
self.assertEqual(inherited.tool_ids, ("tool_global",))
|
self.assertEqual(
|
||||||
self.assertEqual(inherited.system_tools, ("update_state", "skip_turn"))
|
inherited.tool_ids,
|
||||||
|
("tool_global", "update_state", "skip_turn"),
|
||||||
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
inherited.state_variable_names,
|
inherited.state_variable_names,
|
||||||
("customer", "order_status"),
|
("customer", "order_status"),
|
||||||
@@ -412,7 +419,7 @@ class WorkflowGraphTests(unittest.TestCase):
|
|||||||
{
|
{
|
||||||
"inheritGlobalConfig": False,
|
"inheritGlobalConfig": False,
|
||||||
"llmResourceId": "llm_agent",
|
"llmResourceId": "llm_agent",
|
||||||
"toolIds": ["tool_agent"],
|
"toolIds": ["tool_agent", "update_state", "skip_turn"],
|
||||||
"knowledgeBaseId": "",
|
"knowledgeBaseId": "",
|
||||||
"visionEnabled": False,
|
"visionEnabled": False,
|
||||||
"visionModelResourceId": "",
|
"visionModelResourceId": "",
|
||||||
@@ -428,8 +435,10 @@ class WorkflowGraphTests(unittest.TestCase):
|
|||||||
self.assertFalse(custom.vision_enabled)
|
self.assertFalse(custom.vision_enabled)
|
||||||
self.assertIsNone(custom.vision_model_resource_id)
|
self.assertIsNone(custom.vision_model_resource_id)
|
||||||
self.assertFalse(engine.uses_vision())
|
self.assertFalse(engine.uses_vision())
|
||||||
self.assertEqual(custom.tool_ids, ("tool_agent",))
|
self.assertEqual(
|
||||||
self.assertEqual(custom.system_tools, ("update_state", "skip_turn"))
|
custom.tool_ids,
|
||||||
|
("tool_agent", "update_state", "skip_turn"),
|
||||||
|
)
|
||||||
self.assertEqual(custom.knowledge_mode, "disabled")
|
self.assertEqual(custom.knowledge_mode, "disabled")
|
||||||
self.assertTrue(custom.enable_interrupt)
|
self.assertTrue(custom.enable_interrupt)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -463,7 +472,6 @@ class WorkflowGraphTests(unittest.TestCase):
|
|||||||
agent = next(node for node in graph["nodes"] if node["id"] == "agent")
|
agent = next(node for node in graph["nodes"] if node["id"] == "agent")
|
||||||
agent["data"].update(
|
agent["data"].update(
|
||||||
{
|
{
|
||||||
"systemTools": ["update_state"],
|
|
||||||
"stateVariableNames": ["customer"],
|
"stateVariableNames": ["customer"],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -497,8 +505,8 @@ class WorkflowGraphTests(unittest.TestCase):
|
|||||||
|
|
||||||
def test_agent_update_state_tool_requires_an_authorized_variable(self):
|
def test_agent_update_state_tool_requires_an_authorized_variable(self):
|
||||||
graph = valid_graph()
|
graph = valid_graph()
|
||||||
|
graph["settings"]["toolIds"] = ["tool_update_state"]
|
||||||
agent = next(node for node in graph["nodes"] if node["id"] == "agent")
|
agent = next(node for node in graph["nodes"] if node["id"] == "agent")
|
||||||
agent["data"]["systemTools"] = ["update_state"]
|
|
||||||
body = AssistantUpsert(
|
body = AssistantUpsert(
|
||||||
name="缺少授权",
|
name="缺少授权",
|
||||||
type="workflow",
|
type="workflow",
|
||||||
@@ -512,8 +520,19 @@ class WorkflowGraphTests(unittest.TestCase):
|
|||||||
graph=graph,
|
graph=graph,
|
||||||
)
|
)
|
||||||
|
|
||||||
with self.assertRaisesRegex(HTTPException, "必须授权至少一个变量"):
|
|
||||||
_validate_workflow(body)
|
_validate_workflow(body)
|
||||||
|
system_tool = SimpleNamespace(
|
||||||
|
id="tool_update_state",
|
||||||
|
status="active",
|
||||||
|
type="system",
|
||||||
|
definition={"config": {"kind": "update_state"}},
|
||||||
|
)
|
||||||
|
result = SimpleNamespace(
|
||||||
|
scalars=lambda: SimpleNamespace(all=lambda: [system_tool])
|
||||||
|
)
|
||||||
|
session = SimpleNamespace(execute=AsyncMock(return_value=result))
|
||||||
|
with self.assertRaisesRegex(HTTPException, "必须授权至少一个变量"):
|
||||||
|
asyncio.run(_validate_system_tool_selection(session, body))
|
||||||
|
|
||||||
def test_vision_resource_creates_isolated_runtime_config(self):
|
def test_vision_resource_creates_isolated_runtime_config(self):
|
||||||
base = AssistantConfig(type="workflow", model="text-only")
|
base = AssistantConfig(type="workflow", model="text-only")
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
Copy,
|
Copy,
|
||||||
Pencil,
|
Pencil,
|
||||||
PhoneOff,
|
|
||||||
Plus,
|
Plus,
|
||||||
ServerCog,
|
ServerCog,
|
||||||
Settings2,
|
Settings2,
|
||||||
@@ -19,7 +18,6 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import { HelpHint } from "@/components/editor/section-card";
|
import { HelpHint } from "@/components/editor/section-card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -47,10 +45,8 @@ import {
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import type {
|
import type {
|
||||||
KnowledgeRetrievalConfig,
|
KnowledgeRetrievalConfig,
|
||||||
SystemToolKind,
|
|
||||||
Tool,
|
Tool,
|
||||||
} from "@/lib/api";
|
} from "@/lib/api";
|
||||||
import { SYSTEM_TOOL_OPTIONS } from "@/lib/system-tools";
|
|
||||||
|
|
||||||
import type { RuntimeMode } from "./types";
|
import type { RuntimeMode } from "./types";
|
||||||
|
|
||||||
@@ -487,34 +483,20 @@ export function ToolPicker({
|
|||||||
tools,
|
tools,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
onChange,
|
onChange,
|
||||||
selectedSystemTools,
|
|
||||||
onSystemToolsChange,
|
|
||||||
showBuiltInSystemTools,
|
|
||||||
}: {
|
}: {
|
||||||
tools: Tool[];
|
tools: Tool[];
|
||||||
selectedIds: string[];
|
selectedIds: string[];
|
||||||
onChange: (ids: string[]) => void;
|
onChange: (ids: string[]) => void;
|
||||||
selectedSystemTools: SystemToolKind[];
|
|
||||||
onSystemToolsChange: (tools: SystemToolKind[]) => void;
|
|
||||||
showBuiltInSystemTools: boolean;
|
|
||||||
}) {
|
}) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [activeTab, setActiveTab] = useState<Tool["type"]>("system");
|
const [activeTab, setActiveTab] = useState<Tool["type"]>("system");
|
||||||
const [draftIds, setDraftIds] = useState<string[]>(selectedIds);
|
const [draftIds, setDraftIds] = useState<string[]>(selectedIds);
|
||||||
const [draftSystemTools, setDraftSystemTools] =
|
|
||||||
useState<SystemToolKind[]>(selectedSystemTools);
|
|
||||||
const selectedTools = selectedIds
|
const selectedTools = selectedIds
|
||||||
.map((id) => tools.find((tool) => tool.id === id))
|
.map((id) => tools.find((tool) => tool.id === id))
|
||||||
.filter((tool): tool is Tool => Boolean(tool));
|
.filter((tool): tool is Tool => Boolean(tool));
|
||||||
const selectedBuiltInTools = selectedSystemTools
|
|
||||||
.map((kind) => SYSTEM_TOOL_OPTIONS.find((option) => option.value === kind))
|
|
||||||
.filter((option): option is (typeof SYSTEM_TOOL_OPTIONS)[number] =>
|
|
||||||
Boolean(option),
|
|
||||||
);
|
|
||||||
|
|
||||||
function openPicker() {
|
function openPicker() {
|
||||||
setDraftIds(selectedIds);
|
setDraftIds(selectedIds);
|
||||||
setDraftSystemTools(selectedSystemTools);
|
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,14 +508,6 @@ export function ToolPicker({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleBuiltInTool(kind: SystemToolKind) {
|
|
||||||
setDraftSystemTools((current) =>
|
|
||||||
current.includes(kind)
|
|
||||||
? current.filter((item) => item !== kind)
|
|
||||||
: [...current, kind],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabs: Array<{ value: Tool["type"]; label: string }> = [
|
const tabs: Array<{ value: Tool["type"]; label: string }> = [
|
||||||
{ value: "system", label: "System" },
|
{ value: "system", label: "System" },
|
||||||
{ value: "http", label: "HTTP" },
|
{ value: "http", label: "HTTP" },
|
||||||
@@ -544,34 +518,13 @@ export function ToolPicker({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex min-h-9 flex-wrap items-center gap-2">
|
<div className="flex min-h-9 flex-wrap items-center gap-2">
|
||||||
{selectedBuiltInTools.map((tool) => (
|
|
||||||
<div
|
|
||||||
key={`built-in-${tool.value}`}
|
|
||||||
className="flex h-8 items-center gap-2 rounded-lg border border-hairline-strong bg-background px-2.5 text-sm"
|
|
||||||
>
|
|
||||||
<Sparkles size={14} />
|
|
||||||
<span className="max-w-48 truncate">{tool.label}</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() =>
|
|
||||||
onSystemToolsChange(
|
|
||||||
selectedSystemTools.filter((kind) => kind !== tool.value),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="text-muted-soft transition-colors hover:text-foreground"
|
|
||||||
aria-label={`移除系统工具 ${tool.label}`}
|
|
||||||
>
|
|
||||||
<X size={13} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{selectedTools.map((tool) => (
|
{selectedTools.map((tool) => (
|
||||||
<div
|
<div
|
||||||
key={tool.id}
|
key={tool.id}
|
||||||
className="flex h-8 items-center gap-2 rounded-lg border border-hairline-strong bg-background px-2.5 text-sm"
|
className="flex h-8 items-center gap-2 rounded-lg border border-hairline-strong bg-background px-2.5 text-sm"
|
||||||
>
|
>
|
||||||
{tool.type === "system" ? (
|
{tool.type === "system" ? (
|
||||||
<PhoneOff size={14} />
|
<Sparkles size={14} />
|
||||||
) : tool.type === "mcp" ? (
|
) : tool.type === "mcp" ? (
|
||||||
<ServerCog size={14} />
|
<ServerCog size={14} />
|
||||||
) : (
|
) : (
|
||||||
@@ -606,7 +559,7 @@ export function ToolPicker({
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>选择工具</DialogTitle>
|
<DialogTitle>选择工具</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
按类型选择内置系统工具或已经启用的工具资源。
|
按类型选择已经启用的工具资源。
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -631,9 +584,7 @@ export function ToolPicker({
|
|||||||
|
|
||||||
{tabs.map((tab) => {
|
{tabs.map((tab) => {
|
||||||
const resources = tools.filter((tool) => tool.type === tab.value);
|
const resources = tools.filter((tool) => tool.type === tab.value);
|
||||||
const hasBuiltIns =
|
const isEmpty = resources.length === 0;
|
||||||
tab.value === "system" && showBuiltInSystemTools;
|
|
||||||
const isEmpty = resources.length === 0 && !hasBuiltIns;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabsContent key={tab.value} value={tab.value} className="pt-3">
|
<TabsContent key={tab.value} value={tab.value} className="pt-3">
|
||||||
@@ -643,35 +594,6 @@ export function ToolPicker({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="max-h-[280px] divide-y divide-hairline overflow-y-auto rounded-xl border border-hairline">
|
<div className="max-h-[280px] divide-y divide-hairline overflow-y-auto rounded-xl border border-hairline">
|
||||||
{hasBuiltIns &&
|
|
||||||
SYSTEM_TOOL_OPTIONS.map((option) => {
|
|
||||||
const checked = draftSystemTools.includes(option.value);
|
|
||||||
return (
|
|
||||||
<label
|
|
||||||
key={`built-in-${option.value}`}
|
|
||||||
className="flex h-14 cursor-pointer items-center gap-3 px-4 transition-colors hover:bg-surface-strong/40"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={checked}
|
|
||||||
onChange={() => toggleBuiltInTool(option.value)}
|
|
||||||
className="size-4 accent-primary"
|
|
||||||
/>
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="truncate font-medium text-foreground">
|
|
||||||
{option.label}
|
|
||||||
</span>
|
|
||||||
<Badge variant="secondary">内置</Badge>
|
|
||||||
</div>
|
|
||||||
<div className="mt-0.5 truncate text-xs text-muted-foreground">
|
|
||||||
{option.description}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{resources.map((tool) => {
|
{resources.map((tool) => {
|
||||||
const checked = draftIds.includes(tool.id);
|
const checked = draftIds.includes(tool.id);
|
||||||
return (
|
return (
|
||||||
@@ -686,13 +608,8 @@ export function ToolPicker({
|
|||||||
className="size-4 accent-primary"
|
className="size-4 accent-primary"
|
||||||
/>
|
/>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="truncate font-medium text-foreground">
|
||||||
<span className="truncate font-medium text-foreground">
|
|
||||||
{tool.name}
|
{tool.name}
|
||||||
</span>
|
|
||||||
{tool.type === "system" && (
|
|
||||||
<Badge variant="secondary">资源</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-0.5 truncate font-mono text-xs text-muted-foreground">
|
<div className="mt-0.5 truncate font-mono text-xs text-muted-foreground">
|
||||||
{tool.functionName}
|
{tool.functionName}
|
||||||
@@ -715,7 +632,6 @@ export function ToolPicker({
|
|||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onChange(draftIds);
|
onChange(draftIds);
|
||||||
onSystemToolsChange(draftSystemTools);
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -506,8 +506,15 @@ export function PromptEditor({
|
|||||||
openingMessage: null,
|
openingMessage: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (runtimeMode === "realtime" && form.systemTools.length) {
|
if (runtimeMode === "realtime") {
|
||||||
updateForm("systemTools", []);
|
updateForm(
|
||||||
|
"toolIds",
|
||||||
|
form.toolIds.filter(
|
||||||
|
(id) =>
|
||||||
|
tools.find((tool) => tool.id === id)?.type !==
|
||||||
|
"system",
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -602,14 +609,13 @@ export function PromptEditor({
|
|||||||
description="配置该提示词助手可以调用的工具"
|
description="配置该提示词助手可以调用的工具"
|
||||||
>
|
>
|
||||||
<ToolPicker
|
<ToolPicker
|
||||||
tools={tools.filter((tool) => tool.status === "active")}
|
tools={tools.filter(
|
||||||
|
(tool) =>
|
||||||
|
tool.status === "active" &&
|
||||||
|
(form.runtimeMode === "pipeline" || tool.type !== "system"),
|
||||||
|
)}
|
||||||
selectedIds={form.toolIds}
|
selectedIds={form.toolIds}
|
||||||
onChange={(toolIds) => updateForm("toolIds", toolIds)}
|
onChange={(toolIds) => updateForm("toolIds", toolIds)}
|
||||||
selectedSystemTools={form.systemTools}
|
|
||||||
onSystemToolsChange={(systemTools) =>
|
|
||||||
updateForm("systemTools", systemTools)
|
|
||||||
}
|
|
||||||
showBuiltInSystemTools={form.runtimeMode === "pipeline"}
|
|
||||||
/>
|
/>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import type {
|
|||||||
DynamicVariableDefinition,
|
DynamicVariableDefinition,
|
||||||
KnowledgeRetrievalConfig,
|
KnowledgeRetrievalConfig,
|
||||||
StartupConfig,
|
StartupConfig,
|
||||||
SystemToolKind,
|
|
||||||
TurnConfig,
|
TurnConfig,
|
||||||
} from "@/lib/api";
|
} from "@/lib/api";
|
||||||
|
|
||||||
@@ -23,7 +22,6 @@ export type AssistantForm = {
|
|||||||
enableInterrupt: boolean;
|
enableInterrupt: boolean;
|
||||||
turnConfig: TurnConfig;
|
turnConfig: TurnConfig;
|
||||||
startup: StartupConfig;
|
startup: StartupConfig;
|
||||||
systemTools: SystemToolKind[];
|
|
||||||
visionEnabled: boolean;
|
visionEnabled: boolean;
|
||||||
visionModelResourceId: string;
|
visionModelResourceId: string;
|
||||||
toolIds: string[];
|
toolIds: string[];
|
||||||
|
|||||||
@@ -184,7 +184,6 @@ function blankPromptForm(name: string): AssistantForm {
|
|||||||
actions: [],
|
actions: [],
|
||||||
openingMessage: null,
|
openingMessage: null,
|
||||||
},
|
},
|
||||||
systemTools: [],
|
|
||||||
visionEnabled: false,
|
visionEnabled: false,
|
||||||
visionModelResourceId: "",
|
visionModelResourceId: "",
|
||||||
toolIds: [],
|
toolIds: [],
|
||||||
@@ -480,7 +479,6 @@ export function AssistantPage(props: AssistantPageProps) {
|
|||||||
actions: a.startup?.actions ?? [],
|
actions: a.startup?.actions ?? [],
|
||||||
openingMessage: a.startup?.openingMessage ?? null,
|
openingMessage: a.startup?.openingMessage ?? null,
|
||||||
},
|
},
|
||||||
systemTools: a.systemTools ?? [],
|
|
||||||
visionEnabled: a.visionEnabled,
|
visionEnabled: a.visionEnabled,
|
||||||
visionModelResourceId: a.visionModelResourceId ?? "",
|
visionModelResourceId: a.visionModelResourceId ?? "",
|
||||||
toolIds: a.toolIds ?? [],
|
toolIds: a.toolIds ?? [],
|
||||||
@@ -557,7 +555,6 @@ export function AssistantPage(props: AssistantPageProps) {
|
|||||||
actions: [],
|
actions: [],
|
||||||
openingMessage: null,
|
openingMessage: null,
|
||||||
},
|
},
|
||||||
systemTools: [],
|
|
||||||
visionEnabled: false,
|
visionEnabled: false,
|
||||||
visionModelResourceId: null,
|
visionModelResourceId: null,
|
||||||
modelResourceIds: {},
|
modelResourceIds: {},
|
||||||
@@ -617,7 +614,6 @@ export function AssistantPage(props: AssistantPageProps) {
|
|||||||
enableInterrupt: form.enableInterrupt,
|
enableInterrupt: form.enableInterrupt,
|
||||||
turnConfig: form.turnConfig,
|
turnConfig: form.turnConfig,
|
||||||
startup: form.startup,
|
startup: form.startup,
|
||||||
systemTools: form.runtimeMode === "pipeline" ? form.systemTools : [],
|
|
||||||
visionEnabled: form.visionEnabled,
|
visionEnabled: form.visionEnabled,
|
||||||
visionModelResourceId: form.visionModelResourceId || null,
|
visionModelResourceId: form.visionModelResourceId || null,
|
||||||
modelResourceIds: {
|
modelResourceIds: {
|
||||||
@@ -1304,10 +1300,16 @@ export function AssistantPage(props: AssistantPageProps) {
|
|||||||
vision: visionModelOptionsFor(""),
|
vision: visionModelOptionsFor(""),
|
||||||
}}
|
}}
|
||||||
toolOptions={tools
|
toolOptions={tools
|
||||||
.filter(
|
.filter((tool) => tool.status === "active")
|
||||||
(tool) => tool.status === "active" && tool.type !== "system",
|
.map((tool) => ({
|
||||||
)
|
value: tool.id,
|
||||||
.map((tool) => ({ value: tool.id, label: tool.name }))}
|
label: tool.name,
|
||||||
|
toolType: tool.type,
|
||||||
|
systemKind:
|
||||||
|
tool.definition.type === "system"
|
||||||
|
? tool.definition.config.kind
|
||||||
|
: undefined,
|
||||||
|
}))}
|
||||||
knowledgeOptions={kbOptions}
|
knowledgeOptions={kbOptions}
|
||||||
onBack={() => router.push("/assistants")}
|
onBack={() => router.push("/assistants")}
|
||||||
onSave={() => void handleSaveWorkflow()}
|
onSave={() => void handleSaveWorkflow()}
|
||||||
|
|||||||
@@ -57,12 +57,14 @@ import {
|
|||||||
type ClientToolResponseWaitMode,
|
type ClientToolResponseWaitMode,
|
||||||
type HttpToolDefinition,
|
type HttpToolDefinition,
|
||||||
type McpServer,
|
type McpServer,
|
||||||
|
type SystemToolKind,
|
||||||
type Tool,
|
type Tool,
|
||||||
type ToolParameter,
|
type ToolParameter,
|
||||||
type ToolExecutionMode,
|
type ToolExecutionMode,
|
||||||
type ToolStatus,
|
type ToolStatus,
|
||||||
type ToolUpsert,
|
type ToolUpsert,
|
||||||
} from "@/lib/api";
|
} from "@/lib/api";
|
||||||
|
import { SYSTEM_TOOL_OPTIONS } from "@/lib/system-tools";
|
||||||
|
|
||||||
type ToolKind = "system" | "http" | "client";
|
type ToolKind = "system" | "http" | "client";
|
||||||
type HttpMethod = HttpToolDefinition["config"]["method"];
|
type HttpMethod = HttpToolDefinition["config"]["method"];
|
||||||
@@ -86,6 +88,7 @@ type ToolForm = {
|
|||||||
type: ToolKind;
|
type: ToolKind;
|
||||||
description: string;
|
description: string;
|
||||||
status: ToolStatus;
|
status: ToolStatus;
|
||||||
|
systemKind: SystemToolKind;
|
||||||
messageType: "none" | "custom";
|
messageType: "none" | "custom";
|
||||||
customMessage: string;
|
customMessage: string;
|
||||||
captureReason: boolean;
|
captureReason: boolean;
|
||||||
@@ -115,6 +118,7 @@ function blankForm(): ToolForm {
|
|||||||
type: "system",
|
type: "system",
|
||||||
description: "",
|
description: "",
|
||||||
status: "active",
|
status: "active",
|
||||||
|
systemKind: "end_conversation",
|
||||||
messageType: "none",
|
messageType: "none",
|
||||||
customMessage: "",
|
customMessage: "",
|
||||||
captureReason: true,
|
captureReason: true,
|
||||||
@@ -148,6 +152,7 @@ function formFromTool(tool: Tool): ToolForm {
|
|||||||
base.description = tool.description;
|
base.description = tool.description;
|
||||||
base.status = tool.status;
|
base.status = tool.status;
|
||||||
if (tool.definition.type === "system") {
|
if (tool.definition.type === "system") {
|
||||||
|
base.systemKind = tool.definition.config.kind;
|
||||||
base.messageType = tool.definition.config.messageType;
|
base.messageType = tool.definition.config.messageType;
|
||||||
base.customMessage = tool.definition.config.customMessage;
|
base.customMessage = tool.definition.config.customMessage;
|
||||||
base.captureReason = tool.definition.config.captureReason;
|
base.captureReason = tool.definition.config.captureReason;
|
||||||
@@ -246,9 +251,13 @@ function payloadFromForm(form: ToolForm): ToolUpsert {
|
|||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
type: "system",
|
type: "system",
|
||||||
config: {
|
config: {
|
||||||
kind: "end_conversation",
|
kind: form.systemKind,
|
||||||
messageType: form.messageType,
|
messageType: form.messageType,
|
||||||
customMessage: form.messageType === "custom" ? form.customMessage : "",
|
customMessage:
|
||||||
|
form.systemKind === "end_conversation" &&
|
||||||
|
form.messageType === "custom"
|
||||||
|
? form.customMessage
|
||||||
|
: "",
|
||||||
captureReason: form.captureReason,
|
captureReason: form.captureReason,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -871,15 +880,34 @@ function SystemToolFields({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Field label="系统动作">
|
<Field label="系统动作">
|
||||||
<Select value="end_conversation" disabled>
|
<Select
|
||||||
|
value={form.systemKind}
|
||||||
|
onValueChange={(systemKind: SystemToolKind) =>
|
||||||
|
setForm((current) => ({
|
||||||
|
...current,
|
||||||
|
systemKind,
|
||||||
|
functionName: systemKind,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
<SelectTrigger className="w-full border-hairline-strong bg-background">
|
<SelectTrigger className="w-full border-hairline-strong bg-background">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="end_conversation">结束对话</SelectItem>
|
{SYSTEM_TOOL_OPTIONS.map((option) => (
|
||||||
|
<SelectItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</Field>
|
</Field>
|
||||||
|
<p className="text-xs leading-5 text-muted-foreground">
|
||||||
|
{SYSTEM_TOOL_OPTIONS.find((option) => option.value === form.systemKind)
|
||||||
|
?.description ?? "由平台执行的会话控制能力。"}
|
||||||
|
</p>
|
||||||
|
{form.systemKind === "end_conversation" && (
|
||||||
|
<>
|
||||||
<Field label="结束语">
|
<Field label="结束语">
|
||||||
<Select
|
<Select
|
||||||
value={form.messageType}
|
value={form.messageType}
|
||||||
@@ -887,7 +915,9 @@ function SystemToolFields({
|
|||||||
setForm((current) => ({ ...current, messageType }))
|
setForm((current) => ({ ...current, messageType }))
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-full border-hairline-strong bg-background"><SelectValue /></SelectTrigger>
|
<SelectTrigger className="w-full border-hairline-strong bg-background">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="none">不播放固定结束语</SelectItem>
|
<SelectItem value="none">不播放固定结束语</SelectItem>
|
||||||
<SelectItem value="custom">自定义结束语</SelectItem>
|
<SelectItem value="custom">自定义结束语</SelectItem>
|
||||||
@@ -899,7 +929,10 @@ function SystemToolFields({
|
|||||||
<Textarea
|
<Textarea
|
||||||
value={form.customMessage}
|
value={form.customMessage}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
setForm((current) => ({ ...current, customMessage: event.target.value }))
|
setForm((current) => ({
|
||||||
|
...current,
|
||||||
|
customMessage: event.target.value,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
@@ -908,7 +941,9 @@ function SystemToolFields({
|
|||||||
<div className="flex items-center justify-between gap-4 rounded-lg border border-hairline-strong px-4 py-3">
|
<div className="flex items-center justify-between gap-4 rounded-lg border border-hairline-strong px-4 py-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-foreground">记录结束原因</div>
|
<div className="font-medium text-foreground">记录结束原因</div>
|
||||||
<div className="mt-0.5 text-xs text-muted-foreground">要求模型在调用时提供 reason 参数</div>
|
<div className="mt-0.5 text-xs text-muted-foreground">
|
||||||
|
要求模型在调用时提供 reason 参数
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={form.captureReason}
|
checked={form.captureReason}
|
||||||
@@ -917,6 +952,8 @@ function SystemToolFields({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ function defaultNodeData(spec: RuntimeNodeSpec): WorkflowNodeData {
|
|||||||
contextPolicy: "inherit",
|
contextPolicy: "inherit",
|
||||||
inheritGlobalConfig: true,
|
inheritGlobalConfig: true,
|
||||||
entryMode: "wait_user",
|
entryMode: "wait_user",
|
||||||
systemTools: [],
|
|
||||||
stateVariableNames: [],
|
stateVariableNames: [],
|
||||||
});
|
});
|
||||||
} else if (spec.type === "action") {
|
} else if (spec.type === "action") {
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export function ActionNodePanel({
|
|||||||
<NodeSelect
|
<NodeSelect
|
||||||
label="执行工具"
|
label="执行工具"
|
||||||
value={(draft.toolId as string) || ""}
|
value={(draft.toolId as string) || ""}
|
||||||
options={toolOptions}
|
options={toolOptions.filter((option) => option.toolType !== "system")}
|
||||||
onChange={(value) => set("toolId", value || "")}
|
onChange={(value) => set("toolId", value || "")}
|
||||||
noneLabel="请选择工具"
|
noneLabel="请选择工具"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -16,8 +16,7 @@ import { VisionConfigSection } from "@/components/editor/vision-config-section";
|
|||||||
import { TurnConfigEditor } from "@/components/turn-config-editor";
|
import { TurnConfigEditor } from "@/components/turn-config-editor";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import type { KnowledgeRetrievalConfig, SystemToolKind } from "@/lib/api";
|
import type { KnowledgeRetrievalConfig } from "@/lib/api";
|
||||||
import { SYSTEM_TOOL_OPTIONS } from "@/lib/system-tools";
|
|
||||||
import { normalizeTurnConfig } from "@/lib/turn-config";
|
import { normalizeTurnConfig } from "@/lib/turn-config";
|
||||||
|
|
||||||
import { NodeSelect, ToolOptionPicker } from "./controls";
|
import { NodeSelect, ToolOptionPicker } from "./controls";
|
||||||
@@ -104,18 +103,14 @@ export function AgentNodePanel({
|
|||||||
turnConfig: agentTurnConfig,
|
turnConfig: agentTurnConfig,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const toggleSystemTool = (kind: SystemToolKind, enabled: boolean) => {
|
const selectedToolIds = inheritsGlobal
|
||||||
const current = draft.systemTools ?? [];
|
? workflowSettings.toolIds
|
||||||
const systemTools = enabled
|
: draft.toolIds ?? [];
|
||||||
? [...new Set([...current, kind])]
|
const updateStateEnabled = selectedToolIds.some(
|
||||||
: current.filter((item) => item !== kind);
|
(toolId) =>
|
||||||
setPatch({
|
toolOptions.find((option) => option.value === toolId)?.systemKind ===
|
||||||
systemTools,
|
"update_state",
|
||||||
...(!enabled && kind === "update_state"
|
);
|
||||||
? { stateVariableNames: [] }
|
|
||||||
: {}),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const toggleStateVariable = (name: string, enabled: boolean) => {
|
const toggleStateVariable = (name: string, enabled: boolean) => {
|
||||||
const current = draft.stateVariableNames ?? [];
|
const current = draft.stateVariableNames ?? [];
|
||||||
set(
|
set(
|
||||||
@@ -133,7 +128,9 @@ export function AgentNodePanel({
|
|||||||
{ id: "scope", label: "配置范围" },
|
{ id: "scope", label: "配置范围" },
|
||||||
{ id: "prompt", label: inheritsGlobal ? "任务" : "提示词" },
|
{ id: "prompt", label: inheritsGlobal ? "任务" : "提示词" },
|
||||||
{ id: "entry", label: "进入行为" },
|
{ id: "entry", label: "进入行为" },
|
||||||
{ id: "system-tools", label: "系统工具" },
|
...(updateStateEnabled
|
||||||
|
? [{ id: "state-scope", label: "状态更新权限" }]
|
||||||
|
: []),
|
||||||
...(!inheritsGlobal
|
...(!inheritsGlobal
|
||||||
? [
|
? [
|
||||||
{ id: "models", label: "模型与语音" },
|
{ id: "models", label: "模型与语音" },
|
||||||
@@ -211,42 +208,13 @@ export function AgentNodePanel({
|
|||||||
</SectionCard>
|
</SectionCard>
|
||||||
</PanelAnchor>
|
</PanelAnchor>
|
||||||
|
|
||||||
<PanelAnchor id="system-tools">
|
{updateStateEnabled && (
|
||||||
|
<PanelAnchor id="state-scope">
|
||||||
<SectionCard
|
<SectionCard
|
||||||
icon={<Sparkles size={15} />}
|
icon={<Sparkles size={15} />}
|
||||||
title="系统工具"
|
title="状态更新权限"
|
||||||
description="只对当前 Agent 生效的内置会话控制能力"
|
description="更新状态工具只允许写入这里授权的动态变量"
|
||||||
>
|
>
|
||||||
<div className="space-y-3">
|
|
||||||
{SYSTEM_TOOL_OPTIONS.map((option) => {
|
|
||||||
const enabled = (draft.systemTools ?? []).includes(option.value);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={option.value}
|
|
||||||
className="rounded-xl border border-hairline bg-canvas-soft p-3.5"
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between gap-4">
|
|
||||||
<div className="min-w-0">
|
|
||||||
<div className="text-sm font-medium text-foreground">
|
|
||||||
{option.label}
|
|
||||||
</div>
|
|
||||||
<p className="mt-1 text-xs leading-5 text-muted-foreground">
|
|
||||||
{option.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Switch
|
|
||||||
checked={enabled}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
toggleSystemTool(option.value, checked)
|
|
||||||
}
|
|
||||||
aria-label={`启用${option.label}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{option.value === "update_state" && enabled && (
|
|
||||||
<div className="mt-3 border-t border-hairline pt-3">
|
|
||||||
<div className="mb-2 text-xs font-medium text-foreground">
|
|
||||||
允许更新的变量
|
|
||||||
</div>
|
|
||||||
{dynamicVariableOptions.length ? (
|
{dynamicVariableOptions.length ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{dynamicVariableOptions.map((variable) => (
|
{dynamicVariableOptions.map((variable) => (
|
||||||
@@ -274,14 +242,9 @@ export function AgentNodePanel({
|
|||||||
请先在工作流的动态变量面板中声明变量。
|
请先在工作流的动态变量面板中声明变量。
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
</PanelAnchor>
|
</PanelAnchor>
|
||||||
|
)}
|
||||||
|
|
||||||
{!inheritsGlobal && (
|
{!inheritsGlobal && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Plus, Wrench, X } from "lucide-react";
|
import { Plus, ServerCog, Sparkles, Wrench, X } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -19,6 +19,13 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
Tabs,
|
||||||
|
TabsContent,
|
||||||
|
TabsList,
|
||||||
|
TabsTrigger,
|
||||||
|
} from "@/components/ui/tabs";
|
||||||
|
import type { Tool } from "@/lib/api";
|
||||||
|
|
||||||
import type { ModelOption } from "../types";
|
import type { ModelOption } from "../types";
|
||||||
|
|
||||||
@@ -70,10 +77,17 @@ export function ToolOptionPicker({
|
|||||||
onChange: (ids: string[]) => void;
|
onChange: (ids: string[]) => void;
|
||||||
}) {
|
}) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const [activeTab, setActiveTab] = useState<Tool["type"]>("system");
|
||||||
const [draftIds, setDraftIds] = useState<string[]>(selectedIds);
|
const [draftIds, setDraftIds] = useState<string[]>(selectedIds);
|
||||||
const selected = selectedIds
|
const selected = selectedIds
|
||||||
.map((id) => options.find((option) => option.value === id))
|
.map((id) => options.find((option) => option.value === id))
|
||||||
.filter((option): option is ModelOption => Boolean(option));
|
.filter((option): option is ModelOption => Boolean(option));
|
||||||
|
const tabs: Array<{ value: Tool["type"]; label: string }> = [
|
||||||
|
{ value: "system", label: "System" },
|
||||||
|
{ value: "http", label: "HTTP" },
|
||||||
|
{ value: "client", label: "Client" },
|
||||||
|
{ value: "mcp", label: "MCP" },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -83,7 +97,13 @@ export function ToolOptionPicker({
|
|||||||
key={option.value}
|
key={option.value}
|
||||||
className="flex h-8 items-center gap-2 rounded-lg border border-hairline-strong bg-background px-2.5 text-sm"
|
className="flex h-8 items-center gap-2 rounded-lg border border-hairline-strong bg-background px-2.5 text-sm"
|
||||||
>
|
>
|
||||||
|
{option.toolType === "system" ? (
|
||||||
|
<Sparkles size={14} />
|
||||||
|
) : option.toolType === "mcp" ? (
|
||||||
|
<ServerCog size={14} />
|
||||||
|
) : (
|
||||||
<Wrench size={14} />
|
<Wrench size={14} />
|
||||||
|
)}
|
||||||
<span className="max-w-48 truncate">{option.label}</span>
|
<span className="max-w-48 truncate">{option.label}</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -119,18 +139,42 @@ export function ToolOptionPicker({
|
|||||||
<DialogTitle>选择工具</DialogTitle>
|
<DialogTitle>选择工具</DialogTitle>
|
||||||
<DialogDescription>选择已经在工具资源中启用的工具。</DialogDescription>
|
<DialogDescription>选择已经在工具资源中启用的工具。</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{options.length === 0 ? (
|
<Tabs
|
||||||
|
value={activeTab}
|
||||||
|
onValueChange={(value) => setActiveTab(value as Tool["type"])}
|
||||||
|
>
|
||||||
|
<TabsList
|
||||||
|
variant="line"
|
||||||
|
className="w-full justify-start border-b border-hairline px-1"
|
||||||
|
>
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<TabsTrigger
|
||||||
|
key={tab.value}
|
||||||
|
value={tab.value}
|
||||||
|
className="flex-none px-4"
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
const rows = options.filter(
|
||||||
|
(option) => (option.toolType ?? "http") === tab.value,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<TabsContent key={tab.value} value={tab.value} className="pt-3">
|
||||||
|
{rows.length === 0 ? (
|
||||||
<div className="rounded-xl border border-dashed border-hairline-strong px-4 py-10 text-center text-sm text-muted-foreground">
|
<div className="rounded-xl border border-dashed border-hairline-strong px-4 py-10 text-center text-sm text-muted-foreground">
|
||||||
暂无可用工具
|
暂无可用的 {tab.label} 工具
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="max-h-80 divide-y divide-hairline overflow-y-auto rounded-xl border border-hairline">
|
<div className="max-h-[280px] divide-y divide-hairline overflow-y-auto rounded-xl border border-hairline">
|
||||||
{options.map((option) => {
|
{rows.map((option) => {
|
||||||
const checked = draftIds.includes(option.value);
|
const checked = draftIds.includes(option.value);
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
key={option.value}
|
key={option.value}
|
||||||
className="flex cursor-pointer items-center gap-3 px-4 py-3 transition-colors hover:bg-surface-strong/40"
|
className="flex h-14 cursor-pointer items-center gap-3 px-4 transition-colors hover:bg-surface-strong/40"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -138,7 +182,9 @@ export function ToolOptionPicker({
|
|||||||
onChange={() =>
|
onChange={() =>
|
||||||
setDraftIds((current) =>
|
setDraftIds((current) =>
|
||||||
checked
|
checked
|
||||||
? current.filter((id) => id !== option.value)
|
? current.filter(
|
||||||
|
(id) => id !== option.value,
|
||||||
|
)
|
||||||
: [...current, option.value],
|
: [...current, option.value],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -152,6 +198,10 @@ export function ToolOptionPicker({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tabs>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||||
取消
|
取消
|
||||||
@@ -214,4 +264,3 @@ export function NodeSelect({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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, SystemToolKind, TurnConfig } from "@/lib/api";
|
import type { NodeSpecDto, TurnConfig } from "@/lib/api";
|
||||||
import { defaultTurnConfig } from "@/lib/turn-config";
|
import { defaultTurnConfig } from "@/lib/turn-config";
|
||||||
|
|
||||||
export type WorkflowNodeType =
|
export type WorkflowNodeType =
|
||||||
@@ -41,7 +41,6 @@ export type WorkflowNodeData = {
|
|||||||
contextPolicy?: ContextPolicy;
|
contextPolicy?: ContextPolicy;
|
||||||
inheritGlobalConfig?: boolean;
|
inheritGlobalConfig?: boolean;
|
||||||
entryMode?: AgentEntryMode;
|
entryMode?: AgentEntryMode;
|
||||||
systemTools?: SystemToolKind[];
|
|
||||||
stateVariableNames?: string[];
|
stateVariableNames?: string[];
|
||||||
toolIds?: string[];
|
toolIds?: string[];
|
||||||
knowledgeBaseId?: string;
|
knowledgeBaseId?: string;
|
||||||
@@ -283,7 +282,6 @@ export function defaultGraph(): WorkflowGraph {
|
|||||||
contextPolicy: "inherit",
|
contextPolicy: "inherit",
|
||||||
inheritGlobalConfig: true,
|
inheritGlobalConfig: true,
|
||||||
entryMode: "wait_user",
|
entryMode: "wait_user",
|
||||||
systemTools: [],
|
|
||||||
stateVariableNames: [],
|
stateVariableNames: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
import type { KnowledgeRetrievalConfig, TurnConfig } from "@/lib/api";
|
import type {
|
||||||
|
KnowledgeRetrievalConfig,
|
||||||
|
SystemToolKind,
|
||||||
|
Tool,
|
||||||
|
TurnConfig,
|
||||||
|
} from "@/lib/api";
|
||||||
|
|
||||||
import type { WorkflowGraph } from "./specs";
|
import type { WorkflowGraph } from "./specs";
|
||||||
|
|
||||||
@@ -18,7 +23,13 @@ export type WorkflowSettings = {
|
|||||||
turnConfig: TurnConfig;
|
turnConfig: TurnConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ModelOption = { value: string; label: string; disabled?: boolean };
|
export type ModelOption = {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
toolType?: Tool["type"];
|
||||||
|
systemKind?: SystemToolKind;
|
||||||
|
};
|
||||||
|
|
||||||
export type WorkflowEditorProps = {
|
export type WorkflowEditorProps = {
|
||||||
value?: WorkflowGraph;
|
value?: WorkflowGraph;
|
||||||
|
|||||||
@@ -232,7 +232,6 @@ export type Assistant = {
|
|||||||
enableInterrupt: boolean;
|
enableInterrupt: boolean;
|
||||||
turnConfig: TurnConfig;
|
turnConfig: TurnConfig;
|
||||||
startup: StartupConfig;
|
startup: StartupConfig;
|
||||||
systemTools: SystemToolKind[];
|
|
||||||
visionEnabled: boolean;
|
visionEnabled: boolean;
|
||||||
visionModelResourceId: string | null;
|
visionModelResourceId: string | null;
|
||||||
modelResourceIds: Partial<Record<ModelType, string>>;
|
modelResourceIds: Partial<Record<ModelType, string>>;
|
||||||
@@ -365,7 +364,7 @@ export type SystemToolDefinition = {
|
|||||||
schemaVersion: number;
|
schemaVersion: number;
|
||||||
type: "system";
|
type: "system";
|
||||||
config: {
|
config: {
|
||||||
kind: "end_conversation";
|
kind: SystemToolKind;
|
||||||
messageType: "none" | "custom";
|
messageType: "none" | "custom";
|
||||||
customMessage: string;
|
customMessage: string;
|
||||||
captureReason: boolean;
|
captureReason: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user