diff --git a/CHANGELOG.md b/CHANGELOG.md index ca8af80..89167c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,15 @@ callers). and returns a frozen `Response` DTO. Roots are now injected, not pulled from the global memory singleton. +- **Agent tool wrappers partially re-wired** to the new use cases. + `list_folder` now delegates to `list_dir_use_case`; `move_media` + to `move_file_use_case`; `move_to_destination` chains + `create_dir_use_case` + `move_file_use_case`; a new + `create_directory` tool wraps `create_dir_use_case`. Roots are + loaded once via a module-level `_load_directory_roots()` helper + that reads the persisted memory (no more per-call singleton + reads inside the use cases themselves). + ### Removed - `FileManager` / `MediaOrganizer` / `create_folder` / `move` from the @@ -46,6 +55,16 @@ callers). of `alfred.application.filesystem`. Same `_OLD` rename treatment. This intentionally breaks current tool wrappers and tests downstream — re-wiring is the next chunk of work on this branch. +- **Agent tools dropped during the refactor** (to be reintroduced + when the matching domain/application code lands): + `manage_subtitles`, `set_path_for_folder`, `create_seed_links`, + `resolve_season_destination`, `resolve_episode_destination`, + `resolve_movie_destination`, `resolve_series_destination`. + Their wrappers are removed from `alfred.agent.tools.filesystem`; + `alfred.agent.tools.__init__` now re-exports only what still + imports cleanly. `find_media_imdb_id` (already broken before this + branch — name no longer exported by `tools.api`) was also dropped + from the package re-exports. ### Added diff --git a/alfred/agent/tools/__init__.py b/alfred/agent/tools/__init__.py index e7beebe..5e8d3a9 100644 --- a/alfred/agent/tools/__init__.py +++ b/alfred/agent/tools/__init__.py @@ -1,19 +1,20 @@ -"""Tools module - filesystem and API tools for the agent.""" +"""Tools module — agent-exposed wrappers. + +Re-exports are intentionally minimal during the ``unfuck`` refactor. +Tool wiring (registry / specs / LLM-facing surface) is the last +chunk of work on this branch; until then, importers should reach +into the submodules directly (``alfred.agent.tools.filesystem``, …). +""" from .api import ( add_torrent_by_index, add_torrent_to_qbittorrent, - find_media_imdb_id, find_torrent, get_torrent_by_index, ) -from .filesystem import list_folder, set_path_for_folder from .language import set_language __all__ = [ - "set_path_for_folder", - "list_folder", - "find_media_imdb_id", "find_torrent", "get_torrent_by_index", "add_torrent_to_qbittorrent", diff --git a/alfred/agent/tools/filesystem.py b/alfred/agent/tools/filesystem.py index 746c913..d457357 100644 --- a/alfred/agent/tools/filesystem.py +++ b/alfred/agent/tools/filesystem.py @@ -1,4 +1,20 @@ -"""Filesystem tools for folder management.""" +"""Filesystem tools for folder management. + +Thin wrappers around the 5 atomic filesystem use cases +(``alfred.application.filesystem``) plus a few self-contained tools +(``analyze_release``, ``probe_media``, ``learn``, …). + +Tools removed during the ``unfuck`` filesystem refactor — to be +rewired in a later step: + - ``manage_subtitles`` (depends on the rewritten subtitle services) + - ``set_path_for_folder`` (no replacement use case yet) + - ``create_seed_links`` (flow has changed: hard-link straight to + library, no copy back; will be re-introduced per-file when the + organize-release workflow lands) + - ``resolve_season_destination`` / ``resolve_episode_destination`` + / ``resolve_movie_destination`` / ``resolve_series_destination`` + (their use cases moved to ``_OLD`` files pending a rewrite) +""" from pathlib import Path from typing import Any @@ -7,25 +23,11 @@ import yaml import alfred as _alfred_pkg from alfred.application.filesystem import ( - CreateSeedLinksUseCase, - ListFolderUseCase, - ManageSubtitlesUseCase, - MoveMediaUseCase, - SetFolderPathUseCase, + DirectoryRoots, + create_dir_use_case, + list_dir_use_case, + move_file_use_case, ) -from alfred.application.filesystem.resolve_destination import ( - resolve_episode_destination as _resolve_episode_destination, -) -from alfred.application.filesystem.resolve_destination import ( - resolve_movie_destination as _resolve_movie_destination, -) -from alfred.application.filesystem.resolve_destination import ( - resolve_season_destination as _resolve_season_destination, -) -from alfred.application.filesystem.resolve_destination import ( - resolve_series_destination as _resolve_series_destination, -) -from alfred.infrastructure.filesystem import FileManager, create_folder, move from alfred.infrastructure.knowledge.release_kb import YamlReleaseKnowledge from alfred.infrastructure.metadata import MetadataStore from alfred.infrastructure.persistence import get_memory @@ -40,107 +42,117 @@ _PROBER = FfprobeMediaProber() _LEARNED_ROOT = Path(_alfred_pkg.__file__).parent.parent / "data" / "knowledge" +class _RootsNotConfigured(Exception): + """Raised when one of the 4 expected roots is missing from memory.""" + + def __init__(self, missing: list[str]): + super().__init__(f"Roots not configured: {missing}") + self.missing = missing + + +def _load_directory_roots() -> DirectoryRoots: + """Build :class:`DirectoryRoots` from the persisted memory. + + Reads: + - ``ltm.workspace.download`` → ``downloads`` + - ``ltm.workspace.torrent`` → ``torrents`` + - ``ltm.library_paths['movies']`` → ``movies`` + - ``ltm.library_paths['tv_shows']`` → ``tv_shows`` + + Raises: + _RootsNotConfigured: if any of the four paths is unset. + """ + memory = get_memory() + downloads = memory.ltm.workspace.download + torrents = memory.ltm.workspace.torrent + movies = memory.ltm.library_paths.get("movies") + tv_shows = memory.ltm.library_paths.get("tv_shows") + + missing: list[str] = [] + if not downloads: + missing.append("downloads") + if not torrents: + missing.append("torrents") + if not movies: + missing.append("movies") + if not tv_shows: + missing.append("tv_shows") + if missing: + raise _RootsNotConfigured(missing) + + return DirectoryRoots( + downloads=Path(downloads), + torrents=Path(torrents), + movies=Path(movies), + tv_shows=Path(tv_shows), + ) + + +def _roots_error(exc: _RootsNotConfigured) -> dict[str, Any]: + return { + "status": "error", + "error": "roots_not_configured", + "message": ( + f"Missing roots: {exc.missing}. " + "Configure them via /set_path before using filesystem tools." + ), + } + + +# --------------------------------------------------------------------------- +# 5 atomic filesystem tools — thin wrappers over the use cases. +# --------------------------------------------------------------------------- + + +def list_folder(path: str) -> dict[str, Any]: + """Thin tool wrapper — semantics live in alfred/agent/tools/specs/list_folder.yaml.""" + try: + roots = _load_directory_roots() + except _RootsNotConfigured as e: + return _roots_error(e) + return list_dir_use_case(Path(path), roots).to_dict() + + +def create_directory(path: str) -> dict[str, Any]: + """Thin tool wrapper — semantics live in alfred/agent/tools/specs/create_directory.yaml.""" + try: + roots = _load_directory_roots() + except _RootsNotConfigured as e: + return _roots_error(e) + return create_dir_use_case(Path(path), roots).to_dict() + + def move_media(source: str, destination: str) -> dict[str, Any]: """Thin tool wrapper — semantics live in alfred/agent/tools/specs/move_media.yaml.""" - file_manager = FileManager() - use_case = MoveMediaUseCase(file_manager) - return use_case.execute(source, destination).to_dict() + try: + roots = _load_directory_roots() + except _RootsNotConfigured as e: + return _roots_error(e) + return move_file_use_case(Path(source), Path(destination), roots).to_dict() def move_to_destination(source: str, destination: str) -> dict[str, Any]: - """Thin tool wrapper — semantics live in alfred/agent/tools/specs/move_to_destination.yaml.""" - parent = str(Path(destination).parent) - result = create_folder(parent) - if result["status"] != "ok": - return result - return move(source, destination) + """Thin tool wrapper — semantics live in alfred/agent/tools/specs/move_to_destination.yaml. + + Convenience tool that creates the destination's parent directory + if missing, then moves the file. Saves the LLM from having to + chain ``create_directory`` + ``move_media`` explicitly. + """ + try: + roots = _load_directory_roots() + except _RootsNotConfigured as e: + return _roots_error(e) + + dst = Path(destination) + mkdir_resp = create_dir_use_case(dst.parent, roots) + if mkdir_resp.status != "ok": + return mkdir_resp.to_dict() + return move_file_use_case(Path(source), dst, roots).to_dict() -def resolve_season_destination( - release_name: str, - tmdb_title: str, - tmdb_year: int, - confirmed_folder: str | None = None, - source_path: str | None = None, -) -> dict[str, Any]: - """Thin tool wrapper — semantics live in alfred/agent/tools/specs/resolve_season_destination.yaml.""" - return _resolve_season_destination( - release_name, - tmdb_title, - tmdb_year, - _KB, - _PROBER, - confirmed_folder, - source_path, - ).to_dict() - - -def resolve_episode_destination( - release_name: str, - source_file: str, - tmdb_title: str, - tmdb_year: int, - tmdb_episode_title: str | None = None, - confirmed_folder: str | None = None, -) -> dict[str, Any]: - """Thin tool wrapper — semantics live in alfred/agent/tools/specs/resolve_episode_destination.yaml.""" - return _resolve_episode_destination( - release_name, - source_file, - tmdb_title, - tmdb_year, - _KB, - _PROBER, - tmdb_episode_title, - confirmed_folder, - ).to_dict() - - -def resolve_movie_destination( - release_name: str, - source_file: str, - tmdb_title: str, - tmdb_year: int, -) -> dict[str, Any]: - """Thin tool wrapper — semantics live in alfred/agent/tools/specs/resolve_movie_destination.yaml.""" - return _resolve_movie_destination( - release_name, source_file, tmdb_title, tmdb_year, _KB, _PROBER - ).to_dict() - - -def resolve_series_destination( - release_name: str, - tmdb_title: str, - tmdb_year: int, - confirmed_folder: str | None = None, - source_path: str | None = None, -) -> dict[str, Any]: - """Thin tool wrapper — semantics live in alfred/agent/tools/specs/resolve_series_destination.yaml.""" - return _resolve_series_destination( - release_name, - tmdb_title, - tmdb_year, - _KB, - _PROBER, - confirmed_folder, - source_path, - ).to_dict() - - -def create_seed_links( - library_file: str, original_download_folder: str -) -> dict[str, Any]: - """Thin tool wrapper — semantics live in alfred/agent/tools/specs/create_seed_links.yaml.""" - file_manager = FileManager() - use_case = CreateSeedLinksUseCase(file_manager) - return use_case.execute(library_file, original_download_folder).to_dict() - - -def manage_subtitles(source_video: str, destination_video: str) -> dict[str, Any]: - """Thin tool wrapper — semantics live in alfred/agent/tools/specs/manage_subtitles.yaml.""" - file_manager = FileManager() - use_case = ManageSubtitlesUseCase(file_manager) - return use_case.execute(source_video, destination_video).to_dict() +# --------------------------------------------------------------------------- +# Self-contained tools — not impacted by the filesystem refactor. +# --------------------------------------------------------------------------- def learn(pack: str, category: str, key: str, values: list[str]) -> dict[str, Any]: @@ -200,14 +212,6 @@ def learn(pack: str, category: str, key: str, values: list[str]) -> dict[str, An } -def set_path_for_folder(folder_name: str, path_value: str) -> dict[str, Any]: - """Thin tool wrapper — semantics live in alfred/agent/tools/specs/set_path_for_folder.yaml.""" - file_manager = FileManager() - use_case = SetFolderPathUseCase(file_manager) - response = use_case.execute(folder_name, path_value) - return response.to_dict() - - def analyze_release(release_name: str, source_path: str) -> dict[str, Any]: """Thin tool wrapper — semantics live in alfred/agent/tools/specs/analyze_release.yaml.""" from alfred.application.release import inspect_release # noqa: PLC0415 @@ -296,14 +300,6 @@ def probe_media(source_path: str) -> dict[str, Any]: } -def list_folder(folder_type: str, path: str = ".") -> dict[str, Any]: - """Thin tool wrapper — semantics live in alfred/agent/tools/specs/list_folder.yaml.""" - file_manager = FileManager() - use_case = ListFolderUseCase(file_manager) - response = use_case.execute(folder_type, path) - return response.to_dict() - - def read_release_metadata(release_path: str) -> dict[str, Any]: """Thin tool wrapper — semantics live in alfred/agent/tools/specs/read_release_metadata.yaml.""" path = Path(release_path)