refactor(tools): wire filesystem tools to new use cases, drop broken ones

Updates alfred/agent/tools/filesystem.py to use the five free-function
use cases introduced in the previous commit:

  - list_folder            -> list_dir_use_case(Path(path), roots)
  - create_directory (new) -> create_dir_use_case(Path(path), roots)
  - move_media             -> move_file_use_case(src, dst, roots)
  - move_to_destination    -> create_dir_use_case(dst.parent) + move_file

A module-level _load_directory_roots() helper reads memory once per
call and builds the DirectoryRoots VO; missing roots produce an
explicit 'roots_not_configured' error.

Tools whose backing code was moved to *_OLD files are removed entirely
rather than left broken: manage_subtitles, set_path_for_folder,
create_seed_links, and the four resolve_*_destination tools. They will
come back when the matching application/domain code is rebuilt later
on this branch.

alfred/agent/tools/__init__.py shrinks accordingly. find_media_imdb_id
(already broken before this branch — name not exported by tools.api)
is dropped from the package re-exports so the package imports cleanly
again.
This commit is contained in:
2026-05-26 19:46:49 +02:00
parent 2df7843d8b
commit 42fa6139ed
3 changed files with 151 additions and 135 deletions
+19
View File
@@ -34,6 +34,15 @@ callers).
and returns a frozen `<Op>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
+7 -6
View File
@@ -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",
+125 -129
View File
@@ -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)