396 lines
16 KiB
Python
396 lines
16 KiB
Python
"""assistant_id → 运行时配置(把真 key 在服务端组装好)。
|
|
|
|
浏览器只传 assistant_id;真 key 在这里从 model_resources 取出注入。
|
|
助手按 capability binding 引用资源;取不到则回退该能力默认资源。
|
|
"""
|
|
|
|
from db.models import (
|
|
Assistant,
|
|
AssistantModelBinding,
|
|
AssistantToolBinding,
|
|
KnowledgeBase,
|
|
McpServer,
|
|
ModelResource,
|
|
Tool,
|
|
)
|
|
from models import (
|
|
AssistantConfig,
|
|
RuntimeKnowledgeBase,
|
|
RuntimeMcpServer,
|
|
RuntimeModelResource,
|
|
RuntimeTool,
|
|
RuntimeToolBinding,
|
|
)
|
|
from services.node_specs import copy_graph, graph_references
|
|
from services.task_backend_resources import resolve_task_backend
|
|
from sqlalchemy import or_, 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 (not resource.enabled or 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"}:
|
|
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.enabled
|
|
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 _bound_tools(
|
|
session: AsyncSession,
|
|
assistant_id: str,
|
|
) -> list[RuntimeToolBinding]:
|
|
rows = (
|
|
await session.execute(
|
|
select(AssistantToolBinding)
|
|
.where(AssistantToolBinding.assistant_id == assistant_id)
|
|
.order_by(AssistantToolBinding.created_at, AssistantToolBinding.tool_id)
|
|
)
|
|
).scalars().all()
|
|
return [
|
|
RuntimeToolBinding(tool_id=row.tool_id, **(row.config or {}))
|
|
for row in rows
|
|
]
|
|
|
|
|
|
def _startup_tool_ids(startup: dict | None) -> list[str]:
|
|
"""Return lifecycle-only tool references in configured execution order."""
|
|
|
|
result: list[str] = []
|
|
for action in (startup or {}).get("actions") or []:
|
|
if not isinstance(action, dict):
|
|
continue
|
|
binding = action.get("tool_binding") or action.get("toolBinding") or {}
|
|
tool_id = str(binding.get("tool_id") or binding.get("toolId") or "").strip()
|
|
if tool_id and tool_id not in result:
|
|
result.append(tool_id)
|
|
return result
|
|
|
|
|
|
def _runtime_tool_ids(
|
|
bindings: list[RuntimeToolBinding],
|
|
startup: dict | None,
|
|
) -> list[str]:
|
|
"""Merge both tool scopes without changing either scope's meaning."""
|
|
|
|
return list(
|
|
dict.fromkeys(
|
|
[*(binding.tool_id for binding in bindings), *_startup_tool_ids(startup)]
|
|
)
|
|
)
|
|
|
|
|
|
def _implicit_startup_tool_names(startup: dict | None) -> list[str]:
|
|
"""Return platform client tools used by built-in startup experiences.
|
|
|
|
A confirmation opening is configured directly on the assistant instead of
|
|
through an AssistantToolBinding, but text tests still need to simulate the
|
|
underlying client interaction explicitly.
|
|
"""
|
|
|
|
value = startup or {}
|
|
opening_mode = value.get("opening_mode") or value.get("openingMode")
|
|
opening_message = value.get("opening_message") or value.get("openingMessage")
|
|
if opening_mode == "confirmation" and isinstance(opening_message, dict):
|
|
return ["show_message"]
|
|
return []
|
|
|
|
|
|
async def _tools_for(
|
|
session: AsyncSession,
|
|
assistant: Assistant,
|
|
tool_ids: list[str],
|
|
implicit_function_names: list[str] | None = None,
|
|
) -> list[RuntimeTool]:
|
|
function_names = list(dict.fromkeys(implicit_function_names or []))
|
|
if assistant.type not in {"prompt", "workflow"} or not (
|
|
tool_ids or function_names
|
|
):
|
|
return []
|
|
identity_filters = []
|
|
if tool_ids:
|
|
identity_filters.append(Tool.id.in_(tool_ids))
|
|
if function_names:
|
|
identity_filters.append(Tool.function_name.in_(function_names))
|
|
rows = (
|
|
await session.execute(
|
|
select(Tool)
|
|
.where(
|
|
or_(*identity_filters),
|
|
Tool.status == "active",
|
|
Tool.type.in_(("http", "mcp", "client")),
|
|
)
|
|
)
|
|
).scalars().all()
|
|
tool_by_id = {tool.id: tool for tool in rows}
|
|
tool_by_function = {tool.function_name: tool for tool in rows}
|
|
tools = [tool_by_id[tool_id] for tool_id in tool_ids if tool_id in tool_by_id]
|
|
seen_tool_ids = {tool.id for tool in tools}
|
|
for function_name in function_names:
|
|
tool = tool_by_function.get(function_name)
|
|
if tool is not None and tool.id not in seen_tool_ids:
|
|
tools.append(tool)
|
|
seen_tool_ids.add(tool.id)
|
|
server_ids = {
|
|
str(tool.mcp_server_id)
|
|
for tool in tools
|
|
if tool.type == "mcp" and tool.mcp_server_id
|
|
}
|
|
server_rows = (
|
|
(
|
|
await session.execute(
|
|
select(McpServer).where(
|
|
McpServer.id.in_(server_ids),
|
|
McpServer.status == "active",
|
|
)
|
|
)
|
|
).scalars().all()
|
|
if server_ids
|
|
else []
|
|
)
|
|
servers = {server.id: server for server in server_rows}
|
|
resolved: list[RuntimeTool] = []
|
|
for tool in tools:
|
|
mcp_server = None
|
|
if tool.type == "mcp":
|
|
server = servers.get(str(tool.mcp_server_id or ""))
|
|
if server is None:
|
|
continue
|
|
config = server.config or {}
|
|
secrets = server.secrets or {}
|
|
mcp_server = RuntimeMcpServer(
|
|
id=server.id,
|
|
name=server.name,
|
|
transport=server.transport,
|
|
url=server.url,
|
|
timeout_seconds=int(config.get("timeout_seconds") or 30),
|
|
headers=dict(config.get("headers") or {}),
|
|
secret_headers=dict(secrets.get("headers") or {}),
|
|
)
|
|
resolved.append(
|
|
RuntimeTool(
|
|
id=tool.id,
|
|
name=tool.name,
|
|
function_name=tool.function_name,
|
|
type=tool.type,
|
|
description=tool.description,
|
|
definition=tool.definition or {},
|
|
secrets=tool.secrets or {},
|
|
mcp_server=mcp_server,
|
|
)
|
|
)
|
|
return resolved
|
|
|
|
|
|
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
|
|
)
|
|
knowledge_base = (
|
|
await session.get(KnowledgeBase, assistant.knowledge_base_id)
|
|
if assistant.knowledge_base_id
|
|
else None
|
|
)
|
|
|
|
graph = copy_graph(assistant.graph or {}) if assistant.type == "workflow" else {}
|
|
refs = graph_references(graph) if graph else {
|
|
"model_resources": set(),
|
|
"knowledge_bases": set(),
|
|
}
|
|
workflow_resources: dict[str, RuntimeModelResource] = {}
|
|
if refs["model_resources"]:
|
|
resources = (
|
|
await session.execute(
|
|
select(ModelResource).where(ModelResource.id.in_(refs["model_resources"]))
|
|
)
|
|
).scalars().all()
|
|
workflow_resources = {
|
|
resource.id: RuntimeModelResource(
|
|
id=resource.id,
|
|
name=resource.name,
|
|
capability=resource.capability,
|
|
interface_type=resource.interface_type,
|
|
values=resource.values or {},
|
|
secrets=resource.secrets or {},
|
|
support_image_input=bool(resource.support_image_input),
|
|
)
|
|
for resource in resources
|
|
if resource.enabled
|
|
}
|
|
workflow_knowledge: dict[str, RuntimeKnowledgeBase] = {}
|
|
if refs["knowledge_bases"]:
|
|
knowledge_rows = (
|
|
await session.execute(
|
|
select(KnowledgeBase).where(KnowledgeBase.id.in_(refs["knowledge_bases"]))
|
|
)
|
|
).scalars().all()
|
|
workflow_knowledge = {
|
|
kb.id: RuntimeKnowledgeBase(id=kb.id, name=kb.name, description=kb.description)
|
|
for kb in knowledge_rows
|
|
if kb.status == "active"
|
|
}
|
|
|
|
prompt_tool_bindings = await _bound_tools(session, assistant.id)
|
|
runtime_tool_ids = _runtime_tool_ids(prompt_tool_bindings, assistant.startup or {})
|
|
runtime_tools = await _tools_for(
|
|
session,
|
|
assistant,
|
|
runtime_tool_ids,
|
|
_implicit_startup_tool_names(assistant.startup or {}),
|
|
)
|
|
|
|
return AssistantConfig(
|
|
name=assistant.name,
|
|
type=assistant.type,
|
|
greeting=assistant.greeting,
|
|
# prompt 现在是真列;外部类型由其平台编排,这里给个兜底
|
|
prompt=assistant.prompt or "你是一个有帮助的助手。",
|
|
dynamic_variable_definitions=assistant.dynamic_variable_definitions or {},
|
|
runtimeMode=assistant.runtime_mode, # type: ignore[arg-type]
|
|
enableInterrupt=assistant.enable_interrupt,
|
|
turnConfig=assistant.turn_config or {},
|
|
startup=assistant.startup or {},
|
|
analysis_config=assistant.analysis_config or {},
|
|
built_in_tools=assistant.built_in_tools or {},
|
|
tools=runtime_tools,
|
|
tool_bindings=prompt_tool_bindings,
|
|
task_backend=await resolve_task_backend(session, assistant),
|
|
knowledge_base_id=assistant.knowledge_base_id,
|
|
knowledge_base_name=knowledge_base.name if knowledge_base else "",
|
|
knowledge_base_description=knowledge_base.description if knowledge_base else "",
|
|
knowledge_retrieval_config=assistant.knowledge_retrieval_config or {},
|
|
# workflow 图:仅 workflow 类型非空,引擎据此启用图驱动对话
|
|
graph=graph,
|
|
workflow_model_resources=workflow_resources,
|
|
workflow_knowledge_bases=workflow_knowledge,
|
|
# 外部托管类型连接信息(DB 存真 key,直接注入)
|
|
dify_api_url=str(_value(agent_resource, "apiUrl", assistant.api_url)),
|
|
dify_api_key=_secret(agent_resource, "apiKey", assistant.api_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_prompt_mode=assistant.vision_prompt_mode or "runtime",
|
|
vision_prompt=assistant.vision_prompt or "",
|
|
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", "")),
|
|
)
|