Files
ai-video-fullstack/backend/migrations/versions/20260804_0012_unify_system_tool_resources.py
2026-08-04 17:05:26 +08:00

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'
"""
)