236 lines
7.6 KiB
Python
236 lines
7.6 KiB
Python
"""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'
|
|
"""
|
|
)
|