mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-12 07:09:19 +03:00
437 lines
13 KiB
Python
437 lines
13 KiB
Python
"""Stable compatibility facade for WebUI settings.
|
|
|
|
The gateway owns serialization and the explicit config path. Business DTOs,
|
|
validation, and updates live in the model/provider, capability, and system
|
|
domains; this module preserves the established Python and HTTP-facing seams.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Iterable
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING, Any, Literal, cast
|
|
|
|
import httpx
|
|
|
|
from nanobot import __version__
|
|
from nanobot.config.loader import get_config_path, load_config, save_config
|
|
from nanobot.config.schema import Config
|
|
from nanobot.webui import settings_capabilities as capabilities
|
|
from nanobot.webui import settings_contracts as contracts
|
|
from nanobot.webui import settings_models as models
|
|
from nanobot.webui import settings_system as system
|
|
from nanobot.webui.settings_contracts import QueryParams, WebUISettingsError
|
|
from nanobot.webui.workspaces import write_webui_default_access_mode
|
|
|
|
if TYPE_CHECKING:
|
|
from nanobot.webui.settings_services import WebUIOAuthFlowRegistry
|
|
|
|
RuntimeSurface = Literal["browser", "native"]
|
|
|
|
# Preserve established direct imports of focused helpers.
|
|
_docs_version = system.docs_version
|
|
_parse_bool = contracts.parse_bool
|
|
_query_first = contracts.query_first
|
|
_query_first_alias = contracts.query_first_alias
|
|
_query_has_alias = contracts.query_has_alias
|
|
_model_catalog_kind = models.model_catalog_kind
|
|
_oauth_provider_status = models.oauth_provider_status
|
|
_provider_requires_api_key = models.provider_requires_api_key
|
|
_reasoning_effort_values_for = models.reasoning_effort_values_for
|
|
|
|
|
|
_RUNTIME_CAPABILITIES = {
|
|
"can_restart_engine": False,
|
|
"can_pick_folder": False,
|
|
"can_open_logs": False,
|
|
"can_export_diagnostics": False,
|
|
}
|
|
_NATIVE_RUNTIME_CAPABILITIES = {
|
|
**_RUNTIME_CAPABILITIES,
|
|
"can_restart_engine": True,
|
|
"can_pick_folder": True,
|
|
"can_open_logs": True,
|
|
"can_export_diagnostics": True,
|
|
}
|
|
_BROWSER_RESTART_BEHAVIOR_BY_SECTION = {
|
|
"appearance": "none",
|
|
"models": "none",
|
|
"providers": "none",
|
|
"runtime": "engineRestart",
|
|
"browser": "engineRestart",
|
|
"image": "engineRestart",
|
|
"apps": "engineRestart",
|
|
"advanced": "appRestart",
|
|
}
|
|
_NATIVE_RESTART_BEHAVIOR_BY_SECTION = {
|
|
**_BROWSER_RESTART_BEHAVIOR_BY_SECTION,
|
|
"runtime": "engineRestart",
|
|
"browser": "engineRestart",
|
|
"image": "engineRestart",
|
|
"apps": "engineRestart",
|
|
}
|
|
|
|
|
|
def _load_settings_config(config_path: Path | None) -> Config:
|
|
return load_config(config_path) if config_path is not None else load_config()
|
|
|
|
|
|
def _save_settings_config(config: Config, config_path: Path | None) -> None:
|
|
if config_path is None:
|
|
save_config(config)
|
|
else:
|
|
save_config(config, config_path)
|
|
|
|
|
|
def _settings_config_path(config_path: Path | None) -> Path:
|
|
return config_path if config_path is not None else get_config_path()
|
|
|
|
|
|
def _normalize_surface(surface: str | None) -> RuntimeSurface:
|
|
return "native" if surface in {"native", "desktop"} else "browser"
|
|
|
|
|
|
def runtime_capabilities(
|
|
surface: str | None = "browser",
|
|
overrides: dict[str, Any] | None = None,
|
|
) -> dict[str, bool]:
|
|
base = (
|
|
_NATIVE_RUNTIME_CAPABILITIES
|
|
if _normalize_surface(surface) == "native"
|
|
else _RUNTIME_CAPABILITIES
|
|
)
|
|
result = dict(base)
|
|
for key, value in (overrides or {}).items():
|
|
if key in result:
|
|
result[key] = bool(value)
|
|
return result
|
|
|
|
|
|
def restart_behavior_by_section(surface: str | None = "browser") -> dict[str, str]:
|
|
return dict(
|
|
_NATIVE_RESTART_BEHAVIOR_BY_SECTION
|
|
if _normalize_surface(surface) == "native"
|
|
else _BROWSER_RESTART_BEHAVIOR_BY_SECTION
|
|
)
|
|
|
|
|
|
def decorate_settings_payload(
|
|
payload: dict[str, Any],
|
|
*,
|
|
surface: str | None = "browser",
|
|
runtime_capability_overrides: dict[str, Any] | None = None,
|
|
restart_required_sections: list[str] | None = None,
|
|
apply_state: dict[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Attach runtime-surface metadata without changing the core settings shape."""
|
|
surface_value = _normalize_surface(surface)
|
|
sections = restart_required_sections
|
|
if sections is None:
|
|
raw_sections: object = payload.get("restart_required_sections") or []
|
|
sections = [
|
|
section
|
|
for section in cast(Iterable[object], raw_sections)
|
|
if isinstance(section, str)
|
|
]
|
|
sections = sorted(dict.fromkeys(sections))
|
|
result = dict(payload)
|
|
result["surface"] = surface_value
|
|
result["runtime_surface"] = surface_value
|
|
result["runtime_capabilities"] = runtime_capabilities(
|
|
surface_value,
|
|
runtime_capability_overrides,
|
|
)
|
|
result["restart_behavior_by_section"] = restart_behavior_by_section(surface_value)
|
|
result["restart_required_sections"] = sections
|
|
if sections:
|
|
result["requires_restart"] = True
|
|
else:
|
|
result["requires_restart"] = bool(result.get("requires_restart", False))
|
|
result["apply_state"] = apply_state or {
|
|
"status": "pending" if result["requires_restart"] else "idle",
|
|
"sections": sections,
|
|
}
|
|
return result
|
|
|
|
|
|
def settings_payload(
|
|
*,
|
|
requires_restart: bool = False,
|
|
surface: str | None = "browser",
|
|
runtime_capability_overrides: dict[str, Any] | None = None,
|
|
restart_required_sections: list[str] | None = None,
|
|
apply_state: dict[str, Any] | None = None,
|
|
config_path: Path | None = None,
|
|
) -> dict[str, Any]:
|
|
config = _load_settings_config(config_path)
|
|
payload: dict[str, Any] = {
|
|
**models.model_settings_payload(
|
|
config,
|
|
oauth_status=_oauth_provider_status,
|
|
),
|
|
**capabilities.capability_settings_payload(
|
|
config,
|
|
oauth_status=_oauth_provider_status,
|
|
),
|
|
**system.system_settings_payload(
|
|
config,
|
|
config_path=_settings_config_path(config_path),
|
|
version=__version__,
|
|
),
|
|
"requires_restart": requires_restart,
|
|
}
|
|
return decorate_settings_payload(
|
|
payload,
|
|
surface=surface,
|
|
runtime_capability_overrides=runtime_capability_overrides,
|
|
restart_required_sections=restart_required_sections,
|
|
apply_state=apply_state,
|
|
)
|
|
|
|
|
|
def settings_usage_payload(*, config_path: Path | None = None) -> dict[str, Any]:
|
|
return system.settings_usage_payload(_load_settings_config(config_path))
|
|
|
|
|
|
def update_agent_settings(
|
|
query: QueryParams,
|
|
*,
|
|
config_path: Path | None = None,
|
|
) -> dict[str, Any]:
|
|
config = _load_settings_config(config_path)
|
|
model_changed = models.update_agent_model_settings(
|
|
config,
|
|
query,
|
|
oauth_status=_oauth_provider_status,
|
|
)
|
|
system_changed, restart_required = system.update_agent_system_settings(config, query)
|
|
if model_changed or system_changed:
|
|
_save_settings_config(config, config_path)
|
|
return settings_payload(
|
|
requires_restart=restart_required,
|
|
config_path=config_path,
|
|
)
|
|
|
|
|
|
def create_model_configuration(
|
|
query: QueryParams,
|
|
*,
|
|
config_path: Path | None = None,
|
|
) -> dict[str, Any]:
|
|
config = _load_settings_config(config_path)
|
|
name = models.create_model_configuration(
|
|
config,
|
|
query,
|
|
oauth_status=_oauth_provider_status,
|
|
)
|
|
_save_settings_config(config, config_path)
|
|
payload = settings_payload(config_path=config_path)
|
|
payload["created_model_preset"] = name
|
|
return payload
|
|
|
|
|
|
def update_model_configuration(
|
|
query: QueryParams,
|
|
*,
|
|
config_path: Path | None = None,
|
|
) -> dict[str, Any]:
|
|
config = _load_settings_config(config_path)
|
|
if models.update_model_configuration(
|
|
config,
|
|
query,
|
|
oauth_status=_oauth_provider_status,
|
|
):
|
|
_save_settings_config(config, config_path)
|
|
return settings_payload(config_path=config_path)
|
|
|
|
|
|
def update_model_call_order(
|
|
query: QueryParams,
|
|
*,
|
|
config_path: Path | None = None,
|
|
) -> dict[str, Any]:
|
|
config = _load_settings_config(config_path)
|
|
if models.update_model_call_order(config, query):
|
|
_save_settings_config(config, config_path)
|
|
return settings_payload(config_path=config_path)
|
|
|
|
|
|
def migrate_model_configurations(
|
|
_query: QueryParams | None = None,
|
|
*,
|
|
config_path: Path | None = None,
|
|
) -> dict[str, Any]:
|
|
config = _load_settings_config(config_path)
|
|
if models.migrate_model_configurations(config):
|
|
_save_settings_config(config, config_path)
|
|
return settings_payload(config_path=config_path)
|
|
|
|
|
|
def delete_model_configuration(
|
|
query: QueryParams,
|
|
*,
|
|
config_path: Path | None = None,
|
|
) -> dict[str, Any]:
|
|
config = _load_settings_config(config_path)
|
|
models.delete_model_configuration(config, query)
|
|
_save_settings_config(config, config_path)
|
|
return settings_payload(config_path=config_path)
|
|
|
|
|
|
def create_provider_settings(
|
|
query: QueryParams,
|
|
*,
|
|
config_path: Path | None = None,
|
|
) -> dict[str, Any]:
|
|
config = _load_settings_config(config_path)
|
|
provider_key = models.create_provider_settings(config, query)
|
|
_save_settings_config(config, config_path)
|
|
payload = settings_payload(config_path=config_path)
|
|
payload["created_provider"] = provider_key
|
|
return payload
|
|
|
|
|
|
def update_provider_settings(
|
|
query: QueryParams,
|
|
*,
|
|
config_path: Path | None = None,
|
|
) -> dict[str, Any]:
|
|
config = _load_settings_config(config_path)
|
|
changed, restart_required = models.update_provider_settings(config, query)
|
|
if changed:
|
|
_save_settings_config(config, config_path)
|
|
return settings_payload(
|
|
requires_restart=restart_required,
|
|
config_path=config_path,
|
|
)
|
|
|
|
|
|
def provider_models_payload(
|
|
query: QueryParams,
|
|
*,
|
|
config_path: Path | None = None,
|
|
) -> dict[str, Any]:
|
|
return models.provider_models_payload(
|
|
_load_settings_config(config_path),
|
|
query,
|
|
http_get=httpx.get,
|
|
)
|
|
|
|
|
|
def login_oauth_provider(
|
|
query: QueryParams,
|
|
*,
|
|
oauth_flows: WebUIOAuthFlowRegistry,
|
|
config_path: Path | None = None,
|
|
) -> dict[str, Any]:
|
|
return models.login_oauth_provider(
|
|
_load_settings_config(config_path),
|
|
query,
|
|
oauth_flows=oauth_flows,
|
|
config_path=config_path,
|
|
settings_payload=settings_payload,
|
|
)
|
|
|
|
|
|
def complete_oauth_provider(
|
|
query: QueryParams,
|
|
authorization_response: str | None = None,
|
|
*,
|
|
oauth_flows: WebUIOAuthFlowRegistry,
|
|
config_path: Path | None = None,
|
|
) -> dict[str, Any]:
|
|
return models.complete_oauth_provider(
|
|
query,
|
|
authorization_response,
|
|
oauth_flows=oauth_flows,
|
|
config_path=config_path,
|
|
settings_payload=settings_payload,
|
|
)
|
|
|
|
|
|
def logout_oauth_provider(
|
|
query: QueryParams,
|
|
*,
|
|
oauth_flows: WebUIOAuthFlowRegistry,
|
|
config_path: Path | None = None,
|
|
) -> dict[str, Any]:
|
|
return models.logout_oauth_provider(
|
|
query,
|
|
oauth_flows=oauth_flows,
|
|
config_path=config_path,
|
|
settings_payload=settings_payload,
|
|
)
|
|
|
|
|
|
def update_network_safety_settings(
|
|
query: QueryParams,
|
|
*,
|
|
config_path: Path | None = None,
|
|
) -> dict[str, Any]:
|
|
config = _load_settings_config(config_path)
|
|
changed, default_access_mode = capabilities.update_network_safety_settings(
|
|
config,
|
|
query,
|
|
)
|
|
if changed:
|
|
_save_settings_config(config, config_path)
|
|
if default_access_mode is not None:
|
|
try:
|
|
write_webui_default_access_mode(default_access_mode)
|
|
except ValueError as exc:
|
|
raise WebUISettingsError(str(exc)) from exc
|
|
return settings_payload(requires_restart=changed, config_path=config_path)
|
|
|
|
|
|
def update_web_search_settings(
|
|
query: QueryParams,
|
|
*,
|
|
config_path: Path | None = None,
|
|
) -> dict[str, Any]:
|
|
config = _load_settings_config(config_path)
|
|
changed, restart_required = capabilities.update_web_search_settings(config, query)
|
|
if changed:
|
|
_save_settings_config(config, config_path)
|
|
return settings_payload(
|
|
requires_restart=restart_required,
|
|
config_path=config_path,
|
|
)
|
|
|
|
|
|
def update_api_settings(
|
|
query: QueryParams,
|
|
*,
|
|
config_path: Path | None = None,
|
|
) -> dict[str, Any]:
|
|
config = _load_settings_config(config_path)
|
|
capabilities.update_api_settings(config, query)
|
|
_save_settings_config(config, config_path)
|
|
return settings_payload(config_path=config_path)
|
|
|
|
|
|
def update_image_generation_settings(
|
|
query: QueryParams,
|
|
*,
|
|
config_path: Path | None = None,
|
|
) -> dict[str, Any]:
|
|
config = _load_settings_config(config_path)
|
|
changed = capabilities.update_image_generation_settings(
|
|
config,
|
|
query,
|
|
oauth_status=_oauth_provider_status,
|
|
)
|
|
if changed:
|
|
_save_settings_config(config, config_path)
|
|
return settings_payload(requires_restart=changed, config_path=config_path)
|
|
|
|
|
|
def update_transcription_settings(
|
|
query: QueryParams,
|
|
*,
|
|
config_path: Path | None = None,
|
|
) -> dict[str, Any]:
|
|
config = _load_settings_config(config_path)
|
|
if capabilities.update_transcription_settings(config, query):
|
|
_save_settings_config(config, config_path)
|
|
return settings_payload(config_path=config_path)
|