mirror of
https://github.com/AstrBotDevs/AstrBot.git
synced 2026-08-30 17:33:24 +08:00
refactor: embed agent runner configuration in profiles (#9821)
* refactor: embed agent runner configuration in profiles * fix: limit personas to local agent runner * style(dashboard): refine unsaved config notice * refactor: refine embedded local runner configuration * refactor: centralize agent runner migrations
This commit is contained in:
@@ -5,7 +5,6 @@ from astrbot.api import sp, star
|
||||
from astrbot.api.event import AstrMessageEvent, MessageEventResult
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.agent.runners.deerflow.constants import (
|
||||
DEERFLOW_AGENT_RUNNER_PROVIDER_ID_KEY,
|
||||
DEERFLOW_PROVIDER_TYPE,
|
||||
DEERFLOW_THREAD_ID_KEY,
|
||||
)
|
||||
@@ -39,32 +38,21 @@ async def _cleanup_deerflow_thread_if_present(
|
||||
return
|
||||
|
||||
cfg = context.get_config(umo=umo)
|
||||
provider_id = cfg["provider_settings"].get(
|
||||
DEERFLOW_AGENT_RUNNER_PROVIDER_ID_KEY,
|
||||
"",
|
||||
)
|
||||
if not provider_id:
|
||||
agent_runner = cfg.get("agent_runner", {})
|
||||
if agent_runner.get("runner_type") != DEERFLOW_PROVIDER_TYPE:
|
||||
return
|
||||
|
||||
merged_provider_config = context.provider_manager.get_provider_config_by_id(
|
||||
provider_id,
|
||||
merged=True,
|
||||
)
|
||||
if not merged_provider_config:
|
||||
logger.warning(
|
||||
"Failed to resolve DeerFlow provider config for remote thread cleanup: provider_id=%s",
|
||||
provider_id,
|
||||
)
|
||||
runner_config = agent_runner.get("config", {})
|
||||
if not isinstance(runner_config, dict):
|
||||
return
|
||||
|
||||
client = DeerFlowAPIClient(
|
||||
api_base=merged_provider_config.get(
|
||||
api_base=runner_config.get(
|
||||
"deerflow_api_base",
|
||||
"http://127.0.0.1:2026",
|
||||
),
|
||||
api_key=merged_provider_config.get("deerflow_api_key", ""),
|
||||
auth_header=merged_provider_config.get("deerflow_auth_header", ""),
|
||||
proxy=merged_provider_config.get("proxy", ""),
|
||||
api_key=runner_config.get("deerflow_api_key", ""),
|
||||
auth_header=runner_config.get("deerflow_auth_header", ""),
|
||||
proxy=runner_config.get("proxy", ""),
|
||||
)
|
||||
try:
|
||||
await client.delete_thread(thread_id)
|
||||
@@ -148,7 +136,7 @@ class ConversationCommands:
|
||||
)
|
||||
return
|
||||
|
||||
agent_runner_type = cfg["provider_settings"]["agent_runner_type"]
|
||||
agent_runner_type = cfg["agent_runner"]["runner_type"]
|
||||
if agent_runner_type in THIRD_PARTY_AGENT_RUNNER_KEY:
|
||||
active_event_registry.stop_all(umo, exclude=message)
|
||||
await _clear_third_party_agent_runner_state(
|
||||
@@ -196,7 +184,7 @@ class ConversationCommands:
|
||||
async def stop(self, message: AstrMessageEvent) -> None:
|
||||
"""停止当前会话正在运行的 Agent"""
|
||||
cfg = self.context.get_config(umo=message.unified_msg_origin)
|
||||
agent_runner_type = cfg["provider_settings"]["agent_runner_type"]
|
||||
agent_runner_type = cfg["agent_runner"]["runner_type"]
|
||||
umo = message.unified_msg_origin
|
||||
|
||||
if agent_runner_type in THIRD_PARTY_AGENT_RUNNER_KEY:
|
||||
@@ -222,7 +210,7 @@ class ConversationCommands:
|
||||
async def new_conv(self, message: AstrMessageEvent) -> None:
|
||||
"""创建新对话"""
|
||||
cfg = self.context.get_config(umo=message.unified_msg_origin)
|
||||
agent_runner_type = cfg["provider_settings"]["agent_runner_type"]
|
||||
agent_runner_type = cfg["agent_runner"]["runner_type"]
|
||||
if agent_runner_type in THIRD_PARTY_AGENT_RUNNER_KEY:
|
||||
active_event_registry.stop_all(message.unified_msg_origin, exclude=message)
|
||||
await _clear_third_party_agent_runner_state(
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
DEERFLOW_PROVIDER_TYPE = "deerflow"
|
||||
DEERFLOW_THREAD_ID_KEY = "deerflow_thread_id"
|
||||
DEERFLOW_SESSION_PREFIX = "deerflow-ephemeral"
|
||||
DEERFLOW_AGENT_RUNNER_PROVIDER_ID_KEY = "deerflow_agent_runner_provider_id"
|
||||
|
||||
@@ -358,8 +358,14 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
prov_settings: dict = ctx.get_config(umo=umo).get("provider_settings", {})
|
||||
agent_max_step = int(prov_settings.get("max_agent_step", 30))
|
||||
config = ctx.get_config(umo=umo)
|
||||
prov_settings: dict = config.get("provider_settings", {})
|
||||
agent_max_step = int(
|
||||
config.get("agent_runner", {})
|
||||
.get("config", {})
|
||||
.get("misc", {})
|
||||
.get("max_steps", 30)
|
||||
)
|
||||
stream = prov_settings.get("streaming_response", False)
|
||||
llm_resp = await ctx.tool_loop_agent(
|
||||
event=event,
|
||||
@@ -551,10 +557,13 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
|
||||
cfg = ctx.get_config(umo=event.unified_msg_origin) or {}
|
||||
provider_settings = cfg.get("provider_settings") or {}
|
||||
agent_max_step = coerce_int_config(
|
||||
provider_settings.get("max_agent_step", 30),
|
||||
cfg.get("agent_runner", {})
|
||||
.get("config", {})
|
||||
.get("misc", {})
|
||||
.get("max_steps", 30),
|
||||
default=30,
|
||||
min_value=1,
|
||||
field_name="provider_settings.max_agent_step",
|
||||
field_name="agent_runner.config.misc.max_steps",
|
||||
)
|
||||
config = MainAgentBuildConfig(
|
||||
tool_call_timeout=run_context.tool_call_timeout,
|
||||
|
||||
@@ -209,6 +209,8 @@ class MainAgentBuildConfig:
|
||||
add_cron_tools: bool = True
|
||||
"""This will add cron job management tools to the main agent for proactive cron job execution."""
|
||||
provider_settings: dict = field(default_factory=dict)
|
||||
fallback_provider_ids: list[str] = field(default_factory=list)
|
||||
request_max_retries: int = 5
|
||||
subagent_orchestrator: dict = field(default_factory=dict)
|
||||
timezone: str | None = None
|
||||
max_quoted_fallback_images: int = 20
|
||||
@@ -1341,12 +1343,11 @@ async def _get_compress_provider(
|
||||
|
||||
|
||||
def _get_fallback_chat_providers(
|
||||
provider: Provider, plugin_context: Context, provider_settings: dict
|
||||
provider: Provider, plugin_context: Context, fallback_ids: list[str]
|
||||
) -> list[Provider]:
|
||||
fallback_ids = provider_settings.get("fallback_chat_models", [])
|
||||
if not isinstance(fallback_ids, list):
|
||||
logger.warning(
|
||||
"fallback_chat_models setting is not a list, skip fallback providers."
|
||||
"Agent Runner fallback_provider_ids is not a list, skip fallback providers."
|
||||
)
|
||||
return []
|
||||
|
||||
@@ -1657,7 +1658,7 @@ async def build_main_agent(
|
||||
)
|
||||
|
||||
fallback_providers = _get_fallback_chat_providers(
|
||||
provider, plugin_context, config.provider_settings
|
||||
provider, plugin_context, config.fallback_provider_ids
|
||||
)
|
||||
selected_provider = _select_image_chat_provider(provider, req, fallback_providers)
|
||||
if selected_provider is not provider:
|
||||
@@ -1732,7 +1733,7 @@ async def build_main_agent(
|
||||
enforce_max_turns=config.max_context_length,
|
||||
tool_schema_mode=config.tool_schema_mode,
|
||||
fallback_providers=fallback_providers,
|
||||
request_max_retries=config.provider_settings.get("request_max_retries", 5),
|
||||
request_max_retries=config.request_max_retries,
|
||||
tool_result_overflow_dir=(
|
||||
get_astrbot_system_tmp_path()
|
||||
if req.func_tool and req.func_tool.get_tool("astrbot_file_read_tool")
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from typing import Any
|
||||
|
||||
AGENT_RUNNER_TYPES = ("local", "dify", "coze", "dashscope", "deerflow")
|
||||
THIRD_PARTY_AGENT_RUNNER_TYPES = AGENT_RUNNER_TYPES[1:]
|
||||
|
||||
AGENT_RUNNER_CONFIG_DEFAULTS: dict[str, dict[str, Any]] = {
|
||||
"local": {
|
||||
"model": {
|
||||
"provider_id": "",
|
||||
"fallback_provider_ids": [],
|
||||
"request_max_retries": 5,
|
||||
},
|
||||
"persona": {
|
||||
"persona_id": "default",
|
||||
"safety_mode": True,
|
||||
"safety_mode_strategy": "system_prompt",
|
||||
},
|
||||
"compression": {
|
||||
"max_turns": -1,
|
||||
"trim_turns": 1,
|
||||
"overflow_strategy": "llm_compress",
|
||||
"instruction": "",
|
||||
"keep_recent_ratio": 0.15,
|
||||
"provider_id": "",
|
||||
"fallback_max_tokens": 128000,
|
||||
},
|
||||
"misc": {
|
||||
"max_steps": 30,
|
||||
"tool_schema_mode": "full",
|
||||
"tool_call_timeout": 120,
|
||||
"sanitize_context_by_modalities": False,
|
||||
},
|
||||
},
|
||||
"dify": {
|
||||
"dify_api_type": "chat",
|
||||
"dify_api_key": "",
|
||||
"dify_api_base": "https://api.dify.ai/v1",
|
||||
"dify_workflow_output_key": "astrbot_wf_output",
|
||||
"dify_query_input_key": "astrbot_text_query",
|
||||
"variables": {},
|
||||
"timeout": 60,
|
||||
"proxy": "",
|
||||
},
|
||||
"coze": {
|
||||
"coze_api_key": "",
|
||||
"bot_id": "",
|
||||
"coze_api_base": "https://api.coze.cn",
|
||||
"auto_save_history": True,
|
||||
"timeout": 60,
|
||||
"proxy": "",
|
||||
},
|
||||
"dashscope": {
|
||||
"dashscope_app_type": "agent",
|
||||
"dashscope_api_key": "",
|
||||
"dashscope_app_id": "",
|
||||
"rag_options": {
|
||||
"pipeline_ids": [],
|
||||
"file_ids": [],
|
||||
"output_reference": False,
|
||||
},
|
||||
"variables": {},
|
||||
"timeout": 60,
|
||||
"proxy": "",
|
||||
},
|
||||
"deerflow": {
|
||||
"deerflow_api_base": "http://127.0.0.1:2026",
|
||||
"deerflow_api_key": "",
|
||||
"deerflow_auth_header": "",
|
||||
"deerflow_assistant_id": "lead_agent",
|
||||
"deerflow_model_name": "",
|
||||
"deerflow_thinking_enabled": False,
|
||||
"deerflow_plan_mode": False,
|
||||
"deerflow_subagent_enabled": False,
|
||||
"deerflow_max_concurrent_subagents": 3,
|
||||
"deerflow_recursion_limit": 1000,
|
||||
"timeout": 300,
|
||||
"proxy": "",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_agent_runner_config_default(runner_type: str) -> dict[str, Any]:
|
||||
"""Return an isolated default configuration for an Agent Runner type.
|
||||
|
||||
Args:
|
||||
runner_type: Short runner type name.
|
||||
|
||||
Returns:
|
||||
A deep copy of the runner configuration defaults.
|
||||
|
||||
Raises:
|
||||
ValueError: If the runner type is unsupported.
|
||||
"""
|
||||
if runner_type not in AGENT_RUNNER_CONFIG_DEFAULTS:
|
||||
raise ValueError(f"Unsupported Agent Runner type: {runner_type}")
|
||||
return copy.deepcopy(AGENT_RUNNER_CONFIG_DEFAULTS[runner_type])
|
||||
|
||||
|
||||
def _normalize_value(value: Any, default: Any) -> Any:
|
||||
if isinstance(default, dict):
|
||||
if not isinstance(value, dict):
|
||||
return copy.deepcopy(default)
|
||||
if not default:
|
||||
return copy.deepcopy(value)
|
||||
return {
|
||||
key: _normalize_value(value.get(key), child_default)
|
||||
for key, child_default in default.items()
|
||||
}
|
||||
if isinstance(default, list):
|
||||
return (
|
||||
copy.deepcopy(value) if isinstance(value, list) else copy.deepcopy(default)
|
||||
)
|
||||
if isinstance(default, bool):
|
||||
return value if isinstance(value, bool) else default
|
||||
if isinstance(default, int):
|
||||
if isinstance(value, bool):
|
||||
return default
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
if isinstance(default, float):
|
||||
if isinstance(value, bool):
|
||||
return default
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
if isinstance(default, str):
|
||||
return value if isinstance(value, str) else default
|
||||
return copy.deepcopy(value) if value is not None else copy.deepcopy(default)
|
||||
|
||||
|
||||
def normalize_agent_runner(agent_runner: object) -> dict[str, Any]:
|
||||
"""Validate and normalize a complete Agent Runner configuration.
|
||||
|
||||
Args:
|
||||
agent_runner: Untrusted root Agent Runner configuration.
|
||||
|
||||
Returns:
|
||||
A normalized configuration containing only fields for the selected runner.
|
||||
|
||||
Raises:
|
||||
ValueError: If the root value or runner type is invalid.
|
||||
"""
|
||||
if not isinstance(agent_runner, dict):
|
||||
raise ValueError("agent_runner must be an object")
|
||||
runner_type = agent_runner.get("runner_type")
|
||||
if runner_type not in AGENT_RUNNER_TYPES:
|
||||
raise ValueError(f"Unsupported Agent Runner type: {runner_type}")
|
||||
config = agent_runner.get("config", {})
|
||||
default = AGENT_RUNNER_CONFIG_DEFAULTS[runner_type]
|
||||
normalized = _normalize_value(config, default)
|
||||
if runner_type == "local":
|
||||
ratio = normalized["compression"]["keep_recent_ratio"]
|
||||
normalized["compression"]["keep_recent_ratio"] = min(0.3, max(0.0, ratio))
|
||||
if normalized["model"]["request_max_retries"] < 1:
|
||||
normalized["model"]["request_max_retries"] = 1
|
||||
if normalized["misc"]["max_steps"] < 1:
|
||||
normalized["misc"]["max_steps"] = 1
|
||||
if normalized["compression"]["trim_turns"] < 1:
|
||||
normalized["compression"]["trim_turns"] = 1
|
||||
return {"runner_type": runner_type, "config": normalized}
|
||||
@@ -6,6 +6,7 @@ import logging
|
||||
import os
|
||||
import tempfile
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.core.utils.auth_password import (
|
||||
@@ -14,6 +15,7 @@ from astrbot.core.utils.auth_password import (
|
||||
hash_md5_dashboard_password,
|
||||
validate_dashboard_password,
|
||||
)
|
||||
from astrbot.core.utils.migra_helper import migrate_config_on_load
|
||||
|
||||
from .default import DEFAULT_CONFIG, DEFAULT_VALUE_MAP
|
||||
|
||||
@@ -84,8 +86,12 @@ class AstrBotConfig(dict):
|
||||
"_dashboard_password_change_required_from_config",
|
||||
True,
|
||||
)
|
||||
config_migrated = False
|
||||
if default_config is DEFAULT_CONFIG:
|
||||
config_migrated = migrate_config_on_load(conf, Path(config_path))
|
||||
# 检查配置完整性,并插入
|
||||
has_new = self.check_config_integrity(default_config, conf)
|
||||
has_new |= config_migrated
|
||||
reset_dashboard_password = self._consume_reset_dashboard_password_flag()
|
||||
if reset_dashboard_password and "dashboard" in conf:
|
||||
self._reset_generated_dashboard_password(conf)
|
||||
@@ -188,6 +194,9 @@ class AstrBotConfig(dict):
|
||||
# 类型不匹配,使用默认值
|
||||
new_conf[key] = value
|
||||
has_new = True
|
||||
elif (path + "." + key if path else key) == "agent_runner.config":
|
||||
# Runner config is normalized according to runner_type when saved.
|
||||
new_conf[key] = conf[key]
|
||||
else:
|
||||
# 递归检查并同步顺序
|
||||
child_has_new = self.check_config_integrity(
|
||||
|
||||
+280
-236
@@ -6,6 +6,8 @@ from astrbot import __version__
|
||||
from astrbot.core.computer.booters.cua_defaults import CUA_DEFAULT_CONFIG
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
from .agent_runner import get_agent_runner_config_default
|
||||
|
||||
VERSION = __version__
|
||||
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||
@@ -61,7 +63,7 @@ WEBHOOK_SUPPORTED_PLATFORMS = [
|
||||
|
||||
# 默认配置
|
||||
DEFAULT_CONFIG = {
|
||||
"config_version": 2,
|
||||
"config_version": 3,
|
||||
"platform_settings": {
|
||||
"unique_session": False,
|
||||
"rate_limit": {
|
||||
@@ -108,9 +110,6 @@ DEFAULT_CONFIG = {
|
||||
"provider": [], # models from provider_sources
|
||||
"provider_settings": {
|
||||
"enable": True,
|
||||
"default_provider_id": "",
|
||||
"fallback_chat_models": [],
|
||||
"request_max_retries": 5,
|
||||
"default_image_caption_provider_id": "",
|
||||
"image_caption_prompt": "Please describe the image using Chinese.",
|
||||
"provider_pool": ["*"], # "*" 表示使用所有可用的提供者
|
||||
@@ -128,28 +127,12 @@ DEFAULT_CONFIG = {
|
||||
"identifier": False,
|
||||
"group_name_display": False,
|
||||
"datetime_system_prompt": True,
|
||||
"default_personality": "default",
|
||||
"persona_pool": ["*"],
|
||||
"prompt_prefix": "{{prompt}}",
|
||||
"context_limit_reached_strategy": "llm_compress", # or truncate_by_turns
|
||||
"llm_compress_instruction": (
|
||||
"Based on our full conversation history, produce a concise summary of key takeaways and/or project progress.\n"
|
||||
"The primary goal of this summary is to enable seamless continuation of the work that follows.\n"
|
||||
"1. Systematically cover all core topics discussed and the final conclusion/outcome for each; clearly highlight the latest primary focus.\n"
|
||||
"2. If any tools were used, summarize tool usage (total call count) and extract the most valuable insights from tool outputs.\n"
|
||||
"3. If any materials (files, documents, code, references) were read during the conversation that may be helpful for subsequent work, list each one with its scope and path.\n"
|
||||
"4. If there was an initial user goal, state it first and describe the current progress/status.\n"
|
||||
"5. Write the summary in the user's language.\n"
|
||||
),
|
||||
"llm_compress_keep_recent_ratio": 0.15,
|
||||
"llm_compress_provider_id": "",
|
||||
"max_context_length": -1, # 默认不限制
|
||||
"dequeue_context_length": 1,
|
||||
"streaming_response": False,
|
||||
"show_tool_use_status": False,
|
||||
"show_tool_call_result": False,
|
||||
"buffer_intermediate_messages": False,
|
||||
"sanitize_context_by_modalities": False,
|
||||
"max_quoted_fallback_images": 20,
|
||||
"quoted_message_parser": {
|
||||
"max_component_chain_depth": 4,
|
||||
@@ -157,18 +140,8 @@ DEFAULT_CONFIG = {
|
||||
"max_forward_fetch": 32,
|
||||
"warn_on_action_failure": False,
|
||||
},
|
||||
"agent_runner_type": "local",
|
||||
"dify_agent_runner_provider_id": "",
|
||||
"coze_agent_runner_provider_id": "",
|
||||
"dashscope_agent_runner_provider_id": "",
|
||||
"deerflow_agent_runner_provider_id": "",
|
||||
"unsupported_streaming_strategy": "realtime_segmenting",
|
||||
"reachability_check": False,
|
||||
"max_agent_step": 30,
|
||||
"tool_call_timeout": 120,
|
||||
"tool_schema_mode": "full",
|
||||
"llm_safety_mode": True,
|
||||
"safety_mode_strategy": "system_prompt", # TODO: llm judge
|
||||
"file_extract": {
|
||||
"enable": False,
|
||||
"provider": "moonshotai",
|
||||
@@ -202,6 +175,10 @@ DEFAULT_CONFIG = {
|
||||
"quality": 95,
|
||||
},
|
||||
},
|
||||
"agent_runner": {
|
||||
"runner_type": "local",
|
||||
"config": get_agent_runner_config_default("local"),
|
||||
},
|
||||
# SubAgent orchestrator mode:
|
||||
# - main_enable = False: disabled; main LLM mounts tools normally (persona selection).
|
||||
# - main_enable = True: enabled; main LLM keeps its own tools and includes handoff
|
||||
@@ -1680,71 +1657,6 @@ CONFIG_METADATA_2 = {
|
||||
"proxy": "",
|
||||
"custom_headers": {},
|
||||
},
|
||||
"Dify": {
|
||||
"id": "dify_app_default",
|
||||
"provider": "dify",
|
||||
"type": "dify",
|
||||
"provider_type": "agent_runner",
|
||||
"enable": True,
|
||||
"dify_api_type": "chat",
|
||||
"dify_api_key": "",
|
||||
"dify_api_base": "https://api.dify.ai/v1",
|
||||
"dify_workflow_output_key": "astrbot_wf_output",
|
||||
"dify_query_input_key": "astrbot_text_query",
|
||||
"variables": {},
|
||||
"timeout": 60,
|
||||
"proxy": "",
|
||||
},
|
||||
"Coze": {
|
||||
"id": "coze",
|
||||
"provider": "coze",
|
||||
"provider_type": "agent_runner",
|
||||
"type": "coze",
|
||||
"enable": True,
|
||||
"coze_api_key": "",
|
||||
"bot_id": "",
|
||||
"coze_api_base": "https://api.coze.cn",
|
||||
"timeout": 60,
|
||||
"proxy": "",
|
||||
# "auto_save_history": True,
|
||||
},
|
||||
"阿里云百炼应用": {
|
||||
"id": "dashscope",
|
||||
"provider": "dashscope",
|
||||
"type": "dashscope",
|
||||
"provider_type": "agent_runner",
|
||||
"enable": True,
|
||||
"dashscope_app_type": "agent",
|
||||
"dashscope_api_key": "",
|
||||
"dashscope_app_id": "",
|
||||
"rag_options": {
|
||||
"pipeline_ids": [],
|
||||
"file_ids": [],
|
||||
"output_reference": False,
|
||||
},
|
||||
"variables": {},
|
||||
"timeout": 60,
|
||||
"proxy": "",
|
||||
},
|
||||
"DeerFlow": {
|
||||
"id": "deerflow",
|
||||
"provider": "deerflow",
|
||||
"type": "deerflow",
|
||||
"provider_type": "agent_runner",
|
||||
"enable": True,
|
||||
"deerflow_api_base": "http://127.0.0.1:2026",
|
||||
"deerflow_api_key": "",
|
||||
"deerflow_auth_header": "",
|
||||
"deerflow_assistant_id": "lead_agent",
|
||||
"deerflow_model_name": "",
|
||||
"deerflow_thinking_enabled": False,
|
||||
"deerflow_plan_mode": False,
|
||||
"deerflow_subagent_enabled": False,
|
||||
"deerflow_max_concurrent_subagents": 3,
|
||||
"deerflow_recursion_limit": 1000,
|
||||
"timeout": 300,
|
||||
"proxy": "",
|
||||
},
|
||||
"FastGPT": {
|
||||
"id": "fastgpt",
|
||||
"provider": "fastgpt",
|
||||
@@ -3068,16 +2980,6 @@ CONFIG_METADATA_2 = {
|
||||
"enable": {
|
||||
"type": "bool",
|
||||
},
|
||||
"default_provider_id": {
|
||||
"type": "string",
|
||||
},
|
||||
"fallback_chat_models": {
|
||||
"type": "list",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"request_max_retries": {
|
||||
"type": "int",
|
||||
},
|
||||
"wake_prefix": {
|
||||
"type": "string",
|
||||
},
|
||||
@@ -3099,18 +3001,9 @@ CONFIG_METADATA_2 = {
|
||||
"datetime_system_prompt": {
|
||||
"type": "bool",
|
||||
},
|
||||
"default_personality": {
|
||||
"type": "string",
|
||||
},
|
||||
"prompt_prefix": {
|
||||
"type": "string",
|
||||
},
|
||||
"max_context_length": {
|
||||
"type": "int",
|
||||
},
|
||||
"dequeue_context_length": {
|
||||
"type": "int",
|
||||
},
|
||||
"streaming_response": {
|
||||
"type": "bool",
|
||||
},
|
||||
@@ -3126,30 +3019,6 @@ CONFIG_METADATA_2 = {
|
||||
"unsupported_streaming_strategy": {
|
||||
"type": "string",
|
||||
},
|
||||
"agent_runner_type": {
|
||||
"type": "string",
|
||||
},
|
||||
"dify_agent_runner_provider_id": {
|
||||
"type": "string",
|
||||
},
|
||||
"coze_agent_runner_provider_id": {
|
||||
"type": "string",
|
||||
},
|
||||
"dashscope_agent_runner_provider_id": {
|
||||
"type": "string",
|
||||
},
|
||||
"deerflow_agent_runner_provider_id": {
|
||||
"type": "string",
|
||||
},
|
||||
"max_agent_step": {
|
||||
"type": "int",
|
||||
},
|
||||
"tool_call_timeout": {
|
||||
"type": "int",
|
||||
},
|
||||
"tool_schema_mode": {
|
||||
"type": "string",
|
||||
},
|
||||
"file_extract": {
|
||||
"type": "object",
|
||||
"items": {
|
||||
@@ -3175,6 +3044,13 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"agent_runner": {
|
||||
"type": "object",
|
||||
"items": {
|
||||
"runner_type": {"type": "string"},
|
||||
"config": {"type": "dict"},
|
||||
},
|
||||
},
|
||||
"provider_stt_settings": {
|
||||
"type": "object",
|
||||
"items": {
|
||||
@@ -3365,7 +3241,7 @@ CONFIG_METADATA_3 = {
|
||||
"metadata": {
|
||||
"agent_runner": {
|
||||
"description": "Agent 执行方式",
|
||||
"hint": "选择 AI 对话的执行器,默认为 AstrBot 内置 Agent 执行器,可使用 AstrBot 内的知识库、人格、工具调用功能。如果不打算接入 Dify、Coze、DeerFlow 等第三方 Agent 执行器,不需要修改此节。",
|
||||
"hint": "选择 AI 对话的执行器。切换执行器会使用新类型的默认配置,不保留上一类型的参数。",
|
||||
"type": "object",
|
||||
"items": {
|
||||
"provider_settings.enable": {
|
||||
@@ -3373,7 +3249,7 @@ CONFIG_METADATA_3 = {
|
||||
"type": "bool",
|
||||
"hint": "AI 对话总开关",
|
||||
},
|
||||
"provider_settings.agent_runner_type": {
|
||||
"agent_runner.runner_type": {
|
||||
"description": "执行器",
|
||||
"type": "string",
|
||||
"options": ["local", "dify", "coze", "dashscope", "deerflow"],
|
||||
@@ -3384,70 +3260,238 @@ CONFIG_METADATA_3 = {
|
||||
"阿里云百炼应用",
|
||||
"DeerFlow",
|
||||
],
|
||||
"condition": {
|
||||
"provider_settings.enable": True,
|
||||
"_special": "agent_runner_type",
|
||||
"runner_defaults": {
|
||||
runner_type: get_agent_runner_config_default(runner_type)
|
||||
for runner_type in (
|
||||
"local",
|
||||
"dify",
|
||||
"coze",
|
||||
"dashscope",
|
||||
"deerflow",
|
||||
)
|
||||
},
|
||||
},
|
||||
"provider_settings.coze_agent_runner_provider_id": {
|
||||
"description": "Coze Agent 执行器提供商 ID",
|
||||
"type": "string",
|
||||
"_special": "select_agent_runner_provider:coze",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "coze",
|
||||
"provider_settings.enable": True,
|
||||
},
|
||||
},
|
||||
"provider_settings.dify_agent_runner_provider_id": {
|
||||
"description": "Dify Agent 执行器提供商 ID",
|
||||
"type": "string",
|
||||
"_special": "select_agent_runner_provider:dify",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "dify",
|
||||
"provider_settings.enable": True,
|
||||
},
|
||||
},
|
||||
"provider_settings.dashscope_agent_runner_provider_id": {
|
||||
"description": "阿里云百炼应用 Agent 执行器提供商 ID",
|
||||
"type": "string",
|
||||
"_special": "select_agent_runner_provider:dashscope",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "dashscope",
|
||||
"provider_settings.enable": True,
|
||||
},
|
||||
},
|
||||
"provider_settings.deerflow_agent_runner_provider_id": {
|
||||
"description": "DeerFlow Agent 执行器提供商 ID",
|
||||
"type": "string",
|
||||
"_special": "select_agent_runner_provider:deerflow",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "deerflow",
|
||||
"provider_settings.enable": True,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"dify_runner": {
|
||||
"description": "Dify 配置",
|
||||
"type": "object",
|
||||
"condition": {
|
||||
"provider_settings.enable": True,
|
||||
"agent_runner.runner_type": "dify",
|
||||
},
|
||||
"items": {
|
||||
"agent_runner.config.dify_api_type": {
|
||||
"description": "应用类型",
|
||||
"type": "string",
|
||||
"options": ["chat", "chatflow", "agent", "workflow"],
|
||||
},
|
||||
"agent_runner.config.dify_api_key": {
|
||||
"description": "API Key",
|
||||
"type": "string",
|
||||
},
|
||||
"agent_runner.config.dify_api_base": {
|
||||
"description": "API Base URL",
|
||||
"type": "string",
|
||||
},
|
||||
"agent_runner.config.dify_workflow_output_key": {
|
||||
"description": "Workflow 输出变量名",
|
||||
"type": "string",
|
||||
},
|
||||
"agent_runner.config.dify_query_input_key": {
|
||||
"description": "Prompt 输入变量名",
|
||||
"type": "string",
|
||||
},
|
||||
"agent_runner.config.variables": {
|
||||
"description": "变量",
|
||||
"type": "dict",
|
||||
},
|
||||
"agent_runner.config.timeout": {
|
||||
"description": "超时时间(秒)",
|
||||
"type": "int",
|
||||
},
|
||||
"agent_runner.config.proxy": {
|
||||
"description": "代理地址",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"coze_runner": {
|
||||
"description": "Coze 配置",
|
||||
"type": "object",
|
||||
"condition": {
|
||||
"provider_settings.enable": True,
|
||||
"agent_runner.runner_type": "coze",
|
||||
},
|
||||
"items": {
|
||||
"agent_runner.config.coze_api_key": {
|
||||
"description": "API Key",
|
||||
"type": "string",
|
||||
},
|
||||
"agent_runner.config.bot_id": {
|
||||
"description": "Bot ID",
|
||||
"type": "string",
|
||||
},
|
||||
"agent_runner.config.coze_api_base": {
|
||||
"description": "API Base URL",
|
||||
"type": "string",
|
||||
},
|
||||
"agent_runner.config.auto_save_history": {
|
||||
"description": "由 Coze 管理对话记录",
|
||||
"type": "bool",
|
||||
},
|
||||
"agent_runner.config.timeout": {
|
||||
"description": "超时时间(秒)",
|
||||
"type": "int",
|
||||
},
|
||||
"agent_runner.config.proxy": {
|
||||
"description": "代理地址",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"dashscope_runner": {
|
||||
"description": "阿里云百炼应用配置",
|
||||
"type": "object",
|
||||
"condition": {
|
||||
"provider_settings.enable": True,
|
||||
"agent_runner.runner_type": "dashscope",
|
||||
},
|
||||
"items": {
|
||||
"agent_runner.config.dashscope_app_type": {
|
||||
"description": "应用类型",
|
||||
"type": "string",
|
||||
"options": ["agent", "workflow"],
|
||||
},
|
||||
"agent_runner.config.dashscope_api_key": {
|
||||
"description": "API Key",
|
||||
"type": "string",
|
||||
},
|
||||
"agent_runner.config.dashscope_app_id": {
|
||||
"description": "应用 ID",
|
||||
"type": "string",
|
||||
},
|
||||
"agent_runner.config.rag_options.pipeline_ids": {
|
||||
"description": "知识库 Pipeline ID",
|
||||
"type": "list",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"agent_runner.config.rag_options.file_ids": {
|
||||
"description": "文件 ID",
|
||||
"type": "list",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"agent_runner.config.rag_options.output_reference": {
|
||||
"description": "输出引用",
|
||||
"type": "bool",
|
||||
},
|
||||
"agent_runner.config.variables": {
|
||||
"description": "变量",
|
||||
"type": "dict",
|
||||
},
|
||||
"agent_runner.config.timeout": {
|
||||
"description": "超时时间(秒)",
|
||||
"type": "int",
|
||||
},
|
||||
"agent_runner.config.proxy": {
|
||||
"description": "代理地址",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"deerflow_runner": {
|
||||
"description": "DeerFlow 配置",
|
||||
"type": "object",
|
||||
"condition": {
|
||||
"provider_settings.enable": True,
|
||||
"agent_runner.runner_type": "deerflow",
|
||||
},
|
||||
"items": {
|
||||
"agent_runner.config.deerflow_api_base": {
|
||||
"description": "API Base URL",
|
||||
"type": "string",
|
||||
},
|
||||
"agent_runner.config.deerflow_api_key": {
|
||||
"description": "API Key",
|
||||
"type": "string",
|
||||
},
|
||||
"agent_runner.config.deerflow_auth_header": {
|
||||
"description": "Authorization Header",
|
||||
"type": "string",
|
||||
},
|
||||
"agent_runner.config.deerflow_assistant_id": {
|
||||
"description": "Assistant ID",
|
||||
"type": "string",
|
||||
},
|
||||
"agent_runner.config.deerflow_model_name": {
|
||||
"description": "模型名称覆盖",
|
||||
"type": "string",
|
||||
},
|
||||
"agent_runner.config.deerflow_thinking_enabled": {
|
||||
"description": "启用思考模式",
|
||||
"type": "bool",
|
||||
},
|
||||
"agent_runner.config.deerflow_plan_mode": {
|
||||
"description": "启用计划模式",
|
||||
"type": "bool",
|
||||
},
|
||||
"agent_runner.config.deerflow_subagent_enabled": {
|
||||
"description": "启用子智能体",
|
||||
"type": "bool",
|
||||
},
|
||||
"agent_runner.config.deerflow_max_concurrent_subagents": {
|
||||
"description": "子智能体最大并发数",
|
||||
"type": "int",
|
||||
},
|
||||
"agent_runner.config.deerflow_recursion_limit": {
|
||||
"description": "递归深度上限",
|
||||
"type": "int",
|
||||
},
|
||||
"agent_runner.config.timeout": {
|
||||
"description": "超时时间(秒)",
|
||||
"type": "int",
|
||||
},
|
||||
"agent_runner.config.proxy": {
|
||||
"description": "代理地址",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"ai": {
|
||||
"description": "模型",
|
||||
"hint": "当使用非内置 Agent 执行器时,默认对话模型和默认图片转述模型可能会无效,但某些插件会依赖此配置项来调用 AI 能力。",
|
||||
"hint": "配置内置 Agent 使用的对话模型,以及通用的图片转述、语音模型。",
|
||||
"type": "object",
|
||||
"items": {
|
||||
"provider_settings.default_provider_id": {
|
||||
"agent_runner.config.model.provider_id": {
|
||||
"description": "默认对话模型",
|
||||
"type": "string",
|
||||
"_special": "select_provider",
|
||||
"hint": "留空时使用第一个模型",
|
||||
"condition": {
|
||||
"agent_runner.runner_type": "local",
|
||||
},
|
||||
},
|
||||
"provider_settings.fallback_chat_models": {
|
||||
"agent_runner.config.model.fallback_provider_ids": {
|
||||
"description": "回退对话模型列表",
|
||||
"type": "list",
|
||||
"items": {"type": "string"},
|
||||
"_special": "select_providers",
|
||||
"hint": "主聊天模型请求失败时,按顺序切换到这些模型。",
|
||||
"condition": {
|
||||
"agent_runner.runner_type": "local",
|
||||
},
|
||||
},
|
||||
"provider_settings.request_max_retries": {
|
||||
"agent_runner.config.model.request_max_retries": {
|
||||
"description": "请求最大重试次数",
|
||||
"type": "int",
|
||||
"hint": "单次模型请求遇到可重试错误时的最大尝试次数。",
|
||||
"condition": {
|
||||
"agent_runner.runner_type": "local",
|
||||
},
|
||||
},
|
||||
"provider_settings.default_image_caption_provider_id": {
|
||||
"description": "默认图片转述模型",
|
||||
@@ -3504,14 +3548,28 @@ CONFIG_METADATA_3 = {
|
||||
"hint": "",
|
||||
"type": "object",
|
||||
"items": {
|
||||
"provider_settings.default_personality": {
|
||||
"agent_runner.config.persona.persona_id": {
|
||||
"description": "默认采用的人格",
|
||||
"type": "string",
|
||||
"_special": "select_persona",
|
||||
},
|
||||
"agent_runner.config.persona.safety_mode": {
|
||||
"description": "健康模式",
|
||||
"type": "bool",
|
||||
"hint": "引导模型输出健康、安全的内容,避免有害或敏感话题。",
|
||||
},
|
||||
"agent_runner.config.persona.safety_mode_strategy": {
|
||||
"description": "健康模式策略",
|
||||
"type": "string",
|
||||
"options": ["system_prompt"],
|
||||
"hint": "选择健康模式的实现策略。",
|
||||
"condition": {
|
||||
"agent_runner.config.persona.safety_mode": True,
|
||||
},
|
||||
},
|
||||
},
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"agent_runner.runner_type": "local",
|
||||
"provider_settings.enable": True,
|
||||
},
|
||||
},
|
||||
@@ -3544,7 +3602,7 @@ CONFIG_METADATA_3 = {
|
||||
},
|
||||
},
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"agent_runner.runner_type": "local",
|
||||
"provider_settings.enable": True,
|
||||
},
|
||||
},
|
||||
@@ -3646,7 +3704,7 @@ CONFIG_METADATA_3 = {
|
||||
},
|
||||
},
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"agent_runner.runner_type": "local",
|
||||
"provider_settings.enable": True,
|
||||
},
|
||||
},
|
||||
@@ -3812,7 +3870,7 @@ CONFIG_METADATA_3 = {
|
||||
},
|
||||
},
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"agent_runner.runner_type": "local",
|
||||
"provider_settings.enable": True,
|
||||
},
|
||||
},
|
||||
@@ -3842,7 +3900,7 @@ CONFIG_METADATA_3 = {
|
||||
# },
|
||||
# },
|
||||
# "condition": {
|
||||
# "provider_settings.agent_runner_type": "local",
|
||||
# "agent_runner.runner_type": "local",
|
||||
# "provider_settings.enable": True,
|
||||
# },
|
||||
# },
|
||||
@@ -3858,7 +3916,7 @@ CONFIG_METADATA_3 = {
|
||||
},
|
||||
},
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"agent_runner.runner_type": "local",
|
||||
"provider_settings.enable": True,
|
||||
},
|
||||
},
|
||||
@@ -3867,72 +3925,72 @@ CONFIG_METADATA_3 = {
|
||||
"description": "上下文管理策略",
|
||||
"type": "object",
|
||||
"items": {
|
||||
"provider_settings.max_context_length": {
|
||||
"agent_runner.config.compression.max_turns": {
|
||||
"description": "压缩前最多保留对话轮数",
|
||||
"type": "int",
|
||||
"hint": "普通会话历史超过该轮数后,才会按下方策略进行持久化截断或 LLM 压缩;请求发送前也会先按该值约束上下文。-1 表示不按轮数限制。",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"agent_runner.runner_type": "local",
|
||||
},
|
||||
},
|
||||
"provider_settings.dequeue_context_length": {
|
||||
"agent_runner.config.compression.trim_turns": {
|
||||
"description": "轮次超限时一次丢弃轮数",
|
||||
"type": "int",
|
||||
"hint": "当超过“压缩前最多保留对话轮数”且无法使用 LLM 压缩时,一次丢弃多少轮旧对话;请求期截断也会复用该值。",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"agent_runner.runner_type": "local",
|
||||
},
|
||||
},
|
||||
"provider_settings.context_limit_reached_strategy": {
|
||||
"agent_runner.config.compression.overflow_strategy": {
|
||||
"description": "历史超限或上下文接近上限时的处理方式",
|
||||
"type": "string",
|
||||
"options": ["truncate_by_turns", "llm_compress"],
|
||||
"labels": ["按对话轮数截断", "由 LLM 压缩上下文"],
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"agent_runner.runner_type": "local",
|
||||
},
|
||||
"hint": "普通会话历史仅在超过“压缩前最多保留对话轮数”后执行该策略;请求发送前也会在上下文 token 接近模型窗口时使用同一策略保护本次请求。",
|
||||
},
|
||||
"provider_settings.llm_compress_instruction": {
|
||||
"agent_runner.config.compression.instruction": {
|
||||
"description": "上下文压缩提示词",
|
||||
"type": "text",
|
||||
"hint": "如果为空则使用默认提示词。",
|
||||
"condition": {
|
||||
"provider_settings.context_limit_reached_strategy": "llm_compress",
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"agent_runner.config.compression.overflow_strategy": "llm_compress",
|
||||
"agent_runner.runner_type": "local",
|
||||
},
|
||||
},
|
||||
"provider_settings.llm_compress_keep_recent_ratio": {
|
||||
"agent_runner.config.compression.keep_recent_ratio": {
|
||||
"description": "压缩时保留最近上下文比例",
|
||||
"type": "float",
|
||||
"slider": {"min": 0, "max": 0.3, "step": 0.01},
|
||||
"hint": "按当前上下文 token 数保留最近内容,范围 0-0.3。0.15 表示保留 15%;比例大于 0 时至少保留最后一轮。",
|
||||
"condition": {
|
||||
"provider_settings.context_limit_reached_strategy": "llm_compress",
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"agent_runner.config.compression.overflow_strategy": "llm_compress",
|
||||
"agent_runner.runner_type": "local",
|
||||
},
|
||||
},
|
||||
"provider_settings.llm_compress_provider_id": {
|
||||
"agent_runner.config.compression.provider_id": {
|
||||
"description": "用于上下文压缩的模型提供商 ID",
|
||||
"type": "string",
|
||||
"_special": "select_provider",
|
||||
"hint": "留空时使用当前聊天模型进行压缩;如果模型不可用或压缩失败,将回退为“按对话轮数截断”的策略。",
|
||||
"condition": {
|
||||
"provider_settings.context_limit_reached_strategy": "llm_compress",
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"agent_runner.config.compression.overflow_strategy": "llm_compress",
|
||||
"agent_runner.runner_type": "local",
|
||||
},
|
||||
},
|
||||
"provider_settings.fallback_max_context_tokens": {
|
||||
"agent_runner.config.compression.fallback_max_tokens": {
|
||||
"description": "上下文窗口兜底值",
|
||||
"type": "int",
|
||||
"hint": "当 max_context_tokens 为 0 且模型不在内置元数据中时,使用此值作为上下文窗口大小。默认 128000。",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"agent_runner.runner_type": "local",
|
||||
},
|
||||
},
|
||||
},
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"agent_runner.runner_type": "local",
|
||||
"provider_settings.enable": True,
|
||||
},
|
||||
},
|
||||
@@ -3944,7 +4002,7 @@ CONFIG_METADATA_3 = {
|
||||
"description": "显示思考内容",
|
||||
"type": "bool",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"agent_runner.runner_type": "local",
|
||||
},
|
||||
},
|
||||
"provider_settings.streaming_response": {
|
||||
@@ -3961,20 +4019,6 @@ CONFIG_METADATA_3 = {
|
||||
"provider_settings.streaming_response": True,
|
||||
},
|
||||
},
|
||||
"provider_settings.llm_safety_mode": {
|
||||
"description": "健康模式",
|
||||
"type": "bool",
|
||||
"hint": "引导模型输出健康、安全的内容,避免有害或敏感话题。",
|
||||
},
|
||||
"provider_settings.safety_mode_strategy": {
|
||||
"description": "健康模式策略",
|
||||
"type": "string",
|
||||
"options": ["system_prompt"],
|
||||
"hint": "选择健康模式的实现策略。",
|
||||
"condition": {
|
||||
"provider_settings.llm_safety_mode": True,
|
||||
},
|
||||
},
|
||||
"provider_settings.identifier": {
|
||||
"description": "用户识别",
|
||||
"type": "bool",
|
||||
@@ -3990,14 +4034,14 @@ CONFIG_METADATA_3 = {
|
||||
"type": "bool",
|
||||
"hint": "启用后,会在系统提示词中附带当前时间信息。",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"agent_runner.runner_type": "local",
|
||||
},
|
||||
},
|
||||
"provider_settings.show_tool_use_status": {
|
||||
"description": "输出函数调用状态",
|
||||
"type": "bool",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"agent_runner.runner_type": "local",
|
||||
},
|
||||
},
|
||||
"provider_settings.show_tool_call_result": {
|
||||
@@ -4005,7 +4049,7 @@ CONFIG_METADATA_3 = {
|
||||
"type": "bool",
|
||||
"hint": "仅在输出函数调用状态启用时生效,展示结果前 70 个字符。",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"agent_runner.runner_type": "local",
|
||||
"provider_settings.show_tool_use_status": True,
|
||||
},
|
||||
},
|
||||
@@ -4014,40 +4058,40 @@ CONFIG_METADATA_3 = {
|
||||
"type": "bool",
|
||||
"hint": "开启后,非流式模式下多步工具调用过程中产生的中间文本将缓冲,待 Agent 完成后合并为一条回复发送。",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"agent_runner.runner_type": "local",
|
||||
"provider_settings.streaming_response": False,
|
||||
},
|
||||
},
|
||||
"provider_settings.sanitize_context_by_modalities": {
|
||||
"agent_runner.config.misc.sanitize_context_by_modalities": {
|
||||
"description": "按模型能力清理历史上下文",
|
||||
"type": "bool",
|
||||
"hint": "开启后,在每次请求 LLM 前会按当前模型提供商中所选择的模型能力删除对话中不支持的图片/工具调用结构(会改变模型看到的历史)",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"agent_runner.runner_type": "local",
|
||||
},
|
||||
},
|
||||
"provider_settings.max_agent_step": {
|
||||
"agent_runner.config.misc.max_steps": {
|
||||
"description": "工具调用轮数上限",
|
||||
"type": "int",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"agent_runner.runner_type": "local",
|
||||
},
|
||||
},
|
||||
"provider_settings.tool_call_timeout": {
|
||||
"agent_runner.config.misc.tool_call_timeout": {
|
||||
"description": "工具调用超时时间(秒)",
|
||||
"type": "int",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"agent_runner.runner_type": "local",
|
||||
},
|
||||
},
|
||||
"provider_settings.tool_schema_mode": {
|
||||
"agent_runner.config.misc.tool_schema_mode": {
|
||||
"description": "工具调用模式",
|
||||
"type": "string",
|
||||
"options": ["skills_like", "full"],
|
||||
"labels": ["Skills-like(两阶段)", "Full(完整参数)"],
|
||||
"hint": "skills-like 先下发工具名称与描述,再下发参数;full 一次性下发完整参数。",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"agent_runner.runner_type": "local",
|
||||
},
|
||||
},
|
||||
"provider_settings.wake_prefix": {
|
||||
@@ -4100,7 +4144,7 @@ CONFIG_METADATA_3 = {
|
||||
"type": "int",
|
||||
"hint": "引用/转发消息回退解析图片时的最大注入数量,超出会截断。",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"agent_runner.runner_type": "local",
|
||||
},
|
||||
"collapsed": True,
|
||||
},
|
||||
@@ -4109,7 +4153,7 @@ CONFIG_METADATA_3 = {
|
||||
"type": "int",
|
||||
"hint": "解析 Reply 组件链时允许的最大递归深度。",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"agent_runner.runner_type": "local",
|
||||
},
|
||||
"collapsed": True,
|
||||
},
|
||||
@@ -4118,7 +4162,7 @@ CONFIG_METADATA_3 = {
|
||||
"type": "int",
|
||||
"hint": "解析合并转发节点时允许的最大递归深度。",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"agent_runner.runner_type": "local",
|
||||
},
|
||||
"collapsed": True,
|
||||
},
|
||||
@@ -4127,7 +4171,7 @@ CONFIG_METADATA_3 = {
|
||||
"type": "int",
|
||||
"hint": "递归拉取 get_forward_msg 的最大次数。",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"agent_runner.runner_type": "local",
|
||||
},
|
||||
"collapsed": True,
|
||||
},
|
||||
@@ -4136,7 +4180,7 @@ CONFIG_METADATA_3 = {
|
||||
"type": "bool",
|
||||
"hint": "开启后,get_msg/get_forward_msg 全部尝试失败时输出 warning 日志。",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"agent_runner.runner_type": "local",
|
||||
},
|
||||
"collapsed": True,
|
||||
},
|
||||
|
||||
@@ -126,8 +126,7 @@ class AstrBotCoreLifecycle:
|
||||
if len(providers) == 0:
|
||||
return
|
||||
|
||||
provider_settings = getattr(pm, "provider_settings", None) or {}
|
||||
default_id = provider_settings.get("default_provider_id")
|
||||
default_id = getattr(pm, "default_chat_provider_id", "")
|
||||
fallback = pm.curr_provider_inst or providers[0]
|
||||
fallback_id = fallback.provider_config.get("id") or "unknown"
|
||||
|
||||
@@ -136,7 +135,7 @@ class AstrBotCoreLifecycle:
|
||||
return
|
||||
self._default_chat_provider_warning_emitted = True
|
||||
logger.warning(
|
||||
"Detected %d enabled chat providers but `provider_settings.default_provider_id` is empty. "
|
||||
"Detected %d enabled chat providers but `agent_runner.config.model.provider_id` is empty. "
|
||||
"AstrBot will use `%s` as the startup fallback chat provider. "
|
||||
"Set a default chat model in the WebUI configuration page to avoid unexpected provider switching.",
|
||||
len(providers),
|
||||
@@ -148,7 +147,7 @@ class AstrBotCoreLifecycle:
|
||||
if not found:
|
||||
self._default_chat_provider_warning_emitted = True
|
||||
logger.warning(
|
||||
"Configured `default_provider_id` is `%s` but no enabled provider matches that ID. "
|
||||
"Configured Agent Runner model provider ID `%s` does not match an enabled provider. "
|
||||
"AstrBot will use `%s` as the fallback chat provider. "
|
||||
"Please check the WebUI configuration page.",
|
||||
default_id,
|
||||
|
||||
@@ -442,12 +442,20 @@ class CronJobManager:
|
||||
cron_event.role = "admin"
|
||||
|
||||
provider_settings = cfg.get("provider_settings", {}) or {}
|
||||
tool_call_timeout = provider_settings.get("tool_call_timeout", 120)
|
||||
tool_call_timeout = (
|
||||
cfg.get("agent_runner", {})
|
||||
.get("config", {})
|
||||
.get("misc", {})
|
||||
.get("tool_call_timeout", 120)
|
||||
)
|
||||
agent_max_step = coerce_int_config(
|
||||
provider_settings.get("max_agent_step", 30),
|
||||
cfg.get("agent_runner", {})
|
||||
.get("config", {})
|
||||
.get("misc", {})
|
||||
.get("max_steps", 30),
|
||||
default=30,
|
||||
min_value=1,
|
||||
field_name="provider_settings.max_agent_step",
|
||||
field_name="agent_runner.config.misc.max_steps",
|
||||
)
|
||||
config = MainAgentBuildConfig(
|
||||
tool_call_timeout=tool_call_timeout,
|
||||
|
||||
@@ -23,8 +23,13 @@ class PersonaManager:
|
||||
def __init__(self, db_helper: BaseDatabase, acm: AstrBotConfigManager) -> None:
|
||||
self.db = db_helper
|
||||
self.acm = acm
|
||||
default_ps = acm.default_conf.get("provider_settings", {})
|
||||
self.default_persona: str = default_ps.get("default_personality", "default")
|
||||
default_runner = acm.default_conf.get("agent_runner", {})
|
||||
default_runner_config = default_runner.get("config", {})
|
||||
self.default_persona: str = (
|
||||
default_runner_config.get("persona", {}).get("persona_id", "default")
|
||||
if default_runner.get("runner_type") == "local"
|
||||
else default_runner_config.get("persona_id", "default")
|
||||
)
|
||||
self.personas: list[Persona] = []
|
||||
self.selected_default_persona: Persona | None = None
|
||||
|
||||
@@ -66,9 +71,12 @@ class PersonaManager:
|
||||
) -> Personality:
|
||||
"""获取默认 persona"""
|
||||
cfg = self.acm.get_conf(umo)
|
||||
default_persona_id = cfg.get("provider_settings", {}).get(
|
||||
"default_personality",
|
||||
"default",
|
||||
agent_runner = cfg.get("agent_runner", {})
|
||||
runner_config = agent_runner.get("config", {})
|
||||
default_persona_id = (
|
||||
runner_config.get("persona", {}).get("persona_id", "default")
|
||||
if agent_runner.get("runner_type") == "local"
|
||||
else runner_config.get("persona_id", "default")
|
||||
)
|
||||
return self.get_persona_v3_by_id(default_persona_id) or DEFAULT_PERSONALITY
|
||||
|
||||
@@ -107,7 +115,14 @@ class PersonaManager:
|
||||
if persona_id == "[%None]":
|
||||
pass
|
||||
elif persona_id is None:
|
||||
persona_id = (provider_settings or {}).get("default_personality")
|
||||
cfg = self.acm.get_conf(umo)
|
||||
agent_runner = cfg.get("agent_runner", {})
|
||||
runner_config = agent_runner.get("config", {})
|
||||
persona_id = (
|
||||
runner_config.get("persona", {}).get("persona_id", "default")
|
||||
if agent_runner.get("runner_type") == "local"
|
||||
else runner_config.get("persona_id", "default")
|
||||
)
|
||||
|
||||
persona = next(
|
||||
(item for item in self.personas_v3 if item["name"] == persona_id),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.config.agent_runner import normalize_agent_runner
|
||||
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||
from astrbot.core.star.session_llm_manager import SessionServiceManager
|
||||
|
||||
@@ -26,7 +27,9 @@ class AgentRequestSubStage(Stage):
|
||||
)
|
||||
self.prov_wake_prefix = self.prov_wake_prefix[len(bwp) :]
|
||||
|
||||
agent_runner_type = self.config["provider_settings"]["agent_runner_type"]
|
||||
agent_runner = normalize_agent_runner(self.config.get("agent_runner"))
|
||||
self.config["agent_runner"] = agent_runner
|
||||
agent_runner_type = agent_runner["runner_type"]
|
||||
if agent_runner_type == "local":
|
||||
self.agent_sub_stage = InternalAgentSubStage()
|
||||
else:
|
||||
|
||||
@@ -55,13 +55,18 @@ class InternalAgentSubStage(Stage):
|
||||
self.ctx = ctx
|
||||
conf = ctx.astrbot_config
|
||||
settings = conf["provider_settings"]
|
||||
runner_config = conf["agent_runner"]["config"]
|
||||
model_config = runner_config["model"]
|
||||
persona_config = runner_config["persona"]
|
||||
compression_config = runner_config["compression"]
|
||||
misc_config = runner_config["misc"]
|
||||
self.streaming_response: bool = settings["streaming_response"]
|
||||
self.unsupported_streaming_strategy: str = settings[
|
||||
"unsupported_streaming_strategy"
|
||||
]
|
||||
self.max_step: int = settings.get("max_agent_step", 30)
|
||||
self.tool_call_timeout: int = settings.get("tool_call_timeout", 60)
|
||||
self.tool_schema_mode: str = settings.get("tool_schema_mode", "full")
|
||||
self.max_step: int = misc_config.get("max_steps", 30)
|
||||
self.tool_call_timeout: int = misc_config.get("tool_call_timeout", 120)
|
||||
self.tool_schema_mode: str = misc_config.get("tool_schema_mode", "full")
|
||||
if self.tool_schema_mode not in ("skills_like", "full"):
|
||||
logger.warning(
|
||||
"Unsupported tool_schema_mode: %s, fallback to skills_like",
|
||||
@@ -77,7 +82,7 @@ class InternalAgentSubStage(Stage):
|
||||
False,
|
||||
)
|
||||
self.show_reasoning = settings.get("display_reasoning_text", False)
|
||||
self.sanitize_context_by_modalities: bool = settings.get(
|
||||
self.sanitize_context_by_modalities: bool = misc_config.get(
|
||||
"sanitize_context_by_modalities",
|
||||
False,
|
||||
)
|
||||
@@ -91,31 +96,29 @@ class InternalAgentSubStage(Stage):
|
||||
)
|
||||
|
||||
# 上下文管理相关
|
||||
self.context_limit_reached_strategy: str = settings.get(
|
||||
"context_limit_reached_strategy", "truncate_by_turns"
|
||||
self.context_limit_reached_strategy: str = compression_config.get(
|
||||
"overflow_strategy", "truncate_by_turns"
|
||||
)
|
||||
self.llm_compress_instruction: str = settings.get(
|
||||
"llm_compress_instruction", ""
|
||||
self.llm_compress_instruction: str = compression_config.get("instruction", "")
|
||||
self.llm_compress_keep_recent_ratio: float = compression_config.get(
|
||||
"keep_recent_ratio", 0.15
|
||||
)
|
||||
self.llm_compress_keep_recent_ratio: float = settings.get(
|
||||
"llm_compress_keep_recent_ratio", 0.15
|
||||
)
|
||||
self.llm_compress_provider_id: str = settings.get(
|
||||
"llm_compress_provider_id", ""
|
||||
)
|
||||
self.max_context_length = settings["max_context_length"] # int
|
||||
self.llm_compress_provider_id: str = compression_config.get("provider_id", "")
|
||||
self.max_context_length = compression_config.get("max_turns", -1)
|
||||
self.dequeue_context_length: int = min(
|
||||
max(1, settings["dequeue_context_length"]),
|
||||
self.max_context_length - 1,
|
||||
max(1, compression_config.get("trim_turns", 1)),
|
||||
self.max_context_length - 1
|
||||
if self.max_context_length > 0
|
||||
else compression_config.get("trim_turns", 1),
|
||||
)
|
||||
if self.dequeue_context_length <= 0:
|
||||
self.dequeue_context_length = 1
|
||||
self.fallback_max_context_tokens: int = settings.get(
|
||||
"fallback_max_context_tokens", 128000
|
||||
self.fallback_max_context_tokens: int = compression_config.get(
|
||||
"fallback_max_tokens", 128000
|
||||
)
|
||||
|
||||
self.llm_safety_mode = settings.get("llm_safety_mode", True)
|
||||
self.safety_mode_strategy = settings.get(
|
||||
self.llm_safety_mode = persona_config.get("safety_mode", True)
|
||||
self.safety_mode_strategy = persona_config.get(
|
||||
"safety_mode_strategy", "system_prompt"
|
||||
)
|
||||
|
||||
@@ -148,7 +151,12 @@ class InternalAgentSubStage(Stage):
|
||||
computer_use_runtime=self.computer_use_runtime,
|
||||
sandbox_cfg=self.sandbox_cfg,
|
||||
add_cron_tools=self.add_cron_tools,
|
||||
provider_settings=settings,
|
||||
provider_settings={
|
||||
**settings,
|
||||
"default_personality": persona_config.get("persona_id", "default"),
|
||||
},
|
||||
fallback_provider_ids=model_config.get("fallback_provider_ids", []),
|
||||
request_max_retries=model_config.get("request_max_retries", 5),
|
||||
subagent_orchestrator=conf.get("subagent_orchestrator", {}),
|
||||
timezone=self.ctx.plugin_manager.context.get_config().get("timezone"),
|
||||
max_quoted_fallback_images=settings.get("max_quoted_fallback_images", 20),
|
||||
|
||||
@@ -3,15 +3,12 @@ import inspect
|
||||
from collections.abc import AsyncGenerator, Awaitable, Callable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from astrbot.core import astrbot_config, logger
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.agent.runners.coze.coze_agent_runner import CozeAgentRunner
|
||||
from astrbot.core.agent.runners.dashscope.dashscope_agent_runner import (
|
||||
DashscopeAgentRunner,
|
||||
)
|
||||
from astrbot.core.agent.runners.deerflow.constants import (
|
||||
DEERFLOW_AGENT_RUNNER_PROVIDER_ID_KEY,
|
||||
DEERFLOW_PROVIDER_TYPE,
|
||||
)
|
||||
from astrbot.core.agent.runners.deerflow.constants import DEERFLOW_PROVIDER_TYPE
|
||||
from astrbot.core.agent.runners.deerflow.deerflow_agent_runner import (
|
||||
DeerFlowAgentRunner,
|
||||
)
|
||||
@@ -44,12 +41,6 @@ from astrbot.core.utils.metrics import Metric
|
||||
from .....astr_agent_context import AgentContextWrapper, AstrAgentContext
|
||||
from ....context import PipelineContext, call_event_hook
|
||||
|
||||
AGENT_RUNNER_TYPE_KEY = {
|
||||
"dify": "dify_agent_runner_provider_id",
|
||||
"coze": "coze_agent_runner_provider_id",
|
||||
"dashscope": "dashscope_agent_runner_provider_id",
|
||||
DEERFLOW_PROVIDER_TYPE: DEERFLOW_AGENT_RUNNER_PROVIDER_ID_KEY,
|
||||
}
|
||||
THIRD_PARTY_RUNNER_ERROR_EXTRA_KEY = "_third_party_runner_error"
|
||||
STREAM_CONSUMPTION_CLOSE_TIMEOUT_SEC = 30
|
||||
RUNNER_NO_RESULT_FALLBACK_MESSAGE = "Agent Runner did not return any result."
|
||||
@@ -165,11 +156,9 @@ class ThirdPartyAgentSubStage(Stage):
|
||||
async def initialize(self, ctx: PipelineContext) -> None:
|
||||
self.ctx = ctx
|
||||
self.conf = ctx.astrbot_config
|
||||
self.runner_type = self.conf["provider_settings"]["agent_runner_type"]
|
||||
self.prov_id = self.conf["provider_settings"].get(
|
||||
AGENT_RUNNER_TYPE_KEY.get(self.runner_type, ""),
|
||||
"",
|
||||
)
|
||||
agent_runner = self.conf["agent_runner"]
|
||||
self.runner_type = agent_runner["runner_type"]
|
||||
self.runner_config = agent_runner["config"]
|
||||
settings = ctx.astrbot_config["provider_settings"]
|
||||
self.streaming_response: bool = settings["streaming_response"]
|
||||
self.unsupported_streaming_strategy: str = settings[
|
||||
@@ -197,7 +186,7 @@ class ThirdPartyAgentSubStage(Stage):
|
||||
return await resolve_persona_custom_error_message(
|
||||
event=event,
|
||||
persona_manager=self.ctx.plugin_manager.context.persona_manager,
|
||||
provider_settings=self.conf["provider_settings"],
|
||||
provider_settings={"default_personality": "default"},
|
||||
conversation_persona_id=conversation_persona_id,
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -296,23 +285,6 @@ class ThirdPartyAgentSubStage(Stage):
|
||||
):
|
||||
return
|
||||
|
||||
self.prov_cfg: dict = next(
|
||||
(p for p in astrbot_config["provider"] if p["id"] == self.prov_id),
|
||||
{},
|
||||
)
|
||||
if not self.prov_id:
|
||||
logger.error(
|
||||
"No Agent Runner provider ID is configured. Configure one on the "
|
||||
"settings page."
|
||||
)
|
||||
return
|
||||
if not self.prov_cfg:
|
||||
logger.error(
|
||||
f"Configuration for Agent Runner provider {self.prov_id} does not "
|
||||
"exist. Update it on the settings page."
|
||||
)
|
||||
return
|
||||
|
||||
# make provider request
|
||||
req = ProviderRequest()
|
||||
req.session_id = event.unified_msg_origin
|
||||
@@ -388,7 +360,7 @@ class ThirdPartyAgentSubStage(Stage):
|
||||
tool_call_timeout=120,
|
||||
),
|
||||
agent_hooks=MAIN_AGENT_HOOKS,
|
||||
provider_config=self.prov_cfg,
|
||||
provider_config=self.runner_config,
|
||||
streaming=streaming_response,
|
||||
)
|
||||
|
||||
|
||||
@@ -45,6 +45,13 @@ class ProviderManager:
|
||||
self.providers_config: list = config["provider"]
|
||||
self.provider_sources_config: list = config.get("provider_sources", [])
|
||||
self.provider_settings: dict = config["provider_settings"]
|
||||
agent_runner = config.get("agent_runner", {})
|
||||
agent_runner_config = agent_runner.get("config", {})
|
||||
self.default_chat_provider_id = (
|
||||
agent_runner_config.get("model", {}).get("provider_id", "")
|
||||
if agent_runner.get("runner_type") == "local"
|
||||
else ""
|
||||
)
|
||||
self.provider_stt_settings: dict = config.get("provider_stt_settings", {})
|
||||
self.provider_tts_settings: dict = config.get("provider_tts_settings", {})
|
||||
|
||||
@@ -240,7 +247,12 @@ class ProviderManager:
|
||||
# default setting
|
||||
config = self.acm.get_conf(umo)
|
||||
if provider_type == ProviderType.CHAT_COMPLETION:
|
||||
provider_id = config["provider_settings"].get("default_provider_id")
|
||||
agent_runner = config.get("agent_runner", {})
|
||||
provider_id = (
|
||||
agent_runner.get("config", {}).get("model", {}).get("provider_id")
|
||||
if agent_runner.get("runner_type") == "local"
|
||||
else None
|
||||
)
|
||||
provider = self.inst_map.get(provider_id)
|
||||
if not provider:
|
||||
provider = self.provider_insts[0] if self.provider_insts else None
|
||||
@@ -337,7 +349,7 @@ class ProviderManager:
|
||||
|
||||
selected_provider_id = await sp.get_async(
|
||||
key="curr_provider",
|
||||
default=self.provider_settings.get("default_provider_id"),
|
||||
default=self.default_chat_provider_id,
|
||||
scope="global",
|
||||
scope_id="global",
|
||||
)
|
||||
@@ -760,10 +772,7 @@ class ProviderManager:
|
||||
await inst.initialize()
|
||||
|
||||
self.provider_insts.append(inst)
|
||||
if (
|
||||
self.provider_settings.get("default_provider_id")
|
||||
== provider_config["id"]
|
||||
):
|
||||
if self.default_chat_provider_id == provider_config["id"]:
|
||||
self.curr_provider_inst = inst
|
||||
logger.info(
|
||||
f"Selected {provider_config['type']}({provider_config['id']}) as default chat model provider",
|
||||
|
||||
+398
-106
@@ -1,60 +1,375 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from astrbot.core import astrbot_config, logger
|
||||
from astrbot.core.agent.runners.deerflow.constants import (
|
||||
DEERFLOW_AGENT_RUNNER_PROVIDER_ID_KEY,
|
||||
DEERFLOW_PROVIDER_TYPE,
|
||||
from astrbot.core.config.agent_runner import (
|
||||
AGENT_RUNNER_TYPES,
|
||||
THIRD_PARTY_AGENT_RUNNER_TYPES,
|
||||
get_agent_runner_config_default,
|
||||
normalize_agent_runner,
|
||||
)
|
||||
from astrbot.core.astrbot_config_mgr import AstrBotConfig, AstrBotConfigManager
|
||||
from astrbot.core.db.migration.migra_45_to_46 import migrate_45_to_46
|
||||
from astrbot.core.db.migration.migra_token_usage import migrate_token_usage
|
||||
from astrbot.core.db.migration.migra_webchat_session import migrate_webchat_session
|
||||
from astrbot.core.utils.astrbot_path import (
|
||||
get_astrbot_config_path,
|
||||
get_astrbot_data_path,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("astrbot")
|
||||
|
||||
_LEGACY_AGENT_RUNNER_PROVIDER_ID_KEYS = {
|
||||
"dify": "dify_agent_runner_provider_id",
|
||||
"coze": "coze_agent_runner_provider_id",
|
||||
"dashscope": "dashscope_agent_runner_provider_id",
|
||||
"deerflow": "deerflow_agent_runner_provider_id",
|
||||
}
|
||||
_LEGACY_AGENT_RUNNER_SETTING_KEYS = (
|
||||
"agent_runner_type",
|
||||
*_LEGACY_AGENT_RUNNER_PROVIDER_ID_KEYS.values(),
|
||||
"default_provider_id",
|
||||
"fallback_chat_models",
|
||||
"request_max_retries",
|
||||
"default_personality",
|
||||
"llm_safety_mode",
|
||||
"safety_mode_strategy",
|
||||
"max_agent_step",
|
||||
"tool_schema_mode",
|
||||
"tool_call_timeout",
|
||||
"sanitize_context_by_modalities",
|
||||
"context_limit_reached_strategy",
|
||||
"llm_compress_instruction",
|
||||
"llm_compress_keep_recent_ratio",
|
||||
"llm_compress_provider_id",
|
||||
"max_context_length",
|
||||
"dequeue_context_length",
|
||||
"fallback_max_context_tokens",
|
||||
)
|
||||
_LEGACY_PROVIDER_IDENTITY_FIELDS = {
|
||||
"id",
|
||||
"type",
|
||||
"provider",
|
||||
"provider_type",
|
||||
"enable",
|
||||
"provider_source_id",
|
||||
"model_config",
|
||||
}
|
||||
|
||||
|
||||
def _migra_agent_runner_configs(conf: AstrBotConfig, ids_map: dict) -> None:
|
||||
def _get_effective_provider_map(config: object) -> dict[str, dict[str, Any]]:
|
||||
"""Build providers with their Provider Source fields merged in.
|
||||
|
||||
Args:
|
||||
config: Configuration containing provider and provider_sources lists.
|
||||
|
||||
Returns:
|
||||
Effective providers indexed by provider ID.
|
||||
"""
|
||||
Migra agent runner configs from provider configs.
|
||||
"""
|
||||
try:
|
||||
default_prov_id = conf["provider_settings"]["default_provider_id"]
|
||||
if default_prov_id in ids_map:
|
||||
conf["provider_settings"]["default_provider_id"] = ""
|
||||
p = ids_map[default_prov_id]
|
||||
if p["type"] == "dify":
|
||||
conf["provider_settings"]["dify_agent_runner_provider_id"] = p["id"]
|
||||
conf["provider_settings"]["agent_runner_type"] = "dify"
|
||||
elif p["type"] == "coze":
|
||||
conf["provider_settings"]["coze_agent_runner_provider_id"] = p["id"]
|
||||
conf["provider_settings"]["agent_runner_type"] = "coze"
|
||||
elif p["type"] == "dashscope":
|
||||
conf["provider_settings"]["dashscope_agent_runner_provider_id"] = p[
|
||||
"id"
|
||||
]
|
||||
conf["provider_settings"]["agent_runner_type"] = "dashscope"
|
||||
elif p["type"] == DEERFLOW_PROVIDER_TYPE:
|
||||
conf["provider_settings"][DEERFLOW_AGENT_RUNNER_PROVIDER_ID_KEY] = p[
|
||||
"id"
|
||||
]
|
||||
conf["provider_settings"]["agent_runner_type"] = DEERFLOW_PROVIDER_TYPE
|
||||
conf.save_config()
|
||||
except Exception as e:
|
||||
logger.error(f"Migration for third party agent runner configs failed: {e!s}")
|
||||
logger.error(traceback.format_exc())
|
||||
if not isinstance(config, dict):
|
||||
return {}
|
||||
provider_sources = config.get("provider_sources", [])
|
||||
source_map = {
|
||||
source.get("id"): source
|
||||
for source in provider_sources
|
||||
if isinstance(source, dict) and source.get("id")
|
||||
}
|
||||
provider_map: dict[str, dict[str, Any]] = {}
|
||||
for provider in config.get("provider", []):
|
||||
if not isinstance(provider, dict) or not provider.get("id"):
|
||||
continue
|
||||
effective_provider = copy.deepcopy(
|
||||
source_map.get(provider.get("provider_source_id"), {})
|
||||
)
|
||||
effective_provider.update(copy.deepcopy(provider))
|
||||
provider_map[provider["id"]] = effective_provider
|
||||
return provider_map
|
||||
|
||||
|
||||
def _migra_provider_to_source_structure(conf: AstrBotConfig) -> None:
|
||||
def _get_provider_runner_type(provider: object) -> str | None:
|
||||
"""Return the third-party runner type represented by a provider.
|
||||
|
||||
Args:
|
||||
provider: Effective provider configuration.
|
||||
|
||||
Returns:
|
||||
Runner type when the provider is a known Agent Runner, otherwise None.
|
||||
"""
|
||||
Migrate old provider structure to new provider-source separation.
|
||||
Provider only keeps: id, provider_source_id, model, modalities, custom_extra_body
|
||||
All other fields move to provider_sources.
|
||||
if not isinstance(provider, dict):
|
||||
return None
|
||||
provider_type = provider.get("provider_type")
|
||||
runner_type = provider.get("type") or provider.get("provider")
|
||||
if (
|
||||
provider_type == "agent_runner"
|
||||
and runner_type in THIRD_PARTY_AGENT_RUNNER_TYPES
|
||||
):
|
||||
return runner_type
|
||||
expected_field = {
|
||||
"dify": "dify_api_key",
|
||||
"coze": "coze_api_key",
|
||||
"dashscope": "dashscope_app_id",
|
||||
"deerflow": "deerflow_api_base",
|
||||
}
|
||||
if (
|
||||
runner_type in THIRD_PARTY_AGENT_RUNNER_TYPES
|
||||
and expected_field[runner_type] in provider
|
||||
):
|
||||
return runner_type
|
||||
return None
|
||||
|
||||
|
||||
def _copy_provider_config(
|
||||
runner_type: str,
|
||||
provider: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""Copy an effective legacy provider into an inline runner configuration.
|
||||
|
||||
Args:
|
||||
runner_type: Destination Agent Runner type.
|
||||
provider: Effective provider configuration.
|
||||
|
||||
Returns:
|
||||
Normalized inline runner configuration.
|
||||
"""
|
||||
runner_config = {
|
||||
key: copy.deepcopy(value)
|
||||
for key, value in provider.items()
|
||||
if key not in _LEGACY_PROVIDER_IDENTITY_FIELDS
|
||||
}
|
||||
return normalize_agent_runner(
|
||||
{"runner_type": runner_type, "config": runner_config}
|
||||
)["config"]
|
||||
|
||||
|
||||
def _migrate_agent_runner_config(
|
||||
config: dict[str, Any],
|
||||
fallback_config: dict[str, Any] | None = None,
|
||||
) -> bool:
|
||||
"""Migrate legacy Agent Runner fields in one core configuration.
|
||||
|
||||
Args:
|
||||
config: Mutable AstrBot configuration loaded from disk.
|
||||
fallback_config: Default configuration used to resolve shared providers.
|
||||
|
||||
Returns:
|
||||
Whether the configuration changed.
|
||||
"""
|
||||
changed = False
|
||||
provider_settings = config.get("provider_settings")
|
||||
if not isinstance(provider_settings, dict):
|
||||
provider_settings = {}
|
||||
config["provider_settings"] = provider_settings
|
||||
changed = True
|
||||
|
||||
existing_agent_runner = config.get("agent_runner")
|
||||
config_version = config.get("config_version")
|
||||
legacy_version = not isinstance(config_version, int) or config_version < 3
|
||||
default_local_agent_runner = {
|
||||
"runner_type": "local",
|
||||
"config": get_agent_runner_config_default("local"),
|
||||
}
|
||||
default_root_inserted_before_migration = (
|
||||
legacy_version
|
||||
and existing_agent_runner == default_local_agent_runner
|
||||
and any(key in provider_settings for key in _LEGACY_AGENT_RUNNER_SETTING_KEYS)
|
||||
)
|
||||
|
||||
if isinstance(existing_agent_runner, dict) and not (
|
||||
default_root_inserted_before_migration
|
||||
):
|
||||
for key in _LEGACY_AGENT_RUNNER_SETTING_KEYS:
|
||||
if key in provider_settings:
|
||||
provider_settings.pop(key)
|
||||
changed = True
|
||||
else:
|
||||
provider_map = _get_effective_provider_map(fallback_config)
|
||||
provider_map.update(_get_effective_provider_map(config))
|
||||
|
||||
runner_type = provider_settings.get("agent_runner_type", "local")
|
||||
if runner_type not in AGENT_RUNNER_TYPES:
|
||||
runner_type = "local"
|
||||
default_provider_id = provider_settings.get("default_provider_id", "")
|
||||
if not isinstance(default_provider_id, str):
|
||||
default_provider_id = ""
|
||||
default_provider = provider_map.get(default_provider_id)
|
||||
default_provider_runner_type = _get_provider_runner_type(default_provider)
|
||||
if runner_type == "local" and default_provider_runner_type:
|
||||
runner_type = default_provider_runner_type
|
||||
|
||||
if runner_type == "local":
|
||||
persona_id = provider_settings.get("default_personality", "default")
|
||||
if not isinstance(persona_id, str) or not persona_id:
|
||||
persona_id = "default"
|
||||
runner_config = get_agent_runner_config_default("local")
|
||||
runner_config["model"] = {
|
||||
"provider_id": default_provider_id,
|
||||
"fallback_provider_ids": copy.deepcopy(
|
||||
provider_settings.get("fallback_chat_models", [])
|
||||
),
|
||||
"request_max_retries": provider_settings.get("request_max_retries", 5),
|
||||
}
|
||||
runner_config["persona"] = {
|
||||
"persona_id": persona_id,
|
||||
"safety_mode": provider_settings.get("llm_safety_mode", True),
|
||||
"safety_mode_strategy": provider_settings.get(
|
||||
"safety_mode_strategy", "system_prompt"
|
||||
),
|
||||
}
|
||||
runner_config["compression"] = {
|
||||
"max_turns": provider_settings.get("max_context_length", -1),
|
||||
"trim_turns": provider_settings.get("dequeue_context_length", 1),
|
||||
"overflow_strategy": provider_settings.get(
|
||||
"context_limit_reached_strategy", "llm_compress"
|
||||
),
|
||||
"instruction": provider_settings.get("llm_compress_instruction", ""),
|
||||
"keep_recent_ratio": provider_settings.get(
|
||||
"llm_compress_keep_recent_ratio", 0.15
|
||||
),
|
||||
"provider_id": provider_settings.get("llm_compress_provider_id", ""),
|
||||
"fallback_max_tokens": provider_settings.get(
|
||||
"fallback_max_context_tokens", 128000
|
||||
),
|
||||
}
|
||||
runner_config["misc"] = {
|
||||
"max_steps": provider_settings.get("max_agent_step", 30),
|
||||
"tool_schema_mode": provider_settings.get("tool_schema_mode", "full"),
|
||||
"tool_call_timeout": provider_settings.get("tool_call_timeout", 120),
|
||||
"sanitize_context_by_modalities": provider_settings.get(
|
||||
"sanitize_context_by_modalities", False
|
||||
),
|
||||
}
|
||||
runner_config = normalize_agent_runner(
|
||||
{"runner_type": "local", "config": runner_config}
|
||||
)["config"]
|
||||
available_model_provider_ids = {
|
||||
provider_id
|
||||
for provider_id, provider in provider_map.items()
|
||||
if provider.get("provider_type") != "agent_runner"
|
||||
and _get_provider_runner_type(provider) is None
|
||||
}
|
||||
if (
|
||||
runner_config["model"]["provider_id"]
|
||||
not in available_model_provider_ids
|
||||
):
|
||||
runner_config["model"]["provider_id"] = ""
|
||||
runner_config["model"]["fallback_provider_ids"] = [
|
||||
provider_id
|
||||
for provider_id in runner_config["model"]["fallback_provider_ids"]
|
||||
if provider_id in available_model_provider_ids
|
||||
]
|
||||
if (
|
||||
runner_config["compression"]["provider_id"]
|
||||
not in available_model_provider_ids
|
||||
):
|
||||
runner_config["compression"]["provider_id"] = ""
|
||||
else:
|
||||
provider_id = provider_settings.get(
|
||||
_LEGACY_AGENT_RUNNER_PROVIDER_ID_KEYS[runner_type], ""
|
||||
)
|
||||
if not provider_id and default_provider_runner_type == runner_type:
|
||||
provider_id = default_provider_id
|
||||
provider = provider_map.get(provider_id)
|
||||
if provider and _get_provider_runner_type(provider) == runner_type:
|
||||
runner_config = _copy_provider_config(runner_type, provider)
|
||||
else:
|
||||
runner_config = get_agent_runner_config_default(runner_type)
|
||||
|
||||
config["agent_runner"] = {
|
||||
"runner_type": runner_type,
|
||||
"config": runner_config,
|
||||
}
|
||||
for key in _LEGACY_AGENT_RUNNER_SETTING_KEYS:
|
||||
provider_settings.pop(key, None)
|
||||
changed = True
|
||||
|
||||
if config.get("config_version") != 3:
|
||||
config["config_version"] = 3
|
||||
changed = True
|
||||
return changed
|
||||
|
||||
|
||||
def migrate_config_on_load(config: dict[str, Any], config_path: Path) -> bool:
|
||||
"""Run core configuration migrations before integrity cleanup.
|
||||
|
||||
Profile configurations can reference providers stored in the default
|
||||
configuration, which has already been loaded and persisted at this point.
|
||||
|
||||
Args:
|
||||
config: Mutable AstrBot configuration loaded from disk.
|
||||
config_path: Path of the configuration being loaded.
|
||||
|
||||
Returns:
|
||||
Whether the configuration changed.
|
||||
"""
|
||||
fallback_config = None
|
||||
resolved_path = config_path.resolve()
|
||||
profile_root = Path(get_astrbot_config_path()).resolve()
|
||||
if resolved_path.is_relative_to(profile_root):
|
||||
default_path = Path(get_astrbot_data_path()) / "cmd_config.json"
|
||||
try:
|
||||
with default_path.open(encoding="utf-8-sig") as default_file:
|
||||
loaded_default = json.load(default_file)
|
||||
if isinstance(loaded_default, dict):
|
||||
fallback_config = loaded_default
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
logger.warning(
|
||||
"Failed to load default configuration while migrating %s: %s",
|
||||
resolved_path,
|
||||
exc,
|
||||
)
|
||||
return _migrate_agent_runner_config(config, fallback_config)
|
||||
|
||||
|
||||
def finalize_config_migrations(configs: list[dict[str, Any]]) -> bool:
|
||||
"""Clean legacy shared data after every profile has been migrated.
|
||||
|
||||
Args:
|
||||
configs: Loaded configurations with the default configuration first.
|
||||
|
||||
Returns:
|
||||
Whether the default configuration changed.
|
||||
"""
|
||||
if not configs:
|
||||
return False
|
||||
default_config = configs[0]
|
||||
providers = default_config.get("provider", [])
|
||||
if not isinstance(providers, list):
|
||||
return False
|
||||
effective_provider_map = _get_effective_provider_map(default_config)
|
||||
filtered_providers = [
|
||||
provider
|
||||
for provider in providers
|
||||
if not (
|
||||
isinstance(provider, dict)
|
||||
and (
|
||||
provider.get("provider_type") == "agent_runner"
|
||||
or effective_provider_map.get(provider.get("id"), {}).get(
|
||||
"provider_type"
|
||||
)
|
||||
== "agent_runner"
|
||||
or _get_provider_runner_type(
|
||||
effective_provider_map.get(provider.get("id"), provider)
|
||||
)
|
||||
is not None
|
||||
)
|
||||
)
|
||||
]
|
||||
if len(filtered_providers) == len(providers):
|
||||
return False
|
||||
default_config["provider"] = filtered_providers
|
||||
return True
|
||||
|
||||
|
||||
def _migra_provider_to_source_structure(conf: Any) -> None:
|
||||
"""Migrate old providers to the provider-source structure.
|
||||
|
||||
Args:
|
||||
conf: Mutable default configuration with a save_config method.
|
||||
"""
|
||||
providers = conf.get("provider", [])
|
||||
provider_sources = conf.get("provider_sources", [])
|
||||
|
||||
# Track if any migration happened
|
||||
migrated = False
|
||||
|
||||
# Provider-only fields that should stay in provider
|
||||
provider_only_fields = {
|
||||
"id",
|
||||
"provider_source_id",
|
||||
@@ -63,63 +378,44 @@ def _migra_provider_to_source_structure(conf: AstrBotConfig) -> None:
|
||||
"custom_extra_body",
|
||||
"enable",
|
||||
}
|
||||
|
||||
# Fields that should not go to source
|
||||
source_exclude_fields = provider_only_fields | {"model_config"}
|
||||
|
||||
for provider in providers:
|
||||
# Skip if already has provider_source_id
|
||||
if provider.get("provider_source_id"):
|
||||
continue
|
||||
|
||||
# Skip non-chat-completion types (they don't need source separation)
|
||||
provider_type = provider.get("provider_type", "")
|
||||
if provider_type != "chat_completion":
|
||||
# For old types without provider_type, check type field
|
||||
old_type = provider.get("type", "")
|
||||
if "chat_completion" not in old_type:
|
||||
continue
|
||||
|
||||
migrated = True
|
||||
logger.info(f"Migrating provider {provider.get('id')} to new structure")
|
||||
|
||||
# Extract source fields from provider
|
||||
source_fields = {}
|
||||
for key, value in list(provider.items()):
|
||||
if key not in source_exclude_fields:
|
||||
source_fields[key] = value
|
||||
|
||||
# Create new provider_source
|
||||
logger.info("Migrating provider %s to new structure", provider.get("id"))
|
||||
source_fields = {
|
||||
key: value
|
||||
for key, value in list(provider.items())
|
||||
if key not in source_exclude_fields
|
||||
}
|
||||
source_id = provider.get("id", "") + "_source"
|
||||
new_source = {"id": source_id, **source_fields}
|
||||
|
||||
# Update provider to only keep necessary fields
|
||||
provider["provider_source_id"] = source_id
|
||||
|
||||
# Extract model from model_config if exists
|
||||
if "model_config" in provider and isinstance(provider["model_config"], dict):
|
||||
model_config = provider["model_config"]
|
||||
provider["model"] = model_config.get("model", "")
|
||||
|
||||
# Put other model_config fields into custom_extra_body
|
||||
extra_body_fields = {k: v for k, v in model_config.items() if k != "model"}
|
||||
if extra_body_fields:
|
||||
if "custom_extra_body" not in provider:
|
||||
provider["custom_extra_body"] = {}
|
||||
provider["custom_extra_body"].update(extra_body_fields)
|
||||
|
||||
# Initialize new fields if not present
|
||||
if "modalities" not in provider:
|
||||
provider["modalities"] = []
|
||||
if "custom_extra_body" not in provider:
|
||||
provider["custom_extra_body"] = {}
|
||||
|
||||
# Remove fields that should be in source
|
||||
keys_to_remove = [k for k in provider.keys() if k not in provider_only_fields]
|
||||
keys_to_remove = [key for key in provider if key not in provider_only_fields]
|
||||
for key in keys_to_remove:
|
||||
del provider[key]
|
||||
|
||||
# Add source to provider_sources
|
||||
provider_sources.append(new_source)
|
||||
|
||||
if migrated:
|
||||
@@ -129,55 +425,51 @@ def _migra_provider_to_source_structure(conf: AstrBotConfig) -> None:
|
||||
|
||||
|
||||
async def migra(
|
||||
db, astrbot_config_mgr, umop_config_router, acm: AstrBotConfigManager
|
||||
db: Any, astrbot_config_mgr: Any, umop_config_router: Any, acm: Any
|
||||
) -> None:
|
||||
"""Run migrations that require initialized configuration or database state.
|
||||
|
||||
Args:
|
||||
db: Initialized AstrBot database.
|
||||
astrbot_config_mgr: Configuration manager used by legacy migrations.
|
||||
umop_config_router: Initialized UMOP configuration router.
|
||||
acm: Initialized AstrBot configuration manager.
|
||||
"""
|
||||
Stores the migration logic here.
|
||||
btw, i really don't like migration :(
|
||||
"""
|
||||
# 4.5 to 4.6 migration for umop_config_router
|
||||
from astrbot.core.db.migration.migra_45_to_46 import migrate_45_to_46
|
||||
from astrbot.core.db.migration.migra_token_usage import migrate_token_usage
|
||||
from astrbot.core.db.migration.migra_webchat_session import (
|
||||
migrate_webchat_session,
|
||||
)
|
||||
|
||||
try:
|
||||
await migrate_45_to_46(astrbot_config_mgr, umop_config_router)
|
||||
except Exception as e:
|
||||
logger.error(f"Migration from version 4.5 to 4.6 failed: {e!s}")
|
||||
except Exception as exc:
|
||||
logger.error("Migration from version 4.5 to 4.6 failed: %s", exc)
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
# migration for webchat session
|
||||
try:
|
||||
await migrate_webchat_session(db)
|
||||
except Exception as e:
|
||||
logger.error(f"Migration for webchat session failed: {e!s}")
|
||||
except Exception as exc:
|
||||
logger.error("Migration for webchat session failed: %s", exc)
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
# migration for token_usage column
|
||||
try:
|
||||
await migrate_token_usage(db)
|
||||
except Exception as e:
|
||||
logger.error(f"Migration for token_usage column failed: {e!s}")
|
||||
except Exception as exc:
|
||||
logger.error("Migration for token_usage column failed: %s", exc)
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
# migra third party agent runner configs
|
||||
_c = False
|
||||
providers = astrbot_config["provider"]
|
||||
ids_map = {}
|
||||
for prov in providers:
|
||||
type_ = prov.get("type")
|
||||
if type_ in ["dify", "coze", "dashscope", DEERFLOW_PROVIDER_TYPE]:
|
||||
prov["provider_type"] = "agent_runner"
|
||||
ids_map[prov["id"]] = {
|
||||
"type": type_,
|
||||
"id": prov["id"],
|
||||
}
|
||||
_c = True
|
||||
if _c:
|
||||
astrbot_config.save_config()
|
||||
|
||||
for conf in acm.confs.values():
|
||||
_migra_agent_runner_configs(conf, ids_map)
|
||||
|
||||
# Migrate providers to new structure: extract source fields to provider_sources
|
||||
configs = list(acm.confs.values())
|
||||
try:
|
||||
_migra_provider_to_source_structure(astrbot_config)
|
||||
except Exception as e:
|
||||
logger.error(f"Migration for provider-source structure failed: {e!s}")
|
||||
if finalize_config_migrations(configs):
|
||||
configs[0].save_config()
|
||||
logger.info("Agent Runner configuration migration completed")
|
||||
except Exception as exc:
|
||||
logger.error("Agent Runner configuration migration failed: %s", exc)
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
try:
|
||||
_migra_provider_to_source_structure(acm.default_conf)
|
||||
except Exception as exc:
|
||||
logger.error("Migration for provider-source structure failed: %s", exc)
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
@@ -527,7 +527,6 @@ class ProviderConfigRequest(OpenModel):
|
||||
if self.capability and "provider_type" not in config:
|
||||
capability_map = {
|
||||
"chat": "chat_completion",
|
||||
"agent": "agent_runner",
|
||||
"stt": "speech_to_text",
|
||||
"tts": "text_to_speech",
|
||||
"embedding": "embedding",
|
||||
|
||||
@@ -11,6 +11,7 @@ from typing import Any
|
||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||
|
||||
from astrbot.core import file_token_service, logger
|
||||
from astrbot.core.config.agent_runner import normalize_agent_runner
|
||||
from astrbot.core.config.astrbot_config import AstrBotConfig
|
||||
from astrbot.core.config.default import (
|
||||
CONFIG_METADATA_2,
|
||||
@@ -427,6 +428,9 @@ def save_config(
|
||||
) -> None:
|
||||
if is_core:
|
||||
_log_computer_config_changes(dict(config), post_config)
|
||||
post_config["agent_runner"] = normalize_agent_runner(
|
||||
post_config.get("agent_runner")
|
||||
)
|
||||
|
||||
try:
|
||||
if is_core:
|
||||
@@ -532,9 +536,14 @@ class ConfigProfileService:
|
||||
"config:edit_admin scope is required to change admins_id",
|
||||
status_code=403,
|
||||
)
|
||||
profile_config = copy.deepcopy(config or DEFAULT_CONFIG)
|
||||
if "agent_runner" in profile_config:
|
||||
profile_config["agent_runner"] = normalize_agent_runner(
|
||||
profile_config["agent_runner"]
|
||||
)
|
||||
conf_id = await self.acm.create_conf(
|
||||
name=name,
|
||||
config=config or DEFAULT_CONFIG,
|
||||
config=profile_config,
|
||||
)
|
||||
await self.core_lifecycle.reload_pipeline_scheduler(conf_id)
|
||||
return {"conf_id": conf_id}
|
||||
@@ -1346,7 +1355,6 @@ class BotConfigService:
|
||||
class ProviderConfigService:
|
||||
CAPABILITY_TO_PROVIDER_TYPE = {
|
||||
"chat": "chat_completion",
|
||||
"agent": "agent_runner",
|
||||
"stt": "speech_to_text",
|
||||
"tts": "text_to_speech",
|
||||
"embedding": "embedding",
|
||||
@@ -1391,7 +1399,11 @@ class ProviderConfigService:
|
||||
for provider in provider_registry:
|
||||
if provider.default_config_tmpl:
|
||||
provider_default_tmpl[provider.type] = provider.default_config_tmpl
|
||||
providers = copy.deepcopy(self.config.get("provider", []))
|
||||
providers = [
|
||||
copy.deepcopy(provider)
|
||||
for provider in self.config.get("provider", [])
|
||||
if provider.get("provider_type") != "agent_runner"
|
||||
]
|
||||
from astrbot.core.utils.llm_metadata import LLM_METADATAS
|
||||
|
||||
model_metadata = {}
|
||||
@@ -1657,6 +1669,8 @@ class ProviderConfigService:
|
||||
for source in self.provider_manager.provider_sources_config
|
||||
}
|
||||
for provider in self.provider_manager.providers_config:
|
||||
if provider.get("provider_type") == "agent_runner":
|
||||
continue
|
||||
if source_id and provider.get("provider_source_id") != source_id:
|
||||
continue
|
||||
if enabled is not None and bool(provider.get("enable", False)) != enabled:
|
||||
|
||||
@@ -151,7 +151,7 @@ export interface BotListParams {
|
||||
}
|
||||
|
||||
export interface ProviderListParams {
|
||||
capability?: 'chat' | 'agent' | 'stt' | 'tts' | 'embedding' | 'rerank';
|
||||
capability?: 'chat' | 'stt' | 'tts' | 'embedding' | 'rerank';
|
||||
source_id?: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
@@ -199,7 +199,6 @@ type ProviderCapability = NonNullable<ProviderListParams['capability']>;
|
||||
|
||||
const PROVIDER_TYPE_TO_CAPABILITY: Record<string, ProviderCapability> = {
|
||||
chat_completion: 'chat',
|
||||
agent_runner: 'agent',
|
||||
speech_to_text: 'stt',
|
||||
text_to_speech: 'tts',
|
||||
embedding: 'embedding',
|
||||
|
||||
@@ -220,7 +220,7 @@ async function getAgentRunnerType(confId: string): Promise<string> {
|
||||
try {
|
||||
const res = await configProfileApi.get(confId);
|
||||
const config = ((res.data.data as any).config || {}) as any;
|
||||
const type = config?.provider_settings?.agent_runner_type || 'local';
|
||||
const type = config?.agent_runner?.runner_type || 'local';
|
||||
configCache.value[confId] = type;
|
||||
return type;
|
||||
} catch (error) {
|
||||
|
||||
@@ -336,7 +336,7 @@ function getSpecialSubtype(value) {
|
||||
</v-row>
|
||||
|
||||
<v-row
|
||||
v-if="!itemMeta?.invisible && itemMeta?._special === 'select_persona' && itemKey === 'provider_settings.default_personality'"
|
||||
v-if="!itemMeta?.invisible && itemMeta?._special === 'select_persona'"
|
||||
class="persona-preview-row"
|
||||
>
|
||||
<v-col cols="12" class="persona-preview-display">
|
||||
@@ -423,7 +423,7 @@ function getSpecialSubtype(value) {
|
||||
</v-row>
|
||||
|
||||
<v-row
|
||||
v-if="!itemMeta?.invisible && itemMeta?._special === 'select_persona' && itemKey === 'provider_settings.default_personality'"
|
||||
v-if="!itemMeta?.invisible && itemMeta?._special === 'select_persona'"
|
||||
class="persona-preview-row"
|
||||
>
|
||||
<v-col cols="12" class="persona-preview-display">
|
||||
|
||||
@@ -18,14 +18,6 @@
|
||||
:multiple="true"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="getSpecialName(itemMeta?._special) === 'select_agent_runner_provider'">
|
||||
<ProviderSelector
|
||||
:model-value="modelValue"
|
||||
@update:model-value="emitUpdate"
|
||||
:provider-type="'agent_runner'"
|
||||
:provider-subtype="getSpecialSubtype(itemMeta?._special)"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="itemMeta?._special === 'provider_pool'">
|
||||
<ProviderSelector :model-value="modelValue" @update:model-value="emitUpdate" :provider-type="'chat_completion'"
|
||||
:button-text="t('core.shared.providerSelector.selectProviderPool')" />
|
||||
@@ -308,6 +300,15 @@ const { getRaw } = useModuleI18n('features/config-metadata')
|
||||
const { configText } = usePluginI18n()
|
||||
|
||||
function emitUpdate(val) {
|
||||
if (
|
||||
props.itemMeta?._special === 'agent_runner_type'
|
||||
&& props.configRoot?.agent_runner
|
||||
&& props.itemMeta?.runner_defaults?.[val]
|
||||
) {
|
||||
props.configRoot.agent_runner.config = JSON.parse(
|
||||
JSON.stringify(props.itemMeta.runner_defaults[val])
|
||||
)
|
||||
}
|
||||
emit('update:modelValue', val)
|
||||
}
|
||||
|
||||
@@ -371,24 +372,6 @@ function getSelectItems(itemMeta) {
|
||||
return itemMeta.options || []
|
||||
}
|
||||
|
||||
function parseSpecialValue(value) {
|
||||
if (!value || typeof value !== 'string') {
|
||||
return { name: '', subtype: '' }
|
||||
}
|
||||
const [name, ...rest] = value.split(':')
|
||||
return {
|
||||
name,
|
||||
subtype: rest.join(':') || ''
|
||||
}
|
||||
}
|
||||
|
||||
function getSpecialName(value) {
|
||||
return parseSpecialValue(value).name
|
||||
}
|
||||
|
||||
function getSpecialSubtype(value) {
|
||||
return parseSpecialValue(value).subtype
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -254,10 +254,6 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: 'chat_completion'
|
||||
},
|
||||
providerSubtype: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
buttonText: {
|
||||
type: String,
|
||||
default: ''
|
||||
@@ -290,9 +286,6 @@ const hasSelection = computed(() => {
|
||||
})
|
||||
|
||||
const defaultTab = computed(() => {
|
||||
if (props.providerType === 'agent_runner' && props.providerSubtype) {
|
||||
return `select_agent_runner_provider:${props.providerSubtype}`
|
||||
}
|
||||
return props.providerType || 'chat_completion'
|
||||
})
|
||||
|
||||
@@ -331,10 +324,7 @@ async function loadProviders() {
|
||||
const response = await providerApi.listByProviderType(props.providerType)
|
||||
if (response.data.status === 'ok') {
|
||||
modelMetadata.value = response.data.model_metadata || {}
|
||||
const providers = response.data.data || []
|
||||
providerList.value = props.providerSubtype
|
||||
? providers.filter((provider) => matchesProviderSubtype(provider, props.providerSubtype))
|
||||
: providers
|
||||
providerList.value = response.data.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载提供商列表失败:', error)
|
||||
@@ -344,17 +334,6 @@ async function loadProviders() {
|
||||
}
|
||||
}
|
||||
|
||||
function matchesProviderSubtype(provider, subtype) {
|
||||
if (!subtype) {
|
||||
return true
|
||||
}
|
||||
const normalized = String(subtype).toLowerCase()
|
||||
const candidates = [provider.type, provider.provider, provider.id]
|
||||
.filter(Boolean)
|
||||
.map((value) => String(value).toLowerCase())
|
||||
return candidates.includes(normalized)
|
||||
}
|
||||
|
||||
function selectProvider(provider) {
|
||||
if (props.multiple) {
|
||||
if (!provider.id) {
|
||||
|
||||
@@ -24,10 +24,6 @@ interface ProviderIconSource {
|
||||
export function resolveDefaultTab(value?: string) {
|
||||
const normalized = (value || '').toLowerCase()
|
||||
|
||||
if (normalized.startsWith('select_agent_runner_provider') || normalized === 'agent_runner') {
|
||||
return 'agent_runner'
|
||||
}
|
||||
|
||||
if (normalized === 'select_provider_stt' || normalized === 'speech_to_text' || normalized.includes('stt')) {
|
||||
return 'speech_to_text'
|
||||
}
|
||||
@@ -83,7 +79,6 @@ export function useProviderSources(options: UseProviderSourcesOptions) {
|
||||
|
||||
const providerTypes = computed(() => [
|
||||
{ value: 'chat_completion', label: tm('providers.tabs.chatCompletion'), icon: 'mdi-message-text' },
|
||||
{ value: 'agent_runner', label: tm('providers.tabs.agentRunner'), icon: 'mdi-robot' },
|
||||
{ value: 'speech_to_text', label: tm('providers.tabs.speechToText'), icon: 'mdi-microphone-message' },
|
||||
{ value: 'text_to_speech', label: tm('providers.tabs.textToSpeech'), icon: 'mdi-volume-high' },
|
||||
{ value: 'embedding', label: tm('providers.tabs.embedding'), icon: 'mdi-code-json' },
|
||||
@@ -361,8 +356,6 @@ export function useProviderSources(options: UseProviderSourcesOptions) {
|
||||
anthropic_chat_completion: 'chat_completion',
|
||||
googlegenai_chat_completion: 'chat_completion',
|
||||
zhipu_chat_completion: 'chat_completion',
|
||||
dify: 'agent_runner',
|
||||
coze: 'agent_runner',
|
||||
dashscope: 'chat_completion',
|
||||
openai_whisper_api: 'speech_to_text',
|
||||
mimo_stt_api: 'speech_to_text',
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
"name": "AI",
|
||||
"agent_runner": {
|
||||
"description": "Agent Runner",
|
||||
"hint": "Select the runner for AI conversations. Defaults to AstrBot's built-in Agent runner, which supports knowledge base, persona, and tool calling features. You don't need to modify this section unless you plan to integrate third-party Agent runners like Dify, Coze, or DeerFlow.",
|
||||
"hint": "Select the runner for AI conversations. Switching runners resets the configuration to the new type's defaults.",
|
||||
"provider_settings": {
|
||||
"enable": {
|
||||
"description": "Enable",
|
||||
"hint": "Master switch for AI conversations"
|
||||
},
|
||||
"agent_runner_type": {
|
||||
}
|
||||
},
|
||||
"agent_runner": {
|
||||
"runner_type": {
|
||||
"description": "Runner",
|
||||
"labels": [
|
||||
"Built-in Agent",
|
||||
@@ -18,37 +20,84 @@
|
||||
"Alibaba Cloud Bailian Application",
|
||||
"DeerFlow"
|
||||
]
|
||||
},
|
||||
"coze_agent_runner_provider_id": {
|
||||
"description": "Coze Agent Runner Provider ID"
|
||||
},
|
||||
"dify_agent_runner_provider_id": {
|
||||
"description": "Dify Agent Runner Provider ID"
|
||||
},
|
||||
"dashscope_agent_runner_provider_id": {
|
||||
"description": "Alibaba Cloud Bailian Application Agent Runner Provider ID"
|
||||
},
|
||||
"deerflow_agent_runner_provider_id": {
|
||||
"description": "DeerFlow Agent Runner Provider ID"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dify_runner": {
|
||||
"description": "Dify Configuration",
|
||||
"agent_runner": { "config": {
|
||||
"dify_api_type": { "description": "Application Type" },
|
||||
"dify_api_key": { "description": "API Key" },
|
||||
"dify_api_base": { "description": "API Base URL" },
|
||||
"dify_workflow_output_key": { "description": "Workflow Output Variable" },
|
||||
"dify_query_input_key": { "description": "Prompt Input Variable" },
|
||||
"variables": { "description": "Variables" },
|
||||
"timeout": { "description": "Timeout (seconds)" },
|
||||
"proxy": { "description": "Proxy URL" }
|
||||
} }
|
||||
},
|
||||
"coze_runner": {
|
||||
"description": "Coze Configuration",
|
||||
"agent_runner": { "config": {
|
||||
"coze_api_key": { "description": "API Key" },
|
||||
"bot_id": { "description": "Bot ID" },
|
||||
"coze_api_base": { "description": "API Base URL" },
|
||||
"auto_save_history": { "description": "Let Coze Manage Conversation History" },
|
||||
"timeout": { "description": "Timeout (seconds)" },
|
||||
"proxy": { "description": "Proxy URL" }
|
||||
} }
|
||||
},
|
||||
"dashscope_runner": {
|
||||
"description": "Alibaba Cloud Bailian Application Configuration",
|
||||
"agent_runner": { "config": {
|
||||
"dashscope_app_type": { "description": "Application Type" },
|
||||
"dashscope_api_key": { "description": "API Key" },
|
||||
"dashscope_app_id": { "description": "Application ID" },
|
||||
"rag_options": {
|
||||
"pipeline_ids": { "description": "Knowledge Base Pipeline IDs" },
|
||||
"file_ids": { "description": "File IDs" },
|
||||
"output_reference": { "description": "Include References" }
|
||||
},
|
||||
"variables": { "description": "Variables" },
|
||||
"timeout": { "description": "Timeout (seconds)" },
|
||||
"proxy": { "description": "Proxy URL" }
|
||||
} }
|
||||
},
|
||||
"deerflow_runner": {
|
||||
"description": "DeerFlow Configuration",
|
||||
"agent_runner": { "config": {
|
||||
"deerflow_api_base": { "description": "API Base URL" },
|
||||
"deerflow_api_key": { "description": "API Key" },
|
||||
"deerflow_auth_header": { "description": "Authorization Header" },
|
||||
"deerflow_assistant_id": { "description": "Assistant ID" },
|
||||
"deerflow_model_name": { "description": "Model Name Override" },
|
||||
"deerflow_thinking_enabled": { "description": "Enable Thinking Mode" },
|
||||
"deerflow_plan_mode": { "description": "Enable Plan Mode" },
|
||||
"deerflow_subagent_enabled": { "description": "Enable Subagents" },
|
||||
"deerflow_max_concurrent_subagents": { "description": "Maximum Concurrent Subagents" },
|
||||
"deerflow_recursion_limit": { "description": "Recursion Limit" },
|
||||
"timeout": { "description": "Timeout (seconds)" },
|
||||
"proxy": { "description": "Proxy URL" }
|
||||
} }
|
||||
},
|
||||
"ai": {
|
||||
"description": "Model",
|
||||
"hint": "When using non-built-in Agent runners, the default chat model and default image caption model may not take effect, but some plugins rely on these settings to invoke AI capabilities.",
|
||||
"provider_settings": {
|
||||
"default_provider_id": {
|
||||
"hint": "Configure the built-in Agent's chat models and shared image caption and speech models.",
|
||||
"agent_runner": { "config": { "model": {
|
||||
"provider_id": {
|
||||
"description": "Default Chat Model",
|
||||
"hint": "Uses the first model when left empty"
|
||||
},
|
||||
"fallback_chat_models": {
|
||||
"description": "Fallback chat model IDs",
|
||||
"hint": "When the primary chat model request fails, fallback to these chat models in order."
|
||||
"fallback_provider_ids": {
|
||||
"description": "Fallback Chat Model IDs",
|
||||
"hint": "Try these chat models in order when the primary model request fails."
|
||||
},
|
||||
"request_max_retries": {
|
||||
"description": "Request Max Retries",
|
||||
"hint": "Maximum attempts for a single model request when retryable errors occur."
|
||||
},
|
||||
}
|
||||
} } },
|
||||
"provider_settings": {
|
||||
"default_image_caption_provider_id": {
|
||||
"description": "Default Image Caption Model",
|
||||
"hint": "Leave empty to disable; useful for non-multimodal models"
|
||||
@@ -83,11 +132,17 @@
|
||||
"persona": {
|
||||
"description": "Persona",
|
||||
"hint": "Set the default persona for AI conversations. Personas can be managed in the Persona tab.",
|
||||
"provider_settings": {
|
||||
"default_personality": {
|
||||
"description": "Default Persona"
|
||||
"agent_runner": { "config": { "persona": {
|
||||
"persona_id": { "description": "Default Persona" },
|
||||
"safety_mode": {
|
||||
"description": "Safety Mode",
|
||||
"hint": "Guide the model toward safe content and away from harmful or sensitive topics."
|
||||
},
|
||||
"safety_mode_strategy": {
|
||||
"description": "Safety Mode Strategy",
|
||||
"hint": "Select how safety mode is applied."
|
||||
}
|
||||
}
|
||||
} } }
|
||||
},
|
||||
"knowledgebase": {
|
||||
"description": "Knowledge Base",
|
||||
@@ -252,16 +307,16 @@
|
||||
"truncate_and_compress": {
|
||||
"hint": "[Context Management](https://docs.astrbot.app/en/use/context-compress.html)",
|
||||
"description": "Context Management Strategy",
|
||||
"provider_settings": {
|
||||
"max_context_length": {
|
||||
"agent_runner": { "config": { "compression": {
|
||||
"max_turns": {
|
||||
"description": "Max Turns Before Compression",
|
||||
"hint": "Persistent conversation history is truncated or LLM-compressed by the strategy below only after it exceeds this many turns. Request-time contexts are also constrained by this value before sending. -1 means no turn-based limit."
|
||||
},
|
||||
"dequeue_context_length": {
|
||||
"trim_turns": {
|
||||
"description": "Turns to Discard When Limit Exceeded",
|
||||
"hint": "When history exceeds 'Max Turns Before Compression' and LLM compression is unavailable, discard this many oldest turns at once. Request-time truncation also reuses this value."
|
||||
},
|
||||
"context_limit_reached_strategy": {
|
||||
"overflow_strategy": {
|
||||
"description": "Handling for History Limits or Context Window Pressure",
|
||||
"labels": [
|
||||
"Truncate by Turns",
|
||||
@@ -269,38 +324,45 @@
|
||||
],
|
||||
"hint": "Persistent conversation history uses this strategy only after exceeding 'Max Turns Before Compression'. Before each request, the same strategy may also protect the in-flight context when tokens approach the model window."
|
||||
},
|
||||
"llm_compress_instruction": {
|
||||
"instruction": {
|
||||
"description": "Context Compression Instruction",
|
||||
"hint": "If empty, the default prompt will be used."
|
||||
},
|
||||
"llm_compress_keep_recent_ratio": {
|
||||
"keep_recent_ratio": {
|
||||
"description": "Recent Context Token Ratio to Keep",
|
||||
"hint": "Keep recent exact context by current context token ratio, from 0-0.3. 0.15 means keeping 15%; values above 0 keep at least the latest round."
|
||||
},
|
||||
"llm_compress_provider_id": {
|
||||
"provider_id": {
|
||||
"description": "Model Provider ID for Context Compression",
|
||||
"hint": "When left empty, the current chat model will be used for compression. If the model is unavailable or compression fails, AstrBot falls back to the 'Truncate by Turns' strategy."
|
||||
},
|
||||
"fallback_max_context_tokens": {
|
||||
"fallback_max_tokens": {
|
||||
"description": "Fallback context window size",
|
||||
"hint": "When max_context_tokens is 0 and the model is not in built-in metadata, use this value as the context window size. Default: 128000."
|
||||
}
|
||||
}
|
||||
} } }
|
||||
},
|
||||
"others": {
|
||||
"description": "Other Settings",
|
||||
"agent_runner": { "config": { "misc": {
|
||||
"max_steps": { "description": "Maximum Tool Call Rounds" },
|
||||
"tool_schema_mode": {
|
||||
"description": "Tool Schema Mode",
|
||||
"hint": "Skills-like sends name/description first and re-queries for parameters; Full sends the complete schema in one step.",
|
||||
"labels": ["Skills-like (two-stage)", "Full schema"]
|
||||
},
|
||||
"tool_call_timeout": {
|
||||
"description": "Tool Call Timeout (seconds)"
|
||||
},
|
||||
"sanitize_context_by_modalities": {
|
||||
"description": "Sanitize History by Modalities",
|
||||
"hint": "When enabled, sanitizes contexts before each LLM request by removing image blocks and tool-call structures that the current provider's modalities do not support (this changes what the model sees)."
|
||||
}
|
||||
} } },
|
||||
"provider_settings": {
|
||||
"display_reasoning_text": {
|
||||
"description": "Display Reasoning Content"
|
||||
},
|
||||
"llm_safety_mode": {
|
||||
"description": "Healthy Mode",
|
||||
"hint": "Add safety guardrails to model replies."
|
||||
},
|
||||
"safety_mode_strategy": {
|
||||
"description": "Healthy Mode Strategy",
|
||||
"hint": "How to apply healthy mode."
|
||||
},
|
||||
"identifier": {
|
||||
"description": "User Identification",
|
||||
"hint": "When enabled, user ID information will be included in the prompt."
|
||||
@@ -324,10 +386,6 @@
|
||||
"description": "Merge Agent Intermediate Messages",
|
||||
"hint": "When enabled, intermediate text generated during multi-step tool calls in non-streaming mode will be buffered and sent as a single merged reply after the Agent finishes."
|
||||
},
|
||||
"sanitize_context_by_modalities": {
|
||||
"description": "Sanitize History by Modalities",
|
||||
"hint": "When enabled, sanitizes contexts before each LLM request by removing image blocks and tool-call structures that the current provider's modalities do not support (this changes what the model sees)."
|
||||
},
|
||||
"max_quoted_fallback_images": {
|
||||
"description": "Forwarded Image Fetch Limit",
|
||||
"hint": "Maximum number of images injected from forwarded-message parsing; extra images are truncated."
|
||||
@@ -350,20 +408,6 @@
|
||||
"hint": "When enabled, log warnings when all get_msg/get_forward_msg attempts fail."
|
||||
}
|
||||
},
|
||||
"max_agent_step": {
|
||||
"description": "Maximum Tool Call Rounds"
|
||||
},
|
||||
"tool_call_timeout": {
|
||||
"description": "Tool Call Timeout (seconds)"
|
||||
},
|
||||
"tool_schema_mode": {
|
||||
"description": "Tool Schema Mode",
|
||||
"hint": "Skills-like sends name/description first and re-queries for parameters; Full sends the complete schema in one step.",
|
||||
"labels": [
|
||||
"Skills-like (two-stage)",
|
||||
"Full schema"
|
||||
]
|
||||
},
|
||||
"streaming_response": {
|
||||
"description": "Streaming Output"
|
||||
},
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"messages": {
|
||||
"configApplied": "Configuration successfully applied. To save, you need to click the save button in the bottom right corner.",
|
||||
"configApplyError": "Configuration not applied, JSON format error.",
|
||||
"unsavedChangesNotice": "You have unsaved configuration changes. Click the save button in the bottom-right corner to apply them.",
|
||||
"unsavedChangesNotice": "Unsaved. Click the button in the bottom-right corner to save.",
|
||||
"saveSuccess": "Configuration saved successfully",
|
||||
"saveError": "Failed to save configuration",
|
||||
"loadError": "Failed to load configuration",
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"tabs": {
|
||||
"all": "All",
|
||||
"chatCompletion": "Chat Completion",
|
||||
"agentRunner": "Agent Runner",
|
||||
"speechToText": "Speech to Text",
|
||||
"textToSpeech": "Text to Speech",
|
||||
"embedding": "Embedding",
|
||||
@@ -48,7 +47,6 @@
|
||||
"title": "Service Provider",
|
||||
"tabs": {
|
||||
"basic": "Basic",
|
||||
"agentRunner": "Agent Runner",
|
||||
"speechToText": "Speech to Text",
|
||||
"textToSpeech": "Text to Speech",
|
||||
"embedding": "Embedding",
|
||||
|
||||
@@ -252,43 +252,71 @@
|
||||
"truncate_and_compress": {
|
||||
"hint": "[Управление контекстом](https://docs.astrbot.app/en/use/context-compress.html)",
|
||||
"description": "Стратегия управления контекстом",
|
||||
"provider_settings": {
|
||||
"max_context_length": {
|
||||
"description": "Макс. раундов перед сжатием",
|
||||
"hint": "Постоянная история диалога обрезается или сжимается LLM по стратегии ниже только после превышения этого числа раундов. Контекст перед запросом также ограничивается этим значением. -1 означает без ограничений по раундам."
|
||||
},
|
||||
"dequeue_context_length": {
|
||||
"description": "Раундов для удаления при превышении лимита",
|
||||
"hint": "Когда история превышает лимит раундов и LLM-сжатие недоступно, за один раз удаляется это число самых старых раундов. Обрезка перед запросом также использует это значение."
|
||||
},
|
||||
"context_limit_reached_strategy": {
|
||||
"description": "Действие при лимите истории или давлении окна контекста",
|
||||
"labels": [
|
||||
"Обрезать по раундам",
|
||||
"Сжать с помощью LLM"
|
||||
],
|
||||
"hint": "Постоянная история диалога использует эту стратегию только после превышения лимита раундов. Перед каждым запросом та же стратегия может защищать текущий контекст, когда токены приближаются к окну модели."
|
||||
},
|
||||
"llm_compress_instruction": {
|
||||
"description": "Инструкция для сжатия контекста",
|
||||
"hint": "Если пусто, используется промпт по умолчанию."
|
||||
},
|
||||
"llm_compress_keep_recent_ratio": {
|
||||
"description": "Доля последних токенов контекста при сжатии",
|
||||
"hint": "Сохраняет последние сообщения по доле текущих токенов контекста, от 0 до 0.3. 0.15 означает 15%; значение выше 0 сохраняет как минимум последний раунд."
|
||||
},
|
||||
"llm_compress_provider_id": {
|
||||
"description": "Модель для сжатия контекста",
|
||||
"hint": "Если не выбрано, для сжатия используется текущая модель чата. Если модель недоступна или сжатие завершается ошибкой, AstrBot откатывается к обрезке по раундам."
|
||||
},
|
||||
"fallback_max_context_tokens": {
|
||||
"description": "Запасной размер окна контекста",
|
||||
"hint": "Если max_context_tokens равен 0 и модель отсутствует во встроенных метаданных, используется это значение. По умолчанию: 128000."
|
||||
"agent_runner": {
|
||||
"config": {
|
||||
"compression": {
|
||||
"max_turns": {
|
||||
"description": "Макс. раундов перед сжатием",
|
||||
"hint": "Постоянная история диалога обрезается или сжимается LLM по стратегии ниже только после превышения этого числа раундов. Контекст перед запросом также ограничивается этим значением. -1 означает без ограничений по раундам."
|
||||
},
|
||||
"trim_turns": {
|
||||
"description": "Раундов для удаления при превышении лимита",
|
||||
"hint": "Когда история превышает лимит раундов и LLM-сжатие недоступно, за один раз удаляется это число самых старых раундов. Обрезка перед запросом также использует это значение."
|
||||
},
|
||||
"overflow_strategy": {
|
||||
"description": "Действие при лимите истории или давлении окна контекста",
|
||||
"labels": [
|
||||
"Обрезать по раундам",
|
||||
"Сжать с помощью LLM"
|
||||
],
|
||||
"hint": "Постоянная история диалога использует эту стратегию только после превышения лимита раундов. Перед каждым запросом та же стратегия может защищать текущий контекст, когда токены приближаются к окну модели."
|
||||
},
|
||||
"instruction": {
|
||||
"description": "Инструкция для сжатия контекста",
|
||||
"hint": "Если пусто, используется промпт по умолчанию."
|
||||
},
|
||||
"keep_recent_ratio": {
|
||||
"description": "Доля последних токенов контекста при сжатии",
|
||||
"hint": "Сохраняет последние сообщения по доле текущих токенов контекста, от 0 до 0.3. 0.15 означает 15%; значение выше 0 сохраняет как минимум последний раунд."
|
||||
},
|
||||
"provider_id": {
|
||||
"description": "Модель для сжатия контекста",
|
||||
"hint": "Если не выбрано, для сжатия используется текущая модель чата. Если модель недоступна или сжатие завершается ошибкой, AstrBot откатывается к обрезке по раундам."
|
||||
},
|
||||
"fallback_max_tokens": {
|
||||
"description": "Запасной размер окна контекста",
|
||||
"hint": "Если max_context_tokens равен 0 и модель отсутствует во встроенных метаданных, используется это значение. По умолчанию: 128000."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"others": {
|
||||
"description": "Прочие настройки",
|
||||
"agent_runner": {
|
||||
"config": {
|
||||
"misc": {
|
||||
"max_steps": {
|
||||
"description": "Макс. количество раундов вызова инструментов"
|
||||
},
|
||||
"tool_call_timeout": {
|
||||
"description": "Таймаут вызова инструмента (сек)"
|
||||
},
|
||||
"tool_schema_mode": {
|
||||
"description": "Режим схемы инструментов",
|
||||
"hint": "Skills-like сначала отправляет имя/описание и дозапрашивает параметры; Full отправляет полную схему сразу.",
|
||||
"labels": [
|
||||
"Skills-like (двухэтапный)",
|
||||
"Полная схема (Full)"
|
||||
]
|
||||
},
|
||||
"sanitize_context_by_modalities": {
|
||||
"description": "Очистка истории по модальностям",
|
||||
"hint": "Если включено, очищает контекст перед запросом, удаляя блоки (например, изображения), которые не поддерживаются выбранным провайдером."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"provider_settings": {
|
||||
"display_reasoning_text": {
|
||||
"description": "Отображать процесс рассуждения (Reasoning)"
|
||||
@@ -324,10 +352,6 @@
|
||||
"description": "Объединять промежуточные сообщения Agent",
|
||||
"hint": "Если включено, промежуточный текст, созданный во время многошаговых вызовов инструментов в непотоковом режиме, будет буферизован и отправлен одним объединенным ответом после завершения Agent."
|
||||
},
|
||||
"sanitize_context_by_modalities": {
|
||||
"description": "Очистка истории по модальностям",
|
||||
"hint": "Если включено, очищает контекст перед запросом, удаляя блоки (например, изображения), которые не поддерживаются выбранным провайдером."
|
||||
},
|
||||
"max_quoted_fallback_images": {
|
||||
"description": "Лимит загрузки изображений из пересланных сообщений",
|
||||
"hint": "Максимальное количество изображений при парсинге цитируемых сообщений."
|
||||
@@ -350,20 +374,6 @@
|
||||
"hint": "Если включено, логирует предупреждения при неудачных попытках получения сообщений."
|
||||
}
|
||||
},
|
||||
"max_agent_step": {
|
||||
"description": "Макс. количество раундов вызова инструментов"
|
||||
},
|
||||
"tool_call_timeout": {
|
||||
"description": "Таймаут вызова инструмента (сек)"
|
||||
},
|
||||
"tool_schema_mode": {
|
||||
"description": "Режим схемы инструментов",
|
||||
"hint": "Skills-like сначала отправляет имя/описание и дозапрашивает параметры; Full отправляет полную схему сразу.",
|
||||
"labels": [
|
||||
"Skills-like (двухэтапный)",
|
||||
"Полная схема (Full)"
|
||||
]
|
||||
},
|
||||
"streaming_response": {
|
||||
"description": "Потоковый вывод (Streaming)"
|
||||
},
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"messages": {
|
||||
"configApplied": "Настройки применены образно. Нажмите «Сохранить» для окончательной записи.",
|
||||
"configApplyError": "Ошибка применения: некорректный формат JSON.",
|
||||
"unsavedChangesNotice": "Есть несохраненные изменения. Пожалуйста, нажмите «Сохранить», чтобы они вступили в силу.",
|
||||
"unsavedChangesNotice": "Не сохранено. Нажмите кнопку в правом нижнем углу, чтобы сохранить.",
|
||||
"saveSuccess": "Настройки успешно сохранены",
|
||||
"saveError": "Ошибка при сохранении",
|
||||
"loadError": "Ошибка при загрузке настроек",
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"tabs": {
|
||||
"all": "Все",
|
||||
"chatCompletion": "Диалоги",
|
||||
"agentRunner": "Агенты",
|
||||
"speechToText": "STT (Речь -> Текст)",
|
||||
"textToSpeech": "TTS (Текст -> Речь)",
|
||||
"embedding": "Эмбеддинги",
|
||||
@@ -49,7 +48,6 @@
|
||||
"title": "Новый провайдер",
|
||||
"tabs": {
|
||||
"basic": "Диалоги",
|
||||
"agentRunner": "Агенты",
|
||||
"speechToText": "Преобразование текста в речь",
|
||||
"textToSpeech": "Переранжирование",
|
||||
"embedding": "Эмбеддинги",
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
"name": "AI 配置",
|
||||
"agent_runner": {
|
||||
"description": "Agent 执行方式",
|
||||
"hint": "选择 AI 对话的执行器,默认为 AstrBot 内置 Agent 执行器,可使用 AstrBot 内的知识库、人格、工具调用功能。如果不打算接入 Dify、Coze、DeerFlow 等第三方 Agent 执行器,不需要修改此节。",
|
||||
"hint": "选择 AI 对话的执行器。切换执行器会使用新类型的默认配置,不保留上一类型的参数。",
|
||||
"provider_settings": {
|
||||
"enable": {
|
||||
"description": "启用",
|
||||
"hint": "AI 对话总开关"
|
||||
},
|
||||
"agent_runner_type": {
|
||||
}
|
||||
},
|
||||
"agent_runner": {
|
||||
"runner_type": {
|
||||
"description": "执行器",
|
||||
"labels": [
|
||||
"内置 Agent",
|
||||
@@ -18,37 +20,96 @@
|
||||
"阿里云百炼应用",
|
||||
"DeerFlow"
|
||||
]
|
||||
},
|
||||
"coze_agent_runner_provider_id": {
|
||||
"description": "Coze Agent 执行器提供商 ID"
|
||||
},
|
||||
"dify_agent_runner_provider_id": {
|
||||
"description": "Dify Agent 执行器提供商 ID"
|
||||
},
|
||||
"dashscope_agent_runner_provider_id": {
|
||||
"description": "阿里云百炼应用 Agent 执行器提供商 ID"
|
||||
},
|
||||
"deerflow_agent_runner_provider_id": {
|
||||
"description": "DeerFlow Agent 执行器提供商 ID"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dify_runner": {
|
||||
"description": "Dify 配置",
|
||||
"agent_runner": {
|
||||
"config": {
|
||||
"dify_api_type": { "description": "应用类型" },
|
||||
"dify_api_key": { "description": "API Key" },
|
||||
"dify_api_base": { "description": "API Base URL" },
|
||||
"dify_workflow_output_key": { "description": "Workflow 输出变量名" },
|
||||
"dify_query_input_key": { "description": "Prompt 输入变量名" },
|
||||
"variables": { "description": "变量" },
|
||||
"timeout": { "description": "超时时间(秒)" },
|
||||
"proxy": { "description": "代理地址" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"coze_runner": {
|
||||
"description": "Coze 配置",
|
||||
"agent_runner": {
|
||||
"config": {
|
||||
"coze_api_key": { "description": "API Key" },
|
||||
"bot_id": { "description": "Bot ID" },
|
||||
"coze_api_base": { "description": "API Base URL" },
|
||||
"auto_save_history": { "description": "由 Coze 管理对话记录" },
|
||||
"timeout": { "description": "超时时间(秒)" },
|
||||
"proxy": { "description": "代理地址" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"dashscope_runner": {
|
||||
"description": "阿里云百炼应用配置",
|
||||
"agent_runner": {
|
||||
"config": {
|
||||
"dashscope_app_type": { "description": "应用类型" },
|
||||
"dashscope_api_key": { "description": "API Key" },
|
||||
"dashscope_app_id": { "description": "应用 ID" },
|
||||
"rag_options": {
|
||||
"pipeline_ids": { "description": "知识库 Pipeline ID" },
|
||||
"file_ids": { "description": "文件 ID" },
|
||||
"output_reference": { "description": "输出引用" }
|
||||
},
|
||||
"variables": { "description": "变量" },
|
||||
"timeout": { "description": "超时时间(秒)" },
|
||||
"proxy": { "description": "代理地址" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"deerflow_runner": {
|
||||
"description": "DeerFlow 配置",
|
||||
"agent_runner": {
|
||||
"config": {
|
||||
"deerflow_api_base": { "description": "API Base URL" },
|
||||
"deerflow_api_key": { "description": "API Key" },
|
||||
"deerflow_auth_header": { "description": "Authorization Header" },
|
||||
"deerflow_assistant_id": { "description": "Assistant ID" },
|
||||
"deerflow_model_name": { "description": "模型名称覆盖" },
|
||||
"deerflow_thinking_enabled": { "description": "启用思考模式" },
|
||||
"deerflow_plan_mode": { "description": "启用计划模式" },
|
||||
"deerflow_subagent_enabled": { "description": "启用子智能体" },
|
||||
"deerflow_max_concurrent_subagents": { "description": "子智能体最大并发数" },
|
||||
"deerflow_recursion_limit": { "description": "递归深度上限" },
|
||||
"timeout": { "description": "超时时间(秒)" },
|
||||
"proxy": { "description": "代理地址" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"ai": {
|
||||
"description": "模型",
|
||||
"hint": "当使用非内置 Agent 执行器时,默认对话模型和默认图片转述模型可能会无效,但某些插件会依赖此配置项来调用 AI 能力。",
|
||||
"hint": "配置内置 Agent 使用的对话模型,以及通用的图片转述、语音模型。",
|
||||
"agent_runner": {
|
||||
"config": {
|
||||
"model": {
|
||||
"provider_id": {
|
||||
"description": "默认对话模型",
|
||||
"hint": "留空时使用第一个模型"
|
||||
},
|
||||
"fallback_provider_ids": {
|
||||
"description": "回退对话模型列表",
|
||||
"hint": "主对话模型请求失败时,按顺序切换到这些对话模型。"
|
||||
},
|
||||
"request_max_retries": {
|
||||
"description": "请求最大重试次数",
|
||||
"hint": "单次模型请求遇到可重试错误时的最大尝试次数。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"provider_settings": {
|
||||
"default_provider_id": {
|
||||
"description": "默认对话模型",
|
||||
"hint": "留空时使用第一个模型"
|
||||
},
|
||||
"fallback_chat_models": {
|
||||
"description": "回退对话模型列表",
|
||||
"hint": "主对话模型请求失败时,按顺序切换到这些对话模型。"
|
||||
},
|
||||
"request_max_retries": {
|
||||
"description": "请求最大重试次数",
|
||||
"hint": "单次模型请求遇到可重试错误时的最大尝试次数。"
|
||||
},
|
||||
"default_image_caption_provider_id": {
|
||||
"description": "默认图片转述模型",
|
||||
"hint": "留空代表不使用,可用于非多模态模型"
|
||||
@@ -83,9 +144,19 @@
|
||||
"persona": {
|
||||
"description": "人格",
|
||||
"hint": "赋予 AstrBot 人格。",
|
||||
"provider_settings": {
|
||||
"default_personality": {
|
||||
"description": "默认采用的人格"
|
||||
"agent_runner": {
|
||||
"config": {
|
||||
"persona": {
|
||||
"persona_id": { "description": "默认采用的人格" },
|
||||
"safety_mode": {
|
||||
"description": "健康模式",
|
||||
"hint": "引导模型输出健康、安全的内容,避免有害或敏感话题。"
|
||||
},
|
||||
"safety_mode_strategy": {
|
||||
"description": "健康模式策略",
|
||||
"hint": "选择健康模式的实现策略。"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -254,55 +325,67 @@
|
||||
"truncate_and_compress": {
|
||||
"hint": "AstrBot 如何管理工作记忆。详见: [上下文管理策略](https://docs.astrbot.app/use/context-compress.html)。",
|
||||
"description": "上下文管理策略",
|
||||
"provider_settings": {
|
||||
"max_context_length": {
|
||||
"description": "压缩前最多保留对话轮数",
|
||||
"hint": "普通会话历史超过该轮数后,才会按下方策略进行持久化截断或 LLM 压缩;请求发送前也会先按该值约束上下文。-1 表示不按轮数限制。"
|
||||
},
|
||||
"dequeue_context_length": {
|
||||
"description": "轮次超限时一次丢弃轮数",
|
||||
"hint": "当超过\"压缩前最多保留对话轮数\"且无法使用 LLM 压缩时,一次丢弃多少轮旧对话;请求期截断也会复用该值。"
|
||||
},
|
||||
"context_limit_reached_strategy": {
|
||||
"description": "历史超限或上下文接近上限时的处理方式",
|
||||
"labels": [
|
||||
"按对话轮数截断",
|
||||
"由 LLM 压缩上下文"
|
||||
],
|
||||
"hint": "普通会话历史仅在超过\"压缩前最多保留对话轮数\"后执行该策略;请求发送前也会在上下文 token 接近模型窗口时使用同一策略保护本次请求。"
|
||||
},
|
||||
"llm_compress_instruction": {
|
||||
"description": "上下文压缩提示词",
|
||||
"hint": "如果为空则使用默认提示词。"
|
||||
},
|
||||
"llm_compress_keep_recent_ratio": {
|
||||
"description": "压缩时保留最近上下文比例",
|
||||
"hint": "按当前上下文 token 数保留最近内容,范围 0-0.3。0.15 表示保留 15%;比例大于 0 时至少保留最后一轮。"
|
||||
},
|
||||
"llm_compress_provider_id": {
|
||||
"description": "用于上下文压缩的模型提供商 ID",
|
||||
"hint": "留空时使用当前聊天模型进行压缩;如果模型不可用或压缩失败,将回退为\"按对话轮数截断\"的策略。"
|
||||
},
|
||||
"fallback_max_context_tokens": {
|
||||
"description": "上下文窗口兜底值",
|
||||
"hint": "当 max_context_tokens 为 0 且模型不在内置元数据中时,使用此值作为上下文窗口大小。默认 128000。"
|
||||
"agent_runner": {
|
||||
"config": {
|
||||
"compression": {
|
||||
"max_turns": {
|
||||
"description": "压缩前最多保留对话轮数",
|
||||
"hint": "普通会话历史超过该轮数后,才会按下方策略进行持久化截断或 LLM 压缩;请求发送前也会先按该值约束上下文。-1 表示不按轮数限制。"
|
||||
},
|
||||
"trim_turns": {
|
||||
"description": "轮次超限时一次丢弃轮数",
|
||||
"hint": "当超过\"压缩前最多保留对话轮数\"且无法使用 LLM 压缩时,一次丢弃多少轮旧对话;请求期截断也会复用该值。"
|
||||
},
|
||||
"overflow_strategy": {
|
||||
"description": "历史超限或上下文接近上限时的处理方式",
|
||||
"labels": ["按对话轮数截断", "由 LLM 压缩上下文"],
|
||||
"hint": "普通会话历史仅在超过\"压缩前最多保留对话轮数\"后执行该策略;请求发送前也会在上下文 token 接近模型窗口时使用同一策略保护本次请求。"
|
||||
},
|
||||
"instruction": {
|
||||
"description": "上下文压缩提示词",
|
||||
"hint": "如果为空则使用默认提示词。"
|
||||
},
|
||||
"keep_recent_ratio": {
|
||||
"description": "压缩时保留最近上下文比例",
|
||||
"hint": "按当前上下文 token 数保留最近内容,范围 0-0.3。0.15 表示保留 15%;比例大于 0 时至少保留最后一轮。"
|
||||
},
|
||||
"provider_id": {
|
||||
"description": "用于上下文压缩的模型提供商 ID",
|
||||
"hint": "留空时使用当前聊天模型进行压缩;如果模型不可用或压缩失败,将回退为\"按对话轮数截断\"的策略。"
|
||||
},
|
||||
"fallback_max_tokens": {
|
||||
"description": "上下文窗口兜底值",
|
||||
"hint": "当 max_context_tokens 为 0 且模型不在内置元数据中时,使用此值作为上下文窗口大小。默认 128000。"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"others": {
|
||||
"description": "其他配置",
|
||||
"agent_runner": {
|
||||
"config": {
|
||||
"misc": {
|
||||
"max_steps": { "description": "工具调用轮数上限" },
|
||||
"tool_schema_mode": {
|
||||
"description": "工具调用模式",
|
||||
"hint": "skills-like 先下发工具名称与描述,再下发参数;full 一次性下发完整参数。",
|
||||
"labels": ["Skills-like(两阶段)", "Full(完整参数)"]
|
||||
},
|
||||
"tool_call_timeout": {
|
||||
"description": "工具调用超时时间(秒)"
|
||||
},
|
||||
"sanitize_context_by_modalities": {
|
||||
"description": "按模型能力清理历史上下文",
|
||||
"hint": "开启后,在每次请求 LLM 前会按当前模型提供商中所选择的模型能力删除对话中不支持的图片/工具调用结构(会改变模型看到的历史)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"provider_settings": {
|
||||
"display_reasoning_text": {
|
||||
"description": "显示思考内容"
|
||||
},
|
||||
"llm_safety_mode": {
|
||||
"description": "健康模式",
|
||||
"hint": "引导模型输出健康、安全、积极的内容,避免有害或敏感话题。"
|
||||
},
|
||||
"safety_mode_strategy": {
|
||||
"description": "健康模式策略",
|
||||
"hint": "选择健康模式的实现方式。"
|
||||
},
|
||||
"identifier": {
|
||||
"description": "用户识别",
|
||||
"hint": "启用后,会在提示词前包含用户 ID 信息。"
|
||||
@@ -326,10 +409,6 @@
|
||||
"description": "合并 Agent 中间消息",
|
||||
"hint": "开启后,非流式模式下多步工具调用过程中产生的中间文本将缓冲,待 Agent 完成后合并为一条回复发送。"
|
||||
},
|
||||
"sanitize_context_by_modalities": {
|
||||
"description": "按模型能力清理历史上下文",
|
||||
"hint": "开启后,在每次请求 LLM 前会按当前模型提供商中所选择的模型能力删除对话中不支持的图片/工具调用结构(会改变模型看到的历史)"
|
||||
},
|
||||
"max_quoted_fallback_images": {
|
||||
"description": "转发消息中图片获取上限",
|
||||
"hint": "转发消息解析到的图片最多注入数量,超出部分会截断。"
|
||||
@@ -352,20 +431,6 @@
|
||||
"hint": "开启后,get_msg/get_forward_msg 全部尝试失败时输出 warning 日志。"
|
||||
}
|
||||
},
|
||||
"max_agent_step": {
|
||||
"description": "工具调用轮数上限"
|
||||
},
|
||||
"tool_call_timeout": {
|
||||
"description": "工具调用超时时间(秒)"
|
||||
},
|
||||
"tool_schema_mode": {
|
||||
"description": "工具调用模式",
|
||||
"hint": "skills-like 先下发工具名称与描述,再下发参数;full 一次性下发完整参数。",
|
||||
"labels": [
|
||||
"Skills-like(两阶段)",
|
||||
"Full(完整参数)"
|
||||
]
|
||||
},
|
||||
"streaming_response": {
|
||||
"description": "流式输出"
|
||||
},
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"messages": {
|
||||
"configApplied": "配置成功应用。如要保存,需再点击右下角保存按钮。",
|
||||
"configApplyError": "配置未应用,Json 格式错误。",
|
||||
"unsavedChangesNotice": "当前配置有未保存修改。请点击右下角保存按钮以生效。",
|
||||
"unsavedChangesNotice": "尚未保存,点击右下角按钮以保存。",
|
||||
"saveSuccess": "配置保存成功",
|
||||
"saveError": "配置保存失败",
|
||||
"loadError": "配置加载失败",
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"tabs": {
|
||||
"all": "全部",
|
||||
"chatCompletion": "对话",
|
||||
"agentRunner": "Agent 执行器",
|
||||
"speechToText": "语音转文字",
|
||||
"textToSpeech": "文字转语音",
|
||||
"embedding": "嵌入",
|
||||
@@ -49,7 +48,6 @@
|
||||
"title": "模型提供商",
|
||||
"tabs": {
|
||||
"basic": "对话",
|
||||
"agentRunner": "Agent 执行器",
|
||||
"speechToText": "语音转文字",
|
||||
"textToSpeech": "文字转语音",
|
||||
"embedding": "嵌入",
|
||||
|
||||
@@ -28,17 +28,6 @@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<v-slide-y-transition>
|
||||
<div v-if="fetched && hasUnsavedChanges" class="unsaved-changes-banner-wrap">
|
||||
<v-banner
|
||||
icon="$warning"
|
||||
lines="one"
|
||||
class="unsaved-changes-banner my-4"
|
||||
>
|
||||
{{ tm('messages.unsavedChangesNotice') }}
|
||||
</v-banner>
|
||||
</div>
|
||||
</v-slide-y-transition>
|
||||
<!-- <v-progress-linear v-if="!fetched" indeterminate color="primary"></v-progress-linear> -->
|
||||
|
||||
<v-slide-y-transition mode="out-in">
|
||||
@@ -83,6 +72,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-slide-y-reverse-transition>
|
||||
<div
|
||||
v-if="fetched && hasUnsavedChanges"
|
||||
class="unsaved-changes-pill"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<v-icon size="18">mdi-alert-circle-outline</v-icon>
|
||||
<span>{{ tm('messages.unsavedChangesNotice') }}</span>
|
||||
</div>
|
||||
</v-slide-y-reverse-transition>
|
||||
|
||||
|
||||
<!-- Full Screen Editor Dialog -->
|
||||
<v-dialog v-model="codeEditorDialog" fullscreen transition="dialog-bottom-transition" scrollable>
|
||||
@@ -965,24 +966,25 @@ export default {
|
||||
text-transform: none !important;
|
||||
}
|
||||
|
||||
.unsaved-changes-banner {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.v-theme--light .unsaved-changes-banner {
|
||||
background-color: #f1f4f9 !important;
|
||||
}
|
||||
|
||||
.v-theme--dark .unsaved-changes-banner {
|
||||
background-color: #2d2d2d !important;
|
||||
}
|
||||
|
||||
.unsaved-changes-banner-wrap {
|
||||
position: sticky;
|
||||
top: calc(var(--v-layout-top, 64px));
|
||||
z-index: 20;
|
||||
width: 100%;
|
||||
margin-bottom: 6px;
|
||||
.unsaved-changes-pill {
|
||||
position: fixed;
|
||||
left: calc(var(--v-layout-left, 0px) + 32px);
|
||||
bottom: 52px;
|
||||
z-index: 1005;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
max-width: min(440px, calc(100vw - var(--v-layout-left, 0px) - 160px));
|
||||
padding: 9px 14px 9px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(var(--v-theme-surface), 0.94);
|
||||
color: rgba(var(--v-theme-on-surface), 0.82);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);
|
||||
backdrop-filter: blur(16px);
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.25rem;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 按钮切换样式优化 */
|
||||
@@ -1047,6 +1049,14 @@ export default {
|
||||
width: 100%;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
.unsaved-changes-pill {
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
left: 16px;
|
||||
max-width: none;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
/* 测试聊天抽屉样式 */
|
||||
|
||||
@@ -293,34 +293,11 @@
|
||||
{{ snackbar.message }}
|
||||
</v-snackbar>
|
||||
|
||||
<v-dialog v-model="showAgentRunnerDialog" max-width="520" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="text-h3 pa-4 pb-0 pl-6 d-flex align-center">
|
||||
<v-icon start class="me-2">mdi-information</v-icon>
|
||||
请前往「配置文件」页测试 Agent 执行器
|
||||
</v-card-title>
|
||||
<v-card-text class="py-4 text-body-1 text-medium-emphasis">
|
||||
Agent 执行器的测试请在「配置文件」页进行。
|
||||
<ol class="ml-4 mt-4 mb-4">
|
||||
<li>找到对应的配置文件并打开。</li>
|
||||
<li>找到 Agent 执行方式部分,修改执行器后点击保存。</li>
|
||||
<li>点击右下角的 💬 聊天按钮进行测试。</li>
|
||||
</ol>
|
||||
要让机器人应用这个 Agent 执行器,你也需要前往修改 Agent 执行器。
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" variant="text" @click="showAgentRunnerDialog = false">好的</v-btn>
|
||||
<v-btn color="primary" variant="tonal" @click="goToConfigPage">点击前往</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { providerApi } from '@/api/v1'
|
||||
import { useModuleI18n } from '@/i18n/composables'
|
||||
import AstrBotConfig from '@/components/shared/AstrBotConfig.vue'
|
||||
@@ -337,7 +314,6 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const { tm } = useModuleI18n('features/provider')
|
||||
const router = useRouter()
|
||||
|
||||
const snackbar = ref({
|
||||
show: false,
|
||||
@@ -405,7 +381,6 @@ const newProviderOriginalId = ref('')
|
||||
const updatingMode = ref(false)
|
||||
const loading = ref(false)
|
||||
const isLegacyProviderModified = ref(false)
|
||||
const showAgentRunnerDialog = ref(false)
|
||||
const showManualModelDialog = ref(false)
|
||||
let suppressLegacyProviderWatch = false
|
||||
|
||||
@@ -663,21 +638,8 @@ async function testSingleProvider(provider) {
|
||||
showMessage('该提供商未被用户启用', 'error')
|
||||
return
|
||||
}
|
||||
if (
|
||||
provider.provider_type === 'agent_runner' ||
|
||||
selectedProviderType.value === 'agent_runner'
|
||||
) {
|
||||
showAgentRunnerDialog.value = true
|
||||
return
|
||||
}
|
||||
|
||||
await testProvider(provider)
|
||||
}
|
||||
|
||||
function goToConfigPage() {
|
||||
router.push('/config')
|
||||
showAgentRunnerDialog.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -380,13 +380,15 @@ async function syncDefaultConfigProviderIfNeeded() {
|
||||
if (!targetProviderId) return;
|
||||
|
||||
const configData = await fetchDefaultConfig();
|
||||
if (!configData.provider_settings) {
|
||||
configData.provider_settings = {};
|
||||
if (configData?.agent_runner?.runner_type !== 'local') {
|
||||
return;
|
||||
}
|
||||
const modelConfig = configData.agent_runner.config?.model;
|
||||
if (!modelConfig) return;
|
||||
|
||||
if (configData.provider_settings.default_provider_id === targetProviderId) return;
|
||||
if (modelConfig.provider_id === targetProviderId) return;
|
||||
|
||||
configData.provider_settings.default_provider_id = targetProviderId;
|
||||
modelConfig.provider_id = targetProviderId;
|
||||
|
||||
const updateRes = await configProfileApi.update('default', configData);
|
||||
if (updateRes.data.status !== 'ok') {
|
||||
|
||||
@@ -32,23 +32,16 @@ async def test_clear_third_party_agent_runner_state_deletes_deerflow_thread_befo
|
||||
|
||||
context = SimpleNamespace(
|
||||
get_config=lambda **kwargs: {
|
||||
"provider_settings": {
|
||||
"deerflow_agent_runner_provider_id": "deerflow-runner"
|
||||
}
|
||||
},
|
||||
provider_manager=SimpleNamespace(
|
||||
get_provider_config_by_id=lambda provider_id, merged=False: (
|
||||
{
|
||||
"id": provider_id,
|
||||
"agent_runner": {
|
||||
"runner_type": "deerflow",
|
||||
"config": {
|
||||
"deerflow_api_base": "http://127.0.0.1:2026",
|
||||
"deerflow_api_key": "token",
|
||||
"deerflow_auth_header": "",
|
||||
"proxy": "",
|
||||
}
|
||||
if merged
|
||||
else {"id": provider_id}
|
||||
),
|
||||
),
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
monkeypatch.setattr(conversation_module, "DeerFlowAPIClient", FakeClient)
|
||||
@@ -99,23 +92,16 @@ async def test_clear_third_party_agent_runner_state_removes_local_state_when_dee
|
||||
|
||||
context = SimpleNamespace(
|
||||
get_config=lambda **kwargs: {
|
||||
"provider_settings": {
|
||||
"deerflow_agent_runner_provider_id": "deerflow-runner"
|
||||
}
|
||||
},
|
||||
provider_manager=SimpleNamespace(
|
||||
get_provider_config_by_id=lambda provider_id, merged=False: (
|
||||
{
|
||||
"id": provider_id,
|
||||
"agent_runner": {
|
||||
"runner_type": "deerflow",
|
||||
"config": {
|
||||
"deerflow_api_base": "http://127.0.0.1:2026",
|
||||
"deerflow_api_key": "",
|
||||
"deerflow_auth_header": "",
|
||||
"proxy": "",
|
||||
}
|
||||
if merged
|
||||
else {"id": provider_id}
|
||||
),
|
||||
),
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
monkeypatch.setattr(conversation_module, "DeerFlowAPIClient", FakeClient)
|
||||
@@ -156,23 +142,16 @@ async def test_clear_third_party_agent_runner_state_removes_local_state_when_dee
|
||||
|
||||
context = SimpleNamespace(
|
||||
get_config=lambda **kwargs: {
|
||||
"provider_settings": {
|
||||
"deerflow_agent_runner_provider_id": "deerflow-runner"
|
||||
}
|
||||
},
|
||||
provider_manager=SimpleNamespace(
|
||||
get_provider_config_by_id=lambda provider_id, merged=False: (
|
||||
{
|
||||
"id": provider_id,
|
||||
"agent_runner": {
|
||||
"runner_type": "deerflow",
|
||||
"config": {
|
||||
"deerflow_api_base": "http://127.0.0.1:2026",
|
||||
"deerflow_api_key": "",
|
||||
"deerflow_auth_header": "",
|
||||
"proxy": "",
|
||||
}
|
||||
if merged
|
||||
else {"id": provider_id}
|
||||
),
|
||||
),
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
monkeypatch.setattr(conversation_module, "DeerFlowAPIClient", FakeClient)
|
||||
|
||||
@@ -0,0 +1,547 @@
|
||||
import copy
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from astrbot.core.config.agent_runner import (
|
||||
AGENT_RUNNER_CONFIG_DEFAULTS,
|
||||
get_agent_runner_config_default,
|
||||
normalize_agent_runner,
|
||||
)
|
||||
from astrbot.core.config.astrbot_config import AstrBotConfig
|
||||
from astrbot.core.config.default import DEFAULT_CONFIG
|
||||
from astrbot.core.pipeline.process_stage.stage import AgentRequestSubStage
|
||||
from astrbot.core.utils.migra_helper import (
|
||||
_migrate_agent_runner_config,
|
||||
finalize_config_migrations,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"runner_type", ["local", "dify", "coze", "dashscope", "deerflow"]
|
||||
)
|
||||
def test_agent_runner_defaults_are_isolated_and_normalized(runner_type: str):
|
||||
first = get_agent_runner_config_default(runner_type)
|
||||
second = get_agent_runner_config_default(runner_type)
|
||||
|
||||
first["test_mutation"] = True
|
||||
|
||||
assert second == AGENT_RUNNER_CONFIG_DEFAULTS[runner_type]
|
||||
assert normalize_agent_runner({"runner_type": runner_type, "config": second}) == {
|
||||
"runner_type": runner_type,
|
||||
"config": second,
|
||||
}
|
||||
if runner_type != "local":
|
||||
assert "persona_id" not in second
|
||||
|
||||
|
||||
def test_switching_runner_type_discards_previous_runner_fields():
|
||||
normalized = normalize_agent_runner(
|
||||
{
|
||||
"runner_type": "dify",
|
||||
"config": {
|
||||
"provider_id": "legacy-provider",
|
||||
"persona_id": "legacy-persona",
|
||||
"model": {"provider_id": "chat-model"},
|
||||
"dify_api_key": "secret",
|
||||
"unexpected": True,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
assert normalized["config"] == {
|
||||
**get_agent_runner_config_default("dify"),
|
||||
"dify_api_key": "secret",
|
||||
}
|
||||
assert "provider_id" not in normalized["config"]
|
||||
assert "persona_id" not in normalized["config"]
|
||||
assert "model" not in normalized["config"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_request_normalizes_incomplete_runner_config():
|
||||
config = {
|
||||
"wake_prefix": [],
|
||||
"provider_settings": {
|
||||
"wake_prefix": "",
|
||||
"streaming_response": True,
|
||||
"unsupported_streaming_strategy": "aggregate",
|
||||
},
|
||||
"agent_runner": {
|
||||
"runner_type": "dify",
|
||||
"config": {"dify_api_key": "saved-key"},
|
||||
},
|
||||
}
|
||||
stage = AgentRequestSubStage()
|
||||
|
||||
await stage.initialize(SimpleNamespace(astrbot_config=config))
|
||||
|
||||
assert stage.agent_sub_stage.runner_config == {
|
||||
**get_agent_runner_config_default("dify"),
|
||||
"dify_api_key": "saved-key",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"runner_type", ["local", "dify", "coze", "dashscope", "deerflow"]
|
||||
)
|
||||
def test_each_runner_configuration_round_trips(tmp_path, runner_type: str):
|
||||
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||
expected = {
|
||||
"runner_type": runner_type,
|
||||
"config": get_agent_runner_config_default(runner_type),
|
||||
}
|
||||
config["agent_runner"] = expected
|
||||
config_path = tmp_path / f"{runner_type}.json"
|
||||
config_path.write_text(json.dumps(config), encoding="utf-8")
|
||||
|
||||
loaded = AstrBotConfig(config_path=str(config_path))
|
||||
loaded.save_config()
|
||||
reloaded = AstrBotConfig(config_path=str(config_path))
|
||||
|
||||
assert reloaded["agent_runner"] == expected
|
||||
|
||||
|
||||
def test_local_legacy_fields_are_fully_migrated():
|
||||
config = {
|
||||
"config_version": 2,
|
||||
"provider": [],
|
||||
"provider_settings": {
|
||||
"agent_runner_type": "local",
|
||||
"default_provider_id": "chat-main",
|
||||
"fallback_chat_models": ["chat-backup"],
|
||||
"request_max_retries": 7,
|
||||
"default_personality": "developer",
|
||||
"llm_safety_mode": False,
|
||||
"safety_mode_strategy": "system_prompt",
|
||||
"max_agent_step": 42,
|
||||
"tool_schema_mode": "skills_like",
|
||||
"tool_call_timeout": 88,
|
||||
"sanitize_context_by_modalities": True,
|
||||
"context_limit_reached_strategy": "truncate_by_turns",
|
||||
"llm_compress_instruction": "Summarize",
|
||||
"llm_compress_keep_recent_ratio": 0.2,
|
||||
"llm_compress_provider_id": "compressor",
|
||||
"max_context_length": 20,
|
||||
"dequeue_context_length": 3,
|
||||
"fallback_max_context_tokens": 64000,
|
||||
},
|
||||
}
|
||||
|
||||
default_config = {
|
||||
"provider": [
|
||||
{"id": "chat-main", "provider_type": "chat_completion"},
|
||||
{"id": "chat-backup", "provider_type": "chat_completion"},
|
||||
{"id": "compressor", "provider_type": "chat_completion"},
|
||||
]
|
||||
}
|
||||
assert _migrate_agent_runner_config(config, default_config)
|
||||
assert config["config_version"] == 3
|
||||
assert config["agent_runner"] == {
|
||||
"runner_type": "local",
|
||||
"config": {
|
||||
"model": {
|
||||
"provider_id": "chat-main",
|
||||
"fallback_provider_ids": ["chat-backup"],
|
||||
"request_max_retries": 7,
|
||||
},
|
||||
"persona": {
|
||||
"persona_id": "developer",
|
||||
"safety_mode": False,
|
||||
"safety_mode_strategy": "system_prompt",
|
||||
},
|
||||
"compression": {
|
||||
"max_turns": 20,
|
||||
"trim_turns": 3,
|
||||
"overflow_strategy": "truncate_by_turns",
|
||||
"instruction": "Summarize",
|
||||
"keep_recent_ratio": 0.2,
|
||||
"provider_id": "compressor",
|
||||
"fallback_max_tokens": 64000,
|
||||
},
|
||||
"misc": {
|
||||
"max_steps": 42,
|
||||
"tool_schema_mode": "skills_like",
|
||||
"tool_call_timeout": 88,
|
||||
"sanitize_context_by_modalities": True,
|
||||
},
|
||||
},
|
||||
}
|
||||
assert not {
|
||||
"agent_runner_type",
|
||||
"default_provider_id",
|
||||
"fallback_chat_models",
|
||||
"request_max_retries",
|
||||
"default_personality",
|
||||
"max_agent_step",
|
||||
"tool_call_timeout",
|
||||
"sanitize_context_by_modalities",
|
||||
}.intersection(config["provider_settings"])
|
||||
|
||||
|
||||
def test_local_migration_replaces_default_root_inserted_before_version_bump():
|
||||
config = {
|
||||
"config_version": 2,
|
||||
"provider": [
|
||||
{"id": "chat-main", "provider_type": "chat_completion"},
|
||||
{"id": "compressor", "provider_type": "chat_completion"},
|
||||
],
|
||||
"provider_settings": {
|
||||
"agent_runner_type": "local",
|
||||
"default_provider_id": "chat-main",
|
||||
"fallback_chat_models": ["chat-backup"],
|
||||
"request_max_retries": 8,
|
||||
"default_personality": "developer",
|
||||
"llm_safety_mode": False,
|
||||
"max_agent_step": 48,
|
||||
"tool_call_timeout": 96,
|
||||
"sanitize_context_by_modalities": True,
|
||||
"context_limit_reached_strategy": "llm_compress",
|
||||
"llm_compress_instruction": "Keep decisions",
|
||||
"llm_compress_provider_id": "compressor",
|
||||
"max_context_length": 24,
|
||||
"dequeue_context_length": 4,
|
||||
},
|
||||
"agent_runner": {
|
||||
"runner_type": "local",
|
||||
"config": get_agent_runner_config_default("local"),
|
||||
},
|
||||
}
|
||||
|
||||
default_config = {
|
||||
"provider": [
|
||||
{"id": "chat-main", "provider_type": "chat_completion"},
|
||||
{"id": "chat-backup", "provider_type": "chat_completion"},
|
||||
{"id": "compressor", "provider_type": "chat_completion"},
|
||||
]
|
||||
}
|
||||
assert _migrate_agent_runner_config(config, default_config)
|
||||
|
||||
assert config["agent_runner"] == {
|
||||
"runner_type": "local",
|
||||
"config": {
|
||||
"model": {
|
||||
"provider_id": "chat-main",
|
||||
"fallback_provider_ids": ["chat-backup"],
|
||||
"request_max_retries": 8,
|
||||
},
|
||||
"persona": {
|
||||
"persona_id": "developer",
|
||||
"safety_mode": False,
|
||||
"safety_mode_strategy": "system_prompt",
|
||||
},
|
||||
"compression": {
|
||||
"max_turns": 24,
|
||||
"trim_turns": 4,
|
||||
"overflow_strategy": "llm_compress",
|
||||
"instruction": "Keep decisions",
|
||||
"keep_recent_ratio": 0.15,
|
||||
"provider_id": "compressor",
|
||||
"fallback_max_tokens": 128000,
|
||||
},
|
||||
"misc": {
|
||||
"max_steps": 48,
|
||||
"tool_schema_mode": "full",
|
||||
"tool_call_timeout": 96,
|
||||
"sanitize_context_by_modalities": True,
|
||||
},
|
||||
},
|
||||
}
|
||||
assert not set(config["provider_settings"]).intersection(
|
||||
{
|
||||
"agent_runner_type",
|
||||
"default_provider_id",
|
||||
"fallback_chat_models",
|
||||
"default_personality",
|
||||
"llm_compress_provider_id",
|
||||
"tool_call_timeout",
|
||||
"sanitize_context_by_modalities",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_missing_local_provider_references_are_removed():
|
||||
config = {
|
||||
"provider": [],
|
||||
"provider_settings": {
|
||||
"agent_runner_type": "local",
|
||||
"default_provider_id": "missing-main",
|
||||
"fallback_chat_models": ["available", "missing-fallback"],
|
||||
"llm_compress_provider_id": "missing-compressor",
|
||||
},
|
||||
}
|
||||
global_config = {
|
||||
"provider": [{"id": "available", "provider_type": "chat_completion"}]
|
||||
}
|
||||
|
||||
_migrate_agent_runner_config(config, global_config)
|
||||
|
||||
runner_config = config["agent_runner"]["config"]
|
||||
assert runner_config["model"]["provider_id"] == ""
|
||||
assert runner_config["model"]["fallback_provider_ids"] == ["available"]
|
||||
assert runner_config["compression"]["provider_id"] == ""
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("runner_type", "provider_config", "expected_key"),
|
||||
[
|
||||
(
|
||||
"dify",
|
||||
{"dify_api_key": "dify-key", "dify_api_type": "workflow"},
|
||||
"dify_api_key",
|
||||
),
|
||||
(
|
||||
"coze",
|
||||
{"coze_api_key": "coze-key", "bot_id": "bot"},
|
||||
"coze_api_key",
|
||||
),
|
||||
(
|
||||
"dashscope",
|
||||
{"dashscope_api_key": "dash-key", "dashscope_app_id": "app"},
|
||||
"dashscope_api_key",
|
||||
),
|
||||
(
|
||||
"deerflow",
|
||||
{"deerflow_api_key": "deer-key", "deerflow_plan_mode": True},
|
||||
"deerflow_api_key",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_third_party_provider_config_is_copied_inline(
|
||||
runner_type: str,
|
||||
provider_config: dict,
|
||||
expected_key: str,
|
||||
):
|
||||
provider_id = f"{runner_type}-provider"
|
||||
config = {
|
||||
"config_version": 2,
|
||||
"provider": [],
|
||||
"provider_settings": {
|
||||
"agent_runner_type": runner_type,
|
||||
f"{runner_type}_agent_runner_provider_id": provider_id,
|
||||
"default_personality": "operator",
|
||||
},
|
||||
}
|
||||
global_config = {
|
||||
"provider": [
|
||||
{
|
||||
"id": provider_id,
|
||||
"type": runner_type,
|
||||
"provider": runner_type,
|
||||
"provider_type": "agent_runner",
|
||||
"enable": True,
|
||||
**provider_config,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
assert _migrate_agent_runner_config(config, global_config)
|
||||
assert finalize_config_migrations([global_config, config])
|
||||
|
||||
runner_config = config["agent_runner"]["config"]
|
||||
assert config["agent_runner"]["runner_type"] == runner_type
|
||||
assert runner_config[expected_key] == provider_config[expected_key]
|
||||
assert not {
|
||||
"id",
|
||||
"type",
|
||||
"provider",
|
||||
"provider_type",
|
||||
"enable",
|
||||
"persona_id",
|
||||
}.intersection(runner_config)
|
||||
assert global_config["provider"] == []
|
||||
|
||||
|
||||
def test_profile_migration_merges_runner_provider_source(
|
||||
tmp_path, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
data_path = tmp_path / "data"
|
||||
profile_path = data_path / "config" / "abconf_profile.json"
|
||||
profile_path.parent.mkdir(parents=True)
|
||||
default_config = {
|
||||
"provider_sources": [
|
||||
{
|
||||
"id": "dify-source",
|
||||
"type": "dify",
|
||||
"provider_type": "agent_runner",
|
||||
"dify_api_key": "source-key",
|
||||
"dify_api_base": "https://example.test/v1",
|
||||
}
|
||||
],
|
||||
"provider": [
|
||||
{
|
||||
"id": "dify-provider",
|
||||
"provider_source_id": "dify-source",
|
||||
"enable": True,
|
||||
}
|
||||
],
|
||||
}
|
||||
(data_path / "cmd_config.json").write_text(
|
||||
json.dumps(default_config), encoding="utf-8"
|
||||
)
|
||||
profile_config = {
|
||||
"config_version": 2,
|
||||
"provider": [],
|
||||
"provider_settings": {
|
||||
"agent_runner_type": "dify",
|
||||
"dify_agent_runner_provider_id": "dify-provider",
|
||||
},
|
||||
}
|
||||
profile_path.write_text(json.dumps(profile_config), encoding="utf-8")
|
||||
monkeypatch.setattr(
|
||||
"astrbot.core.utils.migra_helper.get_astrbot_config_path",
|
||||
lambda: str(profile_path.parent),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"astrbot.core.utils.migra_helper.get_astrbot_data_path",
|
||||
lambda: str(data_path),
|
||||
)
|
||||
|
||||
loaded = AstrBotConfig(config_path=str(profile_path))
|
||||
|
||||
assert loaded["agent_runner"]["runner_type"] == "dify"
|
||||
assert loaded["agent_runner"]["config"]["dify_api_key"] == "source-key"
|
||||
assert (
|
||||
loaded["agent_runner"]["config"]["dify_api_base"] == "https://example.test/v1"
|
||||
)
|
||||
assert "provider_source_id" not in loaded["agent_runner"]["config"]
|
||||
|
||||
assert finalize_config_migrations([default_config, loaded])
|
||||
assert default_config["provider"] == []
|
||||
|
||||
|
||||
def test_missing_third_party_provider_uses_runner_defaults():
|
||||
config = {
|
||||
"provider": [],
|
||||
"provider_settings": {
|
||||
"agent_runner_type": "coze",
|
||||
"coze_agent_runner_provider_id": "missing",
|
||||
"default_personality": "operator",
|
||||
},
|
||||
}
|
||||
|
||||
_migrate_agent_runner_config(config)
|
||||
|
||||
expected = get_agent_runner_config_default("coze")
|
||||
assert config["agent_runner"] == {
|
||||
"runner_type": "coze",
|
||||
"config": expected,
|
||||
}
|
||||
|
||||
|
||||
def test_legacy_default_provider_only_selects_actual_third_party_runner():
|
||||
chat_config = {
|
||||
"provider": [],
|
||||
"provider_settings": {
|
||||
"agent_runner_type": "local",
|
||||
"default_provider_id": "chat-model",
|
||||
},
|
||||
}
|
||||
runner_config = {
|
||||
"provider": [],
|
||||
"provider_settings": {
|
||||
"agent_runner_type": "local",
|
||||
"default_provider_id": "dify-provider",
|
||||
},
|
||||
}
|
||||
global_config = {
|
||||
"provider": [
|
||||
{
|
||||
"id": "chat-model",
|
||||
"type": "openai_chat_completion",
|
||||
"provider_type": "chat_completion",
|
||||
},
|
||||
{
|
||||
"id": "dify-provider",
|
||||
"type": "dify",
|
||||
"provider_type": "agent_runner",
|
||||
"dify_api_key": "secret",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
_migrate_agent_runner_config(chat_config, global_config)
|
||||
_migrate_agent_runner_config(runner_config, global_config)
|
||||
|
||||
assert chat_config["agent_runner"]["runner_type"] == "local"
|
||||
assert chat_config["agent_runner"]["config"]["model"]["provider_id"] == "chat-model"
|
||||
assert runner_config["agent_runner"]["runner_type"] == "dify"
|
||||
assert runner_config["agent_runner"]["config"]["dify_api_key"] == "secret"
|
||||
|
||||
|
||||
def test_multiple_profiles_can_copy_one_provider_and_migration_is_idempotent():
|
||||
global_config = {
|
||||
"provider": [
|
||||
{
|
||||
"id": "shared-deerflow",
|
||||
"type": "deerflow",
|
||||
"provider_type": "agent_runner",
|
||||
"deerflow_api_key": "shared-key",
|
||||
},
|
||||
{
|
||||
"id": "unused-coze",
|
||||
"type": "coze",
|
||||
"provider_type": "agent_runner",
|
||||
"coze_api_key": "unused",
|
||||
},
|
||||
{
|
||||
"id": "unused-custom-runner",
|
||||
"type": "custom-runner",
|
||||
"provider_type": "agent_runner",
|
||||
},
|
||||
],
|
||||
"provider_settings": {},
|
||||
"agent_runner": {
|
||||
"runner_type": "local",
|
||||
"config": get_agent_runner_config_default("local"),
|
||||
},
|
||||
}
|
||||
profiles = []
|
||||
for persona_id in ("one", "two"):
|
||||
profile = {
|
||||
"provider": [],
|
||||
"provider_settings": {
|
||||
"agent_runner_type": "deerflow",
|
||||
"deerflow_agent_runner_provider_id": "shared-deerflow",
|
||||
"default_personality": persona_id,
|
||||
},
|
||||
}
|
||||
_migrate_agent_runner_config(profile, global_config)
|
||||
profiles.append(profile)
|
||||
|
||||
finalize_config_migrations([global_config, *profiles])
|
||||
first_result = copy.deepcopy([global_config, *profiles])
|
||||
|
||||
assert [
|
||||
profile["agent_runner"]["config"]["deerflow_api_key"] for profile in profiles
|
||||
] == ["shared-key", "shared-key"]
|
||||
assert all(
|
||||
"persona_id" not in profile["agent_runner"]["config"] for profile in profiles
|
||||
)
|
||||
assert global_config["provider"] == []
|
||||
assert not finalize_config_migrations([global_config, *profiles])
|
||||
assert [global_config, *profiles] == first_result
|
||||
|
||||
|
||||
def test_new_agent_runner_config_is_authoritative_and_opaque_on_reload(tmp_path):
|
||||
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||
config["config_version"] = 2
|
||||
config["provider_settings"]["agent_runner_type"] = "coze"
|
||||
config["agent_runner"] = {
|
||||
"runner_type": "dify",
|
||||
"config": {
|
||||
**get_agent_runner_config_default("dify"),
|
||||
"dify_api_key": "saved-key",
|
||||
"variables": {"nested": {"value": 1}},
|
||||
},
|
||||
}
|
||||
config_path = tmp_path / "config.json"
|
||||
config_path.write_text(json.dumps(config), encoding="utf-8")
|
||||
|
||||
loaded = AstrBotConfig(config_path=str(config_path))
|
||||
|
||||
assert loaded["agent_runner"]["runner_type"] == "dify"
|
||||
assert loaded["agent_runner"]["config"]["dify_api_key"] == "saved-key"
|
||||
assert loaded["agent_runner"]["config"]["variables"] == {"nested": {"value": 1}}
|
||||
assert "agent_runner_type" not in loaded["provider_settings"]
|
||||
@@ -495,7 +495,15 @@ async def test_background_wakeup_applies_max_agent_step(
|
||||
parameters={"type": "object", "properties": {}},
|
||||
)
|
||||
context = SimpleNamespace(
|
||||
get_config=lambda **_kwargs: {"provider_settings": dict(provider_settings)},
|
||||
get_config=lambda **_kwargs: {
|
||||
"provider_settings": {},
|
||||
"agent_runner": {
|
||||
"runner_type": "local",
|
||||
"config": {
|
||||
"misc": {"max_steps": provider_settings.get("max_agent_step", 30)}
|
||||
},
|
||||
},
|
||||
},
|
||||
get_llm_tool_manager=lambda: SimpleNamespace(
|
||||
get_builtin_tool=lambda _tool_cls: send_tool
|
||||
),
|
||||
|
||||
@@ -1874,9 +1874,7 @@ class TestBuildMainAgent:
|
||||
llm_safety_mode=False,
|
||||
computer_use_runtime="none",
|
||||
add_cron_tools=False,
|
||||
provider_settings={
|
||||
"fallback_chat_models": ["image-provider"],
|
||||
},
|
||||
fallback_provider_ids=["image-provider"],
|
||||
),
|
||||
provider=text_provider,
|
||||
req=req,
|
||||
|
||||
@@ -278,7 +278,7 @@ class TestAstrBotCoreLifecycleDefaultChatProviderWarning:
|
||||
provider_a = self._make_provider("openai_source/model-a")
|
||||
provider_b = self._make_provider("openai_source/model-b")
|
||||
lifecycle.provider_manager = MagicMock(
|
||||
provider_settings={"default_provider_id": ""},
|
||||
default_chat_provider_id="",
|
||||
provider_insts=[provider_a, provider_b],
|
||||
curr_provider_inst=provider_b,
|
||||
)
|
||||
@@ -293,7 +293,7 @@ class TestAstrBotCoreLifecycleDefaultChatProviderWarning:
|
||||
def test_warns_only_once_per_lifecycle(self, mock_log_broker, mock_db):
|
||||
lifecycle = AstrBotCoreLifecycle(mock_log_broker, mock_db)
|
||||
lifecycle.provider_manager = MagicMock(
|
||||
provider_settings={"default_provider_id": ""},
|
||||
default_chat_provider_id="",
|
||||
provider_insts=[
|
||||
self._make_provider("openai_source/model-a"),
|
||||
self._make_provider("openai_source/model-b"),
|
||||
@@ -312,7 +312,7 @@ class TestAstrBotCoreLifecycleDefaultChatProviderWarning:
|
||||
):
|
||||
lifecycle = AstrBotCoreLifecycle(mock_log_broker, mock_db)
|
||||
lifecycle.provider_manager = MagicMock(
|
||||
provider_settings={"default_provider_id": ""},
|
||||
default_chat_provider_id="",
|
||||
provider_insts=[self._make_provider("openai_source/model-a")],
|
||||
curr_provider_inst=self._make_provider("openai_source/model-a"),
|
||||
)
|
||||
@@ -327,7 +327,7 @@ class TestAstrBotCoreLifecycleDefaultChatProviderWarning:
|
||||
):
|
||||
lifecycle = AstrBotCoreLifecycle(mock_log_broker, mock_db)
|
||||
lifecycle.provider_manager = MagicMock(
|
||||
provider_settings={"default_provider_id": "openai_source/model-a"},
|
||||
default_chat_provider_id="openai_source/model-a",
|
||||
provider_insts=[
|
||||
self._make_provider("openai_source/model-a"),
|
||||
self._make_provider("openai_source/model-b"),
|
||||
@@ -347,7 +347,7 @@ class TestAstrBotCoreLifecycleDefaultChatProviderWarning:
|
||||
provider_a = self._make_provider("openai_source/model-a")
|
||||
provider_b = self._make_provider("openai_source/model-b")
|
||||
lifecycle.provider_manager = MagicMock(
|
||||
provider_settings={"default_provider_id": ""},
|
||||
default_chat_provider_id="",
|
||||
provider_insts=[provider_a, provider_b],
|
||||
curr_provider_inst=None,
|
||||
)
|
||||
@@ -364,7 +364,7 @@ class TestAstrBotCoreLifecycleDefaultChatProviderWarning:
|
||||
):
|
||||
lifecycle = AstrBotCoreLifecycle(mock_log_broker, mock_db)
|
||||
lifecycle.provider_manager = MagicMock(
|
||||
provider_settings={"default_provider_id": "non-existent-id"},
|
||||
default_chat_provider_id="non-existent-id",
|
||||
provider_insts=[
|
||||
self._make_provider("openai_source/model-a"),
|
||||
self._make_provider("openai_source/model-b"),
|
||||
|
||||
@@ -591,13 +591,16 @@ class TestRunActiveAgentJob:
|
||||
):
|
||||
"""Test active cron agent keeps structured history and provider settings."""
|
||||
provider_settings = {
|
||||
"tool_call_timeout": 77,
|
||||
"fallback_chat_models": ["fallback-provider"],
|
||||
}
|
||||
ctx = MagicMock()
|
||||
ctx.get_config.return_value = {
|
||||
"admins_id": [],
|
||||
"provider_settings": provider_settings,
|
||||
"agent_runner": {
|
||||
"runner_type": "local",
|
||||
"config": {"misc": {"tool_call_timeout": 77}},
|
||||
},
|
||||
}
|
||||
cron_manager.ctx = ctx
|
||||
|
||||
@@ -695,7 +698,13 @@ class TestRunActiveAgentJob:
|
||||
ctx = MagicMock()
|
||||
ctx.get_config.return_value = {
|
||||
"admins_id": [],
|
||||
"provider_settings": dict(provider_settings),
|
||||
"provider_settings": {},
|
||||
"agent_runner": {
|
||||
"runner_type": "local",
|
||||
"config": {
|
||||
"misc": {"max_steps": provider_settings.get("max_agent_step", 30)}
|
||||
},
|
||||
},
|
||||
}
|
||||
cron_manager.ctx = ctx
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from astrbot.core.message.message_event_result import MessageChain
|
||||
from astrbot.core.provider.entities import LLMResponse
|
||||
from astrbot.core.pipeline.process_stage.method.agent_sub_stages import third_party
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("runner_type", "runner_class_name", "config_key"),
|
||||
[
|
||||
("dify", "DifyAgentRunner", "dify_api_key"),
|
||||
("coze", "CozeAgentRunner", "coze_api_key"),
|
||||
("dashscope", "DashscopeAgentRunner", "dashscope_api_key"),
|
||||
("deerflow", "DeerFlowAgentRunner", "deerflow_api_key"),
|
||||
],
|
||||
)
|
||||
async def test_third_party_runner_receives_inline_profile_config(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
runner_type: str,
|
||||
runner_class_name: str,
|
||||
config_key: str,
|
||||
):
|
||||
inline_config = {config_key: "inline-secret"}
|
||||
runner = MagicMock()
|
||||
runner.reset = AsyncMock()
|
||||
runner.get_final_llm_resp.return_value = LLMResponse(
|
||||
role="assistant",
|
||||
result_chain=MessageChain().message("done"),
|
||||
)
|
||||
runner.close = AsyncMock()
|
||||
|
||||
async def step_until_done(max_step: int = 30):
|
||||
_ = max_step
|
||||
if False:
|
||||
yield None
|
||||
|
||||
runner.step_until_done = step_until_done
|
||||
runner_factory_calls = []
|
||||
|
||||
class RunnerFactory:
|
||||
@classmethod
|
||||
def __class_getitem__(cls, item):
|
||||
_ = item
|
||||
return cls
|
||||
|
||||
def __new__(cls):
|
||||
runner_factory_calls.append(True)
|
||||
return runner
|
||||
|
||||
monkeypatch.setattr(third_party, runner_class_name, RunnerFactory)
|
||||
monkeypatch.setattr(
|
||||
third_party, "AstrAgentContext", MagicMock(return_value=object())
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
third_party, "AgentContextWrapper", MagicMock(return_value=object())
|
||||
)
|
||||
monkeypatch.setattr(third_party, "call_event_hook", AsyncMock(return_value=False))
|
||||
monkeypatch.setattr(third_party.Metric, "upload", AsyncMock(return_value=None))
|
||||
|
||||
config = {
|
||||
"agent_runner": {"runner_type": runner_type, "config": inline_config},
|
||||
"provider_settings": {
|
||||
"streaming_response": False,
|
||||
"unsupported_streaming_strategy": "turn_off",
|
||||
"third_party_stream_consumption_close_timeout_sec": 30,
|
||||
},
|
||||
}
|
||||
stage = third_party.ThirdPartyAgentSubStage()
|
||||
await stage.initialize(
|
||||
SimpleNamespace(
|
||||
astrbot_config=config,
|
||||
plugin_manager=SimpleNamespace(
|
||||
context=SimpleNamespace(
|
||||
conversation_manager=MagicMock(),
|
||||
persona_manager=MagicMock(),
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
stage._resolve_persona_custom_error_message = AsyncMock(return_value=None)
|
||||
event = MagicMock()
|
||||
event.message_str = "hello"
|
||||
event.unified_msg_origin = "webchat:FriendMessage:test"
|
||||
event.message_obj.message = []
|
||||
event.platform_meta.support_streaming_message = True
|
||||
event.get_extra.return_value = None
|
||||
|
||||
results = [item async for item in stage.process(event, "")]
|
||||
|
||||
assert results == [None]
|
||||
assert runner.reset.await_args.kwargs["provider_config"] is inline_config
|
||||
assert runner_factory_calls == [True]
|
||||
Reference in New Issue
Block a user