From 2db3198ef22becde496f510035eacf7035ae7363 Mon Sep 17 00:00:00 2001 From: Francwa Date: Thu, 14 May 2026 21:18:43 +0200 Subject: [PATCH] feat(agent): migrate all remaining tools to YAML specs (21/21 covered) Adds YAML specs for the 14 tools that were still description-from-docstring: filesystem: - set_path_for_folder, list_folder, analyze_release, probe_media, move_media, manage_subtitles, create_seed_links, learn api: - find_media_imdb_id, find_torrent, get_torrent_by_index, add_torrent_to_qbittorrent, add_torrent_by_index language: - set_language Each spec follows the established shape (summary / description / when_to_use / when_not_to_use / next_steps / parameters with why_needed + example / returns) and the Python function docstring is slimmed to a one-line pointer. Registry now reports: 21 tools, 21 with YAML spec, 0 doc-only. --- alfred/agent/tools/api.py | 57 +------- alfred/agent/tools/filesystem.py | 135 ++---------------- alfred/agent/tools/language.py | 10 +- .../tools/specs/add_torrent_by_index.yaml | 53 +++++++ .../specs/add_torrent_to_qbittorrent.yaml | 48 +++++++ alfred/agent/tools/specs/analyze_release.yaml | 79 ++++++++++ .../agent/tools/specs/create_seed_links.yaml | 59 ++++++++ .../agent/tools/specs/find_media_imdb_id.yaml | 53 +++++++ alfred/agent/tools/specs/find_torrent.yaml | 49 +++++++ .../tools/specs/get_torrent_by_index.yaml | 48 +++++++ alfred/agent/tools/specs/learn.yaml | 76 ++++++++++ alfred/agent/tools/specs/list_folder.yaml | 60 ++++++++ .../agent/tools/specs/manage_subtitles.yaml | 67 +++++++++ alfred/agent/tools/specs/move_media.yaml | 58 ++++++++ alfred/agent/tools/specs/probe_media.yaml | 53 +++++++ alfred/agent/tools/specs/set_language.yaml | 47 ++++++ .../tools/specs/set_path_for_folder.yaml | 58 ++++++++ 17 files changed, 829 insertions(+), 181 deletions(-) create mode 100644 alfred/agent/tools/specs/add_torrent_by_index.yaml create mode 100644 alfred/agent/tools/specs/add_torrent_to_qbittorrent.yaml create mode 100644 alfred/agent/tools/specs/analyze_release.yaml create mode 100644 alfred/agent/tools/specs/create_seed_links.yaml create mode 100644 alfred/agent/tools/specs/find_media_imdb_id.yaml create mode 100644 alfred/agent/tools/specs/find_torrent.yaml create mode 100644 alfred/agent/tools/specs/get_torrent_by_index.yaml create mode 100644 alfred/agent/tools/specs/learn.yaml create mode 100644 alfred/agent/tools/specs/list_folder.yaml create mode 100644 alfred/agent/tools/specs/manage_subtitles.yaml create mode 100644 alfred/agent/tools/specs/move_media.yaml create mode 100644 alfred/agent/tools/specs/probe_media.yaml create mode 100644 alfred/agent/tools/specs/set_language.yaml create mode 100644 alfred/agent/tools/specs/set_path_for_folder.yaml diff --git a/alfred/agent/tools/api.py b/alfred/agent/tools/api.py index ecca8e5..93ce0b5 100644 --- a/alfred/agent/tools/api.py +++ b/alfred/agent/tools/api.py @@ -14,15 +14,7 @@ logger = logging.getLogger(__name__) def find_media_imdb_id(media_title: str) -> dict[str, Any]: - """ - Find the IMDb ID for a given media title using TMDB API. - - Args: - media_title: Title of the media to search for. - - Returns: - Dict with IMDb ID and media info, or error details. - """ + """Thin tool wrapper — semantics live in alfred/agent/tools/specs/find_media_imdb_id.yaml.""" use_case = SearchMovieUseCase(tmdb_client) response = use_case.execute(media_title) result = response.to_dict() @@ -45,18 +37,7 @@ def find_media_imdb_id(media_title: str) -> dict[str, Any]: def find_torrent(media_title: str) -> dict[str, Any]: - """ - Find torrents for a given media title using Knaben API. - - Results are stored in episodic memory so the user can reference them - by index (e.g., "download the 3rd one"). - - Args: - media_title: Title of the media to search for. - - Returns: - Dict with torrent list or error details. - """ + """Thin tool wrapper — semantics live in alfred/agent/tools/specs/find_torrent.yaml.""" logger.info(f"Searching torrents for: {media_title}") use_case = SearchTorrentsUseCase(knaben_client) @@ -76,17 +57,7 @@ def find_torrent(media_title: str) -> dict[str, Any]: def get_torrent_by_index(index: int) -> dict[str, Any]: - """ - Get a torrent from the last search results by its index. - - Allows the user to reference results by number after a search. - - Args: - index: 1-based index of the torrent in the search results. - - Returns: - Dict with torrent data or error if not found. - """ + """Thin tool wrapper — semantics live in alfred/agent/tools/specs/get_torrent_by_index.yaml.""" logger.info(f"Getting torrent at index: {index}") memory = get_memory() @@ -113,15 +84,7 @@ def get_torrent_by_index(index: int) -> dict[str, Any]: def add_torrent_to_qbittorrent(magnet_link: str) -> dict[str, Any]: - """ - Add a torrent to qBittorrent using a magnet link. - - Args: - magnet_link: Magnet link of the torrent to add. - - Returns: - Dict with success status or error details. - """ + """Thin tool wrapper — semantics live in alfred/agent/tools/specs/add_torrent_to_qbittorrent.yaml.""" logger.info("Adding torrent to qBittorrent") use_case = AddTorrentUseCase(qbittorrent_client) @@ -157,17 +120,7 @@ def add_torrent_to_qbittorrent(magnet_link: str) -> dict[str, Any]: def add_torrent_by_index(index: int) -> dict[str, Any]: - """ - Add a torrent from the last search results by its index. - - Combines get_torrent_by_index and add_torrent_to_qbittorrent. - - Args: - index: 1-based index of the torrent in the search results. - - Returns: - Dict with success status or error details. - """ + """Thin tool wrapper — semantics live in alfred/agent/tools/specs/add_torrent_by_index.yaml.""" logger.info(f"Adding torrent by index: {index}") torrent_result = get_torrent_by_index(index) diff --git a/alfred/agent/tools/filesystem.py b/alfred/agent/tools/filesystem.py index 05badce..c29ac62 100644 --- a/alfred/agent/tools/filesystem.py +++ b/alfred/agent/tools/filesystem.py @@ -17,8 +17,14 @@ from alfred.application.filesystem.detect_media_type import detect_media_type from alfred.application.filesystem.enrich_from_probe import enrich_from_probe 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 @@ -29,19 +35,7 @@ _LEARNED_ROOT = Path(_alfred_pkg.__file__).parent.parent / "data" / "knowledge" def move_media(source: str, destination: str) -> dict[str, Any]: - """ - Move a media file to a destination path. - - Copies the file safely first (with integrity check), then deletes the source. - Use this to organise a downloaded file into the media library. - - Args: - source: Absolute path to the source file. - destination: Absolute path to the destination file (must not already exist). - - Returns: - Dict with status, source, destination, filename, and size — or error details. - """ + """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() @@ -114,72 +108,21 @@ def resolve_series_destination( def create_seed_links( library_file: str, original_download_folder: str ) -> dict[str, Any]: - """ - Prepare a torrent subfolder so qBittorrent can keep seeding after a move. - - Hard-links the video file from the library into torrents//, - then copies all remaining files from the original download folder (subtitles, - .nfo, .jpg, .txt, …) so the torrent data is complete. - - Call this after move_media when the user wants to keep seeding. - - Args: - library_file: Absolute path to the video file now in the library. - original_download_folder: Absolute path to the original download folder - (may still contain subs, nfo, and other release files). - - Returns: - Dict with status, torrent_subfolder, linked_file, copied_files, - copied_count, skipped — or error details. - """ + """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]: - """ - Place subtitle files alongside an organised video file. - - Scans for subtitle files (.srt, .ass, .ssa, .vtt, .sub) next to the source - video, filters them according to the user's SubtitlePreferences (languages, - min size, SDH, forced), and hard-links the passing files next to the - destination video with the correct naming convention: - fr.srt / fr.sdh.srt / fr.forced.srt / en.srt … - - Call this right after move_media or copy_media, passing the same source and - destination paths. If no subtitles are found, returns ok with placed_count=0. - - Args: - source_video: Absolute path to the original video file (in the download folder). - destination_video: Absolute path to the placed video file (in the library). - - Returns: - Dict with status, placed list (source, destination, filename), placed_count, - skipped_count — or error details. - """ + """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]: - """ - Teach Alfred a new token mapping and persist it to the learned knowledge pack. - - Use this when a subtitle file contains an unrecognised token — after confirming - with the user what the token means, call learn() to persist it so Alfred - recognises it in future scans. - - Args: - pack: Knowledge pack name. Currently only "subtitles" is supported. - category: Category within the pack: "languages", "types", or "formats". - key: The entry key — e.g. ISO 639-1 language code ("es"), type id ("sdh"). - values: List of tokens to add — e.g. ["spanish", "espanol", "spa"]. - - Returns: - Dict with status, added_count, and the updated token list. - """ + """Thin tool wrapper — semantics live in alfred/agent/tools/specs/learn.yaml.""" _VALID_PACKS = {"subtitles"} _VALID_CATEGORIES = {"languages", "types", "formats"} @@ -236,16 +179,7 @@ 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]: - """ - Set a folder path in the configuration. - - Args: - folder_name: Name of folder to set (download, tvshow, movie, torrent). - path_value: Absolute path to the folder. - - Returns: - Dict with status or error information. - """ + """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) @@ -253,24 +187,8 @@ def set_path_for_folder(folder_name: str, path_value: str) -> dict[str, Any]: def analyze_release(release_name: str, source_path: str) -> dict[str, Any]: - """ - Fully analyze a release: parse name, detect media type, probe video with ffprobe. - - Combines parse_release + filesystem type detection + ffprobe in a single call. - Use this at the start of any organize workflow to get a complete picture before - deciding how to route the release. - - Args: - release_name: Raw release folder or file name. - source_path: Absolute path to the release folder or file on disk. - - Returns: - Dict with all parsed fields: media_type, title, year, season, episode, - quality, codec, source, group, languages, audio_codec, audio_channels, - bit_depth, hdr_format, edition, site_tag, parse_path, - and probe_used (bool). - """ - from alfred.domain.release.services import parse_release + """Thin tool wrapper — semantics live in alfred/agent/tools/specs/analyze_release.yaml.""" + from alfred.domain.release.services import parse_release # noqa: PLC0415 path = Path(source_path) parsed = parse_release(release_name) @@ -311,21 +229,7 @@ def analyze_release(release_name: str, source_path: str) -> dict[str, Any]: def probe_media(source_path: str) -> dict[str, Any]: - """ - Run ffprobe on a video file and return detailed media information. - - Use this to inspect a specific file for codec, resolution, audio tracks, - languages, and embedded subtitles — independently of release name parsing. - - Args: - source_path: Absolute path to the video file. - - Returns: - Dict with video (codec, resolution, width, height, duration, bitrate), - audio_tracks (list of codec/channels/language), subtitle_tracks - (list of codec/language/forced), audio_languages, is_multi_audio — - or error if ffprobe fails. - """ + """Thin tool wrapper — semantics live in alfred/agent/tools/specs/probe_media.yaml.""" path = Path(source_path) if not path.exists(): return { @@ -379,16 +283,7 @@ def probe_media(source_path: str) -> dict[str, Any]: def list_folder(folder_type: str, path: str = ".") -> dict[str, Any]: - """ - List contents of a configured folder. - - Args: - folder_type: Type of folder to list (download, tvshow, movie, torrent). - path: Relative path within the folder (default: root). - - Returns: - Dict with folder contents or error information. - """ + """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) diff --git a/alfred/agent/tools/language.py b/alfred/agent/tools/language.py index 22b0098..96223c3 100644 --- a/alfred/agent/tools/language.py +++ b/alfred/agent/tools/language.py @@ -9,15 +9,7 @@ logger = logging.getLogger(__name__) def set_language(language: str) -> dict[str, Any]: - """ - Set the conversation language. - - Args: - language: Language code (e.g., 'en', 'fr', 'es', 'de') - - Returns: - Status dictionary - """ + """Thin tool wrapper — semantics live in alfred/agent/tools/specs/set_language.yaml.""" try: memory = get_memory() memory.stm.set_language(language) diff --git a/alfred/agent/tools/specs/add_torrent_by_index.yaml b/alfred/agent/tools/specs/add_torrent_by_index.yaml new file mode 100644 index 0000000..d52440d --- /dev/null +++ b/alfred/agent/tools/specs/add_torrent_by_index.yaml @@ -0,0 +1,53 @@ +name: add_torrent_by_index + +summary: > + Pick a torrent from the last find_torrent results by index and add + it to qBittorrent in one call. + +description: | + Convenience wrapper that combines get_torrent_by_index + + add_torrent_to_qbittorrent. Looks up the torrent at the given + 1-based index, extracts its magnet link, and sends it to + qBittorrent. The result mirrors add_torrent_to_qbittorrent's, with + the chosen torrent's name appended on success. + +when_to_use: | + The default action after find_torrent when the user picks a hit by + number ("download the second one"). One call, two side effects: + episodic memory updated + download started. + +when_not_to_use: | + - When the user only wants to inspect, not download — use + get_torrent_by_index. + - When the magnet comes from outside the search results — use + add_torrent_to_qbittorrent directly. + +next_steps: | + - On status=ok: confirm the download started and end the workflow + if not already ended. + - On status=error (not_found): the index is out of range; show the + available count from episodic memory. + - On status=error (no_magnet): the search result was malformed — + suggest re-running find_torrent. + +parameters: + index: + description: 1-based position of the torrent in the last find_torrent results. + why_needed: | + Identifies which torrent to add. Out-of-range indices return + not_found. + example: 3 + +returns: + ok: + description: Torrent was added to qBittorrent. + fields: + status: "'ok'" + message: Confirmation message. + torrent_name: Name of the torrent that was added. + + error: + description: Failed to add. + fields: + error: Short error code (not_found, no_magnet, ...). + message: Human-readable explanation. diff --git a/alfred/agent/tools/specs/add_torrent_to_qbittorrent.yaml b/alfred/agent/tools/specs/add_torrent_to_qbittorrent.yaml new file mode 100644 index 0000000..bf6b991 --- /dev/null +++ b/alfred/agent/tools/specs/add_torrent_to_qbittorrent.yaml @@ -0,0 +1,48 @@ +name: add_torrent_to_qbittorrent + +summary: > + Send a magnet link to qBittorrent and start the download. + +description: | + Adds a torrent to qBittorrent using its WebUI API. On success, the + download is also recorded in episodic memory as an active_download + so the agent can track its progress later, the STM topic is set to + "downloading", and the current workflow is ended (the user typically + leaves the find-and-download scope at this point). + +when_to_use: | + When the user provides a raw magnet link, or when chaining manually + after get_torrent_by_index. For the common "user picked search hit + N" case, prefer add_torrent_by_index — one call instead of two. + +when_not_to_use: | + - For .torrent files (not supported by this tool — magnet only). + - When qBittorrent is not configured / reachable — the call will + fail and the user has to fix the config first. + +next_steps: | + - On status=ok: the workflow is already ended; confirm to the user + that the download has started. + - On status=error: surface the message; common causes are auth + failure or qBittorrent being unreachable. + +parameters: + magnet_link: + description: Magnet URI of the torrent to add (magnet:?xt=urn:btih:...). + why_needed: | + The actual payload sent to qBittorrent. Must be a full magnet + URI, not a hash alone. + example: "magnet:?xt=urn:btih:abc123..." + +returns: + ok: + description: Torrent accepted by qBittorrent. + fields: + status: "'ok'" + message: Confirmation message. + + error: + description: qBittorrent rejected the request or is unreachable. + fields: + error: Short error code. + message: Human-readable explanation. diff --git a/alfred/agent/tools/specs/analyze_release.yaml b/alfred/agent/tools/specs/analyze_release.yaml new file mode 100644 index 0000000..352e061 --- /dev/null +++ b/alfred/agent/tools/specs/analyze_release.yaml @@ -0,0 +1,79 @@ +name: analyze_release + +summary: > + One-shot analyzer that parses a release name, detects its media type + from the folder layout, and enriches the result with ffprobe data. + +description: | + Combines three steps in a single call so the agent gets a complete + picture before routing: + 1. parse_release(release_name) — extracts title, year, season, + episode, quality, source, codec, group, languages, audio info, + HDR, edition, site tag. + 2. detect_media_type(parsed, path) — uses the on-disk layout + (single file vs. folder, presence of S01 dirs, episode count) + to choose: movie / tv_episode / tv_season / tv_complete / + other / unknown. + 3. ffprobe enrichment — when the media type is recognised, runs + ffprobe on the first video file found and fills in audio + codec/channels, bit depth, HDR format. Sets probe_used=true. + +when_to_use: | + As the very first step of any organize workflow, right after + list_folder, on each release the user wants to handle. The output + drives which resolve_*_destination to call next. + +when_not_to_use: | + - When you only need codec/audio info on a specific video file: + use probe_media (no parsing, no media-type detection). + - For releases the user has already analyzed earlier in the same + workflow — the parse is deterministic, no need to re-run. + +next_steps: | + - media_type == movie → resolve_movie_destination + - media_type == tv_season → resolve_season_destination + - media_type == tv_episode → resolve_episode_destination + - media_type == tv_complete → resolve_series_destination + - media_type in (other, unknown) → ask the user what to do; do not + auto-route. + +parameters: + release_name: + description: Raw release folder or file name as it appears on disk. + why_needed: | + Source of all the parsed tokens (quality, codec, group, ...). + Don't sanitise it — the parser relies on the exact spelling. + example: Breaking.Bad.S01.1080p.BluRay.x265-GROUP + + source_path: + description: Absolute path to the release folder or file on disk. + why_needed: | + Required for layout-based media-type detection and for ffprobe + to find a video file inside the release. + example: /downloads/Breaking.Bad.S01.1080p.BluRay.x265-GROUP + +returns: + ok: + description: Release analyzed. + fields: + status: "'ok'" + media_type: "One of: movie, tv_episode, tv_season, tv_complete, other, unknown." + parse_path: "Which parser branch was taken (debug)." + title: Parsed title. + year: Parsed year (int) or null. + season: Season number (int) or null. + episode: Episode number (int) or null. + episode_end: Range end episode (multi-episode releases) or null. + quality: Resolution token (e.g. 1080p, 2160p). + source: Source token (BluRay, WEB-DL, ...). + codec: Video codec token (x264, x265, ...). + group: Release group name or null. + languages: List of detected language tokens. + audio_codec: Audio codec from ffprobe (when probe_used=true). + audio_channels: Audio channel count from ffprobe. + bit_depth: Bit depth from ffprobe. + hdr_format: HDR format from ffprobe (HDR10, DV, ...) or null. + edition: Edition tag (Extended, Director's Cut, ...) or null. + site_tag: Source-site tag if present. + is_season_pack: True when the folder contains a full season. + probe_used: True when ffprobe successfully enriched the result. diff --git a/alfred/agent/tools/specs/create_seed_links.yaml b/alfred/agent/tools/specs/create_seed_links.yaml new file mode 100644 index 0000000..1ef6ef2 --- /dev/null +++ b/alfred/agent/tools/specs/create_seed_links.yaml @@ -0,0 +1,59 @@ +name: create_seed_links + +summary: > + Recreate the original torrent folder structure with hard-links so + qBittorrent can keep seeding after the library move. + +description: | + Hard-links the library video file back into torrents// + and copies all remaining files from the original download folder + (subtitles, .nfo, .jpg, .txt, …) so the torrent data is complete on + disk. qBittorrent then sees the same content at the location it + expects and can keep seeding without rehashing the whole torrent. + +when_to_use: | + Only when the user has confirmed they want to keep seeding after a + move. Call right after manage_subtitles (or after move_media if there + are no subs). + +when_not_to_use: | + - When the user explicitly answered "no" to "keep seeding?". + - When the download was not from a torrent (e.g. direct download). + - Before the library file is in place — this tool reads it. + +next_steps: | + - After success: optionally call qBittorrent to update the torrent's + save path / force a recheck (not yet covered by a tool). + - End the workflow. + +parameters: + library_file: + description: Absolute path to the video file now in the library. + why_needed: | + The source for the hard-link — same inode means qBittorrent sees + identical bytes at the seeding path. + example: /tv_shows/Oz.1997.1080p.WEBRip.x265-KONTRAST/Season 03/Oz.S03E01.mkv + + original_download_folder: + description: Absolute path to the original download folder. + why_needed: | + Provides the folder name to recreate under torrents/ and the + auxiliary files (subs, nfo, ...) to copy over. + example: /downloads/Oz.S03.1080p.WEBRip.x265-KONTRAST + +returns: + ok: + description: Seeding folder rebuilt. + fields: + status: "'ok'" + torrent_subfolder: Absolute path of the recreated folder under torrents/. + linked_file: Absolute path of the hard-linked video. + copied_files: List of auxiliary files that were copied. + copied_count: Number of auxiliary files copied. + skipped: List of files skipped (already present, unreadable, ...). + + error: + description: Failed to rebuild the seeding folder. + fields: + error: Short error code. + message: Human-readable explanation. diff --git a/alfred/agent/tools/specs/find_media_imdb_id.yaml b/alfred/agent/tools/specs/find_media_imdb_id.yaml new file mode 100644 index 0000000..6d2e207 --- /dev/null +++ b/alfred/agent/tools/specs/find_media_imdb_id.yaml @@ -0,0 +1,53 @@ +name: find_media_imdb_id + +summary: > + Search TMDB for a media title and return its canonical title, year, + IMDb id, and TMDB id. + +description: | + Looks up a title on TMDB and returns the canonical metadata needed by + the resolve_*_destination tools. On success, the result is also + stashed in short-term memory under "last_media_search" so later steps + in the workflow can read it without re-calling TMDB. The STM topic + is set to "searching_media". + +when_to_use: | + Right after analyze_release, before calling resolve_*_destination — + the resolvers need the canonical title + year and refuse to guess + them from the raw release name. + +when_not_to_use: | + - When you already have the IMDb id in STM from an earlier step in + the same workflow. + - For torrent search — use find_torrent instead. + +next_steps: | + - On status=ok: call the appropriate resolve_*_destination with + tmdb_title and tmdb_year from the result. + - On status=error (not_found): show the error and ask the user for + a more precise title. + +parameters: + media_title: + description: Title to search for. Free-form — TMDB does the matching. + why_needed: | + Drives the TMDB query. Pass a sanitized version (no resolution + tokens, no group name) for best results. + example: Breaking Bad + +returns: + ok: + description: Match found. + fields: + status: "'ok'" + title: Canonical title as returned by TMDB. + year: Release year (movies) or first-air year (series). + media_type: "'movie' or 'tv'." + imdb_id: IMDb identifier (ttXXXXXXX) or null. + tmdb_id: TMDB numeric id. + + error: + description: No match or API failure. + fields: + error: Short error code (not_found, api_error, ...). + message: Human-readable explanation. diff --git a/alfred/agent/tools/specs/find_torrent.yaml b/alfred/agent/tools/specs/find_torrent.yaml new file mode 100644 index 0000000..a568939 --- /dev/null +++ b/alfred/agent/tools/specs/find_torrent.yaml @@ -0,0 +1,49 @@ +name: find_torrent + +summary: > + Search Knaben for torrents matching a media title; cache results in + episodic memory. + +description: | + Queries the Knaben aggregator for up to 10 torrents matching the + given title, then stores the result list in episodic memory under + "last_search_results". The user can then refer to a torrent by + 1-based index ("download the 3rd one") via get_torrent_by_index or + add_torrent_by_index. The STM topic is set to "selecting_torrent". + +when_to_use: | + When the user wants to download something new — typically the first + step of a "find + download" sub-task. The agent should usually + pre-filter the title (canonical name + year) before searching for + cleaner results. + +when_not_to_use: | + - For TMDB metadata lookup — use find_media_imdb_id. + - When a search was already performed in the same session and the + user is just picking from the existing list. + +next_steps: | + - Present the indexed results to the user. + - Once chosen: call add_torrent_by_index(N) — that wraps + get_torrent_by_index + add_torrent_to_qbittorrent. + +parameters: + media_title: + description: Title to search for on Knaben. Free-form. + why_needed: | + Drives the search query. Use the canonical title (from + find_media_imdb_id) plus quality preferences for better hits. + example: Inception 2010 1080p + +returns: + ok: + description: Search returned a list of torrents. + fields: + status: "'ok'" + torrents: "List of {name, size, seeders, leechers, magnet, ...}, up to 10." + + error: + description: Search failed. + fields: + error: Short error code. + message: Human-readable explanation. diff --git a/alfred/agent/tools/specs/get_torrent_by_index.yaml b/alfred/agent/tools/specs/get_torrent_by_index.yaml new file mode 100644 index 0000000..8e4f584 --- /dev/null +++ b/alfred/agent/tools/specs/get_torrent_by_index.yaml @@ -0,0 +1,48 @@ +name: get_torrent_by_index + +summary: > + Retrieve a torrent from the last find_torrent search by its 1-based + index. + +description: | + Reads episodic memory's last_search_results and returns the entry at + the given 1-based position. Pure lookup — does not start a download. + Fails when the search results are missing or the index is out of + range. + +when_to_use: | + When the user references a search hit by number ("show me the second + one") but doesn't yet want to download — e.g. inspection, sharing + the magnet, ... + +when_not_to_use: | + - When the user wants to start downloading: use add_torrent_by_index + instead (one call instead of two). + - When no search has been performed yet — the result will be + not_found. + +next_steps: | + - Display the torrent to the user. + - If they then say "add it", call add_torrent_to_qbittorrent with the + magnet, or add_torrent_by_index with the same index. + +parameters: + index: + description: 1-based position in the last find_torrent result list. + why_needed: | + Maps to a specific torrent entry. Out-of-range values return an + error, not a wraparound. + example: 3 + +returns: + ok: + description: Torrent found at that index. + fields: + status: "'ok'" + torrent: "Full torrent dict (name, size, seeders, leechers, magnet, ...)." + + error: + description: No torrent at that index. + fields: + error: Short error code (not_found). + message: Human-readable explanation, e.g. "Search for torrents first." diff --git a/alfred/agent/tools/specs/learn.yaml b/alfred/agent/tools/specs/learn.yaml new file mode 100644 index 0000000..d5cca13 --- /dev/null +++ b/alfred/agent/tools/specs/learn.yaml @@ -0,0 +1,76 @@ +name: learn + +summary: > + Teach Alfred a new token mapping and persist it to the learned + knowledge pack so future scans recognise it. + +description: | + Appends a new token (or list of tokens) to a key inside a knowledge + pack and writes the result to `data/knowledge/_learned.yaml`. + The change is persisted atomically (write-tmp + rename) so a crash + cannot corrupt the file. Currently only the `subtitles` pack is + supported. + +when_to_use: | + When manage_subtitles returns needs_clarification with unresolved + tokens, after confirming with the user what the tokens mean. Call + once per (category, key) — multiple values can be added in a single + call. + +when_not_to_use: | + - Without explicit user confirmation of what the token means. + - For knowledge that belongs in the static pack + (alfred/knowledge/.yaml) — that's editor territory, not + runtime learning. + +next_steps: | + - After success: re-run the workflow step that triggered the + clarification (typically manage_subtitles) so the new mapping is + applied. + +parameters: + pack: + description: Knowledge pack name. Currently only "subtitles" is supported. + why_needed: | + Decides which `*_learned.yaml` file under data/knowledge/ gets + written. The pack name is namespaced to avoid collisions across + domains. + example: subtitles + + category: + description: Category within the pack — "languages", "types", or "formats". + why_needed: | + Different categories use different lookup tables at scan time. + A wrong category silently has no effect. + example: languages + + key: + description: Canonical entry id — ISO 639-1 code, type name, format name. + why_needed: | + The destination bucket for the new tokens. Existing tokens under + this key are kept; only new values are appended. + example: es + + values: + description: List of token spellings to add. + why_needed: | + Release groups use many spellings for the same language/type; + pass them all in one call instead of multiple round-trips. + example: '["spanish", "espanol", "spa"]' + +returns: + ok: + description: Mapping saved. + fields: + status: "'ok'" + pack: Name of the pack that was written to. + category: Category that was updated. + key: Key that was updated. + added_count: Number of values that were actually new (deduplicated). + tokens: Full updated token list for that key. + + error: + description: Save failed. + fields: + error: Short error code (unknown_pack, unknown_category, read_failed, write_failed). + message: Human-readable explanation. diff --git a/alfred/agent/tools/specs/list_folder.yaml b/alfred/agent/tools/specs/list_folder.yaml new file mode 100644 index 0000000..67600a3 --- /dev/null +++ b/alfred/agent/tools/specs/list_folder.yaml @@ -0,0 +1,60 @@ +name: list_folder + +summary: > + List the contents of a configured folder, optionally below a + relative subpath. + +description: | + Reads a folder previously configured via set_path_for_folder and + returns its entries (files + directories). A relative `path` lets you + drill down without re-specifying the absolute root each time. Path + traversal is rejected (no `..`, no absolute paths) so the agent + cannot escape the configured root. + +when_to_use: | + - At the start of an organize workflow to discover what's available + in the download folder. + - To browse a library collection ("what tv shows do I have?"). + - As a sanity check before any move to confirm the target exists. + +when_not_to_use: | + - For folders that are not configured — call set_path_for_folder + first. + - To list arbitrary system paths — this tool is intentionally scoped + to the known roots. + +next_steps: | + - After listing the download folder: typically call analyze_release + on a specific entry. + - After listing a library folder: use the result to disambiguate a + destination during resolve_*_destination. + +parameters: + folder_type: + description: Logical folder key (download, torrent, movie, tv_show, ...). + why_needed: | + Resolves to an absolute root through LTM. Must have been set via + set_path_for_folder beforehand. + example: download + + path: + description: Relative subpath inside the root (default "."). + why_needed: | + Lets you drill into a subfolder without expanding the root. No + ".." or absolute path is allowed. + example: Breaking.Bad.S01.1080p.BluRay.x265-GROUP + +returns: + ok: + description: Listing returned. + fields: + status: "'ok'" + folder_type: The key that was listed. + path: The relative path that was listed. + entries: List of {name, type, size?} for each entry. + + error: + description: Could not list the folder. + fields: + error: Short error code (folder_not_configured, path_not_found, path_traversal, ...). + message: Human-readable explanation. diff --git a/alfred/agent/tools/specs/manage_subtitles.yaml b/alfred/agent/tools/specs/manage_subtitles.yaml new file mode 100644 index 0000000..ba7d710 --- /dev/null +++ b/alfred/agent/tools/specs/manage_subtitles.yaml @@ -0,0 +1,67 @@ +name: manage_subtitles + +summary: > + Detect, filter, and place subtitle tracks next to a video that has just + been organised into the library. + +description: | + Scans the source video's surroundings for subtitle files + (.srt, .ass, .ssa, .vtt, .sub), classifies them by language and type + (standard / SDH / forced), filters by the user's SubtitlePreferences + (languages, min size, keep_sdh, keep_forced), and hard-links the + passing files next to the destination video using the convention + `.`, `.sdh.`, `.forced.`. + If no subtitles are found, returns status=ok with placed_count=0 — not + an error. + +when_to_use: | + Always after a successful move_media / move_to_destination, before + closing the workflow. Pass the original source path (where subs live) + and the new library path (where they should land). + +when_not_to_use: | + - Do not call before the video itself has been moved — the destination + must exist for hard-links to make sense. + - Skip when the user explicitly asks not to handle subtitles. + +next_steps: | + - On status=ok: continue with create_seed_links (if seeding) or end + the workflow. + - On status=needs_clarification: ask the user about the unresolved + tokens, then optionally call learn() to teach the new mapping. + +parameters: + source_video: + description: Absolute path to the original video file (in the download folder). + why_needed: | + Subtitles typically live next to the source, either as siblings or + in a Subs/ subfolder. The scanner walks from this path. + example: /downloads/Oz.S03.1080p.WEBRip.x265-KONTRAST/Oz.S03E01.mkv + + destination_video: + description: Absolute path to the video file in its library location. + why_needed: | + Subtitles are hard-linked next to this file so media players pick + them up automatically. + example: /tv_shows/Oz.1997.1080p.WEBRip.x265-KONTRAST/Season 03/Oz.S03E01.mkv + +returns: + ok: + description: Subtitles scanned (and possibly placed). + fields: + status: "'ok'" + placed: List of {source, destination, filename} for each linked file. + placed_count: Number of subtitle files placed. + skipped_count: Number of subtitle files filtered out. + + needs_clarification: + description: One or more tokens could not be classified. + fields: + unresolved: List of unrecognised tokens with their context. + question: Human-readable question to relay to the user. + + error: + description: Scan or placement failed. + fields: + error: Short error code. + message: Human-readable explanation. diff --git a/alfred/agent/tools/specs/move_media.yaml b/alfred/agent/tools/specs/move_media.yaml new file mode 100644 index 0000000..eddf380 --- /dev/null +++ b/alfred/agent/tools/specs/move_media.yaml @@ -0,0 +1,58 @@ +name: move_media + +summary: > + Safely move a media file with copy + integrity check + delete source. + +description: | + Copies the source file to the destination with an integrity check, + then deletes the source. Slower than move_to_destination (which is a + plain rename) but safer across filesystems where rename is not atomic + or when you want a checksum verification. + +when_to_use: | + Use to move a single file across filesystems or when paranoia about + data integrity is justified — e.g. moving a finished download from a + scratch disk to the main library array. + +when_not_to_use: | + - For same-filesystem moves where speed matters: use move_to_destination + (instant rename on ZFS/ext4 within the same dataset). + - For folder-level moves of complete packs: use move_to_destination — + move_media is a single-file operation. + +next_steps: | + - After a successful move: call manage_subtitles to place any subtitle + tracks, then create_seed_links if the user wants to keep seeding. + - On error: surface the error code (file_not_found, destination_exists, + integrity_check_failed) and ask the user how to proceed. + +parameters: + source: + description: Absolute path to the source video file. + why_needed: | + The file being moved. Typically lives under the downloads folder + after a torrent completes. + example: /downloads/Inception.2010.1080p.BluRay.x265-GROUP/movie.mkv + + destination: + description: Absolute path of the destination file — must not already exist. + why_needed: | + Where the file lands in the library. Comes from a resolve_*_destination + call so the naming convention is respected. + example: /movies/Inception.2010.1080p.BluRay.x265-GROUP/Inception.2010.1080p.BluRay.x265-GROUP.mkv + +returns: + ok: + description: Move succeeded. + fields: + status: "'ok'" + source: Absolute path of the source (now gone). + destination: Absolute path of the destination (now in place). + filename: Basename of the destination file. + size: Size in bytes. + + error: + description: Move failed. + fields: + error: Short error code (file_not_found, destination_exists, integrity_check_failed, ...). + message: Human-readable explanation. diff --git a/alfred/agent/tools/specs/probe_media.yaml b/alfred/agent/tools/specs/probe_media.yaml new file mode 100644 index 0000000..a0c7a74 --- /dev/null +++ b/alfred/agent/tools/specs/probe_media.yaml @@ -0,0 +1,53 @@ +name: probe_media + +summary: > + Run ffprobe on a single video file and return its technical details. + +description: | + Inspects a specific video file with ffprobe and returns codec, + resolution, duration, bitrate, the list of audio tracks (with + language and channel layout), and the list of embedded subtitle + tracks. Independent of any release-name parsing — works on any file + you can point at. + +when_to_use: | + - To inspect a file's audio/subtitle tracks before deciding what to + do (e.g. choose a default audio language). + - To verify a video's resolution / codec when the release name is + unreliable. + - As a building block when analyze_release is overkill. + +when_not_to_use: | + - For full release routing — analyze_release does parsing + media + type detection + probe in one call. + - On non-video files — ffprobe will return probe_failed. + +next_steps: | + - The returned info typically feeds a user-facing decision (e.g. + "this is 7.1 DTS, want to keep it?"); rarely chained directly to + another tool. + +parameters: + source_path: + description: Absolute path to the video file to probe. + why_needed: | + ffprobe needs the exact file (not a folder). For releases use + analyze_release; for a known file path, pass it here. + example: /downloads/Inception.2010.1080p.BluRay.x265-GROUP/movie.mkv + +returns: + ok: + description: Probe succeeded. + fields: + status: "'ok'" + video: "Dict with codec, resolution, width, height, duration_seconds, bitrate_kbps." + audio_tracks: "List of {index, codec, channels, channel_layout, language, is_default}." + subtitle_tracks: "List of {index, codec, language, is_default, is_forced}." + audio_languages: List of language codes present in audio tracks. + is_multi_audio: True when more than one audio language is present. + + error: + description: Probe failed. + fields: + error: Short error code (not_found, probe_failed). + message: Human-readable explanation. diff --git a/alfred/agent/tools/specs/set_language.yaml b/alfred/agent/tools/specs/set_language.yaml new file mode 100644 index 0000000..81c276c --- /dev/null +++ b/alfred/agent/tools/specs/set_language.yaml @@ -0,0 +1,47 @@ +name: set_language + +summary: > + Set the conversation language so all subsequent assistant messages + match it. + +description: | + Persists an ISO 639-1 language code in short-term memory under + conversation.language. Read by the prompt builder and any tool that + needs to localise output. Does not validate the code against an ISO + list — the LLM is trusted to pass a sensible value. + +when_to_use: | + As the very first call when the user writes in a language different + from the current STM language. Doing it before answering avoids a + mid-reply switch. + +when_not_to_use: | + - On every turn — only when the language actually changes. + - To pick a subtitle language — that lives in SubtitlePreferences, + not the conversation language. + +next_steps: | + - After success: continue the user's request in the newly set + language. + +parameters: + language: + description: ISO 639-1 language code (en, fr, es, de, ...). + why_needed: | + Identifies the target language unambiguously across the UI and + any localisation logic. + example: fr + +returns: + ok: + description: Language saved. + fields: + status: "'ok'" + message: Confirmation message. + language: The language code that was saved. + + error: + description: Could not save the language. + fields: + status: "'error'" + error: Short error code or exception message. diff --git a/alfred/agent/tools/specs/set_path_for_folder.yaml b/alfred/agent/tools/specs/set_path_for_folder.yaml new file mode 100644 index 0000000..662d6d6 --- /dev/null +++ b/alfred/agent/tools/specs/set_path_for_folder.yaml @@ -0,0 +1,58 @@ +name: set_path_for_folder + +summary: > + Configure where a known folder lives on disk (download, torrent, or + any library collection). + +description: | + Stores an absolute path in long-term memory under a folder key. Two + classes of folders exist: + - Workspace paths: "download", "torrent" — single-valued each, used + by the organize workflows. + - Library paths: any other key (e.g. "movie", "tv_show", + "documentary") — these are the collections you organise into. + The path must exist and be a directory; otherwise the call fails + without changing memory. + +when_to_use: | + On first run, or when the user moves a folder, or when introducing a + new library collection (e.g. "set the documentaries folder to ..."). + +when_not_to_use: | + - For one-off listings — list_folder works without configuration only + if the folder is already set. + - To rename or delete an existing folder — this only sets paths. + +next_steps: | + - After success: typical follow-ups are list_folder on the same key, + or starting a workflow that needs the path. + +parameters: + folder_name: + description: Logical name of the folder (download, torrent, movie, tv_show, ...). + why_needed: | + The key the agent uses everywhere afterwards. "download" and + "torrent" are reserved for workspace; anything else becomes a + library collection. + example: tv_show + + path_value: + description: Absolute path to the folder on disk. + why_needed: | + Must exist and be readable. Stored verbatim in LTM — relative + paths are rejected. + example: /tank/library/tv_shows + +returns: + ok: + description: Path saved to long-term memory. + fields: + status: "'ok'" + folder_name: The logical name that was set. + path_value: The absolute path that was saved. + + error: + description: Could not set the path. + fields: + error: Short error code (path_not_found, not_a_directory, invalid_path, ...). + message: Human-readable explanation.