Files
ai-video-fullstack/backend/services/config_resolver.py
Xin Wang 4a57b290d3 Add RuntimeTool model and enhance AssistantConfig for tool management
- Introduce a new `RuntimeTool` model to encapsulate tool data for runtime sessions, including attributes like `id`, `name`, `function_name`, `type`, and `description`.
- Update the `AssistantConfig` model to include a list of reusable tools, allowing for better management of tools within assistant configurations.
- Modify the `config_resolver` service to fetch and resolve tools associated with assistants, ensuring they are available during runtime.
- Refactor tool-related CRUD operations in the `tools` route to support the new runtime execution model, enhancing the overall tool management system.
- Update documentation and comments to reflect changes in tool execution and configuration handling, improving clarity for future development.
2026-07-10 14:32:10 +08:00

196 lines
8.4 KiB
Python

"""assistant_id → 运行时配置(把真 key 在服务端组装好)。
浏览器只传 assistant_id;真 key 在这里从 model_resources 取出注入。
助手按 capability binding 引用资源;取不到则回退该能力默认资源。
"""
from db.models import (
Assistant,
AssistantModelBinding,
AssistantToolBinding,
ModelResource,
Tool,
)
from models import AssistantConfig, RuntimeTool
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
async def _resource_for(
session: AsyncSession,
assistant_id: str,
capability: str,
) -> ModelResource | None:
binding = await session.get(AssistantModelBinding, (assistant_id, capability))
resource_id = binding.model_resource_id if binding else None
resource = await session.get(ModelResource, resource_id) if resource_id else None
if resource and resource.capability != capability:
resource = None
if resource is None:
resource = (
await session.execute(
select(ModelResource)
.where(ModelResource.capability == capability, ModelResource.enabled.is_(True))
.order_by(ModelResource.is_default.desc(), ModelResource.id.asc())
.limit(1)
)
).scalar_one_or_none()
return resource
async def _agent_resource_for(
session: AsyncSession,
assistant_id: str,
interface_type: str,
) -> ModelResource | None:
if interface_type not in {"dify", "fastgpt", "opencode"}:
return None
binding = await session.get(AssistantModelBinding, (assistant_id, "Agent"))
resource_id = binding.model_resource_id if binding else None
resource = await session.get(ModelResource, resource_id) if resource_id else None
if (
resource
and resource.capability == "Agent"
and resource.interface_type == interface_type
):
return resource
return (
await session.execute(
select(ModelResource)
.where(
ModelResource.capability == "Agent",
ModelResource.interface_type == interface_type,
ModelResource.enabled.is_(True),
)
.order_by(ModelResource.is_default.desc(), ModelResource.id.asc())
.limit(1)
)
).scalar_one_or_none()
def _value(resource: ModelResource | None, key: str, default=""):
if not resource:
return default
value = (resource.values or {}).get(key, default)
return default if value is None else value
def _secret(resource: ModelResource | None, key: str, default: str = "") -> str:
if not resource:
return default
return str((resource.secrets or {}).get(key) or default)
async def _tools_for(session: AsyncSession, assistant: Assistant) -> list[RuntimeTool]:
if assistant.type != "prompt":
return []
tools = (
await session.execute(
select(Tool)
.join(AssistantToolBinding, AssistantToolBinding.tool_id == Tool.id)
.where(
AssistantToolBinding.assistant_id == assistant.id,
Tool.status == "active",
)
.order_by(AssistantToolBinding.created_at, Tool.id)
)
).scalars().all()
return [
RuntimeTool(
id=tool.id,
name=tool.name,
function_name=tool.function_name,
type=tool.type,
description=tool.description,
definition=tool.definition or {},
)
for tool in tools
]
async def resolve_runtime_config(
session: AsyncSession, assistant_id: str
) -> AssistantConfig:
"""加载助手 + 解析模型资源,产出可直接交给管线的运行时配置(含真 key)。"""
assistant = await session.get(Assistant, assistant_id)
if assistant is None:
raise ValueError(f"助手不存在: {assistant_id}")
llm_resource = await _resource_for(session, assistant.id, "LLM")
stt_resource = await _resource_for(session, assistant.id, "ASR")
tts_resource = await _resource_for(session, assistant.id, "TTS")
realtime_resource = await _resource_for(session, assistant.id, "Realtime")
agent_resource = await _agent_resource_for(session, assistant.id, assistant.type)
vision_resource = (
await session.get(ModelResource, assistant.vision_model_resource_id)
if assistant.vision_model_resource_id
else llm_resource
)
return AssistantConfig(
name=assistant.name,
type=assistant.type,
greeting=assistant.greeting,
# prompt 现在是真列;外部类型由其平台编排,这里给个兜底
prompt=assistant.prompt or "你是一个有帮助的助手。",
runtimeMode=assistant.runtime_mode, # type: ignore[arg-type]
enableInterrupt=assistant.enable_interrupt,
tools=await _tools_for(session, assistant),
# workflow 图:仅 workflow 类型非空,引擎据此启用图驱动对话
graph=(assistant.graph or {}) if assistant.type == "workflow" else {},
# 外部托管类型连接信息(DB 存真 key,直接注入)
fastgpt_api_url=str(_value(agent_resource, "apiUrl", assistant.api_url)),
fastgpt_api_key=_secret(agent_resource, "apiKey", assistant.api_key),
fastgpt_app_id=str(_value(agent_resource, "appId", assistant.app_id)),
# 模型/音色:模型资源中的配置优先
model=str(_value(llm_resource, "modelId", "")),
asr=str(_value(stt_resource, "modelId", "")),
tts_model=str(_value(tts_resource, "modelId", "")),
voice=str(_value(tts_resource, "voice", "")),
stt_language=str(_value(stt_resource, "language", "")),
tts_speed=float(_value(tts_resource, "speed", 1.0)),
llm_interface_type=(llm_resource.interface_type if llm_resource else "openai-llm"),
stt_interface_type=(stt_resource.interface_type if stt_resource else "openai-asr"),
tts_interface_type=(tts_resource.interface_type if tts_resource else "openai-tts"),
realtimeModel=str(_value(realtime_resource, "modelId", "")),
llm_values=(llm_resource.values or {}) if llm_resource else {},
llm_secrets=(llm_resource.secrets or {}) if llm_resource else {},
llm_support_image_input=(
bool(llm_resource.support_image_input) if llm_resource else False
),
vision_enabled=assistant.vision_enabled,
vision_model_resource_id=assistant.vision_model_resource_id,
vision_model=str(_value(vision_resource, "modelId", "")),
vision_llm_interface_type=(
vision_resource.interface_type if vision_resource else "openai-llm"
),
vision_llm_values=(vision_resource.values or {}) if vision_resource else {},
vision_llm_secrets=(vision_resource.secrets or {}) if vision_resource else {},
vision_llm_support_image_input=(
bool(vision_resource.support_image_input) if vision_resource else False
),
stt_values=(stt_resource.values or {}) if stt_resource else {},
stt_secrets=(stt_resource.secrets or {}) if stt_resource else {},
tts_values=(tts_resource.values or {}) if tts_resource else {},
tts_secrets=(tts_resource.secrets or {}) if tts_resource else {},
realtime_interface_type=(
realtime_resource.interface_type if realtime_resource else ""
),
realtime_values=(realtime_resource.values or {}) if realtime_resource else {},
realtime_secrets=(realtime_resource.secrets or {}) if realtime_resource else {},
agent_interface_type=(agent_resource.interface_type if agent_resource else ""),
agent_values=(agent_resource.values or {}) if agent_resource else {},
agent_secrets=(agent_resource.secrets or {}) if agent_resource else {},
# 运行时连接信息(真 key + url):来自模型资源,不再从 .env 兜底。
llm_api_key=_secret(llm_resource, "apiKey"),
llm_base_url=str(_value(llm_resource, "apiUrl")),
vision_llm_api_key=_secret(vision_resource, "apiKey"),
vision_llm_base_url=str(_value(vision_resource, "apiUrl")),
stt_api_key=_secret(stt_resource, "apiKey"),
stt_base_url=str(_value(stt_resource, "apiUrl")),
tts_api_key=_secret(tts_resource, "apiKey"),
tts_base_url=str(_value(tts_resource, "apiUrl")),
realtime_api_key=_secret(realtime_resource, "apiKey", ""),
realtime_base_url=str(_value(realtime_resource, "apiUrl", "")),
)