first commit
This commit is contained in:
@@ -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
|
||||
+39
@@ -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
|
||||
+21
@@ -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)}"
|
||||
Reference in New Issue
Block a user