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:
@@ -34,6 +34,15 @@ callers).
|
|||||||
and returns a frozen `<Op>Response` DTO. Roots are now injected, not
|
and returns a frozen `<Op>Response` DTO. Roots are now injected, not
|
||||||
pulled from the global memory singleton.
|
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
|
### Removed
|
||||||
|
|
||||||
- `FileManager` / `MediaOrganizer` / `create_folder` / `move` from the
|
- `FileManager` / `MediaOrganizer` / `create_folder` / `move` from the
|
||||||
@@ -46,6 +55,16 @@ callers).
|
|||||||
of `alfred.application.filesystem`. Same `_OLD` rename treatment.
|
of `alfred.application.filesystem`. Same `_OLD` rename treatment.
|
||||||
This intentionally breaks current tool wrappers and tests downstream
|
This intentionally breaks current tool wrappers and tests downstream
|
||||||
— re-wiring is the next chunk of work on this branch.
|
— 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
|
### Added
|
||||||
|
|
||||||
|
|||||||
@@ -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 (
|
from .api import (
|
||||||
add_torrent_by_index,
|
add_torrent_by_index,
|
||||||
add_torrent_to_qbittorrent,
|
add_torrent_to_qbittorrent,
|
||||||
find_media_imdb_id,
|
|
||||||
find_torrent,
|
find_torrent,
|
||||||
get_torrent_by_index,
|
get_torrent_by_index,
|
||||||
)
|
)
|
||||||
from .filesystem import list_folder, set_path_for_folder
|
|
||||||
from .language import set_language
|
from .language import set_language
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"set_path_for_folder",
|
|
||||||
"list_folder",
|
|
||||||
"find_media_imdb_id",
|
|
||||||
"find_torrent",
|
"find_torrent",
|
||||||
"get_torrent_by_index",
|
"get_torrent_by_index",
|
||||||
"add_torrent_to_qbittorrent",
|
"add_torrent_to_qbittorrent",
|
||||||
|
|||||||
+125
-129
@@ -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 pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -7,25 +23,11 @@ import yaml
|
|||||||
|
|
||||||
import alfred as _alfred_pkg
|
import alfred as _alfred_pkg
|
||||||
from alfred.application.filesystem import (
|
from alfred.application.filesystem import (
|
||||||
CreateSeedLinksUseCase,
|
DirectoryRoots,
|
||||||
ListFolderUseCase,
|
create_dir_use_case,
|
||||||
ManageSubtitlesUseCase,
|
list_dir_use_case,
|
||||||
MoveMediaUseCase,
|
move_file_use_case,
|
||||||
SetFolderPathUseCase,
|
|
||||||
)
|
)
|
||||||
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.knowledge.release_kb import YamlReleaseKnowledge
|
||||||
from alfred.infrastructure.metadata import MetadataStore
|
from alfred.infrastructure.metadata import MetadataStore
|
||||||
from alfred.infrastructure.persistence import get_memory
|
from alfred.infrastructure.persistence import get_memory
|
||||||
@@ -40,107 +42,117 @@ _PROBER = FfprobeMediaProber()
|
|||||||
_LEARNED_ROOT = Path(_alfred_pkg.__file__).parent.parent / "data" / "knowledge"
|
_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]:
|
def move_media(source: str, destination: str) -> dict[str, Any]:
|
||||||
"""Thin tool wrapper — semantics live in alfred/agent/tools/specs/move_media.yaml."""
|
"""Thin tool wrapper — semantics live in alfred/agent/tools/specs/move_media.yaml."""
|
||||||
file_manager = FileManager()
|
try:
|
||||||
use_case = MoveMediaUseCase(file_manager)
|
roots = _load_directory_roots()
|
||||||
return use_case.execute(source, destination).to_dict()
|
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]:
|
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."""
|
"""Thin tool wrapper — semantics live in alfred/agent/tools/specs/move_to_destination.yaml.
|
||||||
parent = str(Path(destination).parent)
|
|
||||||
result = create_folder(parent)
|
Convenience tool that creates the destination's parent directory
|
||||||
if result["status"] != "ok":
|
if missing, then moves the file. Saves the LLM from having to
|
||||||
return result
|
chain ``create_directory`` + ``move_media`` explicitly.
|
||||||
return move(source, destination)
|
"""
|
||||||
|
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,
|
# Self-contained tools — not impacted by the filesystem refactor.
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
def learn(pack: str, category: str, key: str, values: list[str]) -> dict[str, Any]:
|
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]:
|
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."""
|
"""Thin tool wrapper — semantics live in alfred/agent/tools/specs/analyze_release.yaml."""
|
||||||
from alfred.application.release import inspect_release # noqa: PLC0415
|
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]:
|
def read_release_metadata(release_path: str) -> dict[str, Any]:
|
||||||
"""Thin tool wrapper — semantics live in alfred/agent/tools/specs/read_release_metadata.yaml."""
|
"""Thin tool wrapper — semantics live in alfred/agent/tools/specs/read_release_metadata.yaml."""
|
||||||
path = Path(release_path)
|
path = Path(release_path)
|
||||||
|
|||||||
Reference in New Issue
Block a user