my_voice_assistant_v3_jamulix/app/config.py
Dieter Schlüter 16a964032e feat: Konversationsgedaechtnis - Kurzzeit-Gespraechsverlauf pro Session
- Store: messages-Tabelle + append_message/get_recent_messages (mit Mandanten-Schutz)
- LLMProvider.complete um history erweitert (openrouter + local bauen system+history+user)
- Orchestrator.chat_text reicht den Verlauf ans LLM weiter
- chat.py: bei session_id letzte HISTORY_MAX_MESSAGES laden, danach User-/Assistant-Turn speichern
- ohne session_id weiterhin zustandslos; ?debug zeigt history_len
- Config HISTORY_MAX_MESSAGES (Default 10)
- Tests: 32 gruen (3 neue Gedaechtnis-Tests)
- Doku aktualisiert (README, BEDIENUNGSANLEITUNG, Architektur)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 04:16:35 +02:00

150 lines
4.7 KiB
Python

import os
from pathlib import Path
try:
import tomllib # Python >= 3.11 (stdlib)
except ModuleNotFoundError: # pragma: no cover - Fallback fuer aeltere Interpreter
import tomli as tomllib # type: ignore
from pydantic.fields import FieldInfo
from pydantic_settings import (
BaseSettings,
PydanticBaseSettingsSource,
SettingsConfigDict,
)
BASE_DIR = Path(__file__).resolve().parent.parent
ENV_FILE = BASE_DIR / ".env"
DEFAULT_CONFIG_FILE = BASE_DIR / "config" / "voice-assistant.toml"
def _setting_lookup(key: str) -> str | None:
"""Liest einen Steuer-Schluessel: echte Umgebung zuerst, dann die .env-Datei.
Noetig fuer VA_PROFILE/VA_CONFIG_FILE, weil diese gebraucht werden, BEVOR
pydantic-settings die .env laedt - und .env-Werte sonst nicht in os.environ stehen.
"""
value = os.getenv(key)
if value is not None:
return value
try:
from dotenv import dotenv_values
except ModuleNotFoundError: # pragma: no cover
return None
if ENV_FILE.is_file():
return dotenv_values(ENV_FILE).get(key)
return None
def _config_file_path() -> Path:
return Path(_setting_lookup("VA_CONFIG_FILE") or str(DEFAULT_CONFIG_FILE))
def active_profile() -> str | None:
"""Name des aktiven Profils (VA_PROFILE) aus Umgebung oder .env, falls gesetzt."""
profile = _setting_lookup("VA_PROFILE")
return profile.strip() or None if profile else None
def load_profile_config() -> dict:
"""Liest die zentrale TOML-Config und merged [defaults] + [profiles.<VA_PROFILE>].
- Fehlt die Datei, gilt ein leeres dict (nur ENV/Defaults greifen) - kein Fehler,
damit reine Cloud-Deployments ohne Datei (nur ENV) funktionieren.
- Ein gesetztes, aber unbekanntes VA_PROFILE ist ein Konfigurationsfehler.
"""
path = _config_file_path()
if not path.is_file():
return {}
with path.open("rb") as handle:
data = tomllib.load(handle)
merged: dict = dict(data.get("defaults", {}))
profile = active_profile()
if profile:
profiles = data.get("profiles", {})
if profile not in profiles:
raise ValueError(
f"Unbekanntes VA_PROFILE {profile!r}. "
f"Verfuegbar: {sorted(profiles)}"
)
merged.update(profiles[profile])
return merged
class TomlProfileSource(PydanticBaseSettingsSource):
"""Settings-Quelle aus der zentralen TOML-Config (inkl. aktivem Profil).
Liegt in der Praezedenz unter ENV/.env, aber ueber den eingebauten Defaults.
Es werden nur Schluessel durchgereicht, die auch als Settings-Feld existieren.
"""
def __init__(self, settings_cls):
super().__init__(settings_cls)
raw = load_profile_config()
known = set(settings_cls.model_fields)
self._values = {
key.lower(): value
for key, value in raw.items()
if key.lower() in known
}
def get_field_value(self, field: FieldInfo, field_name: str):
if field_name in self._values:
return self._values[field_name], field_name, False
return None, field_name, False
def __call__(self) -> dict:
return dict(self._values)
class Settings(BaseSettings):
app_env: str = "dev"
host: str = "0.0.0.0"
port: int = 8080
log_level: str = "info"
openrouter_api_key: str = ""
openrouter_stt_model: str = "openai/whisper-large-v3"
openrouter_tts_model: str = "openai/gpt-4o-mini-tts"
openrouter_tts_voice: str = "alloy"
openrouter_llm_model: str = "openai/gpt-4.1-mini"
default_language: str = "de"
default_input_endpoint: str = "local-default"
default_output_endpoint: str = "local-default"
default_stt_provider: str = "openrouter"
default_llm_provider: str = "local-openai-compatible"
default_tts_provider: str = "openrouter"
local_llm_base_url: str = "http://127.0.0.1:11434/v1"
local_llm_api_key: str = "dummy"
local_llm_model: str = "llama3.1"
db_path: str = str(BASE_DIR / "data" / "voice-assistant.db")
admin_api_key: str = ""
auth_enabled: bool = True
history_max_messages: int = 10
model_config = SettingsConfigDict(
env_file=ENV_FILE, case_sensitive=False, extra="ignore"
)
@classmethod
def settings_customise_sources(
cls,
settings_cls,
init_settings,
env_settings,
dotenv_settings,
file_secret_settings,
):
# Praezedenz (frueher = hoeher): init > ENV > .env > TOML/Profil > Defaults
return (
init_settings,
env_settings,
dotenv_settings,
TomlProfileSource(settings_cls),
file_secret_settings,
)
settings = Settings()