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.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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/<original_folder_name>/,
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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/<original_folder_name>/
|
||||
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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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."
|
||||
@@ -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/<pack>_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/<pack>.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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
`<lang>.<ext>`, `<lang>.sdh.<ext>`, `<lang>.forced.<ext>`.
|
||||
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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user