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