Implements The Housekeeper, a new expert agent for home automation following the Librarian pattern. Communicates with core-api service which wraps Home Assistant REST API. New agent features: - CoreAPIClient with 13 home automation methods - 13 tools: list_areas, list_devices, get_device_state, turn_on, turn_off, toggle, list_scenes, activate_scene, list_scripts, run_script, list_automations, toggle_automation, get_history - PydanticAI agent with butler-friendly system prompt - HouseholdCapability registration for Steward coordination - delegate_to_housekeeper() wrapper for orchestration Also includes: - Dev port changed from 8123 to 8777 (avoids Home Assistant conflict) - Config: CORE_API_HOST, CORE_API_KEY, CORE_API_TIMEOUT - 44 unit tests for client and capability 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
262 lines
7.6 KiB
Python
262 lines
7.6 KiB
Python
"""
|
|
Global application configuration.
|
|
Following best practice of splitting config across domains.
|
|
"""
|
|
from enum import Enum
|
|
from functools import lru_cache
|
|
from pathlib import Path
|
|
|
|
from pydantic import Field, HttpUrl
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
|
|
def _get_version_from_pyproject() -> str:
|
|
"""
|
|
Load version from pyproject.toml.
|
|
|
|
Falls back to "unknown" if file cannot be read.
|
|
"""
|
|
try:
|
|
# Find pyproject.toml relative to this file
|
|
config_dir = Path(__file__).parent
|
|
pyproject_path = config_dir.parent.parent / "pyproject.toml"
|
|
|
|
if pyproject_path.exists():
|
|
content = pyproject_path.read_text()
|
|
for line in content.splitlines():
|
|
if line.strip().startswith("version"):
|
|
# Parse: version = "1.0.0"
|
|
return line.split("=", 1)[1].strip().strip('"').strip("'")
|
|
except Exception:
|
|
pass
|
|
return "unknown"
|
|
|
|
|
|
class Environment(str, Enum):
|
|
"""Application environment."""
|
|
DEVELOPMENT = "development"
|
|
PRODUCTION = "production"
|
|
TESTING = "testing"
|
|
|
|
|
|
class Config(BaseSettings):
|
|
"""
|
|
Global application configuration.
|
|
|
|
Loads from environment variables and .env file.
|
|
Domain-specific configs should be in their respective modules.
|
|
"""
|
|
model_config = SettingsConfigDict(
|
|
env_file=".env",
|
|
env_file_encoding="utf-8",
|
|
case_sensitive=True,
|
|
extra="ignore",
|
|
)
|
|
|
|
# Application
|
|
APP_NAME: str = "OpenAI-Compatible API"
|
|
APP_VERSION: str = Field(default_factory=_get_version_from_pyproject)
|
|
ENVIRONMENT: Environment = Environment.DEVELOPMENT
|
|
DEBUG: bool = Field(default=False, description="Debug mode")
|
|
|
|
# API Configuration
|
|
API_HOST: str = Field(default="0.0.0.0", description="API host")
|
|
API_PORT: int = Field(default=8000, description="API port")
|
|
API_PREFIX: str = Field(default="/v1", description="API route prefix")
|
|
|
|
# Ollama Configuration
|
|
OLLAMA_HOST: HttpUrl = Field(
|
|
default="http://localhost:11434",
|
|
description="Ollama server URL"
|
|
)
|
|
OLLAMA_DEFAULT_MODEL: str = Field(
|
|
default="mistral-nemo:latest",
|
|
description="Default Ollama model"
|
|
)
|
|
OLLAMA_TIMEOUT: int = Field(
|
|
default=120,
|
|
description="Ollama request timeout in seconds"
|
|
)
|
|
STREAM_TIMEOUT: int = Field(
|
|
default=20,
|
|
description="Timeout for each streaming turn in seconds"
|
|
)
|
|
|
|
# SearXNG Configuration
|
|
SEARXNG_HOST: HttpUrl = Field(
|
|
default="http://localhost:8087",
|
|
description="SearXNG server URL"
|
|
)
|
|
SEARXNG_TIMEOUT: int = Field(
|
|
default=30,
|
|
description="SearXNG request timeout in seconds"
|
|
)
|
|
|
|
# Redis Configuration
|
|
REDIS_HOST: str = Field(
|
|
default="localhost",
|
|
description="Redis server host"
|
|
)
|
|
REDIS_PORT: int = Field(
|
|
default=6379,
|
|
description="Redis server port"
|
|
)
|
|
REDIS_BENCHMARK_DB: int = Field(
|
|
default=6,
|
|
description="Redis database number for benchmarks"
|
|
)
|
|
REDIS_TIMEOUT: int = Field(
|
|
default=5,
|
|
description="Redis connection timeout in seconds"
|
|
)
|
|
|
|
# Library-Desk Configuration (The Librarian backend)
|
|
LIBRARY_DESK_HOST: HttpUrl = Field(
|
|
default="http://localhost:8089",
|
|
description="Library-Desk API URL"
|
|
)
|
|
LIBRARY_DESK_API_KEY: str = Field(
|
|
default="",
|
|
description="API key for Library-Desk authentication"
|
|
)
|
|
LIBRARY_DESK_TIMEOUT: int = Field(
|
|
default=60,
|
|
description="Library-Desk request timeout in seconds"
|
|
)
|
|
|
|
# Core-API Configuration (The Housekeeper backend)
|
|
CORE_API_HOST: HttpUrl = Field(
|
|
default="http://localhost:8090",
|
|
description="Core-API URL for Home Assistant integration"
|
|
)
|
|
CORE_API_KEY: str = Field(
|
|
default="",
|
|
description="API key for Core-API authentication"
|
|
)
|
|
CORE_API_TIMEOUT: int = Field(
|
|
default=30,
|
|
description="Core-API request timeout in seconds"
|
|
)
|
|
|
|
# Qdrant Configuration (Memory vector storage)
|
|
QDRANT_HOST: str = Field(
|
|
default="localhost",
|
|
description="Qdrant server host"
|
|
)
|
|
QDRANT_PORT: int = Field(
|
|
default=6333,
|
|
description="Qdrant server port"
|
|
)
|
|
QDRANT_EMBEDDING_DIM: int = Field(
|
|
default=768,
|
|
description="Embedding dimension (768 for nomic-embed-text)"
|
|
)
|
|
|
|
# Ollama Embedding Configuration
|
|
OLLAMA_EMBEDDING_MODEL: str = Field(
|
|
default="nomic-embed-text",
|
|
description="Ollama model for embeddings"
|
|
)
|
|
|
|
# Redis Memory Database (separate from benchmarks)
|
|
REDIS_MEMORY_DB: int = Field(
|
|
default=1,
|
|
description="Redis database number for memory cache"
|
|
)
|
|
REDIS_MEMORY_TTL_HOURS: int = Field(
|
|
default=24,
|
|
description="TTL for session context in hours"
|
|
)
|
|
|
|
# Logging
|
|
LOG_LEVEL: str | None = Field(
|
|
default=None,
|
|
description="Logging level (auto-set based on environment if not specified)"
|
|
)
|
|
ENABLE_BENCHMARKS: bool = Field(default=True, description="Enable performance benchmarking")
|
|
|
|
# User Configuration
|
|
DEFAULT_USER: str | None = Field(
|
|
default=None,
|
|
description="Default user for single-user setup (auto-set based on environment if not specified)"
|
|
)
|
|
|
|
# CORS
|
|
CORS_ORIGINS: list[str] = Field(
|
|
default=["*"],
|
|
description="Allowed CORS origins"
|
|
)
|
|
CORS_ALLOW_CREDENTIALS: bool = True
|
|
CORS_ALLOW_METHODS: list[str] = ["*"]
|
|
CORS_ALLOW_HEADERS: list[str] = ["*"]
|
|
|
|
@property
|
|
def redis_url(self) -> str:
|
|
"""Construct Redis connection URL for benchmarks."""
|
|
return f"redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_BENCHMARK_DB}"
|
|
|
|
@property
|
|
def redis_memory_url(self) -> str:
|
|
"""Construct Redis connection URL for memory cache."""
|
|
return f"redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_MEMORY_DB}"
|
|
|
|
@property
|
|
def qdrant_url(self) -> str:
|
|
"""Construct Qdrant server URL."""
|
|
return f"http://{self.QDRANT_HOST}:{self.QDRANT_PORT}"
|
|
|
|
@property
|
|
def log_format(self) -> str:
|
|
"""
|
|
Determine log format based on environment.
|
|
|
|
- production: JSON format for machine parsing
|
|
- development/testing: Console format for human readability
|
|
"""
|
|
return "json" if self.ENVIRONMENT == Environment.PRODUCTION else "console"
|
|
|
|
@property
|
|
def effective_log_level(self) -> str:
|
|
"""
|
|
Get effective log level, auto-determining from environment if not set.
|
|
|
|
- development: DEBUG (maximum verbosity)
|
|
- production: WARNING (minimal noise)
|
|
- testing: INFO
|
|
"""
|
|
if self.LOG_LEVEL is not None:
|
|
return self.LOG_LEVEL
|
|
if self.ENVIRONMENT == Environment.DEVELOPMENT:
|
|
return "DEBUG"
|
|
if self.ENVIRONMENT == Environment.PRODUCTION:
|
|
return "WARNING"
|
|
return "INFO"
|
|
|
|
@property
|
|
def effective_default_user(self) -> str:
|
|
"""
|
|
Get effective default user, auto-determining from environment if not set.
|
|
|
|
- development/testing: llm_tester (isolated test scope)
|
|
- production: jpmschweitzer (real user)
|
|
"""
|
|
if self.DEFAULT_USER is not None:
|
|
return self.DEFAULT_USER
|
|
if self.ENVIRONMENT == Environment.PRODUCTION:
|
|
return "jpmschweitzer"
|
|
return "llm_tester"
|
|
|
|
|
|
@lru_cache
|
|
def get_config() -> Config:
|
|
"""
|
|
Get cached configuration instance.
|
|
|
|
Uses lru_cache to ensure config is loaded once and reused.
|
|
"""
|
|
return Config()
|
|
|
|
|
|
# Global config instance
|
|
config = get_config()
|