diff --git a/brain/.bumpversion.toml b/.bumpversion.toml similarity index 100% rename from brain/.bumpversion.toml rename to .bumpversion.toml diff --git a/brain/.dockerignore b/.dockerignore similarity index 85% rename from brain/.dockerignore rename to .dockerignore index 3338a74..3479072 100644 --- a/brain/.dockerignore +++ b/.dockerignore @@ -41,11 +41,8 @@ docs/ *.md !README.md -# Tests -tests/ -pytest.ini - -# Data (will be mounted as volumes) +# Data +data/ memory_data/ logs/ *.log diff --git a/.env.example b/.env.example index 2ea651e..ece1a0c 100644 --- a/.env.example +++ b/.env.example @@ -1,69 +1,53 @@ -# Agent Media - Environment Variables +# Configuration +LIBRECHAT_VERSION=v0.8.1 +RAG_VERSION=v0.7.0 -# LibreChat Security Keys -# Generate secure keys with: openssl rand -base64 32 -JWT_SECRET=your-super-secret-jwt-key-change-this-in-production -JWT_REFRESH_SECRET=your-super-secret-refresh-key-change-this-too +# Keys +# - Deepseek API +DEEPSEEK_API_KEY= -# Generate with: openssl rand -hex 16 (for CREDS_KEY) -CREDS_KEY=your-32-character-secret-key-here +# - Google API +GOOGLE_API_KEY= +#GOOGLE_MODELS=gemini-2.5-pro,gemini-2.5-flash,gemini-2.5-flash-lite,gemini-2.0-flash,gemini-2.0-flash-lite #TODO:Update models -# Generate with: openssl rand -hex 8 (for CREDS_IV) -CREDS_IV=your-16-character-iv-here +# - Anthropic API +ANTHROPIC_API_KEY= -# LibreChat Configuration -DOMAIN_CLIENT=http://localhost:3080 -DOMAIN_SERVER=http://localhost:3080 +# - Kimi API +KIMI_API_KEY= -# Session expiry (in milliseconds) -# Default: 15 minutes -SESSION_EXPIRY=900000 +# - ChatGPT/Open API +OPENAI_API_KEY= -# Refresh token expiry (in milliseconds) -# Default: 7 days -REFRESH_TOKEN_EXPIRY=604800000 +# - Themoviedb.org API (media metadata) +TMDB_API_KEY= -# Meilisearch Configuration -# Master key for Meilisearch (generate with: openssl rand -base64 32) -MEILI_MASTER_KEY=DrhYf7zENyR6AlUCKmnz0eYASOQdl6zxH7s7MKFSfFU +# - Security keys +JWT_SECRET= +JWT_REFRESH_SECRET= +CREDS_KEY= +CREDS_IV= -# PostgreSQL Configuration (for RAG API) -POSTGRES_DB=librechat_rag -POSTGRES_USER=postgres -POSTGRES_PASSWORD=postgres +# Local LLM +OLLAMA_BASE_URL= +OLLAMA_MODEL= -# RAG API Configuration (Vector Database) -RAG_COLLECTION_NAME=testcollection -RAG_EMBEDDINGS_PROVIDER=openai -RAG_EMBEDDINGS_MODEL=text-embedding-3-small - -# API Keys -# OpenAI API Key (required for RAG embeddings) -OPENAI_API_KEY=your-openai-api-key-here - -# Deepseek API Key (for LLM in agent-brain) -DEEPSEEK_API_KEY=your-deepseek-api-key-here - -# Agent Brain Configuration - -# LLM Provider (deepseek or ollama) +# Alfred Configuration LLM_PROVIDER=deepseek # Memory storage directory (inside container) MEMORY_STORAGE_DIR=/data/memory -# API Key for agent-brain (used by LibreChat custom endpoint) -AGENT_BRAIN_API_KEY=agent-brain-secret-key - -# External Services (Optional) -# TMDB API Key (for movie metadata) -TMDB_API_KEY=your-tmdb-key - # qBittorrent Configuration -QBITTORRENT_URL=http://localhost:8080 +QBITTORRENT_URL= QBITTORRENT_USERNAME=admin -QBITTORRENT_PASSWORD=adminpass +QBITTORRENT_PASSWORD=adminadmin # Debug Options DEBUG_LOGGING=false DEBUG_CONSOLE=false + +# Postgres (RAG) +POSTGRES_DB= +POSTGRES_USER= +POSTGRES_PASSWORD= diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index eb544f8..ee4a02c 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -63,7 +63,6 @@ jobs: uses: docker/build-push-action@v5 with: context: . - file: ./brain/Dockerfile push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/.gitignore b/.gitignore index 2282737..69d7317 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,9 @@ Thumbs.db # Backup files *.backup + +# Application data dir +data/* + +# Application logs +logs/* diff --git a/brain/Dockerfile b/Dockerfile similarity index 85% rename from brain/Dockerfile rename to Dockerfile index 8de8721..15ab5f9 100644 --- a/brain/Dockerfile +++ b/Dockerfile @@ -30,7 +30,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \ WORKDIR /tmp # Copy dependency files -COPY brain/pyproject.toml brain/poetry.lock* brain/uv.lock* Makefile ./ +COPY pyproject.toml poetry.lock* uv.lock* Makefile ./ # Install dependencies as root (to avoid permission issues with system packages) RUN --mount=type=cache,target=/root/.cache/pip \ @@ -59,12 +59,8 @@ RUN --mount=type=cache,target=/root/.cache/pip \ uv pip install --system -e .[dev]; \ fi -COPY brain/agent/ ./agent/ -COPY brain/application/ ./application/ -COPY brain/domain/ ./domain/ -COPY brain/infrastructure/ ./infrastructure/ -COPY brain/tests/ ./tests/ -COPY brain/app.py . +COPY alfred/ ./alfred +COPY tests/ ./tests # =========================================== # Stage 3: Runtime @@ -96,18 +92,14 @@ RUN mkdir -p /data/memory /data/logs \ USER appuser # Set working directory (owned by appuser) -WORKDIR /home/appuser/app +WORKDIR /home/appuser # Copy Python packages from builder stage COPY --from=builder /usr/local/lib/python${PYTHON_VERSION_SHORT}/site-packages /usr/local/lib/python${PYTHON_VERSION_SHORT}/site-packages COPY --from=builder /usr/local/bin /usr/local/bin # Copy application code (already owned by appuser) -COPY --chown=appuser:appuser brain/agent/ ./agent/ -COPY --chown=appuser:appuser brain/application/ ./application/ -COPY --chown=appuser:appuser brain/domain/ ./domain/ -COPY --chown=appuser:appuser brain/infrastructure/ ./infrastructure/ -COPY --chown=appuser:appuser brain/app.py . +COPY --chown=appuser:appuser alfred/ ./alfred # Create volumes for persistent data VOLUME ["/data/memory", "/data/logs"] diff --git a/Makefile b/Makefile index 1097bf1..6734100 100644 --- a/Makefile +++ b/Makefile @@ -1,249 +1,155 @@ -.POSIX: -.SUFFIXES: .DEFAULT_GOAL := help -# --- SETTINGS --- -CORE_DIR = brain -IMAGE_NAME = agent_media -# renovate: datasource=docker depName=python -PYTHON_VERSION = $(shell grep "python" $(CORE_DIR)/pyproject.toml | head -n 1 | sed -E 's/.*[=<>^~"]+ *([0-9]+\.[0-9]+(\.[0-9]+)?).*/\1/') -PYTHON_VERSION_SHORT = $(shell echo $(PYTHON_VERSION) | cut -d. -f1,2) -# Change to 'uv' when ready. -RUNNER ?= poetry -SERVICE_NAME = agent_media +# --- Config --- +export IMAGE_NAME := alfred_media_organizer +export LIBRECHAT_VERSION := v0.8.1 +export PYTHON_VERSION := 3.14.2 +export PYTHON_VERSION_SHORT := 3.14 +export RAG_VERSION := v0.7.0 +export RUNNER := poetry +export SERVICE_NAME := alfred -export IMAGE_NAME -export PYTHON_VERSION -export PYTHON_VERSION_SHORT -export RUNNER +# --- Commands --- +CLI := python3 cli.py +DOCKER_COMPOSE := docker compose +DOCKER_BUILD := docker build \ + --build-arg PYTHON_VERSION=$(PYTHON_VERSION) \ + --build-arg PYTHON_VERSION_SHORT=$(PYTHON_VERSION_SHORT) \ + --build-arg RUNNER=$(RUNNER) -# --- ADAPTERS --- -# UV uses "sync", Poetry uses "install". Both install DEV deps by default. -INSTALL_CMD = $(if $(filter uv,$(RUNNER)),sync,install) +# --- Phony --- +.PHONY: setup status check +.PHONY: up down restart logs ps shell +.PHONY: build build-test +.PHONY: install update install-hooks +.PHONY: test coverage lint format clean prune +.PHONY: major minor patch +.PHONY: help -# --- MACROS --- -ARGS = $(filter-out $@,$(MAKECMDGOALS)) -BUMP_CMD = cd $(CORE_DIR) && $(RUNNER) run bump-my-version bump -COMPOSE_CMD = docker-compose -DOCKER_CMD = docker build \ - --build-arg PYTHON_VERSION=$(PYTHON_VERSION) \ - --build-arg PYTHON_VERSION_SHORT=$(PYTHON_VERSION_SHORT) \ - --build-arg RUNNER=$(RUNNER) \ - -f $(CORE_DIR)/Dockerfile \ - -t $(IMAGE_NAME):latest . +# --- Setup --- +setup: + @echo "Initializing environment..." + @$(CLI) setup \ + && echo "✓ Environment ready" \ + || (echo "✗ Setup failed" && exit 1) -RUNNER_ADD = cd $(CORE_DIR) && $(RUNNER) add -RUNNER_HOOKS = cd $(CORE_DIR) && $(RUNNER) run pre-commit install -c ../.pre-commit-config.yaml -RUNNER_INSTALL = cd $(CORE_DIR) && $(RUNNER) $(INSTALL_CMD) -RUNNER_RUN = cd $(CORE_DIR) && $(RUNNER) run -RUNNER_UPDATE = cd $(CORE_DIR) && $(RUNNER) update +status: + @$(CLI) status -# --- STYLES --- -B = \033[1m -G = \033[32m -T = \033[36m -R = \033[0m +check: + @$(CLI) check -# --- TARGETS --- -.PHONY: add build build-test check-docker check-runner clean coverage down format help init-dotenv install install-hooks lint logs major minor patch prune ps python-version restart run shell test up update _check_branch _ci-dump-config _ci-run-tests _push_tag +# --- Docker --- +up: check + @echo "Starting containers..." + @$(DOCKER_COMPOSE) up -d --remove-orphans \ + && echo "✓ Containers started" \ + || (echo "✗ Failed to start containers" && exit 1) -# Catch-all for args -%: - @: +down: + @echo "Stopping containers..." + @$(DOCKER_COMPOSE) down \ + && echo "✓ Containers stopped" \ + || (echo "✗ Failed to stop containers" && exit 1) -add: check-runner - @echo "$(T)➕ Adding dependency ($(RUNNER)): $(ARGS)$(R)" - $(RUNNER_ADD) $(ARGS) +restart: + @echo "Restarting containers..." + @$(DOCKER_COMPOSE) restart \ + && echo "✓ Containers restarted" \ + || (echo "✗ Failed to restart containers" && exit 1) -build: check-docker - @echo "$(T)🐳 Building Docker image...$(R)" - $(DOCKER_CMD) - @echo "✅ Image $(IMAGE_NAME):latest ready." +logs: + @echo "Following logs (Ctrl+C to exit)..." + @$(DOCKER_COMPOSE) logs -f -build-test: check-docker - @echo "$(T)🐳 Building test image (with dev deps)...$(R)" - docker build \ - --build-arg RUNNER=$(RUNNER) \ - --build-arg PYTHON_VERSION=$(PYTHON_VERSION) \ - --build-arg PYTHON_VERSION_SHORT=$(PYTHON_VERSION_SHORT) \ - -f $(CORE_DIR)/Dockerfile \ - --target test \ - -t $(IMAGE_NAME):test . - @echo "✅ Test image $(IMAGE_NAME):test ready." +ps: + @echo "Container status:" + @$(DOCKER_COMPOSE) ps -check-docker: - @command -v docker >/dev/null 2>&1 || { echo "$(R)❌ Docker not installed$(R)"; exit 1; } - @docker info >/dev/null 2>&1 || { echo "$(R)❌ Docker daemon not running$(R)"; exit 1; } +shell: + @echo "Opening shell in $(SERVICE_NAME)..." + @$(DOCKER_COMPOSE) exec $(SERVICE_NAME) /bin/bash -check-runner: - @command -v $(RUNNER) >/dev/null 2>&1 || { echo "$(R)❌ $(RUNNER) not installed$(R)"; exit 1; } +# --- Build --- +build: check + @echo "Building image $(IMAGE_NAME):latest ..." + @$(DOCKER_BUILD) -t $(IMAGE_NAME):latest . \ + && echo "✓ Build complete" \ + || (echo "✗ Build failed" && exit 1) + +build-test: check + @echo "Building test image $(IMAGE_NAME):test..." + @$(DOCKER_BUILD) --target test -t $(IMAGE_NAME):test . \ + && echo "✓ Test image built" \ + || (echo "✗ Build failed" && exit 1) + +# --- Dependencies --- +install: + @echo "Installing dependencies with $(RUNNER)..." + @$(RUNNER) install \ + && echo "✓ Dependencies installed" \ + || (echo "✗ Installation failed" && exit 1) + +update: + @echo "Updating dependencies with $(RUNNER)..." + @$(RUNNER) update \ + && echo "✓ Dependencies updated" \ + || (echo "✗ Update failed" && exit 1) + +install-hooks: + @echo "Installing pre-commit hooks..." + @$(RUNNER) run pre-commit install \ + && echo "✓ Hooks installed" \ + || (echo "✗ Hook installation failed" && exit 1) + +# --- Quality --- +test: + @echo "Running tests..." + @$(RUNNER) run pytest \ + && echo "✓ Tests passed" \ + || (echo "✗ Tests failed" && exit 1) + +coverage: + @echo "Running tests with coverage..." + @$(RUNNER) run pytest --cov=. --cov-report=html --cov-report=term \ + && echo "✓ Coverage report generated" \ + || (echo "✗ Coverage failed" && exit 1) + +lint: + @echo "Linting code..." + @$(RUNNER) run ruff check --fix . \ + && echo "✓ Linting complete" \ + || (echo "✗ Linting failed" && exit 1) + +format: + @echo "Formatting code..." + @$(RUNNER) run ruff format . && $(RUNNER) run ruff check --fix . \ + && echo "✓ Code formatted" \ + || (echo "✗ Formatting failed" && exit 1) clean: - @echo "$(T)🧹 Cleaning caches...$(R)" - cd $(CORE_DIR) && rm -rf .ruff_cache __pycache__ .pytest_cache - find $(CORE_DIR) -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true - find $(CORE_DIR) -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true - find $(CORE_DIR) -type f -name "*.pyc" -delete 2>/dev/null || true - @echo "✅ Caches cleaned." + @echo "Cleaning build artifacts..." + @rm -rf .ruff_cache __pycache__ .pytest_cache htmlcov .coverage + @find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true + @echo "✓ Cleanup complete" -coverage: check-runner - @echo "$(T)📊 Running tests with coverage...$(R)" - $(RUNNER_RUN) pytest --cov=. --cov-report=html --cov-report=term $(ARGS) - @echo "✅ Report generated in htmlcov/" +prune: + @echo "Pruning Docker system..." + @docker system prune -af \ + && echo "✓ Docker pruned" \ + || (echo "✗ Prune failed" && exit 1) -down: check-docker - @echo "$(T)🛑 Stopping containers...$(R)" - $(COMPOSE_CMD) down - @echo "✅ System stopped." +# --- Versioning --- +major minor patch: _check-main + @echo "Bumping $@ version..." + @$(RUNNER) run bump-my-version bump $@ \ + && echo "✓ Version bumped" \ + || (echo "✗ Version bump failed" && exit 1) -format: check-runner - @echo "$(T)✨ Formatting with Ruff...$(R)" - $(RUNNER_RUN) ruff format . - $(RUNNER_RUN) ruff check --fix . - @echo "✅ Code cleaned." - -help: - @echo "$(B)Available commands:$(R)" - @echo "" - @echo "$(G)Setup:$(R)" - @echo " $(T)check-docker $(R) Verify Docker is installed and running." - @echo " $(T)check-runner $(R) Verify package manager ($(RUNNER))." - @echo " $(T)init-dotenv $(R) Create .env from .env.example with generated secrets." - @echo " $(T)install $(R) Install ALL dependencies (Prod + Dev)." - @echo " $(T)install-hooks $(R) Install git pre-commit hooks." - @echo "" - @echo "$(G)Docker:$(R)" - @echo " $(T)build $(R) Build the docker image (production)." - @echo " $(T)build-test $(R) Build the docker image (with dev deps for testing)." - @echo " $(T)down $(R) Stop and remove containers." - @echo " $(T)logs $(R) Follow logs." - @echo " $(T)prune $(R) Clean Docker system." - @echo " $(T)ps $(R) Show container status." - @echo " $(T)restart $(R) Restart all containers." - @echo " $(T)shell $(R) Open shell in container." - @echo " $(T)up $(R) Start the agent." - @echo "" - @echo "$(G)Development:$(R)" - @echo " $(T)add ... $(R) Add dependency (use --group dev or --dev if needed)." - @echo " $(T)clean $(R) Clean caches." - @echo " $(T)coverage $(R) Run tests with coverage." - @echo " $(T)format $(R) Format code (Ruff)." - @echo " $(T)lint $(R) Lint code without fixing." - @echo " $(T)test ... $(R) Run tests (local with $(RUNNER))." - @echo " $(T)update $(R) Update dependencies." - @echo "" - @echo "$(G)Versioning:$(R)" - @echo " $(T)major/minor/patch $(R) Bump version and push tag (triggers CI/CD)." - -init-dotenv: - @echo "$(T)🔑 Initializing .env file...$(R)" - @if [ -f .env ]; then \ - echo "$(R)⚠️ .env already exists. Skipping.$(R)"; \ - exit 0; \ - fi - @if [ ! -f .env.example ]; then \ - echo "$(R)❌ .env.example not found$(R)"; \ - exit 1; \ - fi - @if ! command -v openssl >/dev/null 2>&1; then \ - echo "$(R)❌ openssl not found. Please install it first.$(R)"; \ - exit 1; \ - fi - @echo "$(T) → Copying .env.example...$(R)" - @cp .env.example .env - @echo "$(T) → Generating secrets...$(R)" - @sed -i.bak "s|JWT_SECRET=.*|JWT_SECRET=$$(openssl rand -base64 32)|" .env - @sed -i.bak "s|JWT_REFRESH_SECRET=.*|JWT_REFRESH_SECRET=$$(openssl rand -base64 32)|" .env - @sed -i.bak "s|CREDS_KEY=.*|CREDS_KEY=$$(openssl rand -hex 16)|" .env - @sed -i.bak "s|CREDS_IV=.*|CREDS_IV=$$(openssl rand -hex 8)|" .env - @sed -i.bak "s|MEILI_MASTER_KEY=.*|MEILI_MASTER_KEY=$$(openssl rand -base64 32)|" .env - @sed -i.bak "s|AGENT_BRAIN_API_KEY=.*|AGENT_BRAIN_API_KEY=$$(openssl rand -base64 24)|" .env - @rm -f .env.bak - @echo "$(G)✅ .env created with generated secrets!$(R)" - @echo "$(T)⚠️ Don't forget to add your API keys:$(R)" - @echo " - OPENAI_API_KEY" - @echo " - DEEPSEEK_API_KEY" - @echo " - TMDB_API_KEY (optional)" - -install: check-runner - @echo "$(T)📦 Installing FULL environment ($(RUNNER))...$(R)" - $(RUNNER_INSTALL) - @echo "✅ Environment ready (Prod + Dev)." - -install-hooks: check-runner - @echo "$(T)🔧 Installing hooks...$(R)" - $(RUNNER_HOOKS) - @echo "✅ Hooks ready." - -lint: check-runner - @echo "$(T)🔍 Linting code...$(R)" - $(RUNNER_RUN) ruff check . - -logs: check-docker - @echo "$(T)📋 Following logs...$(R)" - $(COMPOSE_CMD) logs -f - -major: _check_branch - @echo "$(T)💥 Bumping major...$(R)" - SKIP=all $(BUMP_CMD) major - @$(MAKE) -s _push_tag - -minor: _check_branch - @echo "$(T)✨ Bumping minor...$(R)" - SKIP=all $(BUMP_CMD) minor - @$(MAKE) -s _push_tag - -patch: _check_branch - @echo "$(T)🚀 Bumping patch...$(R)" - SKIP=all $(BUMP_CMD) patch - @$(MAKE) -s _push_tag - -prune: check-docker - @echo "$(T)🗑️ Pruning Docker resources...$(R)" - docker system prune -af - @echo "✅ Docker cleaned." - -ps: check-docker - @echo "$(T)📋 Container status:$(R)" - @$(COMPOSE_CMD) ps - -python-version: - @echo "🔍 Reading pyproject.toml..." - @echo "✅ Python version : $(PYTHON_VERSION)" - @echo "ℹ️ Sera utilisé pour : FROM python:$(PYTHON_VERSION)-slim" - - -restart: check-docker - @echo "$(T)🔄 Restarting containers...$(R)" - $(COMPOSE_CMD) restart - @echo "✅ Containers restarted." - -run: check-runner - $(RUNNER_RUN) $(ARGS) - -shell: check-docker - @echo "$(T)🐚 Opening shell in $(SERVICE_NAME)...$(R)" - $(COMPOSE_CMD) exec $(SERVICE_NAME) /bin/sh - -test: check-runner - @echo "$(T)🧪 Running tests...$(R)" - $(RUNNER_RUN) pytest $(ARGS) - -up: check-docker - @echo "$(T)🚀 Starting Agent Media...$(R)" - $(COMPOSE_CMD) up -d - @echo "✅ System is up." - -update: check-runner - @echo "$(T)🔄 Updating dependencies...$(R)" - $(RUNNER_UPDATE) - @echo "✅ All packages up to date." - -_check_branch: - @curr=$$(git rev-parse --abbrev-ref HEAD); \ - if [ "$$curr" != "main" ]; then \ - echo "❌ Error: not on the main branch"; exit 1; \ - fi + @echo "Pushing tags..." + @git push --tags \ + && echo "✓ Tags pushed" \ + || (echo "✗ Push failed" && exit 1) _ci-dump-config: @echo "image_name=$(IMAGE_NAME)" @@ -252,15 +158,46 @@ _ci-dump-config: @echo "runner=$(RUNNER)" @echo "service_name=$(SERVICE_NAME)" -_ci-run-tests: build-test - @echo "$(T)🧪 Running tests in Docker...$(R)" +_ci-run-tests: + @echo "Running tests in Docker..." docker run --rm \ -e DEEPSEEK_API_KEY \ -e TMDB_API_KEY \ + -e QBITTORRENT_URL \ $(IMAGE_NAME):test pytest - @echo "✅ Tests passed." + @echo "✓ Tests passed." -_push_tag: - @echo "$(T)📦 Pushing tag...$(R)" - git push --tags - @echo "✅ Tag pushed. Check CI for build status." +_check-main: + @test "$$(git rev-parse --abbrev-ref HEAD)" = "main" \ + || (echo "✗ ERROR: Not on main branch" && exit 1) + +# --- Help --- +help: + @echo "Usage: make [target]" + @echo "" + @echo "Setup:" + @echo " setup Initialize .env" + @echo " status Show project status" + @echo "" + @echo "Docker:" + @echo " up Start containers" + @echo " down Stop containers" + @echo " restart Restart containers" + @echo " logs Follow logs" + @echo " ps Container status" + @echo " shell Shell into container" + @echo " build Build image" + @echo "" + @echo "Dev:" + @echo " install Install dependencies" + @echo " update Update dependencies" + @echo " test Run tests" + @echo " coverage Run tests with coverage" + @echo " lint Lint code" + @echo " format Format code" + @echo " clean Clean artifacts" + @echo "" + @echo "Release:" + @echo " patch Bump patch version" + @echo " minor Bump minor version" + @echo " major Bump major version" \ No newline at end of file diff --git a/brain/README.md b/README.md similarity index 100% rename from brain/README.md rename to README.md diff --git a/brain/agent/__init__.py b/alfred/agent/__init__.py similarity index 100% rename from brain/agent/__init__.py rename to alfred/agent/__init__.py diff --git a/brain/agent/agent.py b/alfred/agent/agent.py similarity index 99% rename from brain/agent/agent.py rename to alfred/agent/agent.py index 27e9cf0..770fbcf 100644 --- a/brain/agent/agent.py +++ b/alfred/agent/agent.py @@ -5,7 +5,7 @@ import logging from collections.abc import AsyncGenerator from typing import Any -from infrastructure.persistence import get_memory +from alfred.infrastructure.persistence import get_memory from .config import settings from .prompts import PromptBuilder diff --git a/brain/agent/config.py b/alfred/agent/config.py similarity index 100% rename from brain/agent/config.py rename to alfred/agent/config.py diff --git a/brain/agent/llm/__init__.py b/alfred/agent/llm/__init__.py similarity index 100% rename from brain/agent/llm/__init__.py rename to alfred/agent/llm/__init__.py diff --git a/brain/agent/llm/deepseek.py b/alfred/agent/llm/deepseek.py similarity index 100% rename from brain/agent/llm/deepseek.py rename to alfred/agent/llm/deepseek.py diff --git a/brain/agent/llm/exceptions.py b/alfred/agent/llm/exceptions.py similarity index 100% rename from brain/agent/llm/exceptions.py rename to alfred/agent/llm/exceptions.py diff --git a/brain/agent/llm/ollama.py b/alfred/agent/llm/ollama.py similarity index 100% rename from brain/agent/llm/ollama.py rename to alfred/agent/llm/ollama.py diff --git a/brain/agent/parameters.py b/alfred/agent/parameters.py similarity index 100% rename from brain/agent/parameters.py rename to alfred/agent/parameters.py diff --git a/brain/agent/prompts.py b/alfred/agent/prompts.py similarity index 98% rename from brain/agent/prompts.py rename to alfred/agent/prompts.py index 734e627..d568b9d 100644 --- a/brain/agent/prompts.py +++ b/alfred/agent/prompts.py @@ -3,7 +3,7 @@ import json from typing import Any -from infrastructure.persistence import get_memory +from alfred.infrastructure.persistence import get_memory from .registry import Tool @@ -52,7 +52,7 @@ class PromptBuilder: # Show first 5 results for i, result in enumerate(result_list[:5]): name = result.get("name", "Unknown") - lines.append(f" {i+1}. {name}") + lines.append(f" {i + 1}. {name}") if len(result_list) > 5: lines.append(f" ... and {len(result_list) - 5} more") diff --git a/brain/agent/registry.py b/alfred/agent/registry.py similarity index 100% rename from brain/agent/registry.py rename to alfred/agent/registry.py diff --git a/brain/agent/tools/__init__.py b/alfred/agent/tools/__init__.py similarity index 100% rename from brain/agent/tools/__init__.py rename to alfred/agent/tools/__init__.py diff --git a/brain/agent/tools/api.py b/alfred/agent/tools/api.py similarity index 93% rename from brain/agent/tools/api.py rename to alfred/agent/tools/api.py index 0898c60..ecca8e5 100644 --- a/brain/agent/tools/api.py +++ b/alfred/agent/tools/api.py @@ -3,12 +3,12 @@ import logging from typing import Any -from application.movies import SearchMovieUseCase -from application.torrents import AddTorrentUseCase, SearchTorrentsUseCase -from infrastructure.api.knaben import knaben_client -from infrastructure.api.qbittorrent import qbittorrent_client -from infrastructure.api.tmdb import tmdb_client -from infrastructure.persistence import get_memory +from alfred.application.movies import SearchMovieUseCase +from alfred.application.torrents import AddTorrentUseCase, SearchTorrentsUseCase +from alfred.infrastructure.api.knaben import knaben_client +from alfred.infrastructure.api.qbittorrent import qbittorrent_client +from alfred.infrastructure.api.tmdb import tmdb_client +from alfred.infrastructure.persistence import get_memory logger = logging.getLogger(__name__) diff --git a/brain/agent/tools/filesystem.py b/alfred/agent/tools/filesystem.py similarity index 88% rename from brain/agent/tools/filesystem.py rename to alfred/agent/tools/filesystem.py index cc7d547..017c339 100644 --- a/brain/agent/tools/filesystem.py +++ b/alfred/agent/tools/filesystem.py @@ -2,8 +2,8 @@ from typing import Any -from application.filesystem import ListFolderUseCase, SetFolderPathUseCase -from infrastructure.filesystem import FileManager +from alfred.application.filesystem import ListFolderUseCase, SetFolderPathUseCase +from alfred.infrastructure.filesystem import FileManager def set_path_for_folder(folder_name: str, path_value: str) -> dict[str, Any]: diff --git a/brain/agent/tools/language.py b/alfred/agent/tools/language.py similarity index 93% rename from brain/agent/tools/language.py rename to alfred/agent/tools/language.py index e7ea471..22b0098 100644 --- a/brain/agent/tools/language.py +++ b/alfred/agent/tools/language.py @@ -3,7 +3,7 @@ import logging from typing import Any -from infrastructure.persistence import get_memory +from alfred.infrastructure.persistence import get_memory logger = logging.getLogger(__name__) diff --git a/brain/app.py b/alfred/app.py similarity index 96% rename from brain/app.py rename to alfred/app.py index 2788a9f..2532054 100644 --- a/brain/app.py +++ b/alfred/app.py @@ -12,12 +12,12 @@ from fastapi.responses import JSONResponse, StreamingResponse from fastapi.staticfiles import StaticFiles from pydantic import BaseModel, Field, validator -from agent.agent import Agent -from agent.config import settings -from agent.llm.deepseek import DeepSeekClient -from agent.llm.exceptions import LLMAPIError, LLMConfigurationError -from agent.llm.ollama import OllamaClient -from infrastructure.persistence import get_memory, init_memory +from alfred.agent.agent import Agent +from alfred.agent.config import settings +from alfred.agent.llm.deepseek import DeepSeekClient +from alfred.agent.llm.exceptions import LLMAPIError, LLMConfigurationError +from alfred.agent.llm.ollama import OllamaClient +from alfred.infrastructure.persistence import get_memory, init_memory logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" @@ -41,7 +41,6 @@ else: ) # Initialize memory context at startup -# Use /data/memory in Docker, fallback to memory_data for local dev storage_dir = os.getenv("MEMORY_STORAGE_DIR", "memory_data") init_memory(storage_dir=storage_dir) logger.info(f"Memory context initialized (storage: {storage_dir})") diff --git a/brain/application/__init__.py b/alfred/application/__init__.py similarity index 100% rename from brain/application/__init__.py rename to alfred/application/__init__.py diff --git a/brain/application/filesystem/__init__.py b/alfred/application/filesystem/__init__.py similarity index 100% rename from brain/application/filesystem/__init__.py rename to alfred/application/filesystem/__init__.py diff --git a/brain/application/filesystem/dto.py b/alfred/application/filesystem/dto.py similarity index 100% rename from brain/application/filesystem/dto.py rename to alfred/application/filesystem/dto.py diff --git a/brain/application/filesystem/list_folder.py b/alfred/application/filesystem/list_folder.py similarity index 96% rename from brain/application/filesystem/list_folder.py rename to alfred/application/filesystem/list_folder.py index fdae123..02abfb0 100644 --- a/brain/application/filesystem/list_folder.py +++ b/alfred/application/filesystem/list_folder.py @@ -2,7 +2,7 @@ import logging -from infrastructure.filesystem import FileManager +from alfred.infrastructure.filesystem import FileManager from .dto import ListFolderResponse diff --git a/brain/application/filesystem/set_folder_path.py b/alfred/application/filesystem/set_folder_path.py similarity index 95% rename from brain/application/filesystem/set_folder_path.py rename to alfred/application/filesystem/set_folder_path.py index 2f3d0ea..277a35f 100644 --- a/brain/application/filesystem/set_folder_path.py +++ b/alfred/application/filesystem/set_folder_path.py @@ -2,7 +2,7 @@ import logging -from infrastructure.filesystem import FileManager +from alfred.infrastructure.filesystem import FileManager from .dto import SetFolderPathResponse diff --git a/brain/application/movies/__init__.py b/alfred/application/movies/__init__.py similarity index 100% rename from brain/application/movies/__init__.py rename to alfred/application/movies/__init__.py diff --git a/brain/application/movies/dto.py b/alfred/application/movies/dto.py similarity index 100% rename from brain/application/movies/dto.py rename to alfred/application/movies/dto.py diff --git a/brain/application/movies/search_movie.py b/alfred/application/movies/search_movie.py similarity index 98% rename from brain/application/movies/search_movie.py rename to alfred/application/movies/search_movie.py index 940fd0d..652d418 100644 --- a/brain/application/movies/search_movie.py +++ b/alfred/application/movies/search_movie.py @@ -2,7 +2,7 @@ import logging -from infrastructure.api.tmdb import ( +from alfred.infrastructure.api.tmdb import ( TMDBAPIError, TMDBClient, TMDBConfigurationError, diff --git a/brain/application/torrents/__init__.py b/alfred/application/torrents/__init__.py similarity index 100% rename from brain/application/torrents/__init__.py rename to alfred/application/torrents/__init__.py diff --git a/brain/application/torrents/add_torrent.py b/alfred/application/torrents/add_torrent.py similarity index 98% rename from brain/application/torrents/add_torrent.py rename to alfred/application/torrents/add_torrent.py index d6fce1b..2d2214d 100644 --- a/brain/application/torrents/add_torrent.py +++ b/alfred/application/torrents/add_torrent.py @@ -2,7 +2,7 @@ import logging -from infrastructure.api.qbittorrent import ( +from alfred.infrastructure.api.qbittorrent import ( QBittorrentAPIError, QBittorrentAuthError, QBittorrentClient, diff --git a/brain/application/torrents/dto.py b/alfred/application/torrents/dto.py similarity index 100% rename from brain/application/torrents/dto.py rename to alfred/application/torrents/dto.py diff --git a/brain/application/torrents/search_torrents.py b/alfred/application/torrents/search_torrents.py similarity index 96% rename from brain/application/torrents/search_torrents.py rename to alfred/application/torrents/search_torrents.py index 1dd9745..0b83c11 100644 --- a/brain/application/torrents/search_torrents.py +++ b/alfred/application/torrents/search_torrents.py @@ -2,7 +2,11 @@ import logging -from infrastructure.api.knaben import KnabenAPIError, KnabenClient, KnabenNotFoundError +from alfred.infrastructure.api.knaben import ( + KnabenAPIError, + KnabenClient, + KnabenNotFoundError, +) from .dto import SearchTorrentsResponse diff --git a/brain/domain/__init__.py b/alfred/domain/__init__.py similarity index 100% rename from brain/domain/__init__.py rename to alfred/domain/__init__.py diff --git a/brain/domain/movies/__init__.py b/alfred/domain/movies/__init__.py similarity index 100% rename from brain/domain/movies/__init__.py rename to alfred/domain/movies/__init__.py diff --git a/brain/domain/movies/entities.py b/alfred/domain/movies/entities.py similarity index 100% rename from brain/domain/movies/entities.py rename to alfred/domain/movies/entities.py diff --git a/brain/domain/movies/exceptions.py b/alfred/domain/movies/exceptions.py similarity index 100% rename from brain/domain/movies/exceptions.py rename to alfred/domain/movies/exceptions.py diff --git a/brain/domain/movies/repositories.py b/alfred/domain/movies/repositories.py similarity index 100% rename from brain/domain/movies/repositories.py rename to alfred/domain/movies/repositories.py diff --git a/brain/domain/movies/services.py b/alfred/domain/movies/services.py similarity index 100% rename from brain/domain/movies/services.py rename to alfred/domain/movies/services.py diff --git a/brain/domain/movies/value_objects.py b/alfred/domain/movies/value_objects.py similarity index 100% rename from brain/domain/movies/value_objects.py rename to alfred/domain/movies/value_objects.py diff --git a/brain/domain/shared/__init__.py b/alfred/domain/shared/__init__.py similarity index 100% rename from brain/domain/shared/__init__.py rename to alfred/domain/shared/__init__.py diff --git a/brain/domain/shared/exceptions.py b/alfred/domain/shared/exceptions.py similarity index 100% rename from brain/domain/shared/exceptions.py rename to alfred/domain/shared/exceptions.py diff --git a/brain/domain/shared/value_objects.py b/alfred/domain/shared/value_objects.py similarity index 100% rename from brain/domain/shared/value_objects.py rename to alfred/domain/shared/value_objects.py diff --git a/brain/domain/subtitles/__init__.py b/alfred/domain/subtitles/__init__.py similarity index 100% rename from brain/domain/subtitles/__init__.py rename to alfred/domain/subtitles/__init__.py diff --git a/brain/domain/subtitles/entities.py b/alfred/domain/subtitles/entities.py similarity index 100% rename from brain/domain/subtitles/entities.py rename to alfred/domain/subtitles/entities.py diff --git a/brain/domain/subtitles/exceptions.py b/alfred/domain/subtitles/exceptions.py similarity index 100% rename from brain/domain/subtitles/exceptions.py rename to alfred/domain/subtitles/exceptions.py diff --git a/brain/domain/subtitles/repositories.py b/alfred/domain/subtitles/repositories.py similarity index 100% rename from brain/domain/subtitles/repositories.py rename to alfred/domain/subtitles/repositories.py diff --git a/brain/domain/subtitles/services.py b/alfred/domain/subtitles/services.py similarity index 100% rename from brain/domain/subtitles/services.py rename to alfred/domain/subtitles/services.py diff --git a/brain/domain/subtitles/value_objects.py b/alfred/domain/subtitles/value_objects.py similarity index 100% rename from brain/domain/subtitles/value_objects.py rename to alfred/domain/subtitles/value_objects.py diff --git a/brain/domain/tv_shows/__init__.py b/alfred/domain/tv_shows/__init__.py similarity index 100% rename from brain/domain/tv_shows/__init__.py rename to alfred/domain/tv_shows/__init__.py diff --git a/brain/domain/tv_shows/entities.py b/alfred/domain/tv_shows/entities.py similarity index 100% rename from brain/domain/tv_shows/entities.py rename to alfred/domain/tv_shows/entities.py diff --git a/brain/domain/tv_shows/exceptions.py b/alfred/domain/tv_shows/exceptions.py similarity index 100% rename from brain/domain/tv_shows/exceptions.py rename to alfred/domain/tv_shows/exceptions.py diff --git a/brain/domain/tv_shows/repositories.py b/alfred/domain/tv_shows/repositories.py similarity index 100% rename from brain/domain/tv_shows/repositories.py rename to alfred/domain/tv_shows/repositories.py diff --git a/brain/domain/tv_shows/services.py b/alfred/domain/tv_shows/services.py similarity index 100% rename from brain/domain/tv_shows/services.py rename to alfred/domain/tv_shows/services.py diff --git a/brain/domain/tv_shows/value_objects.py b/alfred/domain/tv_shows/value_objects.py similarity index 100% rename from brain/domain/tv_shows/value_objects.py rename to alfred/domain/tv_shows/value_objects.py diff --git a/brain/infrastructure/__init__.py b/alfred/infrastructure/__init__.py similarity index 100% rename from brain/infrastructure/__init__.py rename to alfred/infrastructure/__init__.py diff --git a/brain/infrastructure/api/__init__.py b/alfred/infrastructure/api/__init__.py similarity index 100% rename from brain/infrastructure/api/__init__.py rename to alfred/infrastructure/api/__init__.py diff --git a/brain/infrastructure/api/knaben/__init__.py b/alfred/infrastructure/api/knaben/__init__.py similarity index 100% rename from brain/infrastructure/api/knaben/__init__.py rename to alfred/infrastructure/api/knaben/__init__.py diff --git a/brain/infrastructure/api/knaben/client.py b/alfred/infrastructure/api/knaben/client.py similarity index 99% rename from brain/infrastructure/api/knaben/client.py rename to alfred/infrastructure/api/knaben/client.py index b5300ff..acd399d 100644 --- a/brain/infrastructure/api/knaben/client.py +++ b/alfred/infrastructure/api/knaben/client.py @@ -6,7 +6,7 @@ from typing import Any import requests from requests.exceptions import HTTPError, RequestException, Timeout -from agent.config import Settings, settings +from alfred.agent.config import Settings, settings from .dto import TorrentResult from .exceptions import KnabenAPIError, KnabenNotFoundError diff --git a/brain/infrastructure/api/knaben/dto.py b/alfred/infrastructure/api/knaben/dto.py similarity index 100% rename from brain/infrastructure/api/knaben/dto.py rename to alfred/infrastructure/api/knaben/dto.py diff --git a/brain/infrastructure/api/knaben/exceptions.py b/alfred/infrastructure/api/knaben/exceptions.py similarity index 100% rename from brain/infrastructure/api/knaben/exceptions.py rename to alfred/infrastructure/api/knaben/exceptions.py diff --git a/brain/infrastructure/api/qbittorrent/__init__.py b/alfred/infrastructure/api/qbittorrent/__init__.py similarity index 100% rename from brain/infrastructure/api/qbittorrent/__init__.py rename to alfred/infrastructure/api/qbittorrent/__init__.py diff --git a/brain/infrastructure/api/qbittorrent/client.py b/alfred/infrastructure/api/qbittorrent/client.py similarity index 99% rename from brain/infrastructure/api/qbittorrent/client.py rename to alfred/infrastructure/api/qbittorrent/client.py index 61dc25b..b7bf165 100644 --- a/brain/infrastructure/api/qbittorrent/client.py +++ b/alfred/infrastructure/api/qbittorrent/client.py @@ -6,7 +6,7 @@ from typing import Any import requests from requests.exceptions import HTTPError, RequestException, Timeout -from agent.config import Settings, settings +from alfred.agent.config import Settings, settings from .dto import TorrentInfo from .exceptions import QBittorrentAPIError, QBittorrentAuthError diff --git a/brain/infrastructure/api/qbittorrent/dto.py b/alfred/infrastructure/api/qbittorrent/dto.py similarity index 100% rename from brain/infrastructure/api/qbittorrent/dto.py rename to alfred/infrastructure/api/qbittorrent/dto.py diff --git a/brain/infrastructure/api/qbittorrent/exceptions.py b/alfred/infrastructure/api/qbittorrent/exceptions.py similarity index 100% rename from brain/infrastructure/api/qbittorrent/exceptions.py rename to alfred/infrastructure/api/qbittorrent/exceptions.py diff --git a/brain/infrastructure/api/tmdb/__init__.py b/alfred/infrastructure/api/tmdb/__init__.py similarity index 100% rename from brain/infrastructure/api/tmdb/__init__.py rename to alfred/infrastructure/api/tmdb/__init__.py diff --git a/brain/infrastructure/api/tmdb/client.py b/alfred/infrastructure/api/tmdb/client.py similarity index 99% rename from brain/infrastructure/api/tmdb/client.py rename to alfred/infrastructure/api/tmdb/client.py index 5cd9ce1..1ada28d 100644 --- a/brain/infrastructure/api/tmdb/client.py +++ b/alfred/infrastructure/api/tmdb/client.py @@ -6,7 +6,7 @@ from typing import Any import requests from requests.exceptions import HTTPError, RequestException, Timeout -from agent.config import Settings, settings +from alfred.agent.config import Settings, settings from .dto import MediaResult from .exceptions import ( diff --git a/brain/infrastructure/api/tmdb/dto.py b/alfred/infrastructure/api/tmdb/dto.py similarity index 100% rename from brain/infrastructure/api/tmdb/dto.py rename to alfred/infrastructure/api/tmdb/dto.py diff --git a/brain/infrastructure/api/tmdb/exceptions.py b/alfred/infrastructure/api/tmdb/exceptions.py similarity index 100% rename from brain/infrastructure/api/tmdb/exceptions.py rename to alfred/infrastructure/api/tmdb/exceptions.py diff --git a/brain/infrastructure/filesystem/__init__.py b/alfred/infrastructure/filesystem/__init__.py similarity index 100% rename from brain/infrastructure/filesystem/__init__.py rename to alfred/infrastructure/filesystem/__init__.py diff --git a/brain/infrastructure/filesystem/exceptions.py b/alfred/infrastructure/filesystem/exceptions.py similarity index 100% rename from brain/infrastructure/filesystem/exceptions.py rename to alfred/infrastructure/filesystem/exceptions.py diff --git a/brain/infrastructure/filesystem/file_manager.py b/alfred/infrastructure/filesystem/file_manager.py similarity index 99% rename from brain/infrastructure/filesystem/file_manager.py rename to alfred/infrastructure/filesystem/file_manager.py index c36303b..854bf07 100644 --- a/brain/infrastructure/filesystem/file_manager.py +++ b/alfred/infrastructure/filesystem/file_manager.py @@ -7,7 +7,7 @@ from enum import Enum from pathlib import Path from typing import Any -from infrastructure.persistence import get_memory +from alfred.infrastructure.persistence import get_memory from .exceptions import PathTraversalError diff --git a/brain/infrastructure/filesystem/organizer.py b/alfred/infrastructure/filesystem/organizer.py similarity index 95% rename from brain/infrastructure/filesystem/organizer.py rename to alfred/infrastructure/filesystem/organizer.py index 60864ac..050b529 100644 --- a/brain/infrastructure/filesystem/organizer.py +++ b/alfred/infrastructure/filesystem/organizer.py @@ -3,9 +3,9 @@ import logging from pathlib import Path -from domain.movies.entities import Movie -from domain.tv_shows.entities import Episode, Season, TVShow -from domain.tv_shows.value_objects import SeasonNumber +from alfred.domain.movies.entities import Movie +from alfred.domain.tv_shows.entities import Episode, Season, TVShow +from alfred.domain.tv_shows.value_objects import SeasonNumber logger = logging.getLogger(__name__) diff --git a/brain/infrastructure/persistence/__init__.py b/alfred/infrastructure/persistence/__init__.py similarity index 100% rename from brain/infrastructure/persistence/__init__.py rename to alfred/infrastructure/persistence/__init__.py diff --git a/brain/infrastructure/persistence/context.py b/alfred/infrastructure/persistence/context.py similarity index 95% rename from brain/infrastructure/persistence/context.py rename to alfred/infrastructure/persistence/context.py index 80bf142..8cddbd3 100644 --- a/brain/infrastructure/persistence/context.py +++ b/alfred/infrastructure/persistence/context.py @@ -6,7 +6,7 @@ without passing it explicitly through all function calls. Usage: # At application startup - from infrastructure.persistence import init_memory, get_memory + from alfred.infrastructure.persistence import init_memory, get_memory init_memory("memory_data") diff --git a/brain/infrastructure/persistence/json/__init__.py b/alfred/infrastructure/persistence/json/__init__.py similarity index 100% rename from brain/infrastructure/persistence/json/__init__.py rename to alfred/infrastructure/persistence/json/__init__.py diff --git a/brain/infrastructure/persistence/json/movie_repository.py b/alfred/infrastructure/persistence/json/movie_repository.py similarity index 92% rename from brain/infrastructure/persistence/json/movie_repository.py rename to alfred/infrastructure/persistence/json/movie_repository.py index 243d425..46ace79 100644 --- a/brain/infrastructure/persistence/json/movie_repository.py +++ b/alfred/infrastructure/persistence/json/movie_repository.py @@ -4,11 +4,11 @@ import logging from datetime import datetime from typing import Any -from domain.movies.entities import Movie -from domain.movies.repositories import MovieRepository -from domain.movies.value_objects import MovieTitle, Quality, ReleaseYear -from domain.shared.value_objects import FilePath, FileSize, ImdbId -from infrastructure.persistence import get_memory +from alfred.domain.movies.entities import Movie +from alfred.domain.movies.repositories import MovieRepository +from alfred.domain.movies.value_objects import MovieTitle, Quality, ReleaseYear +from alfred.domain.shared.value_objects import FilePath, FileSize, ImdbId +from alfred.infrastructure.persistence import get_memory logger = logging.getLogger(__name__) diff --git a/brain/infrastructure/persistence/json/subtitle_repository.py b/alfred/infrastructure/persistence/json/subtitle_repository.py similarity index 93% rename from brain/infrastructure/persistence/json/subtitle_repository.py rename to alfred/infrastructure/persistence/json/subtitle_repository.py index f5c92f2..c0ce6c5 100644 --- a/brain/infrastructure/persistence/json/subtitle_repository.py +++ b/alfred/infrastructure/persistence/json/subtitle_repository.py @@ -3,11 +3,11 @@ import logging from typing import Any -from domain.shared.value_objects import FilePath, ImdbId -from domain.subtitles.entities import Subtitle -from domain.subtitles.repositories import SubtitleRepository -from domain.subtitles.value_objects import Language, SubtitleFormat, TimingOffset -from infrastructure.persistence import get_memory +from alfred.domain.shared.value_objects import FilePath, ImdbId +from alfred.domain.subtitles.entities import Subtitle +from alfred.domain.subtitles.repositories import SubtitleRepository +from alfred.domain.subtitles.value_objects import Language, SubtitleFormat, TimingOffset +from alfred.infrastructure.persistence import get_memory logger = logging.getLogger(__name__) diff --git a/brain/infrastructure/persistence/json/tvshow_repository.py b/alfred/infrastructure/persistence/json/tvshow_repository.py similarity index 92% rename from brain/infrastructure/persistence/json/tvshow_repository.py rename to alfred/infrastructure/persistence/json/tvshow_repository.py index 2cb9643..2e79836 100644 --- a/brain/infrastructure/persistence/json/tvshow_repository.py +++ b/alfred/infrastructure/persistence/json/tvshow_repository.py @@ -4,11 +4,11 @@ import logging from datetime import datetime from typing import Any -from domain.shared.value_objects import ImdbId -from domain.tv_shows.entities import TVShow -from domain.tv_shows.repositories import TVShowRepository -from domain.tv_shows.value_objects import ShowStatus -from infrastructure.persistence import get_memory +from alfred.domain.shared.value_objects import ImdbId +from alfred.domain.tv_shows.entities import TVShow +from alfred.domain.tv_shows.repositories import TVShowRepository +from alfred.domain.tv_shows.value_objects import ShowStatus +from alfred.infrastructure.persistence import get_memory logger = logging.getLogger(__name__) diff --git a/brain/infrastructure/persistence/memory.py b/alfred/infrastructure/persistence/memory.py similarity index 100% rename from brain/infrastructure/persistence/memory.py rename to alfred/infrastructure/persistence/memory.py diff --git a/cli.py b/cli.py new file mode 100644 index 0000000..41cf333 --- /dev/null +++ b/cli.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +import os +import secrets +import shutil +import subprocess +import sys +from datetime import datetime +from enum import StrEnum +from pathlib import Path +from typing import NoReturn + +REQUIRED_VARS = ["DEEPSEEK_API_KEY", "TMDB_API_KEY", "QBITTORRENT_URL"] + +# Size in bytes +KEYS_TO_GENERATE = { + "JWT_SECRET": 32, + "JWT_REFRESH_SECRET": 32, + "CREDS_KEY": 32, + "CREDS_IV": 16, +} + + +class Style(StrEnum): + """ANSI codes for styling output. + Usage: f"{Style.RED}Error{Style.RESET}" + """ + + RESET = "\033[0m" + BOLD = "\033[1m" + RED = "\033[31m" + GREEN = "\033[32m" + YELLOW = "\033[33m" + CYAN = "\033[36m" + DIM = "\033[2m" + + +# Only for terminals and if not specified otherwise +USE_COLORS = sys.stdout.isatty() and "NO_COLOR" not in os.environ + + +def styled(text: str, color_code: str) -> str: + """Apply color only if supported by the terminal.""" + if USE_COLORS: + return f"{color_code}{text}{Style.RESET}" + return text + + +def log(msg: str, color: str | None = None, prefix="") -> None: + """Print a formatted message.""" + formatted_msg = styled(msg, color) if color else msg + print(f"{prefix}{formatted_msg}") + + +def error_exit(msg: str) -> NoReturn: + """Print an error message in red and exit.""" + log(f"❌ {msg}", Style.RED) + sys.exit(1) + + +def is_docker_running() -> bool: + """ "Check if Docker is available and responsive.""" + if shutil.which("docker") is None: + error_exit("Docker is not installed.") + + result = subprocess.run( + ["docker", "info"], + # Redirect stdout/stderr to keep output clean on success + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + # Prevent exception being raised + check=False, + ) + return result.returncode == 0 + + +def parse_env(content: str) -> dict[str, str]: + """Parses existing keys and values into a dict (ignoring comments).""" + env_vars = {} + for raw_line in content.splitlines(): + line = raw_line.strip() + if line and not line.startswith("#") and "=" in line: + key, value = line.split("=", 1) + env_vars[key.strip()] = value.strip() + + return env_vars + + +def dump_env(content: str, data: dict[str, str]) -> str: + new_content: list[str] = [] + processed_keys = set() + + for raw_line in content.splitlines(): + line = raw_line.strip() + # Fast line (empty, comment or not an assignation) + if len(line) == 0 or line.startswith("#") or "=" not in line: + new_content.append(raw_line) + continue + + # Slow line (inline comment to be kept) + key_chunk, value_chunk = raw_line.split("=", 1) + key = key_chunk.strip() + + # Not in the update list + if key not in data: + new_content.append(raw_line) + continue + + processed_keys.add(key) + new_value = data[key] + + if " #" not in value_chunk: + new_line = f"{key_chunk}={new_value}" + else: + _, comment = value_chunk.split(" #", 1) + new_line = f"{key_chunk}={new_value} #{comment}" + + new_content.append(new_line) + + for key, value in data.items(): + if key not in processed_keys: + new_content.append(f"{key}={value}") + + return "\n".join(new_content) + "\n" + + +def ensure_env() -> None: + """Manage .env lifecycle: creation, secret generation, prompts.""" + env_path = Path(".env") + env_example_path = Path(".env.example") + updated: bool = False + + # Read .env if exists + if env_path.exists(): + content: str = env_path.read_text(encoding="utf-8") + else: + content: str = env_example_path.read_text(encoding="utf-8") + + existing_vars: dict[str, str] = parse_env(content) + + # Generate missing secrets + for key, length in KEYS_TO_GENERATE.items(): + if key not in existing_vars or not existing_vars[key]: + log(f"Generating {key}...", Style.GREEN, prefix=" ") + existing_vars[key] = secrets.token_hex(length) + updated = True + log("Done", Style.GREEN, prefix=" ") + + # Prompt for missing mandatory keys + color = Style.YELLOW if USE_COLORS else "" + reset = Style.RESET if USE_COLORS else "" + for key in REQUIRED_VARS: + if key not in existing_vars or not existing_vars[key]: + try: + existing_vars[key] = input( + f" {color}Enter value for {key}: {reset}" + ).strip() + updated = True + except KeyboardInterrupt: + print() + error_exit("Aborted by user.") + + # Write to disk + if updated: + # But backup original first + if env_path.exists(): + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + backup_path = Path(f"{env_path}.{timestamp}.bak") + shutil.copy(env_path, backup_path) + log(f"Backup created: {backup_path}", Style.DIM) + + new_content = dump_env(content, existing_vars) + env_path.write_text(new_content, encoding="utf-8") + log(".env updated successfully.", Style.GREEN) + else: + log("Configuration is up to date.", Style.GREEN) + + +def setup() -> None: + """Orchestrate initialization.""" + is_docker_running() + ensure_env() + + +def status() -> None: + """Display simple dashboard.""" + # Hardcoded bold style for title if colors are enabled + title_style = Style.BOLD if USE_COLORS else "" + reset_style = Style.RESET if USE_COLORS else "" + + print(f"\n{title_style}ALFRED STATUS{reset_style}") + print(f"{title_style}==============={reset_style}\n") + + # Docker Check + if is_docker_running(): + print(f" Docker: {styled('✓ running', Style.GREEN)}") + else: + print(f" Docker: {styled('✗ stopped', Style.RED)}") + + # Env Check + if Path(".env").exists(): + print(f" .env: {styled('✓ present', Style.GREEN)}") + else: + print(f" .env: {styled('✗ missing', Style.RED)}") + + print("") + + +def check() -> None: + """Silent check for prerequisites (used by 'make up').""" + setup() + + +def main() -> None: + if len(sys.argv) < 2: + print("Usage: python cli.py [setup|check|status]") + sys.exit(1) + + cmd = sys.argv[1] + + if cmd == "setup": + setup() + elif cmd == "check": + check() + elif cmd == "status": + status() + else: + error_exit(f"Unknown command: {cmd}") + + +if __name__ == "__main__": + main() diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..e0bc461 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,102 @@ +services: + alfred: + container_name: alfred-core + build: + context: . + args: + PYTHON_VERSION: ${PYTHON_VERSION} + PYTHON_VERSION_SHORT: ${PYTHON_VERSION_SHORT} + RUNNER: ${RUNNER} + depends_on: + - librechat + restart: unless-stopped + env_file: + - .env + environment: + # LLM Configuration + LLM_PROVIDER: ${LLM_PROVIDER:-deepseek} + DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY:-} + + # Memory storage + MEMORY_STORAGE_DIR: /data/memory + + # External services + TMDB_API_KEY: ${TMDB_API_KEY:-} + QBITTORRENT_URL: ${QBITTORRENT_URL:-} + QBITTORRENT_USERNAME: ${QBITTORRENT_USERNAME:-} + QBITTORRENT_PASSWORD: ${QBITTORRENT_PASSWORD:-} + volumes: + - ./data/memory:/data/memory + - ./logs:/data/logs + # TODO: Development: mount code for hot reload (comment out in production) + # - ./alfred:/app/alfred + + librechat: + container_name: alfred-librechat + image: ghcr.io/danny-avila/librechat:${LIBRECHAT_VERSION} + depends_on: + - mongodb + - meilisearch + - rag_api + restart: unless-stopped + env_file: + - .env + environment: + - HOST=0.0.0.0 + - MONGO_URI=mongodb://mongodb:27017/LibreChat + - MEILI_HOST=http://meilisearch:7700 + - RAG_PORT=${RAG_PORT:-8000} + - RAG_API_URL=http://rag_api:${RAG_PORT:-8000} + ports: + - "${LIBRECHAT_PORT:-3080}:3080" + volumes: + - ./data/librechat/images:/app/client/public/images + - ./data/librechat/uploads:/app/client/uploads + - ./logs:/app/api/logs + # Mount custom endpoint + - ./librechat/manifests:/app/manifests:ro + - ./librechat/librechat.yaml:/app/librechat.yaml:ro + + mongodb: + container_name: alfred-mongodb + image: mongo:latest + restart: unless-stopped + volumes: + - ./data/mongo:/data/db + command: mongod --noauth + + meilisearch: + container_name: alfred-meilisearch + image: getmeili/meilisearch:v1.12.3 + restart: unless-stopped + environment: + - MEILI_NO_ANALYTICS=true + volumes: + - ./data/meili:/meili_data + #profiles: ["meili", "full"] + + rag_api: + container_name: alfred-rag + image: ghcr.io/danny-avila/librechat-rag-api-dev-lite:${RAG_VERSION} + restart: unless-stopped + env_file: + - .env + environment: + - DB_HOST=vectordb + - DB_PORT=5432 + - RAG_PORT=${RAG_PORT:-8000} + ports: + - "${RAG_PORT:-8000}:${RAG_PORT:-8000}" + #profiles: ["rag", "full"] + + vectordb: + container_name: alfred-vectordb + image: pgvector/pgvector:0.8.0-pg16-bookworm + restart: unless-stopped + env_file: + - .env + ports: + - "${VECTOR_DB_PORT:-5432}:5432" + volumes: + - ./data/vectordb:/var/lib/postgresql/data + #profiles: ["rag", "full"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index b5d87af..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,207 +0,0 @@ -version: "3.4" - -services: - # Da brain - agent-brain: - build: - context: ./brain - dockerfile: Dockerfile - args: - RUNNER: ${RUNNER} # Get it from Makefile - container_name: agent-brain - restart: unless-stopped - env_file: .env - ports: - - "8000:8000" - volumes: - # Persistent data volumes (outside container /app) - - agent-memory:/data/memory - - agent-logs:/data/logs - # Development: mount code for hot reload (comment out in production) - # - ./brain:/app - environment: - # LLM Configuration - LLM_PROVIDER: ${LLM_PROVIDER:-deepseek} - DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY:-} - - # Memory storage - MEMORY_STORAGE_DIR: /data/memory - - # External services - TMDB_API_KEY: ${TMDB_API_KEY:-} - QBITTORRENT_URL: ${QBITTORRENT_URL:-} - QBITTORRENT_USERNAME: ${QBITTORRENT_USERNAME:-} - QBITTORRENT_PASSWORD: ${QBITTORRENT_PASSWORD:-} - networks: - - agent-network - - # Da face (LibreChat) - librechat: - image: ghcr.io/danny-avila/librechat-dev:latest - container_name: librechat-frontend - restart: unless-stopped - ports: - - "3080:3080" - depends_on: - - mongodb - - meilisearch - - rag_api - - agent-brain - env_file: .env - environment: - # MongoDB connection (no auth, matching LibreChat default) - MONGO_URI: mongodb://mongodb:27017/LibreChat - - # App configuration - HOST: 0.0.0.0 - PORT: 3080 - - # Security - JWT_SECRET: ${JWT_SECRET:-your-super-secret-jwt-key-change-this-in-production} - JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET:-your-super-secret-refresh-key-change-this-too} - CREDS_KEY: ${CREDS_KEY:-your-32-character-secret-key-here} - CREDS_IV: ${CREDS_IV:-your-16-character-iv-here} - - # Session - SESSION_EXPIRY: ${SESSION_EXPIRY:-1000 * 60 * 15} - REFRESH_TOKEN_EXPIRY: ${REFRESH_TOKEN_EXPIRY:-1000 * 60 * 60 * 24 * 7} - - # Domain - DOMAIN_CLIENT: ${DOMAIN_CLIENT:-http://localhost:3080} - DOMAIN_SERVER: ${DOMAIN_SERVER:-http://localhost:3080} - - # Meilisearch - MEILI_HOST: http://meilisearch:7700 - MEILI_MASTER_KEY: ${MEILI_MASTER_KEY:-DrhYf7zENyR6AlUCKmnz0eYASOQdl6zxH7s7MKFSfFU} - - # RAG API - RAG_API_URL: http://rag_api:8000 - - # Endpoints - ENDPOINTS: custom - - # Custom endpoint pointing to agent-brain - CUSTOM_API_KEY: ${AGENT_BRAIN_API_KEY:-agent-brain-secret-key} - - # Debug (optional) - DEBUG_LOGGING: ${DEBUG_LOGGING:-false} - DEBUG_CONSOLE: ${DEBUG_CONSOLE:-false} - volumes: - - ./librechat/librechat.yaml:/app/librechat.yaml:ro - - librechat-images:/app/client/public/images - - librechat-logs:/app/api/logs - networks: - - agent-network - - # MongoDB for LibreChat - mongodb: - image: mongo:latest - container_name: librechat-mongodb - restart: unless-stopped - volumes: - - mongodb-data:/data/db - command: mongod --noauth - ports: - - "27017:27017" - networks: - - agent-network - - # Meilisearch - Search engine for LibreChat - #TODO: Follow currently used version on librechat's github - meilisearch: - image: getmeili/meilisearch:v1.12.3 - container_name: librechat-meilisearch - restart: unless-stopped - volumes: - - meilisearch-data:/meili_data - environment: - MEILI_HOST: http://meilisearch:7700 - MEILI_HTTP_ADDR: meilisearch:7700 - MEILI_MASTER_KEY: ${MEILI_MASTER_KEY:-DrhYf7zENyR6AlUCKmnz0eYASOQdl6zxH7s7MKFSfFU} - ports: - - "7700:7700" - networks: - - agent-network - - # PostgreSQL with pgvector for RAG API - pgvector: - image: ankane/pgvector:latest - container_name: librechat-pgvector - restart: unless-stopped - environment: - POSTGRES_DB: ${POSTGRES_DB:-librechat_rag} - POSTGRES_USER: ${POSTGRES_USER:-postgres} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} - volumes: - - pgvector-data:/var/lib/postgresql/data - ports: - - "5432:5432" - networks: - - agent-network - - # RAG API - Vector database for LibreChat - rag_api: - image: ghcr.io/danny-avila/librechat-rag-api-dev-lite:latest - container_name: librechat-rag-api - restart: unless-stopped - depends_on: - - pgvector - environment: - PORT: 8000 - HOST: 0.0.0.0 - # PostgreSQL connection (multiple variable names for compatibility) - DB_HOST: pgvector - DB_PORT: 5432 - DB_NAME: ${POSTGRES_DB:-librechat_rag} - DB_USER: ${POSTGRES_USER:-postgres} - DB_PASSWORD: ${POSTGRES_PASSWORD:-postgres} - POSTGRES_DB: ${POSTGRES_DB:-librechat_rag} - POSTGRES_USER: ${POSTGRES_USER:-postgres} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} - # RAG configuration - COLLECTION_NAME: ${RAG_COLLECTION_NAME:-testcollection} - EMBEDDINGS_PROVIDER: ${RAG_EMBEDDINGS_PROVIDER:-openai} - EMBEDDINGS_MODEL: ${RAG_EMBEDDINGS_MODEL:-text-embedding-3-small} - OPENAI_API_KEY: ${OPENAI_API_KEY:-} - RAG_UPLOAD_DIR: /app/uploads - volumes: - - rag-uploads:/app/uploads - ports: - - "8001:8000" - networks: - - agent-network - -# Named volumes for persistent data -volumes: - # MongoDB data - mongodb-data: - driver: local - - # Meilisearch data - meilisearch-data: - driver: local - - # PostgreSQL pgvector data - pgvector-data: - driver: local - - # RAG API uploads - rag-uploads: - driver: local - - # LibreChat data - librechat-images: - driver: local - librechat-logs: - driver: local - - # Agent Brain data - agent-memory: - driver: local - agent-logs: - driver: local - -# Network for inter-service communication -networks: - agent-network: - driver: bridge diff --git a/brain/docs/architecture_diagram.md b/docs/architecture_diagram.md similarity index 100% rename from brain/docs/architecture_diagram.md rename to docs/architecture_diagram.md diff --git a/brain/docs/class_diagram.md b/docs/class_diagram.md similarity index 100% rename from brain/docs/class_diagram.md rename to docs/class_diagram.md diff --git a/brain/docs/component_diagram.md b/docs/component_diagram.md similarity index 100% rename from brain/docs/component_diagram.md rename to docs/component_diagram.md diff --git a/brain/docs/flowchart.md b/docs/flowchart.md similarity index 100% rename from brain/docs/flowchart.md rename to docs/flowchart.md diff --git a/brain/docs/sequence_diagram.md b/docs/sequence_diagram.md similarity index 100% rename from brain/docs/sequence_diagram.md rename to docs/sequence_diagram.md diff --git a/generate-keys.sh b/generate-keys.sh deleted file mode 100755 index 3923201..0000000 --- a/generate-keys.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -# Script to generate secure keys for LibreChat -# Run this script to generate random secure keys for your .env file - -echo "===================================" -echo "LibreChat Security Keys Generator" -echo "===================================" -echo "" - -echo "# MongoDB Password" -echo "MONGO_PASSWORD=$(openssl rand -base64 24)" -echo "" - -echo "# JWT Secrets" -echo "JWT_SECRET=$(openssl rand -base64 32)" -echo "JWT_REFRESH_SECRET=$(openssl rand -base64 32)" -echo "" - -echo "# Credentials Encryption Keys" -echo "CREDS_KEY=$(openssl rand -hex 16)" -echo "CREDS_IV=$(openssl rand -hex 8)" -echo "" - -echo "===================================" -echo "Copy these values to your .env file" -echo "===================================" diff --git a/librechat/librechat.yaml b/librechat/librechat.yaml index 196c08d..2691484 100644 --- a/librechat/librechat.yaml +++ b/librechat/librechat.yaml @@ -22,7 +22,7 @@ endpoints: manifest: schema: type: openapi - url: "http://agent-brain:8000/manifests/find_media_imdb_id.json" + url: "http://alfred:8000/manifests/find_media_imdb_id.json" auth: type: none @@ -32,7 +32,7 @@ endpoints: manifest: schema: type: openapi - url: "http://agent-brain:8000/manifests/find_torrent.json" + url: "http://alfred:8000/manifests/find_torrent.json" auth: type: none @@ -42,7 +42,7 @@ endpoints: manifest: schema: type: openapi - url: "http://agent-brain:8000/manifests/add_torrent_by_index.json" + url: "http://alfred:8000/manifests/add_torrent_by_index.json" auth: type: none @@ -52,7 +52,7 @@ endpoints: manifest: schema: type: openapi - url: "http://agent-brain:8000/manifests/set_language.json" + url: "http://alfred:8000/manifests/set_language.json" auth: type: none @@ -60,7 +60,7 @@ endpoints: # Backend Local Agent - name: "Local Agent" apiKey: "dummy_key" - baseURL: "http://agent-brain:8000/v1" + baseURL: "http://alfred:8000/v1" models: default: ["local-deepseek-agent"] fetch: false @@ -75,7 +75,7 @@ endpoints: manifest: schema: type: openapi - url: "http://agent-brain:8000/manifests/find_media_imdb_id.json" + url: "http://alfred:8000/manifests/find_media_imdb_id.json" auth: type: none @@ -85,7 +85,7 @@ endpoints: manifest: schema: type: openapi - url: "http://agent-brain:8000/manifests/find_torrent.json" + url: "http://alfred:8000/manifests/find_torrent.json" auth: type: none @@ -95,7 +95,7 @@ endpoints: manifest: schema: type: openapi - url: "http://agent-brain:8000/manifests/add_torrent_by_index.json" + url: "http://alfred:8000/manifests/add_torrent_by_index.json" auth: type: none @@ -105,6 +105,6 @@ endpoints: manifest: schema: type: openapi - url: "http://agent-brain:8000/manifests/set_language.json" + url: "http://alfred:8000/manifests/set_language.json" auth: type: none diff --git a/brain/manifests/add_torrent_by_index.json b/librechat/manifests/add_torrent_by_index.json similarity index 100% rename from brain/manifests/add_torrent_by_index.json rename to librechat/manifests/add_torrent_by_index.json diff --git a/brain/manifests/find_media_imdb_id.json b/librechat/manifests/find_media_imdb_id.json similarity index 100% rename from brain/manifests/find_media_imdb_id.json rename to librechat/manifests/find_media_imdb_id.json diff --git a/brain/manifests/find_torrent.json b/librechat/manifests/find_torrent.json similarity index 100% rename from brain/manifests/find_torrent.json rename to librechat/manifests/find_torrent.json diff --git a/brain/manifests/set_language.json b/librechat/manifests/set_language.json similarity index 100% rename from brain/manifests/set_language.json rename to librechat/manifests/set_language.json diff --git a/brain/poetry.lock b/poetry.lock similarity index 99% rename from brain/poetry.lock rename to poetry.lock index 2e6481c..bb559a5 100644 --- a/brain/poetry.lock +++ b/poetry.lock @@ -372,13 +372,13 @@ testing = ["hatch", "pre-commit", "pytest", "tox"] [[package]] name = "fastapi" -version = "0.127.0" +version = "0.127.1" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.9" files = [ - {file = "fastapi-0.127.0-py3-none-any.whl", hash = "sha256:725aa2bb904e2eff8031557cf4b9b77459bfedd63cae8427634744fd199f6a49"}, - {file = "fastapi-0.127.0.tar.gz", hash = "sha256:5a9246e03dcd1fdb19f1396db30894867c1d630f5107dc167dcbc5ed1ea7d259"}, + {file = "fastapi-0.127.1-py3-none-any.whl", hash = "sha256:31d670a4f9373cc6d7994420f98e4dc46ea693145207abc39696746c83a44430"}, + {file = "fastapi-0.127.1.tar.gz", hash = "sha256:946a87ee5d931883b562b6bada787d6c8178becee2683cb3f9b980d593206359"}, ] [package.dependencies] diff --git a/brain/pyproject.toml b/pyproject.toml similarity index 92% rename from brain/pyproject.toml rename to pyproject.toml index 69b6eaa..423ae5c 100644 --- a/brain/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,18 @@ [tool.poetry] -name = "agent-media" +name = "alfred" version = "0.1.6" description = "AI agent for managing a local media library" authors = ["Francwa "] readme = "README.md" package-mode = false +[tool.alfred] +image_name = "alfred_media_organizer" +librechat_version = "v0.8.1" +rag_version = "v0.7.0" +runner = "poetry" +service_name = "alfred" + [tool.poetry.dependencies] python = "==3.14.2" python-dotenv = "^1.0.0" @@ -31,6 +38,8 @@ build-backend = "poetry.core.masonry.api" [tool.pytest.ini_options] # Chemins où pytest cherche les tests testpaths = ["tests"] +# Ajouter le répertoire racine au PYTHONPATH pour les imports +pythonpath = ["."] # Patterns de fichiers/classes/fonctions à considérer comme tests python_files = ["test_*.py"] # Fichiers commençant par "test_" diff --git a/brain/tests/conftest.py b/tests/conftest.py similarity index 94% rename from brain/tests/conftest.py rename to tests/conftest.py index c1448e4..6326a10 100644 --- a/brain/tests/conftest.py +++ b/tests/conftest.py @@ -1,19 +1,17 @@ """Pytest configuration and shared fixtures.""" -import sys -from pathlib import Path - +# TODO: Moved directory, should not be necessary anymore but need to check !! # Ajouter le dossier parent (brain) au PYTHONPATH -sys.path.insert(0, str(Path(__file__).parent.parent)) - +# sys.path.insert(0, str(Path(__file__).parent.parent)) import shutil +import sys import tempfile from pathlib import Path from unittest.mock import MagicMock, Mock import pytest -from infrastructure.persistence import Memory, set_memory +from alfred.infrastructure.persistence import Memory, set_memory @pytest.fixture @@ -24,6 +22,16 @@ def temp_dir(): shutil.rmtree(dirpath) +@pytest.fixture(autouse=True) +def mock_memory_storage_dir(monkeypatch): + """Override MEMORY_STORAGE_DIR for all tests to use a temp directory.""" + test_dir = tempfile.mkdtemp() + monkeypatch.setenv("MEMORY_STORAGE_DIR", test_dir) + yield + # Cleanup + shutil.rmtree(test_dir, ignore_errors=True) + + @pytest.fixture def memory(temp_dir): """Create a fresh Memory instance for testing.""" @@ -254,7 +262,6 @@ def mock_deepseek(): def test_something(mock_deepseek): # Your test code here """ - import sys from unittest.mock import Mock # Save the original module if it exists diff --git a/brain/tests/test_agent.py b/tests/test_agent.py similarity index 99% rename from brain/tests/test_agent.py rename to tests/test_agent.py index cf3c426..eb846c5 100644 --- a/brain/tests/test_agent.py +++ b/tests/test_agent.py @@ -2,8 +2,8 @@ from unittest.mock import Mock -from agent.agent import Agent -from infrastructure.persistence import get_memory +from alfred.agent.agent import Agent +from alfred.infrastructure.persistence import get_memory class TestAgentInit: diff --git a/brain/tests/test_agent_critical.py b/tests/test_agent_critical.py similarity index 100% rename from brain/tests/test_agent_critical.py rename to tests/test_agent_critical.py diff --git a/brain/tests/test_agent_edge_cases.py b/tests/test_agent_edge_cases.py similarity index 98% rename from brain/tests/test_agent_edge_cases.py rename to tests/test_agent_edge_cases.py index df92abe..5083684 100644 --- a/brain/tests/test_agent_edge_cases.py +++ b/tests/test_agent_edge_cases.py @@ -4,8 +4,8 @@ from unittest.mock import Mock import pytest -from agent.agent import Agent -from infrastructure.persistence import get_memory +from alfred.agent.agent import Agent +from alfred.infrastructure.persistence import get_memory class TestExecuteToolCallEdgeCases: @@ -16,7 +16,7 @@ class TestExecuteToolCallEdgeCases: agent = Agent(llm=mock_llm) # Mock a tool that returns None - from agent.registry import Tool + from alfred.agent.registry import Tool agent.tools["test_tool"] = Tool( name="test_tool", description="Test", func=lambda: None, parameters={} @@ -34,7 +34,7 @@ class TestExecuteToolCallEdgeCases: """Should propagate KeyboardInterrupt.""" agent = Agent(llm=mock_llm) - from agent.registry import Tool + from alfred.agent.registry import Tool def raise_interrupt(): raise KeyboardInterrupt() diff --git a/brain/tests/test_agent_integration.py b/tests/test_agent_integration.py similarity index 100% rename from brain/tests/test_agent_integration.py rename to tests/test_agent_integration.py diff --git a/brain/tests/test_api.py b/tests/test_api.py similarity index 89% rename from brain/tests/test_api.py rename to tests/test_api.py index 85ea1c1..5f72b9a 100644 --- a/brain/tests/test_api.py +++ b/tests/test_api.py @@ -10,7 +10,7 @@ class TestHealthEndpoint: def test_health_check(self, memory): """Should return healthy status.""" - from app import app + from alfred.app import app client = TestClient(app) @@ -25,7 +25,7 @@ class TestModelsEndpoint: def test_list_models(self, memory): """Should return model list.""" - from app import app + from alfred.app import app client = TestClient(app) @@ -43,7 +43,7 @@ class TestMemoryEndpoints: def test_get_memory_state(self, memory): """Should return full memory state.""" - from app import app + from alfred.app import app client = TestClient(app) @@ -57,7 +57,7 @@ class TestMemoryEndpoints: def test_get_search_results_empty(self, memory): """Should return empty when no search results.""" - from app import app + from alfred.app import app client = TestClient(app) @@ -69,7 +69,7 @@ class TestMemoryEndpoints: def test_get_search_results_with_data(self, memory_with_search_results): """Should return search results when available.""" - from app import app + from alfred.app import app client = TestClient(app) @@ -83,7 +83,7 @@ class TestMemoryEndpoints: def test_clear_session(self, memory_with_search_results): """Should clear session memories.""" - from app import app + from alfred.app import app client = TestClient(app) @@ -102,10 +102,10 @@ class TestChatCompletionsEndpoint: def test_chat_completion_success(self, memory): """Should return chat completion.""" - from app import app + from alfred.app import app # Patch the agent's step method directly - with patch("app.agent.step", return_value="Hello! How can I help?"): + with patch("alfred.app.agent.step", return_value="Hello! How can I help?"): client = TestClient(app) response = client.post( @@ -123,7 +123,7 @@ class TestChatCompletionsEndpoint: def test_chat_completion_no_user_message(self, memory): """Should return error if no user message.""" - from app import app + from alfred.app import app client = TestClient(app) @@ -146,7 +146,7 @@ class TestChatCompletionsEndpoint: def test_chat_completion_empty_messages(self, memory): """Should return error for empty messages.""" - from app import app + from alfred.app import app client = TestClient(app) @@ -162,7 +162,7 @@ class TestChatCompletionsEndpoint: def test_chat_completion_invalid_json(self, memory): """Should return error for invalid JSON.""" - from app import app + from alfred.app import app client = TestClient(app) @@ -176,9 +176,9 @@ class TestChatCompletionsEndpoint: def test_chat_completion_streaming(self, memory): """Should support streaming mode.""" - from app import app + from alfred.app import app - with patch("app.agent.step", return_value="Streaming response"): + with patch("alfred.app.agent.step", return_value="Streaming response"): client = TestClient(app) response = client.post( @@ -195,9 +195,9 @@ class TestChatCompletionsEndpoint: def test_chat_completion_extracts_last_user_message(self, memory): """Should use last user message.""" - from app import app + from alfred.app import app - with patch("app.agent.step", return_value="Response") as mock_step: + with patch("alfred.app.agent.step", return_value="Response") as mock_step: client = TestClient(app) response = client.post( @@ -218,9 +218,9 @@ class TestChatCompletionsEndpoint: def test_chat_completion_response_format(self, memory): """Should return OpenAI-compatible format.""" - from app import app + from alfred.app import app - with patch("app.agent.step", return_value="Test response"): + with patch("alfred.app.agent.step", return_value="Test response"): client = TestClient(app) response = client.post( diff --git a/brain/tests/test_api_clients_integration.py b/tests/test_api_clients_integration.py similarity index 100% rename from brain/tests/test_api_clients_integration.py rename to tests/test_api_clients_integration.py diff --git a/brain/tests/test_api_edge_cases.py b/tests/test_api_edge_cases.py similarity index 86% rename from brain/tests/test_api_edge_cases.py rename to tests/test_api_edge_cases.py index 562da55..e61c30e 100644 --- a/brain/tests/test_api_edge_cases.py +++ b/tests/test_api_edge_cases.py @@ -10,7 +10,8 @@ class TestChatCompletionsEdgeCases: def test_very_long_message(self, memory): """Should handle very long user message.""" - from app import agent, app + from alfred.agent import agent + from alfred.app import app # Patch the agent's LLM directly mock_llm = Mock() @@ -32,7 +33,8 @@ class TestChatCompletionsEdgeCases: def test_unicode_message(self, memory): """Should handle unicode in message.""" - from app import agent, app + from alfred.agent import agent + from alfred.app import app mock_llm = Mock() mock_llm.complete.return_value = { @@ -57,7 +59,8 @@ class TestChatCompletionsEdgeCases: def test_special_characters_in_message(self, memory): """Should handle special characters.""" - from app import agent, app + from alfred.agent import agent + from alfred.app import app mock_llm = Mock() mock_llm.complete.return_value = {"role": "assistant", "content": "Response"} @@ -78,12 +81,12 @@ class TestChatCompletionsEdgeCases: def test_empty_content_in_message(self, memory): """Should handle empty content in message.""" - with patch("app.DeepSeekClient") as mock_llm_class: + with patch("alfred.app.DeepSeekClient") as mock_llm_class: mock_llm = Mock() mock_llm.complete.return_value = "Response" mock_llm_class.return_value = mock_llm - from app import app + from alfred.app import app client = TestClient(app) @@ -100,11 +103,11 @@ class TestChatCompletionsEdgeCases: def test_null_content_in_message(self, memory): """Should handle null content in message.""" - with patch("app.DeepSeekClient") as mock_llm_class: + with patch("alfred.app.DeepSeekClient") as mock_llm_class: mock_llm = Mock() mock_llm_class.return_value = mock_llm - from app import app + from alfred.app import app client = TestClient(app) @@ -120,11 +123,11 @@ class TestChatCompletionsEdgeCases: def test_missing_content_field(self, memory): """Should handle missing content field.""" - with patch("app.DeepSeekClient") as mock_llm_class: + with patch("alfred.app.DeepSeekClient") as mock_llm_class: mock_llm = Mock() mock_llm_class.return_value = mock_llm - from app import app + from alfred.app import app client = TestClient(app) @@ -141,11 +144,11 @@ class TestChatCompletionsEdgeCases: def test_missing_role_field(self, memory): """Should handle missing role field.""" - with patch("app.DeepSeekClient") as mock_llm_class: + with patch("alfred.app.DeepSeekClient") as mock_llm_class: mock_llm = Mock() mock_llm_class.return_value = mock_llm - from app import app + from alfred.app import app client = TestClient(app) @@ -162,12 +165,12 @@ class TestChatCompletionsEdgeCases: def test_invalid_role(self, memory): """Should handle invalid role.""" - with patch("app.DeepSeekClient") as mock_llm_class: + with patch("alfred.app.DeepSeekClient") as mock_llm_class: mock_llm = Mock() mock_llm.complete.return_value = "Response" mock_llm_class.return_value = mock_llm - from app import app + from alfred.app import app client = TestClient(app) @@ -184,7 +187,8 @@ class TestChatCompletionsEdgeCases: def test_many_messages(self, memory): """Should handle many messages in conversation.""" - from app import agent, app + from alfred.agent import agent + from alfred.app import app mock_llm = Mock() mock_llm.complete.return_value = {"role": "assistant", "content": "Response"} @@ -210,11 +214,11 @@ class TestChatCompletionsEdgeCases: def test_only_system_messages(self, memory): """Should reject if only system messages.""" - with patch("app.DeepSeekClient") as mock_llm_class: + with patch("alfred.app.DeepSeekClient") as mock_llm_class: mock_llm = Mock() mock_llm_class.return_value = mock_llm - from app import app + from alfred.app import app client = TestClient(app) @@ -233,11 +237,11 @@ class TestChatCompletionsEdgeCases: def test_only_assistant_messages(self, memory): """Should reject if only assistant messages.""" - with patch("app.DeepSeekClient") as mock_llm_class: + with patch("alfred.app.DeepSeekClient") as mock_llm_class: mock_llm = Mock() mock_llm_class.return_value = mock_llm - from app import app + from alfred.app import app client = TestClient(app) @@ -255,11 +259,11 @@ class TestChatCompletionsEdgeCases: def test_messages_not_array(self, memory): """Should reject if messages is not array.""" - with patch("app.DeepSeekClient") as mock_llm_class: + with patch("alfred.app.DeepSeekClient") as mock_llm_class: mock_llm = Mock() mock_llm_class.return_value = mock_llm - from app import app + from alfred.app import app client = TestClient(app) @@ -276,11 +280,11 @@ class TestChatCompletionsEdgeCases: def test_message_not_object(self, memory): """Should handle message that is not object.""" - with patch("app.DeepSeekClient") as mock_llm_class: + with patch("alfred.app.DeepSeekClient") as mock_llm_class: mock_llm = Mock() mock_llm_class.return_value = mock_llm - from app import app + from alfred.app import app client = TestClient(app) @@ -297,7 +301,8 @@ class TestChatCompletionsEdgeCases: def test_extra_fields_in_request(self, memory): """Should ignore extra fields in request.""" - from app import agent, app + from alfred.agent import agent + from alfred.app import app mock_llm = Mock() mock_llm.complete.return_value = {"role": "assistant", "content": "Response"} @@ -320,8 +325,9 @@ class TestChatCompletionsEdgeCases: def test_streaming_with_tool_call(self, memory, real_folder): """Should handle streaming with tool execution.""" - from app import agent, app - from infrastructure.persistence import get_memory + from alfred.agent import agent + from alfred.app import app + from alfred.infrastructure.persistence import get_memory mem = get_memory() mem.ltm.set_config("download_folder", str(real_folder["downloads"])) @@ -365,7 +371,8 @@ class TestChatCompletionsEdgeCases: def test_concurrent_requests_simulation(self, memory): """Should handle rapid sequential requests.""" - from app import agent, app + from alfred.agent import agent + from alfred.app import app mock_llm = Mock() mock_llm.complete.return_value = {"role": "assistant", "content": "Response"} @@ -385,7 +392,8 @@ class TestChatCompletionsEdgeCases: def test_llm_returns_json_in_response(self, memory): """Should handle LLM returning JSON in text response.""" - from app import agent, app + from alfred.agent import agent + from alfred.app import app mock_llm = Mock() mock_llm.complete.return_value = { @@ -414,9 +422,9 @@ class TestMemoryEndpointsEdgeCases: def test_memory_state_with_large_data(self, memory): """Should handle large memory state.""" - with patch("app.DeepSeekClient") as mock_llm: + with patch("alfred.app.DeepSeekClient") as mock_llm: mock_llm.return_value = Mock() - from app import app + from alfred.app import app # Add lots of data to memory for i in range(100): @@ -432,9 +440,9 @@ class TestMemoryEndpointsEdgeCases: def test_memory_state_with_unicode(self, memory): """Should handle unicode in memory state.""" - with patch("app.DeepSeekClient") as mock_llm: + with patch("alfred.app.DeepSeekClient") as mock_llm: mock_llm.return_value = Mock() - from app import app + from alfred.app import app memory.ltm.set_config("japanese", "日本語テスト") memory.stm.add_message("user", "🎬 Movie request") @@ -448,9 +456,9 @@ class TestMemoryEndpointsEdgeCases: def test_search_results_with_special_chars(self, memory): """Should handle special characters in search results.""" - with patch("app.DeepSeekClient") as mock_llm: + with patch("alfred.app.DeepSeekClient") as mock_llm: mock_llm.return_value = Mock() - from app import app + from alfred.app import app memory.episodic.store_search_results( "Test ", @@ -467,9 +475,9 @@ class TestMemoryEndpointsEdgeCases: def test_clear_session_idempotent(self, memory): """Should be idempotent - multiple clears should work.""" - with patch("app.DeepSeekClient") as mock_llm: + with patch("alfred.app.DeepSeekClient") as mock_llm: mock_llm.return_value = Mock() - from app import app + from alfred.app import app client = TestClient(app) @@ -480,9 +488,9 @@ class TestMemoryEndpointsEdgeCases: def test_clear_session_preserves_ltm(self, memory): """Should preserve LTM after clear.""" - with patch("app.DeepSeekClient") as mock_llm: + with patch("alfred.app.DeepSeekClient") as mock_llm: mock_llm.return_value = Mock() - from app import app + from alfred.app import app memory.ltm.set_config("important", "data") memory.stm.add_message("user", "Hello") @@ -502,9 +510,9 @@ class TestHealthEndpointEdgeCases: def test_health_returns_version(self, memory): """Should return version in health check.""" - with patch("app.DeepSeekClient") as mock_llm: + with patch("alfred.app.DeepSeekClient") as mock_llm: mock_llm.return_value = Mock() - from app import app + from alfred.app import app client = TestClient(app) @@ -515,9 +523,9 @@ class TestHealthEndpointEdgeCases: def test_health_with_query_params(self, memory): """Should ignore query parameters.""" - with patch("app.DeepSeekClient") as mock_llm: + with patch("alfred.app.DeepSeekClient") as mock_llm: mock_llm.return_value = Mock() - from app import app + from alfred.app import app client = TestClient(app) @@ -531,9 +539,9 @@ class TestModelsEndpointEdgeCases: def test_models_response_format(self, memory): """Should return OpenAI-compatible format.""" - with patch("app.DeepSeekClient") as mock_llm: + with patch("alfred.app.DeepSeekClient") as mock_llm: mock_llm.return_value = Mock() - from app import app + from alfred.app import app client = TestClient(app) diff --git a/brain/tests/test_config_critical.py b/tests/test_config_critical.py similarity index 99% rename from brain/tests/test_config_critical.py rename to tests/test_config_critical.py index d149f2c..02432d6 100644 --- a/brain/tests/test_config_critical.py +++ b/tests/test_config_critical.py @@ -2,7 +2,7 @@ import pytest -from agent.config import ConfigurationError, Settings +from alfred.agent.config import ConfigurationError, Settings class TestConfigValidation: diff --git a/brain/tests/test_config_edge_cases.py b/tests/test_config_edge_cases.py similarity index 99% rename from brain/tests/test_config_edge_cases.py rename to tests/test_config_edge_cases.py index 3076e85..70fa5e7 100644 --- a/brain/tests/test_config_edge_cases.py +++ b/tests/test_config_edge_cases.py @@ -5,8 +5,8 @@ from unittest.mock import patch import pytest -from agent.config import ConfigurationError, Settings -from agent.parameters import ( +from alfred.agent.config import ConfigurationError, Settings +from alfred.agent.parameters import ( REQUIRED_PARAMETERS, ParameterSchema, format_parameters_for_prompt, diff --git a/brain/tests/test_domain_edge_cases.py b/tests/test_domain_edge_cases.py similarity index 97% rename from brain/tests/test_domain_edge_cases.py rename to tests/test_domain_edge_cases.py index 2e9afee..a38aa67 100644 --- a/brain/tests/test_domain_edge_cases.py +++ b/tests/test_domain_edge_cases.py @@ -4,14 +4,14 @@ from datetime import datetime import pytest -from domain.movies.entities import Movie -from domain.movies.value_objects import MovieTitle, Quality, ReleaseYear -from domain.shared.exceptions import ValidationError -from domain.shared.value_objects import FilePath, FileSize, ImdbId -from domain.subtitles.entities import Subtitle -from domain.subtitles.value_objects import Language, SubtitleFormat, TimingOffset -from domain.tv_shows.entities import TVShow -from domain.tv_shows.value_objects import ShowStatus +from alfred.domain.movies.entities import Movie +from alfred.domain.movies.value_objects import MovieTitle, Quality, ReleaseYear +from alfred.domain.shared.exceptions import ValidationError +from alfred.domain.shared.value_objects import FilePath, FileSize, ImdbId +from alfred.domain.subtitles.entities import Subtitle +from alfred.domain.subtitles.value_objects import Language, SubtitleFormat, TimingOffset +from alfred.domain.tv_shows.entities import TVShow +from alfred.domain.tv_shows.value_objects import ShowStatus class TestImdbIdEdgeCases: diff --git a/brain/tests/test_llm_clients.py b/tests/test_llm_clients.py similarity index 100% rename from brain/tests/test_llm_clients.py rename to tests/test_llm_clients.py diff --git a/brain/tests/test_memory.py b/tests/test_memory.py similarity index 98% rename from brain/tests/test_memory.py rename to tests/test_memory.py index ad1ecfe..2ecca6e 100644 --- a/brain/tests/test_memory.py +++ b/tests/test_memory.py @@ -4,7 +4,7 @@ from datetime import datetime import pytest -from infrastructure.persistence import ( +from alfred.infrastructure.persistence import ( EpisodicMemory, LongTermMemory, Memory, @@ -13,7 +13,7 @@ from infrastructure.persistence import ( has_memory, init_memory, ) -from infrastructure.persistence.context import _memory_ctx +from alfred.infrastructure.persistence.context import _memory_ctx def is_iso_format(s: str) -> bool: diff --git a/brain/tests/test_memory_edge_cases.py b/tests/test_memory_edge_cases.py similarity index 99% rename from brain/tests/test_memory_edge_cases.py rename to tests/test_memory_edge_cases.py index d320b00..12d6295 100644 --- a/brain/tests/test_memory_edge_cases.py +++ b/tests/test_memory_edge_cases.py @@ -5,7 +5,7 @@ import os import pytest -from infrastructure.persistence import ( +from alfred.infrastructure.persistence import ( EpisodicMemory, LongTermMemory, Memory, @@ -14,7 +14,7 @@ from infrastructure.persistence import ( init_memory, set_memory, ) -from infrastructure.persistence.context import _memory_ctx +from alfred.infrastructure.persistence.context import _memory_ctx class TestLongTermMemoryEdgeCases: diff --git a/brain/tests/test_prompts.py b/tests/test_prompts.py similarity index 99% rename from brain/tests/test_prompts.py rename to tests/test_prompts.py index 28e77be..6cd7539 100644 --- a/brain/tests/test_prompts.py +++ b/tests/test_prompts.py @@ -1,7 +1,7 @@ """Tests for PromptBuilder.""" -from agent.prompts import PromptBuilder -from agent.registry import make_tools +from alfred.agent.prompts import PromptBuilder +from alfred.agent.registry import make_tools class TestPromptBuilder: diff --git a/brain/tests/test_prompts_critical.py b/tests/test_prompts_critical.py similarity index 97% rename from brain/tests/test_prompts_critical.py rename to tests/test_prompts_critical.py index 3ef683c..bd34041 100644 --- a/brain/tests/test_prompts_critical.py +++ b/tests/test_prompts_critical.py @@ -1,7 +1,7 @@ """Critical tests for prompt builder - Tests that would have caught bugs.""" -from agent.prompts import PromptBuilder -from agent.registry import make_tools +from alfred.agent.prompts import PromptBuilder +from alfred.agent.registry import make_tools class TestPromptBuilderToolsInjection: @@ -15,9 +15,9 @@ class TestPromptBuilderToolsInjection: # Verify each tool is mentioned for tool_name in tools.keys(): - assert ( - tool_name in prompt - ), f"Tool {tool_name} not mentioned in system prompt" + assert tool_name in prompt, ( + f"Tool {tool_name} not mentioned in system prompt" + ) def test_tools_spec_contains_all_registered_tools(self, memory): """CRITICAL: Verify build_tools_spec() returns all tools.""" diff --git a/brain/tests/test_prompts_edge_cases.py b/tests/test_prompts_edge_cases.py similarity index 98% rename from brain/tests/test_prompts_edge_cases.py rename to tests/test_prompts_edge_cases.py index 5e4a3e8..738b1d7 100644 --- a/brain/tests/test_prompts_edge_cases.py +++ b/tests/test_prompts_edge_cases.py @@ -1,7 +1,7 @@ """Edge case tests for PromptBuilder.""" -from agent.prompts import PromptBuilder -from agent.registry import make_tools +from alfred.agent.prompts import PromptBuilder +from alfred.agent.registry import make_tools class TestPromptBuilderEdgeCases: @@ -266,7 +266,7 @@ class TestFormatToolsDescriptionEdgeCases: def test_format_with_complex_parameters(self, memory): """Should format complex parameter schemas.""" - from agent.registry import Tool + from alfred.agent.registry import Tool tools = { "complex_tool": Tool( diff --git a/brain/tests/test_registry_critical.py b/tests/test_registry_critical.py similarity index 93% rename from brain/tests/test_registry_critical.py rename to tests/test_registry_critical.py index ec275e4..11f5535 100644 --- a/brain/tests/test_registry_critical.py +++ b/tests/test_registry_critical.py @@ -4,8 +4,8 @@ import inspect import pytest -from agent.prompts import PromptBuilder -from agent.registry import Tool, _create_tool_from_function, make_tools +from alfred.agent.prompts import PromptBuilder +from alfred.agent.registry import Tool, _create_tool_from_function, make_tools class TestToolSpecFormat: @@ -23,9 +23,9 @@ class TestToolSpecFormat: for spec in specs: # OpenAI format requires these fields - assert ( - spec["type"] == "function" - ), f"Tool type must be 'function', got {spec.get('type')}" + assert spec["type"] == "function", ( + f"Tool type must be 'function', got {spec.get('type')}" + ) assert "function" in spec, "Tool spec missing 'function' key" func = spec["function"] @@ -56,9 +56,9 @@ class TestToolSpecFormat: # Verify required vs optional assert "name" in tool.parameters["required"], "name should be required" assert "age" in tool.parameters["required"], "age should be required" - assert ( - "active" not in tool.parameters["required"] - ), "active has default, should not be required" + assert "active" not in tool.parameters["required"], ( + "active has default, should not be required" + ) def test_all_registered_tools_are_callable(self): """CRITICAL: Verify all registered tools are actually callable.""" @@ -128,9 +128,9 @@ class TestToolSpecFormat: properties = params.get("properties", {}) for param_name, param_spec in properties.items(): - assert ( - "description" in param_spec - ), f"Parameter {param_name} in {spec['function']['name']} missing description" + assert "description" in param_spec, ( + f"Parameter {param_name} in {spec['function']['name']} missing description" + ) def test_required_parameters_are_marked_correctly(self): """Verify required parameters are correctly identified.""" diff --git a/brain/tests/test_registry_edge_cases.py b/tests/test_registry_edge_cases.py similarity index 96% rename from brain/tests/test_registry_edge_cases.py rename to tests/test_registry_edge_cases.py index 9404bca..952cfad 100644 --- a/brain/tests/test_registry_edge_cases.py +++ b/tests/test_registry_edge_cases.py @@ -2,7 +2,7 @@ import pytest -from agent.registry import Tool, make_tools +from alfred.agent.registry import Tool, make_tools class TestToolEdgeCases: @@ -182,9 +182,9 @@ class TestMakeToolsEdgeCases: params = tool.parameters if "required" in params and "properties" in params: for req in params["required"]: - assert ( - req in params["properties"] - ), f"Required param {req} not in properties for {tool.name}" + assert req in params["properties"], ( + f"Required param {req} not in properties for {tool.name}" + ) def test_make_tools_descriptions_not_empty(self, memory): """Should have non-empty descriptions.""" @@ -235,9 +235,9 @@ class TestMakeToolsEdgeCases: if "properties" in tool.parameters: for prop_name, prop_schema in tool.parameters["properties"].items(): if "type" in prop_schema: - assert ( - prop_schema["type"] in valid_types - ), f"Invalid type for {tool.name}.{prop_name}" + assert prop_schema["type"] in valid_types, ( + f"Invalid type for {tool.name}.{prop_name}" + ) def test_make_tools_enum_values(self, memory): """Should have valid enum values.""" diff --git a/brain/tests/test_repositories.py b/tests/test_repositories.py similarity index 95% rename from brain/tests/test_repositories.py rename to tests/test_repositories.py index b904f4d..a323d60 100644 --- a/brain/tests/test_repositories.py +++ b/tests/test_repositories.py @@ -1,13 +1,13 @@ """Tests for JSON repositories.""" -from domain.movies.entities import Movie -from domain.movies.value_objects import MovieTitle, Quality, ReleaseYear -from domain.shared.value_objects import FilePath, FileSize, ImdbId -from domain.subtitles.entities import Subtitle -from domain.subtitles.value_objects import Language, SubtitleFormat, TimingOffset -from domain.tv_shows.entities import TVShow -from domain.tv_shows.value_objects import ShowStatus -from infrastructure.persistence.json import ( +from alfred.domain.movies.entities import Movie +from alfred.domain.movies.value_objects import MovieTitle, Quality, ReleaseYear +from alfred.domain.shared.value_objects import FilePath, FileSize, ImdbId +from alfred.domain.subtitles.entities import Subtitle +from alfred.domain.subtitles.value_objects import Language, SubtitleFormat, TimingOffset +from alfred.domain.tv_shows.entities import TVShow +from alfred.domain.tv_shows.value_objects import ShowStatus +from alfred.infrastructure.persistence.json import ( JsonMovieRepository, JsonSubtitleRepository, JsonTVShowRepository, @@ -226,13 +226,13 @@ class TestJsonTVShowRepository: [ShowStatus.ONGOING, ShowStatus.ENDED, ShowStatus.UNKNOWN] ): show = TVShow( - imdb_id=ImdbId(f"tt{i+1000000:07d}"), + imdb_id=ImdbId(f"tt{i + 1000000:07d}"), title=f"Show {status.value}", seasons_count=1, status=status, ) repo.save(show) - loaded = repo.find_by_imdb_id(ImdbId(f"tt{i+1000000:07d}")) + loaded = repo.find_by_imdb_id(ImdbId(f"tt{i + 1000000:07d}")) assert loaded.status == status diff --git a/brain/tests/test_repositories_edge_cases.py b/tests/test_repositories_edge_cases.py similarity index 97% rename from brain/tests/test_repositories_edge_cases.py rename to tests/test_repositories_edge_cases.py index f77fe48..97700c6 100644 --- a/brain/tests/test_repositories_edge_cases.py +++ b/tests/test_repositories_edge_cases.py @@ -2,14 +2,14 @@ from datetime import datetime -from domain.movies.entities import Movie -from domain.movies.value_objects import MovieTitle, Quality -from domain.shared.value_objects import FilePath, FileSize, ImdbId -from domain.subtitles.entities import Subtitle -from domain.subtitles.value_objects import Language, SubtitleFormat, TimingOffset -from domain.tv_shows.entities import TVShow -from domain.tv_shows.value_objects import ShowStatus -from infrastructure.persistence.json import ( +from alfred.domain.movies.entities import Movie +from alfred.domain.movies.value_objects import MovieTitle, Quality +from alfred.domain.shared.value_objects import FilePath, FileSize, ImdbId +from alfred.domain.subtitles.entities import Subtitle +from alfred.domain.subtitles.value_objects import Language, SubtitleFormat, TimingOffset +from alfred.domain.tv_shows.entities import TVShow +from alfred.domain.tv_shows.value_objects import ShowStatus +from alfred.infrastructure.persistence.json import ( JsonMovieRepository, JsonSubtitleRepository, JsonTVShowRepository, diff --git a/brain/tests/test_tools_api.py b/tests/test_tools_api.py similarity index 92% rename from brain/tests/test_tools_api.py rename to tests/test_tools_api.py index 137d2e1..7a3ebb0 100644 --- a/brain/tests/test_tools_api.py +++ b/tests/test_tools_api.py @@ -2,8 +2,8 @@ from unittest.mock import Mock, patch -from agent.tools import api as api_tools -from infrastructure.persistence import get_memory +from alfred.agent.tools import api as api_tools +from alfred.infrastructure.persistence import get_memory def create_mock_response(status_code, json_data=None, text=None): @@ -21,7 +21,7 @@ def create_mock_response(status_code, json_data=None, text=None): class TestFindMediaImdbId: """Tests for find_media_imdb_id tool.""" - @patch("infrastructure.api.tmdb.client.requests.get") + @patch("alfred.infrastructure.api.tmdb.client.requests.get") def test_success(self, mock_get, memory): """Should return movie info on success.""" @@ -56,7 +56,7 @@ class TestFindMediaImdbId: # Verify HTTP calls assert mock_get.call_count == 2 - @patch("infrastructure.api.tmdb.client.requests.get") + @patch("alfred.infrastructure.api.tmdb.client.requests.get") def test_stores_in_stm(self, mock_get, memory): """Should store result in STM on success.""" @@ -88,7 +88,7 @@ class TestFindMediaImdbId: assert entity["title"] == "Inception" assert mem.stm.current_topic == "searching_media" - @patch("infrastructure.api.tmdb.client.requests.get") + @patch("alfred.infrastructure.api.tmdb.client.requests.get") def test_not_found(self, mock_get, memory): """Should return error when not found.""" mock_get.return_value = create_mock_response(200, json_data={"results": []}) @@ -98,7 +98,7 @@ class TestFindMediaImdbId: assert result["status"] == "error" assert result["error"] == "not_found" - @patch("infrastructure.api.tmdb.client.requests.get") + @patch("alfred.infrastructure.api.tmdb.client.requests.get") def test_does_not_store_on_error(self, mock_get, memory): """Should not store in STM on error.""" mock_get.return_value = create_mock_response(200, json_data={"results": []}) @@ -112,7 +112,7 @@ class TestFindMediaImdbId: class TestFindTorrent: """Tests for find_torrent tool.""" - @patch("infrastructure.api.knaben.client.requests.post") + @patch("alfred.infrastructure.api.knaben.client.requests.post") def test_success(self, mock_post, memory): """Should return torrents on success.""" mock_post.return_value = create_mock_response( @@ -146,7 +146,7 @@ class TestFindTorrent: payload = mock_post.call_args[1]["json"] assert payload["query"] == "Inception 1080p" - @patch("infrastructure.api.knaben.client.requests.post") + @patch("alfred.infrastructure.api.knaben.client.requests.post") def test_stores_in_episodic(self, mock_post, memory): """Should store results in episodic memory.""" mock_post.return_value = create_mock_response( @@ -171,7 +171,7 @@ class TestFindTorrent: assert mem.episodic.last_search_results["query"] == "Inception" assert mem.stm.current_topic == "selecting_torrent" - @patch("infrastructure.api.knaben.client.requests.post") + @patch("alfred.infrastructure.api.knaben.client.requests.post") def test_results_have_indexes(self, mock_post, memory): """Should add indexes to results.""" mock_post.return_value = create_mock_response( @@ -211,7 +211,7 @@ class TestFindTorrent: assert results[1]["index"] == 2 assert results[2]["index"] == 3 - @patch("infrastructure.api.knaben.client.requests.post") + @patch("alfred.infrastructure.api.knaben.client.requests.post") def test_not_found(self, mock_post, memory): """Should return error when no torrents found.""" mock_post.return_value = create_mock_response(200, json_data={"hits": []}) @@ -286,7 +286,7 @@ class TestAddTorrentToQbittorrent: This is acceptable mocking because we're testing the TOOL logic, not the client. """ - @patch("agent.tools.api.qbittorrent_client") + @patch("alfred.agent.tools.api.qbittorrent_client") def test_success(self, mock_client, memory): """Should add torrent successfully and update memory.""" mock_client.add_torrent.return_value = True @@ -298,7 +298,7 @@ class TestAddTorrentToQbittorrent: # Verify client was called correctly mock_client.add_torrent.assert_called_once_with("magnet:?xt=urn:btih:abc123") - @patch("agent.tools.api.qbittorrent_client") + @patch("alfred.agent.tools.api.qbittorrent_client") def test_adds_to_active_downloads(self, mock_client, memory_with_search_results): """Should add to active downloads on success.""" mock_client.add_torrent.return_value = True @@ -313,7 +313,7 @@ class TestAddTorrentToQbittorrent: == "Inception.2010.1080p.BluRay.x264" ) - @patch("agent.tools.api.qbittorrent_client") + @patch("alfred.agent.tools.api.qbittorrent_client") def test_sets_topic_and_ends_workflow(self, mock_client, memory): """Should set topic and end workflow.""" mock_client.add_torrent.return_value = True @@ -326,10 +326,10 @@ class TestAddTorrentToQbittorrent: assert mem.stm.current_topic == "downloading" assert mem.stm.current_workflow is None - @patch("agent.tools.api.qbittorrent_client") + @patch("alfred.agent.tools.api.qbittorrent_client") def test_error_handling(self, mock_client, memory): """Should handle client errors correctly.""" - from infrastructure.api.qbittorrent.exceptions import QBittorrentAPIError + from alfred.infrastructure.api.qbittorrent.exceptions import QBittorrentAPIError mock_client.add_torrent.side_effect = QBittorrentAPIError("Connection failed") @@ -349,7 +349,7 @@ class TestAddTorrentByIndex: - Error handling for edge cases """ - @patch("agent.tools.api.qbittorrent_client") + @patch("alfred.agent.tools.api.qbittorrent_client") def test_success(self, mock_client, memory_with_search_results): """Should get torrent by index and add it.""" mock_client.add_torrent.return_value = True @@ -362,7 +362,7 @@ class TestAddTorrentByIndex: # Verify correct magnet was extracted and used mock_client.add_torrent.assert_called_once_with("magnet:?xt=urn:btih:abc123") - @patch("agent.tools.api.qbittorrent_client") + @patch("alfred.agent.tools.api.qbittorrent_client") def test_uses_correct_magnet(self, mock_client, memory_with_search_results): """Should extract correct magnet from index.""" mock_client.add_torrent.return_value = True diff --git a/brain/tests/test_tools_edge_cases.py b/tests/test_tools_edge_cases.py similarity index 94% rename from brain/tests/test_tools_edge_cases.py rename to tests/test_tools_edge_cases.py index 23fb50e..6ccc1e4 100644 --- a/brain/tests/test_tools_edge_cases.py +++ b/tests/test_tools_edge_cases.py @@ -4,15 +4,15 @@ from unittest.mock import Mock, patch import pytest -from agent.tools import api as api_tools -from agent.tools import filesystem as fs_tools -from infrastructure.persistence import get_memory +from alfred.agent.tools import api as api_tools +from alfred.agent.tools import filesystem as fs_tools +from alfred.infrastructure.persistence import get_memory class TestFindTorrentEdgeCases: """Edge case tests for find_torrent.""" - @patch("agent.tools.api.SearchTorrentsUseCase") + @patch("alfred.agent.tools.api.SearchTorrentsUseCase") def test_empty_query(self, mock_use_case_class, memory): """Should handle empty query.""" mock_response = Mock() @@ -28,7 +28,7 @@ class TestFindTorrentEdgeCases: assert result["status"] == "error" - @patch("agent.tools.api.SearchTorrentsUseCase") + @patch("alfred.agent.tools.api.SearchTorrentsUseCase") def test_very_long_query(self, mock_use_case_class, memory): """Should handle very long query.""" mock_response = Mock() @@ -47,7 +47,7 @@ class TestFindTorrentEdgeCases: # Should not crash assert "status" in result - @patch("agent.tools.api.SearchTorrentsUseCase") + @patch("alfred.agent.tools.api.SearchTorrentsUseCase") def test_special_characters_in_query(self, mock_use_case_class, memory): """Should handle special characters in query.""" mock_response = Mock() @@ -65,7 +65,7 @@ class TestFindTorrentEdgeCases: assert "status" in result - @patch("agent.tools.api.SearchTorrentsUseCase") + @patch("alfred.agent.tools.api.SearchTorrentsUseCase") def test_unicode_query(self, mock_use_case_class, memory): """Should handle unicode in query.""" mock_response = Mock() @@ -82,7 +82,7 @@ class TestFindTorrentEdgeCases: assert "status" in result - @patch("agent.tools.api.SearchTorrentsUseCase") + @patch("alfred.agent.tools.api.SearchTorrentsUseCase") def test_results_with_missing_fields(self, mock_use_case_class, memory): """Should handle results with missing fields.""" mock_response = Mock() @@ -104,7 +104,7 @@ class TestFindTorrentEdgeCases: mem = get_memory() assert len(mem.episodic.last_search_results["results"]) == 2 - @patch("agent.tools.api.SearchTorrentsUseCase") + @patch("alfred.agent.tools.api.SearchTorrentsUseCase") def test_api_timeout(self, mock_use_case_class, memory): """Should handle API timeout.""" mock_use_case = Mock() @@ -157,7 +157,7 @@ class TestGetTorrentByIndexEdgeCases: class TestAddTorrentEdgeCases: """Edge case tests for add_torrent functions.""" - @patch("agent.tools.api.AddTorrentUseCase") + @patch("alfred.agent.tools.api.AddTorrentUseCase") def test_invalid_magnet_link(self, mock_use_case_class, memory): """Should handle invalid magnet link.""" mock_response = Mock() @@ -173,7 +173,7 @@ class TestAddTorrentEdgeCases: assert result["status"] == "error" - @patch("agent.tools.api.AddTorrentUseCase") + @patch("alfred.agent.tools.api.AddTorrentUseCase") def test_empty_magnet_link(self, mock_use_case_class, memory): """Should handle empty magnet link.""" mock_response = Mock() @@ -189,7 +189,7 @@ class TestAddTorrentEdgeCases: assert result["status"] == "error" - @patch("agent.tools.api.AddTorrentUseCase") + @patch("alfred.agent.tools.api.AddTorrentUseCase") def test_very_long_magnet_link(self, mock_use_case_class, memory): """Should handle very long magnet link.""" mock_response = Mock() @@ -203,7 +203,7 @@ class TestAddTorrentEdgeCases: assert "status" in result - @patch("agent.tools.api.AddTorrentUseCase") + @patch("alfred.agent.tools.api.AddTorrentUseCase") def test_qbittorrent_connection_refused(self, mock_use_case_class, memory): """Should handle qBittorrent connection refused.""" mock_use_case = Mock() @@ -391,7 +391,7 @@ class TestFilesystemEdgeCases: class TestFindMediaImdbIdEdgeCases: """Edge case tests for find_media_imdb_id.""" - @patch("agent.tools.api.SearchMovieUseCase") + @patch("alfred.agent.tools.api.SearchMovieUseCase") def test_movie_with_same_name_different_years(self, mock_use_case_class, memory): """Should handle movies with same name.""" mock_response = Mock() @@ -409,7 +409,7 @@ class TestFindMediaImdbIdEdgeCases: assert result["status"] == "ok" - @patch("agent.tools.api.SearchMovieUseCase") + @patch("alfred.agent.tools.api.SearchMovieUseCase") def test_movie_with_special_title(self, mock_use_case_class, memory): """Should handle movies with special characters in title.""" mock_response = Mock() @@ -426,7 +426,7 @@ class TestFindMediaImdbIdEdgeCases: assert result["status"] == "ok" - @patch("agent.tools.api.SearchMovieUseCase") + @patch("alfred.agent.tools.api.SearchMovieUseCase") def test_tv_show_vs_movie(self, mock_use_case_class, memory): """Should distinguish TV shows from movies.""" mock_response = Mock() diff --git a/brain/tests/test_tools_filesystem.py b/tests/test_tools_filesystem.py similarity index 98% rename from brain/tests/test_tools_filesystem.py rename to tests/test_tools_filesystem.py index 706a477..8245594 100644 --- a/brain/tests/test_tools_filesystem.py +++ b/tests/test_tools_filesystem.py @@ -4,8 +4,8 @@ from pathlib import Path import pytest -from agent.tools import filesystem as fs_tools -from infrastructure.persistence import get_memory +from alfred.agent.tools import filesystem as fs_tools +from alfred.infrastructure.persistence import get_memory class TestSetPathForFolder: