mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
ai.yaml is the source of truth (opt-in: enabled=False by default) with active model, idle-unload minutes, context override, and tool-call bound. The model catalog follows the map pattern: a bundled constant keeps the Manage tab working offline, the CDN manifest (ai/models.json + detached GPG signature) wins when reachable. GGUF downloads clone the map-extract flow — the per-file meta4 is GPG-verified once and the same bytes handed to aria2c, so the verified hash is the enforced one; unverified downloads are refused. Models land in <media>/ai/models/ (gated by the existing media_mounted download rules). Manage endpoints (catalog with RAM-tier recommendation, settings) live under /api/ai/manage/ which the future agent loop excludes from its tool catalog. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
101 lines
2.7 KiB
Python
101 lines
2.7 KiB
Python
"""The ai.yaml config — the source of truth for the AI assistant's state.
|
|
|
|
AI is opt-in: nothing is downloaded and llama-server does not run until the user enables it on
|
|
the Manage tab. YAML-only (no database)."""
|
|
import contextlib
|
|
from dataclasses import dataclass
|
|
from typing import Optional
|
|
|
|
from wrolpi.common import ConfigFile
|
|
|
|
|
|
@dataclass
|
|
class AIConfigValidator:
|
|
version: int = None
|
|
enabled: bool = None
|
|
active_model: str = None
|
|
idle_unload_minutes: int = None
|
|
context_size: Optional[int] = None
|
|
max_tool_calls: int = None
|
|
|
|
|
|
class AIConfig(ConfigFile):
|
|
file_name = 'ai.yaml'
|
|
default_config = dict(
|
|
version=0,
|
|
enabled=False,
|
|
active_model='', # a GGUF file name in <media>/ai/models/
|
|
idle_unload_minutes=15,
|
|
context_size=None, # None uses the active model's default from the catalog.
|
|
max_tool_calls=6,
|
|
)
|
|
validator = AIConfigValidator
|
|
|
|
def import_config(self, file=None, send_events=False):
|
|
super().import_config(file, send_events)
|
|
# YAML-only config (no database); a successful read is a successful import.
|
|
self.successful_import = True
|
|
|
|
@property
|
|
def enabled(self) -> bool:
|
|
return self._config['enabled']
|
|
|
|
@enabled.setter
|
|
def enabled(self, value: bool):
|
|
self.update({'enabled': bool(value)})
|
|
|
|
@property
|
|
def active_model(self) -> str:
|
|
return self._config['active_model']
|
|
|
|
@active_model.setter
|
|
def active_model(self, value: str):
|
|
self.update({'active_model': value or ''})
|
|
|
|
@property
|
|
def idle_unload_minutes(self) -> int:
|
|
return self._config['idle_unload_minutes']
|
|
|
|
@idle_unload_minutes.setter
|
|
def idle_unload_minutes(self, value: int):
|
|
self.update({'idle_unload_minutes': int(value)})
|
|
|
|
@property
|
|
def context_size(self) -> Optional[int]:
|
|
return self._config['context_size']
|
|
|
|
@context_size.setter
|
|
def context_size(self, value: Optional[int]):
|
|
self.update({'context_size': int(value) if value else None})
|
|
|
|
@property
|
|
def max_tool_calls(self) -> int:
|
|
return self._config['max_tool_calls']
|
|
|
|
@max_tool_calls.setter
|
|
def max_tool_calls(self, value: int):
|
|
self.update({'max_tool_calls': int(value)})
|
|
|
|
|
|
AI_CONFIG: AIConfig = AIConfig()
|
|
|
|
# Test override (see get_ai_config).
|
|
TEST_AI_CONFIG: Optional[AIConfig] = None
|
|
|
|
|
|
def get_ai_config() -> AIConfig:
|
|
global TEST_AI_CONFIG
|
|
if isinstance(TEST_AI_CONFIG, ConfigFile):
|
|
return TEST_AI_CONFIG
|
|
return AI_CONFIG
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def ai_config_context():
|
|
"""Used to create a test config."""
|
|
global TEST_AI_CONFIG
|
|
TEST_AI_CONFIG = AIConfig()
|
|
try:
|
|
yield TEST_AI_CONFIG
|
|
finally:
|
|
TEST_AI_CONFIG = None
|