mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-30 04:15:45 +08:00
186 lines
5.9 KiB
Python
186 lines
5.9 KiB
Python
"""Configuration parsing and validation for local-agent runner."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import typing
|
|
|
|
DEFAULT_MAX_TOOL_ITERATIONS = 100
|
|
DEFAULT_MAX_TOOL_RESULT_CHARS = 20000
|
|
DEFAULT_RUN_TIMEOUT_SECONDS = 300
|
|
DEFAULT_TOOL_EXECUTION_MODE = "parallel"
|
|
VALID_TOOL_EXECUTION_MODES = {"parallel", "serial"}
|
|
|
|
|
|
def parse_model_config(
|
|
model_config: typing.Any,
|
|
allowed_model_ids: set[str],
|
|
) -> list[str]:
|
|
"""Parse model configuration into ordered list of model IDs.
|
|
|
|
Supports the Protocol v1 model-fallback-selector shape:
|
|
{"primary": "...", "fallbacks": [...]}
|
|
|
|
Filters out models not in allowed_model_ids.
|
|
|
|
Args:
|
|
model_config: Model configuration from ctx.config["model"]
|
|
allowed_model_ids: Set of authorized model IDs from ctx.resources
|
|
|
|
Returns:
|
|
Ordered list of model IDs to try (primary first, then fallbacks)
|
|
Empty list if no valid models configured.
|
|
"""
|
|
candidates: list[str] = []
|
|
|
|
if model_config is None:
|
|
return candidates
|
|
|
|
if isinstance(model_config, dict):
|
|
# Primary model
|
|
primary = model_config.get("primary")
|
|
if primary:
|
|
primary_id = _normalize_model_id(primary)
|
|
if primary_id and primary_id in allowed_model_ids:
|
|
candidates.append(primary_id)
|
|
|
|
# Fallback models
|
|
fallbacks = model_config.get("fallbacks", [])
|
|
if isinstance(fallbacks, list):
|
|
for fb in fallbacks:
|
|
fb_id = _normalize_model_id(fb)
|
|
if fb_id and fb_id in allowed_model_ids and fb_id not in candidates:
|
|
candidates.append(fb_id)
|
|
|
|
return candidates
|
|
|
|
# Unknown format
|
|
return candidates
|
|
|
|
|
|
def model_reasoning_levels(config: dict[str, typing.Any]) -> dict[str, str]:
|
|
"""Read per-model call options owned by this Runner."""
|
|
selection = config.get("model")
|
|
if not isinstance(selection, dict):
|
|
return {}
|
|
levels = selection.get("reasoning", {})
|
|
if not isinstance(levels, dict):
|
|
raise ValueError("Model reasoning settings must be an object")
|
|
return dict(levels)
|
|
|
|
|
|
def _normalize_model_id(model_id: typing.Any) -> str | None:
|
|
"""Normalize model ID, returning None for invalid/empty values."""
|
|
if not isinstance(model_id, str):
|
|
return None
|
|
model_id = model_id.strip()
|
|
if not model_id or model_id == "__none__":
|
|
return None
|
|
return model_id
|
|
|
|
|
|
def get_knowledge_base_ids(
|
|
config: dict[str, typing.Any],
|
|
allowed_kb_ids: set[str],
|
|
) -> list[str]:
|
|
"""Get knowledge base IDs from config, filtered by allowed set.
|
|
|
|
Args:
|
|
config: Runner configuration
|
|
allowed_kb_ids: Set of authorized KB IDs from ctx.resources
|
|
|
|
Returns:
|
|
List of KB IDs to use (intersection of config and allowed)
|
|
"""
|
|
kb_ids: list[str] = []
|
|
|
|
config_kbs = config.get("knowledge-bases", [])
|
|
if not isinstance(config_kbs, list):
|
|
return kb_ids
|
|
|
|
for kb_id in config_kbs:
|
|
if isinstance(kb_id, str) and kb_id and kb_id != "__none__":
|
|
if kb_id in allowed_kb_ids and kb_id not in kb_ids:
|
|
kb_ids.append(kb_id)
|
|
|
|
return kb_ids
|
|
|
|
|
|
def get_rerank_config(
|
|
config: dict[str, typing.Any],
|
|
) -> tuple[str | None, int]:
|
|
"""Get rerank model configuration.
|
|
|
|
Args:
|
|
config: Runner configuration
|
|
|
|
Returns:
|
|
Tuple of (rerank_model_id, rerank_top_k)
|
|
rerank_model_id is None if not configured or set to "__none__"
|
|
"""
|
|
rerank_model_id = config.get("rerank-model")
|
|
if not isinstance(rerank_model_id, str) or not rerank_model_id or rerank_model_id == "__none__":
|
|
rerank_model_id = None
|
|
|
|
rerank_top_k = config.get("rerank-top-k", 5)
|
|
if not isinstance(rerank_top_k, int) or rerank_top_k < 1:
|
|
rerank_top_k = 5
|
|
|
|
return rerank_model_id, rerank_top_k
|
|
|
|
|
|
def get_retrieval_top_k(config: dict[str, typing.Any]) -> int:
|
|
"""Get the number of retrieval results requested per knowledge base."""
|
|
return _positive_int(config.get("retrieval-top-k"), default=5)
|
|
|
|
|
|
def get_max_tool_iterations(config: dict[str, typing.Any]) -> int:
|
|
"""Get the maximum number of tool-call follow-up iterations."""
|
|
return _positive_int(config.get("max-tool-iterations"), default=DEFAULT_MAX_TOOL_ITERATIONS)
|
|
|
|
|
|
def get_run_timeout_seconds(config: dict[str, typing.Any]) -> int | None:
|
|
"""Get local runner timeout seconds, or None when disabled.
|
|
|
|
Protocol/README behavior treats timeout=0 and timeout=null as an explicit
|
|
request to rely on Host deadline/cancellation instead of a local wall-clock
|
|
deadline. Missing or invalid positive config keeps the conservative default.
|
|
"""
|
|
value = config.get("timeout", DEFAULT_RUN_TIMEOUT_SECONDS)
|
|
if value is None:
|
|
return None
|
|
if isinstance(value, bool) or not isinstance(value, int):
|
|
return DEFAULT_RUN_TIMEOUT_SECONDS
|
|
if value == 0:
|
|
return None
|
|
if value < 0:
|
|
return DEFAULT_RUN_TIMEOUT_SECONDS
|
|
return value
|
|
|
|
|
|
def get_tool_execution_mode(config: dict[str, typing.Any]) -> str:
|
|
"""Get the same-batch tool execution strategy."""
|
|
raw_mode = config.get("tool-execution-mode", DEFAULT_TOOL_EXECUTION_MODE)
|
|
if not isinstance(raw_mode, str):
|
|
return DEFAULT_TOOL_EXECUTION_MODE
|
|
if raw_mode not in VALID_TOOL_EXECUTION_MODES:
|
|
return DEFAULT_TOOL_EXECUTION_MODE
|
|
return raw_mode
|
|
|
|
|
|
def get_max_tool_result_chars(config: dict[str, typing.Any]) -> int:
|
|
"""Get the maximum tool result characters injected into model messages."""
|
|
return _positive_int(config.get("max-tool-result-chars"), default=DEFAULT_MAX_TOOL_RESULT_CHARS)
|
|
|
|
|
|
def get_remove_think(config: dict[str, typing.Any]) -> bool:
|
|
"""Whether to ask Host model APIs to strip provider thinking output."""
|
|
return config.get("remove-think") is True
|
|
|
|
|
|
def _positive_int(value: typing.Any, *, default: int) -> int:
|
|
if isinstance(value, bool) or not isinstance(value, int):
|
|
return default
|
|
if value < 1:
|
|
return default
|
|
return value
|