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:
@@ -1,11 +1,12 @@
|
||||
"""qBittorrent API client."""
|
||||
|
||||
from .client import QBittorrentClient
|
||||
from .dto import TorrentInfo
|
||||
from .exceptions import (
|
||||
QBittorrentError,
|
||||
QBittorrentConfigurationError,
|
||||
QBittorrentAPIError,
|
||||
QBittorrentAuthError,
|
||||
QBittorrentConfigurationError,
|
||||
QBittorrentError,
|
||||
)
|
||||
|
||||
# Global qBittorrent client instance (singleton)
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
"""qBittorrent Web API client."""
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
from requests.exceptions import RequestException, Timeout, HTTPError
|
||||
from requests.exceptions import HTTPError, RequestException, Timeout
|
||||
|
||||
from agent.config import Settings, settings
|
||||
|
||||
from .dto import TorrentInfo
|
||||
from .exceptions import QBittorrentError, QBittorrentAPIError, QBittorrentAuthError
|
||||
from .exceptions import QBittorrentAPIError, QBittorrentAuthError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -27,11 +30,11 @@ class QBittorrentClient:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: Optional[str] = None,
|
||||
username: Optional[str] = None,
|
||||
password: Optional[str] = None,
|
||||
timeout: Optional[int] = None,
|
||||
config: Optional[Settings] = None
|
||||
host: str | None = None,
|
||||
username: str | None = None,
|
||||
password: str | None = None,
|
||||
timeout: int | None = None,
|
||||
config: Settings | None = None,
|
||||
):
|
||||
"""
|
||||
Initialize qBittorrent client.
|
||||
@@ -59,8 +62,8 @@ class QBittorrentClient:
|
||||
self,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
data: Optional[Dict[str, Any]] = None,
|
||||
files: Optional[Dict[str, Any]] = None
|
||||
data: dict[str, Any] | None = None,
|
||||
files: dict[str, Any] | None = None,
|
||||
) -> Any:
|
||||
"""
|
||||
Make a request to qBittorrent API.
|
||||
@@ -85,7 +88,9 @@ class QBittorrentClient:
|
||||
if method.upper() == "GET":
|
||||
response = self.session.get(url, params=data, timeout=self.timeout)
|
||||
elif method.upper() == "POST":
|
||||
response = self.session.post(url, data=data, files=files, timeout=self.timeout)
|
||||
response = self.session.post(
|
||||
url, data=data, files=files, timeout=self.timeout
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unsupported HTTP method: {method}")
|
||||
|
||||
@@ -99,14 +104,18 @@ class QBittorrentClient:
|
||||
|
||||
except Timeout as e:
|
||||
logger.error(f"qBittorrent API timeout: {e}")
|
||||
raise QBittorrentAPIError(f"Request timeout after {self.timeout} seconds") from e
|
||||
raise QBittorrentAPIError(
|
||||
f"Request timeout after {self.timeout} seconds"
|
||||
) from e
|
||||
|
||||
except HTTPError as e:
|
||||
logger.error(f"qBittorrent API HTTP error: {e}")
|
||||
if e.response is not None:
|
||||
status_code = e.response.status_code
|
||||
if status_code == 403:
|
||||
raise QBittorrentAuthError("Authentication required or forbidden") from e
|
||||
raise QBittorrentAuthError(
|
||||
"Authentication required or forbidden"
|
||||
) from e
|
||||
else:
|
||||
raise QBittorrentAPIError(f"HTTP {status_code}: {e}") from e
|
||||
raise QBittorrentAPIError(f"HTTP error: {e}") from e
|
||||
@@ -126,10 +135,7 @@ class QBittorrentClient:
|
||||
QBittorrentAuthError: If authentication fails
|
||||
"""
|
||||
try:
|
||||
data = {
|
||||
"username": self.username,
|
||||
"password": self.password
|
||||
}
|
||||
data = {"username": self.username, "password": self.password}
|
||||
|
||||
response = self._make_request("POST", "/api/v2/auth/login", data=data)
|
||||
|
||||
@@ -161,10 +167,8 @@ class QBittorrentClient:
|
||||
return False
|
||||
|
||||
def get_torrents(
|
||||
self,
|
||||
filter: Optional[str] = None,
|
||||
category: Optional[str] = None
|
||||
) -> List[TorrentInfo]:
|
||||
self, filter: str | None = None, category: str | None = None
|
||||
) -> list[TorrentInfo]:
|
||||
"""
|
||||
Get list of torrents.
|
||||
|
||||
@@ -212,9 +216,9 @@ class QBittorrentClient:
|
||||
def add_torrent(
|
||||
self,
|
||||
magnet: str,
|
||||
category: Optional[str] = None,
|
||||
save_path: Optional[str] = None,
|
||||
paused: bool = False
|
||||
category: str | None = None,
|
||||
save_path: str | None = None,
|
||||
paused: bool = False,
|
||||
) -> bool:
|
||||
"""
|
||||
Add a torrent via magnet link.
|
||||
@@ -234,10 +238,7 @@ class QBittorrentClient:
|
||||
if not self._authenticated:
|
||||
self.login()
|
||||
|
||||
data = {
|
||||
"urls": magnet,
|
||||
"paused": "true" if paused else "false"
|
||||
}
|
||||
data = {"urls": magnet, "paused": "true" if paused else "false"}
|
||||
|
||||
if category:
|
||||
data["category"] = category
|
||||
@@ -248,7 +249,7 @@ class QBittorrentClient:
|
||||
response = self._make_request("POST", "/api/v2/torrents/add", data=data)
|
||||
|
||||
if response == "Ok.":
|
||||
logger.info(f"Successfully added torrent")
|
||||
logger.info("Successfully added torrent")
|
||||
return True
|
||||
else:
|
||||
logger.warning(f"Unexpected response: {response}")
|
||||
@@ -258,11 +259,7 @@ class QBittorrentClient:
|
||||
logger.error(f"Failed to add torrent: {e}")
|
||||
raise
|
||||
|
||||
def delete_torrent(
|
||||
self,
|
||||
torrent_hash: str,
|
||||
delete_files: bool = False
|
||||
) -> bool:
|
||||
def delete_torrent(self, torrent_hash: str, delete_files: bool = False) -> bool:
|
||||
"""
|
||||
Delete a torrent.
|
||||
|
||||
@@ -281,7 +278,7 @@ class QBittorrentClient:
|
||||
|
||||
data = {
|
||||
"hashes": torrent_hash,
|
||||
"deleteFiles": "true" if delete_files else "false"
|
||||
"deleteFiles": "true" if delete_files else "false",
|
||||
}
|
||||
|
||||
try:
|
||||
@@ -339,7 +336,7 @@ class QBittorrentClient:
|
||||
logger.error(f"Failed to resume torrent: {e}")
|
||||
raise
|
||||
|
||||
def get_torrent_properties(self, torrent_hash: str) -> Dict[str, Any]:
|
||||
def get_torrent_properties(self, torrent_hash: str) -> dict[str, Any]:
|
||||
"""
|
||||
Get detailed properties of a torrent.
|
||||
|
||||
@@ -361,7 +358,7 @@ class QBittorrentClient:
|
||||
logger.error(f"Failed to get torrent properties: {e}")
|
||||
raise
|
||||
|
||||
def _parse_torrent(self, torrent: Dict[str, Any]) -> TorrentInfo:
|
||||
def _parse_torrent(self, torrent: dict[str, Any]) -> TorrentInfo:
|
||||
"""
|
||||
Parse a torrent dict into a TorrentInfo object.
|
||||
|
||||
@@ -384,5 +381,5 @@ class QBittorrentClient:
|
||||
num_leechs=torrent.get("num_leechs", 0),
|
||||
ratio=torrent.get("ratio", 0.0),
|
||||
category=torrent.get("category"),
|
||||
save_path=torrent.get("save_path")
|
||||
save_path=torrent.get("save_path"),
|
||||
)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
"""qBittorrent Data Transfer Objects."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class TorrentInfo:
|
||||
"""Represents a torrent in qBittorrent."""
|
||||
|
||||
hash: str
|
||||
name: str
|
||||
size: int
|
||||
@@ -17,5 +18,5 @@ class TorrentInfo:
|
||||
num_seeds: int
|
||||
num_leechs: int
|
||||
ratio: float
|
||||
category: Optional[str] = None
|
||||
save_path: Optional[str] = None
|
||||
category: str | None = None
|
||||
save_path: str | None = None
|
||||
|
||||
@@ -3,19 +3,23 @@
|
||||
|
||||
class QBittorrentError(Exception):
|
||||
"""Base exception for qBittorrent-related errors."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class QBittorrentConfigurationError(QBittorrentError):
|
||||
"""Raised when qBittorrent is not properly configured."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class QBittorrentAPIError(QBittorrentError):
|
||||
"""Raised when qBittorrent API returns an error."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class QBittorrentAuthError(QBittorrentError):
|
||||
"""Raised when authentication fails."""
|
||||
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user