forked from enne2/qbit-agent
commit
88c8bb39f8
7 changed files with 442 additions and 0 deletions
@ -0,0 +1,12 @@ |
|||||||
|
# qBittorrent API Configuration |
||||||
|
QBIT_HOST=http://localhost:8080 |
||||||
|
QBIT_USERNAME=admin |
||||||
|
QBIT_PASSWORD=password |
||||||
|
|
||||||
|
# OpenAI API Key (required for the LangChain agent) |
||||||
|
OPENAI_API_KEY=sk-proj-Rs93xxxxxxxxxxxxxxxxxxxxxUnStmeSHj_gUiEfbGzaFeZf0rgdaQzllQmvcMy6o-SywA |
||||||
|
|
||||||
|
# DuckDuckGo Search Configuration |
||||||
|
DUCKDUCKGO_ENABLED=true |
||||||
|
DUCKDUCKGO_MAX_RESULTS=5 |
||||||
|
OMDB_API_KEY=3b6bc268 |
||||||
@ -0,0 +1,39 @@ |
|||||||
|
# Environment variables |
||||||
|
.env |
||||||
|
|
||||||
|
# Python bytecode |
||||||
|
__pycache__/ |
||||||
|
*.py[cod] |
||||||
|
*$py.class |
||||||
|
|
||||||
|
# Distribution / packaging |
||||||
|
dist/ |
||||||
|
build/ |
||||||
|
*.egg-info/ |
||||||
|
|
||||||
|
# Virtual environments |
||||||
|
venv/ |
||||||
|
env/ |
||||||
|
ENV/ |
||||||
|
.venv/ |
||||||
|
|
||||||
|
# IDE files |
||||||
|
.idea/ |
||||||
|
.vscode/ |
||||||
|
*.swp |
||||||
|
*.swo |
||||||
|
|
||||||
|
# OS files |
||||||
|
.DS_Store |
||||||
|
Thumbs.db |
||||||
|
|
||||||
|
# Logs |
||||||
|
*.log |
||||||
|
|
||||||
|
# Testing |
||||||
|
.pytest_cache/ |
||||||
|
.coverage |
||||||
|
htmlcov/ |
||||||
|
|
||||||
|
# Jupyter Notebooks |
||||||
|
.ipynb_checkpoints |
||||||
@ -0,0 +1,21 @@ |
|||||||
|
MIT License |
||||||
|
|
||||||
|
Copyright (c) 2023 qBittorrent AI Agent |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
of this software and associated documentation files (the "Software"), to deal |
||||||
|
in the Software without restriction, including without limitation the rights |
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
copies of the Software, and to permit persons to whom the Software is |
||||||
|
furnished to do so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all |
||||||
|
copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
SOFTWARE. |
||||||
@ -0,0 +1,53 @@ |
|||||||
|
# qBittorrent AI Agent |
||||||
|
|
||||||
|
An AI-powered assistant for qBittorrent that allows natural language interaction with your torrent client. |
||||||
|
|
||||||
|
## Features |
||||||
|
|
||||||
|
- **Natural Language Interface**: Interact with qBittorrent using natural language commands |
||||||
|
- **Search Torrents**: Search for torrents directly through the AI interface |
||||||
|
- **Download Management**: View active downloads and add new torrents |
||||||
|
- **Web Interface**: Built with Gradio for easy access through your browser |
||||||
|
- **Command Line Interface**: Optional CLI mode for terminal-based interactions |
||||||
|
|
||||||
|
## Requirements |
||||||
|
|
||||||
|
- Python 3.8+ |
||||||
|
- qBittorrent with WebUI enabled |
||||||
|
- OpenAI API key |
||||||
|
|
||||||
|
## Installation |
||||||
|
|
||||||
|
1. Clone this repository |
||||||
|
2. Install dependencies: |
||||||
|
``` |
||||||
|
pip install -r requirements.txt |
||||||
|
``` |
||||||
|
3. Create a `.env` file with your configuration: |
||||||
|
``` |
||||||
|
OPENAI_API_KEY=your_openai_api_key |
||||||
|
QBIT_HOST=http://localhost:8080 |
||||||
|
QBIT_USERNAME=admin |
||||||
|
QBIT_PASSWORD=adminadmin |
||||||
|
``` |
||||||
|
|
||||||
|
## Usage |
||||||
|
|
||||||
|
Run the web interface: |
||||||
|
``` |
||||||
|
python main.py |
||||||
|
``` |
||||||
|
|
||||||
|
Or use the CLI interface by uncommenting the `cli_main()` line in `main.py`. |
||||||
|
|
||||||
|
## Tools |
||||||
|
|
||||||
|
The agent includes several tools: |
||||||
|
- `get_downloads_list`: Get information about current downloads |
||||||
|
- `qbittorrent_search`: Search for torrents using qBittorrent's search functionality |
||||||
|
- `download_torrent`: Add a torrent to the download queue |
||||||
|
- `ForcedDuckDuckGoSearch`: Search for information about media content |
||||||
|
|
||||||
|
## License |
||||||
|
|
||||||
|
MIT |
||||||
@ -0,0 +1,101 @@ |
|||||||
|
import os |
||||||
|
from langchain.agents import Tool, initialize_agent, AgentType |
||||||
|
from dotenv import load_dotenv |
||||||
|
from tools import DownloadListTool, QBitSearchTool, DownloadTorrentTool |
||||||
|
from langchain_community.tools import DuckDuckGoSearchRun |
||||||
|
from langchain.memory import ConversationBufferMemory |
||||||
|
from langchain.chat_models import init_chat_model |
||||||
|
import gradio as gr |
||||||
|
# Load environment variables |
||||||
|
load_dotenv() |
||||||
|
|
||||||
|
def create_agent(): |
||||||
|
# Initialize the language model |
||||||
|
|
||||||
|
|
||||||
|
llm = init_chat_model("gpt-4o-mini", model_provider="openai") |
||||||
|
|
||||||
|
# Initialize memory |
||||||
|
memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True) |
||||||
|
|
||||||
|
# Initialize search tool |
||||||
|
search_tool = DuckDuckGoSearchRun() |
||||||
|
|
||||||
|
# Function to force DuckDuckGo for specific search types |
||||||
|
def forced_duckduckgo_search(query: str) -> str: |
||||||
|
"""Use DuckDuckGo to search for specific information.""" |
||||||
|
return search_tool.run(query) |
||||||
|
|
||||||
|
# Initialize tools |
||||||
|
tools = [ |
||||||
|
DownloadListTool(), |
||||||
|
QBitSearchTool(), |
||||||
|
DownloadTorrentTool(), |
||||||
|
Tool( |
||||||
|
name="ForcedDuckDuckGoSearch", |
||||||
|
func=forced_duckduckgo_search, |
||||||
|
description="Use this tool when you need to find specific information about movies, TV shows. Input should be a search query including the keyword 'imdb'.", |
||||||
|
) |
||||||
|
] |
||||||
|
|
||||||
|
# Initialize the agent with memory |
||||||
|
agent = initialize_agent( |
||||||
|
tools, |
||||||
|
llm, |
||||||
|
agent=AgentType.CHAT_CONVERSATIONAL_REACT_DESCRIPTION, |
||||||
|
verbose=True, |
||||||
|
memory=memory |
||||||
|
) |
||||||
|
|
||||||
|
return agent |
||||||
|
|
||||||
|
def process_query(message, history): |
||||||
|
try: |
||||||
|
# Create agent if it doesn't exist |
||||||
|
if not hasattr(process_query, "agent"): |
||||||
|
process_query.agent = create_agent() |
||||||
|
|
||||||
|
# Run the agent with the user's message |
||||||
|
response = process_query.agent.run(message) |
||||||
|
return response |
||||||
|
except Exception as e: |
||||||
|
return f"Error: {str(e)}" |
||||||
|
|
||||||
|
def main(): |
||||||
|
print("Starting qBittorrent AI Agent...") |
||||||
|
|
||||||
|
# Create Gradio interface |
||||||
|
with gr.Blocks(title="qBittorrent AI Agent") as interface: |
||||||
|
gr.Markdown("# qBittorrent AI Agent") |
||||||
|
gr.Markdown("Ask questions about downloads, search for content, or get recommendations.") |
||||||
|
|
||||||
|
chatbot = gr.ChatInterface( |
||||||
|
process_query, |
||||||
|
examples=["Find me the latest sci-fi movies", |
||||||
|
"What are the top TV shows from 2023?", |
||||||
|
"Download Interstellar in 1080p"], |
||||||
|
title="qBittorrent Assistant" |
||||||
|
) |
||||||
|
|
||||||
|
# Launch the interface |
||||||
|
interface.launch(share=False) |
||||||
|
|
||||||
|
def cli_main(): |
||||||
|
print("Starting qBittorrent AI Agent in CLI mode...") |
||||||
|
agent = create_agent() |
||||||
|
|
||||||
|
while True: |
||||||
|
user_input = input("\nEnter your question (or 'quit' to exit): ") |
||||||
|
if user_input.lower() in ['quit', 'exit']: |
||||||
|
break |
||||||
|
try: |
||||||
|
response = agent.run(user_input) |
||||||
|
print(response) |
||||||
|
except Exception as e: |
||||||
|
print(f"Error: {str(e)}") |
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
# Use main() for Gradio interface or cli_main() for command-line interface |
||||||
|
main() |
||||||
|
# Uncomment the line below to use CLI instead |
||||||
|
# cli_main() |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
langchain>=0.0.267 |
||||||
|
openai>=0.27.8 |
||||||
|
requests>=2.28.2 |
||||||
|
python-dotenv>=1.0.0 |
||||||
|
gradio>=3.0.0 |
||||||
|
langchain_community>=0.0.1 |
||||||
@ -0,0 +1,210 @@ |
|||||||
|
import os |
||||||
|
from langchain.tools.base import BaseTool |
||||||
|
from langchain.callbacks.manager import CallbackManagerForToolRun |
||||||
|
import requests |
||||||
|
from typing import Optional |
||||||
|
|
||||||
|
class DownloadListTool(BaseTool): |
||||||
|
name: str = "get_downloads_list" |
||||||
|
description: str = '''Useful for getting a list of current downloads from the qBittorrent API and |
||||||
|
information about them. The response will include the name, size, and status of each download. |
||||||
|
''' |
||||||
|
|
||||||
|
def _run(self, query: str = "", run_manager: Optional[CallbackManagerForToolRun] = None) -> str: |
||||||
|
"""Get the list of downloads from qBittorrent API.""" |
||||||
|
try: |
||||||
|
# Configuration for qBittorrent API |
||||||
|
QBIT_HOST = os.environ.get("QBIT_HOST", "http://localhost:8080") |
||||||
|
QBIT_USERNAME = os.environ.get("QBIT_USERNAME", "admin") |
||||||
|
QBIT_PASSWORD = os.environ.get("QBIT_PASSWORD", "adminadmin") |
||||||
|
|
||||||
|
# First login to get the auth cookie |
||||||
|
login_url = f"{QBIT_HOST}/api/v2/auth/login" |
||||||
|
login_data = {"username": QBIT_USERNAME, "password": QBIT_PASSWORD} |
||||||
|
|
||||||
|
session = requests.Session() |
||||||
|
login_response = session.post(login_url, data=login_data) |
||||||
|
|
||||||
|
if login_response.status_code != 200: |
||||||
|
return f"Failed to login to qBittorrent API: {login_response.text}" |
||||||
|
|
||||||
|
# Get the list of torrents |
||||||
|
torrents_url = f"{QBIT_HOST}/api/v2/torrents/info" |
||||||
|
response = session.get(torrents_url) |
||||||
|
|
||||||
|
if response.status_code != 200: |
||||||
|
return f"Failed to fetch downloads: {response.text}" |
||||||
|
|
||||||
|
torrents = response.json() |
||||||
|
|
||||||
|
# Format the response |
||||||
|
if not torrents: |
||||||
|
return "No active downloads found." |
||||||
|
|
||||||
|
result = "Current downloads:\n" |
||||||
|
for i, torrent in enumerate(torrents, 1): |
||||||
|
result += f"{i}:" |
||||||
|
for key, value in torrent.items(): |
||||||
|
if key in ["name", "progress", "size", "state", "eta"]: |
||||||
|
result += f" {key}: {value}," |
||||||
|
result += "\n" |
||||||
|
result += "\n" |
||||||
|
result += "Total downloads: " + str(len(torrents)) |
||||||
|
|
||||||
|
return result |
||||||
|
|
||||||
|
except Exception as e: |
||||||
|
return f"Error getting downloads list: {str(e)}" |
||||||
|
|
||||||
|
class QBitSearchTool(BaseTool): |
||||||
|
name: str = "qbittorrent_search" |
||||||
|
description: str = '''Useful for searching torrents using qBittorrent's search functionality. |
||||||
|
Input should be a search query for content the user wants to find. |
||||||
|
The tool will return a list of matching torrents with their details including magnet links. |
||||||
|
''' |
||||||
|
|
||||||
|
def _run(self, query: str, run_manager: Optional[CallbackManagerForToolRun] = None) -> str: |
||||||
|
"""Search for torrents using qBittorrent's search functionality.""" |
||||||
|
try: |
||||||
|
# Configuration for qBittorrent API |
||||||
|
QBIT_HOST = os.environ.get("QBIT_HOST", "http://localhost:8080") |
||||||
|
QBIT_USERNAME = os.environ.get("QBIT_USERNAME", "admin") |
||||||
|
QBIT_PASSWORD = os.environ.get("QBIT_PASSWORD", "adminadmin") |
||||||
|
|
||||||
|
# First login to get the auth cookie |
||||||
|
login_url = f"{QBIT_HOST}/api/v2/auth/login" |
||||||
|
login_data = {"username": QBIT_USERNAME, "password": QBIT_PASSWORD} |
||||||
|
|
||||||
|
session = requests.Session() |
||||||
|
login_response = session.post(login_url, data=login_data) |
||||||
|
|
||||||
|
if login_response.status_code != 200: |
||||||
|
return f"Failed to login to qBittorrent API: {login_response.text}" |
||||||
|
|
||||||
|
# Start a search |
||||||
|
start_search_url = f"{QBIT_HOST}/api/v2/search/start" |
||||||
|
search_data = {"pattern": query, "plugins": "all", "category": "all"} |
||||||
|
|
||||||
|
search_response = session.post(start_search_url, data=search_data) |
||||||
|
|
||||||
|
if search_response.status_code != 200: |
||||||
|
return f"Failed to start search: {search_response.text}" |
||||||
|
|
||||||
|
search_id = search_response.json().get("id") |
||||||
|
|
||||||
|
if not search_id: |
||||||
|
return "Failed to get search ID" |
||||||
|
|
||||||
|
# Wait for results (simple implementation, can be improved) |
||||||
|
import time |
||||||
|
max_wait = 10 # seconds |
||||||
|
wait_time = 0 |
||||||
|
step = 1 |
||||||
|
|
||||||
|
while wait_time < max_wait: |
||||||
|
time.sleep(step) |
||||||
|
wait_time += step |
||||||
|
|
||||||
|
# Get search status |
||||||
|
status_url = f"{QBIT_HOST}/api/v2/search/status" |
||||||
|
status_params = {"id": search_id} |
||||||
|
status_response = session.get(status_url, params=status_params) |
||||||
|
|
||||||
|
if status_response.status_code != 200: |
||||||
|
return f"Failed to get search status: {status_response.text}" |
||||||
|
|
||||||
|
status_data = status_response.json() |
||||||
|
if status_data[0].get("status") == "Stopped": |
||||||
|
break |
||||||
|
|
||||||
|
# Get search results |
||||||
|
results_url = f"{QBIT_HOST}/api/v2/search/results" |
||||||
|
results_params = {"id": search_id, "limit": 10} # Limiting to top 10 results |
||||||
|
|
||||||
|
results_response = session.get(results_url, params=results_params) |
||||||
|
|
||||||
|
if results_response.status_code != 200: |
||||||
|
return f"Failed to get search results: {results_response.text}" |
||||||
|
|
||||||
|
results_data = results_response.json() |
||||||
|
results = results_data.get("results", []) |
||||||
|
|
||||||
|
# Stop the search |
||||||
|
stop_url = f"{QBIT_HOST}/api/v2/search/stop" |
||||||
|
stop_params = {"id": search_id} |
||||||
|
session.post(stop_url, params=stop_params) |
||||||
|
|
||||||
|
# Format the response |
||||||
|
if not results: |
||||||
|
return f"No results found for '{query}'." |
||||||
|
|
||||||
|
response = f"Search results for '{query}':\n\n" |
||||||
|
|
||||||
|
for i, result in enumerate(results, 1): |
||||||
|
name = result.get("fileName", "Unknown") |
||||||
|
size = result.get("fileSize", "Unknown") |
||||||
|
seeds = result.get("seeders", 0) |
||||||
|
leech = result.get("leechers", 0) |
||||||
|
magnet = result.get("fileUrl", "") |
||||||
|
|
||||||
|
# Convert size to human-readable format |
||||||
|
if isinstance(size, (int, float)): |
||||||
|
units = ["B", "KB", "MB", "GB", "TB"] |
||||||
|
size_index = 0 |
||||||
|
while size >= 1024 and size_index < len(units) - 1: |
||||||
|
size /= 1024 |
||||||
|
size_index += 1 |
||||||
|
size = f"{size:.2f} {units[size_index]}" |
||||||
|
|
||||||
|
response += f"{i}. {name}\n" |
||||||
|
response += f" Size: {size}, Seeds: {seeds}, Leechers: {leech}\n" |
||||||
|
if magnet: |
||||||
|
response += f" Magnet: {magnet}\n" |
||||||
|
|
||||||
|
return response |
||||||
|
|
||||||
|
except Exception as e: |
||||||
|
return f"Error searching torrents: {str(e)}" |
||||||
|
|
||||||
|
class DownloadTorrentTool(BaseTool): |
||||||
|
name: str = "download_torrent" |
||||||
|
description: str = '''Useful for starting a new torrent download in qBittorrent. |
||||||
|
Input should be a magnet link or a torrent URL that the user wants to download. |
||||||
|
The tool will add the torrent to qBittorrent's download queue and return status. |
||||||
|
''' |
||||||
|
|
||||||
|
def _run(self, torrent_url: str, run_manager: Optional[CallbackManagerForToolRun] = None) -> str: |
||||||
|
"""Start downloading a torrent by adding it to qBittorrent.""" |
||||||
|
try: |
||||||
|
# Check if the input is a valid torrent URL or magnet link |
||||||
|
if not (torrent_url.startswith("http") or torrent_url.startswith("magnet:")): |
||||||
|
return "Error: Please provide a valid torrent URL or magnet link." |
||||||
|
|
||||||
|
# Configuration for qBittorrent API |
||||||
|
QBIT_HOST = os.environ.get("QBIT_HOST", "http://localhost:8080") |
||||||
|
QBIT_USERNAME = os.environ.get("QBIT_USERNAME", "admin") |
||||||
|
QBIT_PASSWORD = os.environ.get("QBIT_PASSWORD", "adminadmin") |
||||||
|
|
||||||
|
# First login to get the auth cookie |
||||||
|
login_url = f"{QBIT_HOST}/api/v2/auth/login" |
||||||
|
login_data = {"username": QBIT_USERNAME, "password": QBIT_PASSWORD} |
||||||
|
|
||||||
|
session = requests.Session() |
||||||
|
login_response = session.post(login_url, data=login_data) |
||||||
|
|
||||||
|
if login_response.status_code != 200: |
||||||
|
return f"Failed to login to qBittorrent API: {login_response.text}" |
||||||
|
|
||||||
|
# Add torrent to download queue |
||||||
|
add_url = f"{QBIT_HOST}/api/v2/torrents/add" |
||||||
|
add_data = {"urls": torrent_url} |
||||||
|
|
||||||
|
add_response = session.post(add_url, data=add_data) |
||||||
|
|
||||||
|
if add_response.status_code != 200: |
||||||
|
return f"Failed to add torrent: {add_response.text}" |
||||||
|
|
||||||
|
return f"Torrent has been added to download queue successfully. Check downloads list for status." |
||||||
|
|
||||||
|
except Exception as e: |
||||||
|
return f"Error adding torrent: {str(e)}" |
||||||
Loading…
Reference in new issue