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