196 lines
6.7 KiB
Python
196 lines
6.7 KiB
Python
"""Resolve and validate an assistant before creating a media connection."""
|
|||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
from db.session import SessionLocal
|
||
|
|
from models import AssistantConfig
|
||
|
|
from services.config_resolver import resolve_runtime_config
|
||
|
|
from services.node_specs import graph_references
|
||
|
|
from services.runtime_variables import prepare_dynamic_config
|
||
|
|
from services.workflow_engine import WorkflowEngine
|
||
|
|
|
||
|
|
|
||
|
|
async def resolve_assistant_config(
|
||
|
|
assistant_id: str,
|
||
|
|
*,
|
||
|
|
dynamic_variables: dict[str, Any] | None = None,
|
||
|
|
) -> AssistantConfig:
|
||
|
|
async with SessionLocal() as session:
|
||
|
|
config = await resolve_runtime_config(session, assistant_id)
|
||
|
|
return prepare_dynamic_config(
|
||
|
|
config,
|
||
|
|
dynamic_variables or {},
|
||
|
|
assistant_id=assistant_id,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def validate_visual_runtime(config: AssistantConfig) -> bool:
|
||
|
|
"""Return the authoritative video-input permission or fail before connect."""
|
||
|
|
|
||
|
|
if config.type == "workflow":
|
||
|
|
return WorkflowEngine(config.graph).uses_vision()
|
||
|
|
|
||
|
|
vision_enabled = config.vision_enabled
|
||
|
|
if not vision_enabled:
|
||
|
|
return False
|
||
|
|
has_native_vision = (
|
||
|
|
not config.vision_model_resource_id and config.llm_support_image_input
|
||
|
|
)
|
||
|
|
has_aux_vision_model = (
|
||
|
|
bool(config.vision_model_resource_id)
|
||
|
|
and config.vision_llm_support_image_input
|
||
|
|
)
|
||
|
|
if not (has_native_vision or has_aux_vision_model):
|
||
|
|
raise ValueError(
|
||
|
|
"当前模型不支持图片输入,请在模型资源中选择支持图片输入的视觉模型"
|
||
|
|
)
|
||
|
|
return True
|
||
|
|
|
||
|
|
|
||
|
|
def _require_values(labels: list[tuple[str, Any]]) -> None:
|
||
|
|
missing = [label for label, value in labels if not value]
|
||
|
|
if missing:
|
||
|
|
raise ValueError(f"助手运行配置不完整: {', '.join(missing)}")
|
||
|
|
|
||
|
|
|
||
|
|
def _validate_voice_resource(
|
||
|
|
capability: str,
|
||
|
|
*,
|
||
|
|
interface_type: str,
|
||
|
|
values: dict[str, Any],
|
||
|
|
secrets: dict[str, Any],
|
||
|
|
) -> None:
|
||
|
|
labels: list[tuple[str, Any]] = []
|
||
|
|
if capability == "ASR":
|
||
|
|
if interface_type not in {"openai-asr", "dashscope-asr", "xfyun-asr"}:
|
||
|
|
raise ValueError(f"不支持的 ASR 接口类型: {interface_type}")
|
||
|
|
if interface_type == "xfyun-asr":
|
||
|
|
labels.extend(
|
||
|
|
(f"ASR {key}", secrets.get(key))
|
||
|
|
for key in ("appId", "apiKey", "apiSecret")
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
labels.extend(
|
||
|
|
[
|
||
|
|
("ASR modelId", values.get("modelId")),
|
||
|
|
("ASR apiUrl", values.get("apiUrl")),
|
||
|
|
("ASR apiKey", secrets.get("apiKey")),
|
||
|
|
]
|
||
|
|
)
|
||
|
|
elif capability == "TTS":
|
||
|
|
if interface_type not in {
|
||
|
|
"openai-tts",
|
||
|
|
"dashscope-tts",
|
||
|
|
"xfyun-tts",
|
||
|
|
"xfyun-super-tts",
|
||
|
|
}:
|
||
|
|
raise ValueError(f"不支持的 TTS 接口类型: {interface_type}")
|
||
|
|
labels.append(("TTS voice", values.get("voice")))
|
||
|
|
if interface_type in {"xfyun-tts", "xfyun-super-tts"}:
|
||
|
|
labels.extend(
|
||
|
|
(f"TTS {key}", secrets.get(key))
|
||
|
|
for key in ("appId", "apiKey", "apiSecret")
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
labels.extend(
|
||
|
|
[
|
||
|
|
("TTS modelId", values.get("modelId")),
|
||
|
|
("TTS apiUrl", values.get("apiUrl")),
|
||
|
|
("TTS apiKey", secrets.get("apiKey")),
|
||
|
|
]
|
||
|
|
)
|
||
|
|
elif capability == "LLM":
|
||
|
|
if interface_type not in {"openai-llm", "dashscope-llm"}:
|
||
|
|
raise ValueError(f"不支持的 LLM 接口类型: {interface_type}")
|
||
|
|
labels.extend(
|
||
|
|
[
|
||
|
|
("LLM modelId", values.get("modelId")),
|
||
|
|
("LLM apiUrl", values.get("apiUrl")),
|
||
|
|
("LLM apiKey", secrets.get("apiKey")),
|
||
|
|
]
|
||
|
|
)
|
||
|
|
_require_values(labels)
|
||
|
|
|
||
|
|
|
||
|
|
def validate_runtime_requirements(config: AssistantConfig) -> None:
|
||
|
|
"""Reject structurally incomplete assistants before media negotiation."""
|
||
|
|
|
||
|
|
if config.type not in {"prompt", "workflow", "dify", "fastgpt"}:
|
||
|
|
raise ValueError(f"当前助手类型不支持 Realtime API: {config.type}")
|
||
|
|
if config.runtimeMode == "realtime":
|
||
|
|
if config.type not in {"prompt", "workflow"}:
|
||
|
|
raise ValueError(f"助手类型 {config.type} 不支持 realtime 运行模式")
|
||
|
|
if config.realtime_interface_type not in {
|
||
|
|
"qwen-audio-realtime",
|
||
|
|
"stepfun-realtime",
|
||
|
|
}:
|
||
|
|
raise ValueError(
|
||
|
|
f"不支持的 Realtime 接口类型: {config.realtime_interface_type}"
|
||
|
|
)
|
||
|
|
_require_values(
|
||
|
|
[
|
||
|
|
("Realtime interfaceType", config.realtime_interface_type),
|
||
|
|
("Realtime modelId", config.realtimeModel),
|
||
|
|
("Realtime apiUrl", config.realtime_base_url),
|
||
|
|
("Realtime apiKey", config.realtime_api_key),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
return
|
||
|
|
|
||
|
|
_validate_voice_resource(
|
||
|
|
"ASR",
|
||
|
|
interface_type=config.stt_interface_type,
|
||
|
|
values=config.stt_values,
|
||
|
|
secrets=config.stt_secrets,
|
||
|
|
)
|
||
|
|
_validate_voice_resource(
|
||
|
|
"TTS",
|
||
|
|
interface_type=config.tts_interface_type,
|
||
|
|
values=config.tts_values,
|
||
|
|
secrets=config.tts_secrets,
|
||
|
|
)
|
||
|
|
if config.type == "prompt":
|
||
|
|
_validate_voice_resource(
|
||
|
|
"LLM",
|
||
|
|
interface_type=config.llm_interface_type,
|
||
|
|
values=config.llm_values,
|
||
|
|
secrets=config.llm_secrets,
|
||
|
|
)
|
||
|
|
elif config.type == "dify":
|
||
|
|
_require_values(
|
||
|
|
[
|
||
|
|
("Dify apiUrl", config.dify_api_url),
|
||
|
|
("Dify apiKey", config.dify_api_key),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
elif config.type == "fastgpt":
|
||
|
|
_require_values(
|
||
|
|
[
|
||
|
|
("FastGPT apiUrl", config.fastgpt_api_url),
|
||
|
|
("FastGPT apiKey", config.fastgpt_api_key),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
|
||
|
|
if config.type != "workflow":
|
||
|
|
return
|
||
|
|
references = graph_references(config.graph)
|
||
|
|
missing_models = references["model_resources"] - set(
|
||
|
|
config.workflow_model_resources
|
||
|
|
)
|
||
|
|
missing_knowledge = references["knowledge_bases"] - set(
|
||
|
|
config.workflow_knowledge_bases
|
||
|
|
)
|
||
|
|
if missing_models or missing_knowledge:
|
||
|
|
missing = sorted([*missing_models, *missing_knowledge])
|
||
|
|
raise ValueError(f"Workflow 引用了不可用资源: {', '.join(missing)}")
|
||
|
|
for resource in config.workflow_model_resources.values():
|
||
|
|
if resource.capability in {"ASR", "TTS", "LLM"}:
|
||
|
|
_validate_voice_resource(
|
||
|
|
resource.capability,
|
||
|
|
interface_type=resource.interface_type,
|
||
|
|
values=resource.values,
|
||
|
|
secrets=resource.secrets,
|
||
|
|
)
|