mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 10:51:36 -04:00
Response-quality work, all aimed at giving the model enough context to answer accurately: - Every chat's system prompt now carries a live library map (today's date, counts, and the exact names of channels, domains, zims, and inventories, capped and cached 60s). Exact spellings are in context before the first token, and the list_collections round-trip — minutes on slow hardware — is usually unnecessary. - The request is budgeted against the model's real context: oldest turns are trimmed first, the current question is never dropped, and an over-budget turn gets a clear "start a new conversation" error instead of a silently truncated prompt. cache_prompt reuses the KV cache for the stable prefix, which is most of the latency on a Pi. - context_size plumbing fixed: selecting a model adopts its catalog default (the 4B was running at 8k instead of 16k), and llama-server runs --parallel 1 so the single user gets the whole context. - temperature is configurable in ai.yaml (default 0.3 — small models call tools far more reliably near 0.2-0.3 than at server defaults). - One worked tool-use example per mode is prepended to every conversation; small models imitate far better than they obey. - scripts/ai_eval.py scores the assistant against a live WROLPi on mechanical checks (finished, called the right tool first, included a WROLPi link, no external URLs) so prompt changes stop being validated by anecdote. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
112 lines
3.1 KiB
Python
112 lines
3.1 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
|
|
temperature: float = 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,
|
|
temperature=0.3, # Low: tool-calling small models are far more reliable near 0.2-0.3.
|
|
)
|
|
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)})
|
|
|
|
@property
|
|
def temperature(self) -> float:
|
|
# .get(): configs imported before this field existed lack the key.
|
|
return self._config.get('temperature', 0.3)
|
|
|
|
@temperature.setter
|
|
def temperature(self, value: float):
|
|
self.update({'temperature': float(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
|