feat!: migrate to OpenAI native tool calls and fix circular deps (#fuck-gemini)

- Fix circular dependencies in agent/tools
- Migrate from custom JSON to OpenAI tool calls format
- Add async streaming (step_stream, complete_stream)
- Simplify prompt system and remove token counting
- Add 5 new API endpoints (/health, /v1/models, /api/memory/*)
- Add 3 new tools (get_torrent_by_index, add_torrent_by_index, set_language)
- Fix all 500 tests and add coverage config (80% threshold)
- Add comprehensive docs (README, pytest guide)

BREAKING: LLM interface changed, memory injection via get_memory()
This commit is contained in:
2025-12-06 19:11:05 +01:00
parent 2c8cdd3ab1
commit 9ca31e45e0
92 changed files with 7897 additions and 1786 deletions
+108 -50
View File
@@ -1,123 +1,181 @@
"""Tool registry and definitions."""
from dataclasses import dataclass
from typing import Callable, Any, Dict
from functools import partial
"""Tool registry - defines and registers all available tools for the agent."""
from infrastructure.persistence.memory import Memory
from .tools.filesystem import set_path_for_folder, list_folder
from .tools.api import find_media_imdb_id, find_torrent, add_torrent_to_qbittorrent
import logging
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from .tools import api as api_tools
from .tools import filesystem as fs_tools
logger = logging.getLogger(__name__)
@dataclass
class Tool:
"""Represents a tool that can be used by the agent."""
"""Represents a tool that can be used by the agent.
Attributes:
name: Unique identifier for the tool.
description: Human-readable description for the LLM.
func: The callable that implements the tool.
parameters: JSON Schema describing the tool's parameters.
"""
name: str
description: str
func: Callable[..., Dict[str, Any]]
parameters: Dict[str, Any] # JSON Schema des paramètres
func: Callable[..., dict[str, Any]]
parameters: dict[str, Any]
def make_tools(memory: Memory) -> Dict[str, Tool]:
def make_tools() -> dict[str, Tool]:
"""
Create all available tools with memory bound to them.
Create and register all available tools.
Args:
memory: Memory instance to be used by the tools
Tools access memory via get_memory() context.
Returns:
Dictionary mapping tool names to Tool instances
Dictionary mapping tool names to Tool instances.
"""
# Create partial functions with memory pre-bound for filesystem tools
set_path_func = partial(set_path_for_folder, memory)
list_folder_func = partial(list_folder, memory)
tools = [
# Filesystem tools
Tool(
name="set_path_for_folder",
description="Sets a path in the configuration (download_folder, tvshow_folder, movie_folder, or torrent_folder).",
func=set_path_func,
description=(
"Sets a path in the configuration "
"(download_folder, tvshow_folder, movie_folder, or torrent_folder)."
),
func=fs_tools.set_path_for_folder,
parameters={
"type": "object",
"properties": {
"folder_name": {
"type": "string",
"description": "Name of folder to set",
"enum": ["download", "tvshow", "movie", "torrent"]
"enum": ["download", "tvshow", "movie", "torrent"],
},
"path_value": {
"type": "string",
"description": "Absolute path to the folder (e.g., /home/user/downloads)"
}
"description": "Absolute path to the folder",
},
},
"required": ["folder_name", "path_value"]
}
"required": ["folder_name", "path_value"],
},
),
Tool(
name="list_folder",
description="Lists the contents of a specified folder (download, tvshow, movie, or torrent).",
func=list_folder_func,
description="Lists the contents of a configured folder.",
func=fs_tools.list_folder,
parameters={
"type": "object",
"properties": {
"folder_type": {
"type": "string",
"description": "Type of folder to list: 'download', 'tvshow', 'movie', or 'torrent'",
"enum": ["download", "tvshow", "movie", "torrent"]
"description": "Type of folder to list",
"enum": ["download", "tvshow", "movie", "torrent"],
},
"path": {
"type": "string",
"description": "Relative path within the folder (default: '.' for root)",
"default": "."
}
"description": "Relative path within the folder",
"default": ".",
},
},
"required": ["folder_type"]
}
"required": ["folder_type"],
},
),
# Media search tools
Tool(
name="find_media_imdb_id",
description="Finds the IMDb ID for a given media title using TMDB API.",
func=find_media_imdb_id,
description=(
"Finds the IMDb ID for a given media title using TMDB API. "
"Use this to get information about a movie or TV show."
),
func=api_tools.find_media_imdb_id,
parameters={
"type": "object",
"properties": {
"media_title": {
"type": "string",
"description": "Title of the media to find the IMDb ID for"
"description": "Title of the media to search for",
},
},
"required": ["media_title"]
}
"required": ["media_title"],
},
),
# Torrent tools
Tool(
name="find_torrents",
description="Finds torrents for a given media title using Knaben API.",
func=find_torrent,
description=(
"Finds torrents for a given media title. "
"Results are numbered (1, 2, 3...) so the user can select by number."
),
func=api_tools.find_torrent,
parameters={
"type": "object",
"properties": {
"media_title": {
"type": "string",
"description": "Title of the media to find torrents for"
"description": "Title to search for (include quality if specified)",
},
},
"required": ["media_title"]
}
"required": ["media_title"],
},
),
Tool(
name="add_torrent_by_index",
description=(
"Adds a torrent from the previous search results by its number. "
"Use when the user says 'download the 3rd one' or 'take number 2'."
),
func=api_tools.add_torrent_by_index,
parameters={
"type": "object",
"properties": {
"index": {
"type": "integer",
"description": "Number of the torrent in search results (1, 2, 3...)",
},
},
"required": ["index"],
},
),
Tool(
name="add_torrent_to_qbittorrent",
description="Adds a torrent to qBittorrent client.",
func=add_torrent_to_qbittorrent,
description=(
"Adds a torrent to qBittorrent using a magnet link directly. "
"Use add_torrent_by_index if user selected from search results."
),
func=api_tools.add_torrent_to_qbittorrent,
parameters={
"type": "object",
"properties": {
"magnet_link": {
"type": "string",
"description": "Title of the media to find torrents for"
"description": "The magnet link of the torrent",
},
},
"required": ["magnet_link"]
}
"required": ["magnet_link"],
},
),
Tool(
name="get_torrent_by_index",
description=(
"Gets details of a torrent from search results by its number, "
"without downloading it."
),
func=api_tools.get_torrent_by_index,
parameters={
"type": "object",
"properties": {
"index": {
"type": "integer",
"description": "Number of the torrent in search results (1, 2, 3...)",
},
},
"required": ["index"],
},
),
]
logger.info(f"Registered {len(tools)} tools")
return {t.name: t for t in tools}