diff --git a/alfred/agent/__init__.py b/alfred/agent/__init__.py index 928a1c3..d06453b 100644 --- a/alfred/agent/__init__.py +++ b/alfred/agent/__init__.py @@ -1,6 +1,7 @@ """Agent module for media library management.""" -from .agent import Agent from alfred.settings import settings +from .agent import Agent + __all__ = ["Agent", "settings"] diff --git a/alfred/agent/agent.py b/alfred/agent/agent.py index 4a7bfe9..0248a2c 100644 --- a/alfred/agent/agent.py +++ b/alfred/agent/agent.py @@ -6,8 +6,8 @@ from collections.abc import AsyncGenerator from typing import Any from alfred.infrastructure.persistence import get_memory - from alfred.settings import settings + from .prompts import PromptBuilder from .registry import Tool, make_tools diff --git a/alfred/agent/llm/deepseek.py b/alfred/agent/llm/deepseek.py index 7b687a1..4d93e86 100644 --- a/alfred/agent/llm/deepseek.py +++ b/alfred/agent/llm/deepseek.py @@ -6,7 +6,8 @@ from typing import Any import requests from requests.exceptions import HTTPError, RequestException, Timeout -from alfred.settings import settings, Settings +from alfred.settings import Settings, settings + from .exceptions import LLMAPIError, LLMConfigurationError logger = logging.getLogger(__name__) diff --git a/alfred/agent/llm/ollama.py b/alfred/agent/llm/ollama.py index 08d4f67..daaab22 100644 --- a/alfred/agent/llm/ollama.py +++ b/alfred/agent/llm/ollama.py @@ -1,13 +1,13 @@ """Ollama LLM client with robust error handling.""" import logging -import os from typing import Any import requests from requests.exceptions import HTTPError, RequestException, Timeout -from alfred.settings import Settings, settings +from alfred.settings import Settings + from .exceptions import LLMAPIError, LLMConfigurationError logger = logging.getLogger(__name__) diff --git a/alfred/app.py b/alfred/app.py index 2876e53..5a931b8 100644 --- a/alfred/app.py +++ b/alfred/app.py @@ -2,23 +2,21 @@ import json import logging -import os import time import uuid +from pathlib import Path from typing import Any from fastapi import FastAPI, HTTPException from fastapi.responses import JSONResponse, StreamingResponse -from fastapi.staticfiles import StaticFiles -from pathlib import Path from pydantic import BaseModel, Field, validator from alfred.agent.agent import Agent from alfred.agent.llm.deepseek import DeepSeekClient from alfred.agent.llm.exceptions import LLMAPIError, LLMConfigurationError from alfred.agent.llm.ollama import OllamaClient -from alfred.settings import settings from alfred.infrastructure.persistence import get_memory, init_memory +from alfred.settings import settings logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" @@ -55,7 +53,9 @@ except LLMConfigurationError as e: raise # Initialize agent -agent = Agent(settings=settings, llm=llm, max_tool_iterations=settings.max_tool_iterations) +agent = Agent( + settings=settings, llm=llm, max_tool_iterations=settings.max_tool_iterations +) logger.info("Agent Media API initialized") diff --git a/alfred/settings.py b/alfred/settings.py index e3d012e..0b5e7b4 100644 --- a/alfred/settings.py +++ b/alfred/settings.py @@ -1,9 +1,10 @@ import secrets -import tomllib from pathlib import Path +from typing import NamedTuple + +import tomllib from pydantic import Field, computed_field, field_validator from pydantic_settings import BaseSettings, SettingsConfigDict -from typing import NamedTuple, Optional BASE_DIR = Path(__file__).resolve().parent.parent ENV_FILE_PATH = BASE_DIR / ".env" @@ -12,13 +13,16 @@ toml_path = BASE_DIR / "pyproject.toml" class ConfigurationError(Exception): """Raised when configuration is invalid.""" + pass + class ProjectVersions(NamedTuple): """ Immutable structure for project versions. Forces explicit naming and prevents accidental swaps. """ + librechat: str rag: str @@ -36,21 +40,23 @@ def get_versions_from_toml() -> ProjectVersions: data = tomllib.load(f) try: return ProjectVersions( - librechat = data["tool"]["alfred"]["settings"]["librechat_version"], - rag = data["tool"]["alfred"]["settings"]["rag_version"] + librechat=data["tool"]["alfred"]["settings"]["librechat_version"], + rag=data["tool"]["alfred"]["settings"]["rag_version"], ) except KeyError as e: - raise KeyError(f"Error: Missing key {e} in pyproject.toml") + raise KeyError(f"Error: Missing key {e} in pyproject.toml") from e + # Load versions once VERSIONS: ProjectVersions = get_versions_from_toml() + class Settings(BaseSettings): model_config = SettingsConfigDict( env_file=ENV_FILE_PATH, env_file_encoding="utf-8", extra="ignore", - case_sensitive=False + case_sensitive=False, ) # --- GENERAL SETTINGS --- host: str = "0.0.0.0" @@ -66,16 +72,16 @@ class Settings(BaseSettings): max_tool_iterations: int = 10 request_timeout: int = 30 - #TODO: Finish + # TODO: Finish deepseek_base_url: str = "https://api.deepseek.com" deepseek_model: str = "deepseek-chat" # --- API KEYS --- - anthropic_api_key: Optional[str] = Field(None, description="Claude API key") - deepseek_api_key: Optional[str] = Field(None, description="Deepseek API key") - google_api_key: Optional[str] = Field(None, description="Gemini API key") - kimi_api_key: Optional[str] = Field(None, description="Kimi API key") - openai_api_key: Optional[str] = Field(None, description="ChatGPT API key") + anthropic_api_key: str | None = Field(None, description="Claude API key") + deepseek_api_key: str | None = Field(None, description="Deepseek API key") + google_api_key: str | None = Field(None, description="Gemini API key") + kimi_api_key: str | None = Field(None, description="Kimi API key") + openai_api_key: str | None = Field(None, description="ChatGPT API key") # --- SECURITY KEYS --- # Generated automatically if not in .env to ensure "Secure by Default" @@ -93,8 +99,9 @@ class Settings(BaseSettings): mongo_host: str = "mongodb" mongo_user: str = "alfred" - mongo_password: str = Field(default_factory=lambda: secrets.token_urlsafe(24), - repr=False, exclude=True) + mongo_password: str = Field( + default_factory=lambda: secrets.token_urlsafe(24), repr=False, exclude=True + ) mongo_port: int = 27017 mongo_db_name: str = "alfred" @@ -109,8 +116,9 @@ class Settings(BaseSettings): postgres_host: str = "vectordb" postgres_user: str = "alfred" - postgres_password: str = Field(default_factory=lambda: secrets.token_urlsafe(24), - repr=False, exclude=True) + postgres_password: str = Field( + default_factory=lambda: secrets.token_urlsafe(24), repr=False, exclude=True + ) postgres_port: int = 5432 postgres_db_name: str = "alfred" @@ -122,7 +130,7 @@ class Settings(BaseSettings): f"@{self.postgres_host}:{self.postgres_port}/{self.postgres_db_name}" ) - tmdb_api_key: Optional[str] = Field(None, description="The Movie Database API key") + tmdb_api_key: str | None = Field(None, description="The Movie Database API key") tmdb_base_url: str = "https://api.themoviedb.org/3" # --- LLM PICKER & CONFIG --- @@ -134,7 +142,7 @@ class Settings(BaseSettings): llm_temperature: float = 0.2 # --- RAG ENGINE --- - rag_enabled: bool = True # TODO: Handle False + rag_enabled: bool = True # TODO: Handle False rag_api_url: str = "http://rag_api:8000" embeddings_provider: str = "ollama" # Models: ... @@ -144,10 +152,10 @@ class Settings(BaseSettings): meili_enabled: bool = Field(True, description="Enable meili") meili_no_analytics: bool = True meili_host: str = "http://meilisearch:7700" - meili_master_key:str = Field( + meili_master_key: str = Field( default_factory=lambda: secrets.token_urlsafe(32), description="Master key for Meilisearch", - repr=False + repr=False, ) # --- VALIDATORS --- @@ -155,21 +163,27 @@ class Settings(BaseSettings): @classmethod def validate_temperature(cls, v: float) -> float: if not 0.0 <= v <= 2.0: - raise ConfigurationError(f"Temperature must be between 0.0 and 2.0, got {v}") + raise ConfigurationError( + f"Temperature must be between 0.0 and 2.0, got {v}" + ) return v @field_validator("max_tool_iterations") @classmethod def validate_max_iterations(cls, v: int) -> int: if not 1 <= v <= 20: - raise ConfigurationError(f"max_tool_iterations must be between 1 and 50, got {v}") + raise ConfigurationError( + f"max_tool_iterations must be between 1 and 50, got {v}" + ) return v @field_validator("request_timeout") @classmethod def validate_timeout(cls, v: int) -> int: if not 1 <= v <= 300: - raise ConfigurationError(f"request_timeout must be between 1 and 300 seconds, got {v}") + raise ConfigurationError( + f"request_timeout must be between 1 and 300 seconds, got {v}" + ) return v @field_validator("deepseek_base_url", "tmdb_base_url") @@ -188,4 +202,5 @@ class Settings(BaseSettings): def dump_safe(self): return self.model_dump(exclude_none=False) + settings = Settings() diff --git a/tests/conftest.py b/tests/conftest.py index a2515bc..e376e39 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,9 +11,8 @@ from unittest.mock import MagicMock, Mock import pytest -from alfred.settings import Settings, settings from alfred.infrastructure.persistence import Memory, set_memory - +from alfred.settings import settings @pytest.fixture diff --git a/tests/test_agent.py b/tests/test_agent.py index 66be297..39d03ed 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -2,7 +2,6 @@ from unittest.mock import Mock -from conftest import mock_llm from alfred.agent.agent import Agent from alfred.infrastructure.persistence import get_memory @@ -142,7 +141,9 @@ class TestStep: assert history[0]["content"] == "Hi there" assert history[1]["role"] == "assistant" - def test_step_with_tool_call(self, memory, mock_settings, mock_llm_with_tool_call, real_folder): + def test_step_with_tool_call( + self, memory, mock_settings, mock_llm_with_tool_call, real_folder + ): """Should execute tool and continue.""" memory.ltm.set_config("download_folder", str(real_folder["downloads"])) diff --git a/tests/test_agent_edge_cases.py b/tests/test_agent_edge_cases.py index 27b402d..61af156 100644 --- a/tests/test_agent_edge_cases.py +++ b/tests/test_agent_edge_cases.py @@ -7,7 +7,6 @@ import pytest from alfred.agent.agent import Agent from alfred.infrastructure.persistence import get_memory from alfred.settings import settings -from conftest import mock_llm class TestExecuteToolCallEdgeCases: diff --git a/tests/test_api_edge_cases.py b/tests/test_api_edge_cases.py index 4c7b4c9..7910909 100644 --- a/tests/test_api_edge_cases.py +++ b/tests/test_api_edge_cases.py @@ -1,8 +1,8 @@ """Edge case tests for FastAPI endpoints.""" -import pytest from unittest.mock import Mock, patch +import pytest from fastapi.testclient import TestClient diff --git a/tests/test_config_critical.py b/tests/test_config_critical.py index c49b0c6..d05c11d 100644 --- a/tests/test_config_critical.py +++ b/tests/test_config_critical.py @@ -2,7 +2,7 @@ import pytest -from alfred.settings import Settings, ConfigurationError +from alfred.settings import ConfigurationError, Settings class TestConfigValidation: diff --git a/tests/test_config_edge_cases.py b/tests/test_config_edge_cases.py index e0b904c..acd6da2 100644 --- a/tests/test_config_edge_cases.py +++ b/tests/test_config_edge_cases.py @@ -5,13 +5,13 @@ from unittest.mock import patch import pytest -from alfred.settings import Settings, ConfigurationError from alfred.agent.parameters import ( REQUIRED_PARAMETERS, ParameterSchema, format_parameters_for_prompt, get_missing_required_parameters, ) +from alfred.settings import ConfigurationError, Settings class TestSettingsEdgeCases: diff --git a/tests/test_prompts_edge_cases.py b/tests/test_prompts_edge_cases.py index 466a012..fe86b86 100644 --- a/tests/test_prompts_edge_cases.py +++ b/tests/test_prompts_edge_cases.py @@ -4,6 +4,7 @@ from alfred.agent.prompts import PromptBuilder from alfred.agent.registry import make_tools from alfred.settings import settings + class TestPromptBuilderEdgeCases: """Edge case tests for PromptBuilder."""