refactor: unify system tools as resources
This commit is contained in:
@@ -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'
|
||||
"""
|
||||
)
|
||||
Reference in New Issue
Block a user