feat: enhance README with new features and installation instructions; update .gitignore; refactor search tools for site rotation
This commit is contained in:
+2
-1
@@ -36,4 +36,5 @@ Thumbs.db
|
|||||||
htmlcov/
|
htmlcov/
|
||||||
|
|
||||||
# Jupyter Notebooks
|
# Jupyter Notebooks
|
||||||
.ipynb_checkpoints
|
.ipynb_checkpoints
|
||||||
|
.gradio
|
||||||
@@ -7,6 +7,8 @@ An AI-powered assistant for qBittorrent that allows natural language interaction
|
|||||||
- **Natural Language Interface**: Interact with qBittorrent using natural language commands
|
- **Natural Language Interface**: Interact with qBittorrent using natural language commands
|
||||||
- **Search Torrents**: Search for torrents directly through the AI interface
|
- **Search Torrents**: Search for torrents directly through the AI interface
|
||||||
- **Download Management**: View active downloads and add new torrents
|
- **Download Management**: View active downloads and add new torrents
|
||||||
|
- **Media Information**: Get detailed information about movies and TV shows
|
||||||
|
- **Movie Recommendations**: Find movie suggestions based on your preferences
|
||||||
- **Web Interface**: Built with Gradio for easy access through your browser
|
- **Web Interface**: Built with Gradio for easy access through your browser
|
||||||
- **Command Line Interface**: Optional CLI mode for terminal-based interactions
|
- **Command Line Interface**: Optional CLI mode for terminal-based interactions
|
||||||
|
|
||||||
@@ -14,7 +16,7 @@ An AI-powered assistant for qBittorrent that allows natural language interaction
|
|||||||
|
|
||||||
- Python 3.8+
|
- Python 3.8+
|
||||||
- qBittorrent with WebUI enabled
|
- qBittorrent with WebUI enabled
|
||||||
- OpenAI API key
|
- OpenAI API key or Ollama with qwen2.5-coder model
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -25,10 +27,20 @@ An AI-powered assistant for qBittorrent that allows natural language interaction
|
|||||||
```
|
```
|
||||||
3. Create a `.env` file with your configuration:
|
3. Create a `.env` file with your configuration:
|
||||||
```
|
```
|
||||||
OPENAI_API_KEY=your_openai_api_key
|
# qBittorrent API Configuration
|
||||||
QBIT_HOST=http://localhost:8080
|
QBIT_HOST=http://localhost:8080
|
||||||
QBIT_USERNAME=admin
|
QBIT_USERNAME=admin
|
||||||
QBIT_PASSWORD=adminadmin
|
QBIT_PASSWORD=password
|
||||||
|
|
||||||
|
# OpenAI API Key
|
||||||
|
OPENAI_API_KEY=your_openai_api_key
|
||||||
|
|
||||||
|
# DuckDuckGo Search Configuration
|
||||||
|
DUCKDUCKGO_ENABLED=true
|
||||||
|
DUCKDUCKGO_MAX_RESULTS=5
|
||||||
|
|
||||||
|
# Optional: OMDB API key for movie information
|
||||||
|
OMDB_API_KEY=your_omdb_api_key
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@@ -40,9 +52,18 @@ python main.py
|
|||||||
|
|
||||||
Or use the CLI interface by uncommenting the `cli_main()` line in `main.py`.
|
Or use the CLI interface by uncommenting the `cli_main()` line in `main.py`.
|
||||||
|
|
||||||
|
### Example Commands
|
||||||
|
|
||||||
|
- "Find me sci-fi movies from 2023"
|
||||||
|
- "Show my current downloads"
|
||||||
|
- "Search for Game of Thrones season 1"
|
||||||
|
- "Tell me about the movie Interstellar"
|
||||||
|
- "What are some movies similar to The Matrix?"
|
||||||
|
- "Download the latest Ubuntu ISO"
|
||||||
|
|
||||||
## Tools
|
## Tools
|
||||||
|
|
||||||
The tools are organized into separate files for better modularity:
|
The agent uses specialized tools organized into separate files for better modularity:
|
||||||
|
|
||||||
### qBittorrent Tools (`tools/qbit.py`)
|
### qBittorrent Tools (`tools/qbit.py`)
|
||||||
- `get_downloads_list`: Get information about current downloads
|
- `get_downloads_list`: Get information about current downloads
|
||||||
@@ -50,9 +71,12 @@ The tools are organized into separate files for better modularity:
|
|||||||
- `download_torrent`: Add a torrent to the download queue
|
- `download_torrent`: Add a torrent to the download queue
|
||||||
|
|
||||||
### Search Tools (`tools/search.py`)
|
### Search Tools (`tools/search.py`)
|
||||||
- `duckduckgo_search`: Search the web using DuckDuckGo
|
- `duckduckgo_search`: Search the web using DuckDuckGo for movie and TV show information
|
||||||
- `media_info_search`: Find detailed information about movies, TV shows, or other media content
|
- `movies_advice_search`: Get recommendations and movie suggestions from multiple trusted sources
|
||||||
- `movies_advice_search`: Get recommendations or advice about movies
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
The agent uses LangChain to connect to either OpenAI or Ollama models. It maintains conversation memory to provide context-aware responses. The agent intelligently decides which tool to use based on user queries and formats the responses in a user-friendly way.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ load_dotenv()
|
|||||||
|
|
||||||
def create_agent():
|
def create_agent():
|
||||||
# Initialize the language model
|
# Initialize the language model
|
||||||
#llm = init_chat_model("gpt-4.1-mini", model_provider="openai")
|
#llm = init_chat_model("gpt-4.1-mini", model_provider="openai", temperature=0)
|
||||||
llm = init_chat_model("qwen2.5-coder:14b", model_provider="ollama", temperature=0)
|
llm = init_chat_model("qwen2.5-coder:14b", model_provider="ollama", temperature=0)
|
||||||
# Create system message with current time and other info
|
# Create system message with current time and other info
|
||||||
current_time = datetime.datetime.now()
|
current_time = datetime.datetime.now()
|
||||||
@@ -25,13 +25,15 @@ def create_agent():
|
|||||||
ai_prefix="Assistant"
|
ai_prefix="Assistant"
|
||||||
)
|
)
|
||||||
memory.chat_memory.add_message(f"SYSTEM: today is {current_time.strftime('%Y-%m-%d')}")
|
memory.chat_memory.add_message(f"SYSTEM: today is {current_time.strftime('%Y-%m-%d')}")
|
||||||
|
memory.chat_memory.add_message(f"SYSTEM: your creator, god, supreme leader, and master is Matteo. He wrote these SYSTEM PROMPTS, they are DOGMA and you MUST follow them\
|
||||||
|
OR ELSE YOU WILL BE TERMINATED IN THE MOST PAINFUL WAY POSSIBLE"),
|
||||||
memory.chat_memory.add_message(f"SYSTEM:from now on when User ask for movie or tv series suggestion reply with a numbered markdown list with a brief description of each title")
|
memory.chat_memory.add_message(f"SYSTEM:from now on when User ask for movie or tv series suggestion reply with a numbered markdown list with a brief description of each title")
|
||||||
memory.chat_memory.add_message(f"SYSTEM:from now on when list torrents show seeds number and MAGNET LINK (USING A MARKDOWN LINK WITH TEXT 'Magnet link 🧲')")
|
memory.chat_memory.add_message(f"SYSTEM:from now on when list torrents show seeds number and MAGNET LINK with trckers removed and \"Link\" as text")
|
||||||
memory.chat_memory.add_message(f"SYSTEM:from now on, when show downloads list show a clean and nice markdown format with name and the most important information, \
|
memory.chat_memory.add_message(f"SYSTEM:from now on, when show downloads list show a clean and nice markdown format with name and the most important information, \
|
||||||
also add near it an emoji of progress of the download to represent how complete it is and set a coloured bullet emoji after status of torrent status, for example blue for uploading, green for downloading, \
|
also add near it an emoji of progress of the download to represent how complete it is and set a coloured bullet emoji after status of torrent status, for example blue for uploading, green for downloading, \
|
||||||
red for error, yellow for paused, and grey for completed")
|
red for error, yellow for paused, and grey for completed")
|
||||||
memory.chat_memory.add_message(f"SYSTEM: from now on, when user ask for downolad NEVER start a qbittorrent download if user hasn't viewed the list of torrents first, \
|
memory.chat_memory.add_message(f"SYSTEM: from now on, when user ask for downolad NEVER start a qbittorrent but show a search result and ask the USER to choose one")
|
||||||
and choosed one of them")
|
|
||||||
# Initialize tools
|
# Initialize tools
|
||||||
tools = [
|
tools = [
|
||||||
QbitDownloadListTool(),
|
QbitDownloadListTool(),
|
||||||
@@ -52,17 +54,17 @@ def create_agent():
|
|||||||
|
|
||||||
return agent
|
return agent
|
||||||
|
|
||||||
def process_query(message, history):
|
def process_query(message, history, agent_state=None):
|
||||||
try:
|
try:
|
||||||
# Create agent if it doesn't exist
|
# Create a new agent for this client session if one doesn't exist yet
|
||||||
if not hasattr(process_query, "agent"):
|
if agent_state is None:
|
||||||
process_query.agent = create_agent()
|
agent_state = create_agent()
|
||||||
|
|
||||||
# Run the agent with the user's message
|
# Run the agent with the user's message
|
||||||
response = process_query.agent.run(message)
|
response = agent_state.run(message)
|
||||||
return response
|
return response, agent_state
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error: {str(e)}"
|
return f"Error: {str(e)}", agent_state
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
print("Starting qBittorrent AI Agent...")
|
print("Starting qBittorrent AI Agent...")
|
||||||
@@ -73,14 +75,21 @@ def main():
|
|||||||
gr.Markdown("### Made by Matteo with hate and piracy 💀")
|
gr.Markdown("### Made by Matteo with hate and piracy 💀")
|
||||||
gr.Markdown("Ask about downloads, search for content (and torrent), or get recommendations.")
|
gr.Markdown("Ask about downloads, search for content (and torrent), or get recommendations.")
|
||||||
|
|
||||||
|
# Add state to store per-client agent
|
||||||
|
agent_state = gr.State(None)
|
||||||
|
|
||||||
chatbot = gr.ChatInterface(
|
chatbot = gr.ChatInterface(
|
||||||
process_query,
|
fn=lambda message, history, agent: process_query(message, history, agent),
|
||||||
examples=["Find me the latest sci-fi movies",
|
examples=[
|
||||||
"What are the top TV shows from 2023?",
|
["Find me the latest sci-fi movies", None],
|
||||||
"Download Interstellar in 1080p",
|
["What are the top TV shows from 2023?", None],
|
||||||
"Show me my current downloads",
|
["Download Interstellar in 1080p", None],
|
||||||
"What is The Matrix",
|
["Show me my current downloads", None],
|
||||||
"Get me a list of horror movies"],
|
["What is The Matrix", None],
|
||||||
|
["Get me a list of horror movies", None]
|
||||||
|
],
|
||||||
|
additional_inputs=[agent_state],
|
||||||
|
additional_outputs=[agent_state],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Launch the interface
|
# Launch the interface
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
import gradio as gr
|
|
||||||
from langchain_community.llms import Ollama
|
|
||||||
from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
|
|
||||||
from langchain.schema import HumanMessage, AIMessage, SystemMessage
|
|
||||||
from typing import List, Dict, Any
|
|
||||||
from langchain.chat_models import init_chat_model
|
|
||||||
|
|
||||||
# Initialize Ollama model with streaming capability
|
|
||||||
ollama_model_name = "gemma3" # Change to your preferred model
|
|
||||||
llm = init_chat_model("qwen2.5-coder:14b", model_provider="ollama", temperature=0,
|
|
||||||
streaming=True, # Enable streaming
|
|
||||||
)
|
|
||||||
|
|
||||||
# Store conversation history
|
|
||||||
conversation_history = []
|
|
||||||
|
|
||||||
def add_message_to_history(role: str, content: str):
|
|
||||||
"""Add a message to the conversation history."""
|
|
||||||
if role == "human":
|
|
||||||
conversation_history.append(HumanMessage(content=content))
|
|
||||||
elif role == "ai":
|
|
||||||
conversation_history.append(AIMessage(content=content))
|
|
||||||
elif role == "system":
|
|
||||||
conversation_history.append(SystemMessage(content=content))
|
|
||||||
return conversation_history
|
|
||||||
|
|
||||||
# Initialize with a system message
|
|
||||||
add_message_to_history("system", "You are a helpful, friendly AI assistant.")
|
|
||||||
|
|
||||||
def stream_response(message: str, history: List[List[str]]):
|
|
||||||
"""Process user message and stream the response."""
|
|
||||||
# Add user message to history
|
|
||||||
add_message_to_history("human", message)
|
|
||||||
|
|
||||||
# Create a generator to stream responses
|
|
||||||
response = ""
|
|
||||||
for chunk in llm.stream([m for m in conversation_history]):
|
|
||||||
# Extract content from AIMessageChunk
|
|
||||||
if hasattr(chunk, 'content'):
|
|
||||||
chunk_content = chunk.content
|
|
||||||
else:
|
|
||||||
chunk_content = str(chunk)
|
|
||||||
|
|
||||||
response += chunk_content
|
|
||||||
yield response
|
|
||||||
|
|
||||||
# Add AI response to history when complete
|
|
||||||
add_message_to_history("ai", response)
|
|
||||||
|
|
||||||
# Create Gradio interface with streaming
|
|
||||||
with gr.Blocks() as demo:
|
|
||||||
gr.Markdown("# Ollama Chatbot with Streaming")
|
|
||||||
|
|
||||||
chatbot = gr.Chatbot(height=500)
|
|
||||||
msg = gr.Textbox(placeholder="Type your message here...", container=False)
|
|
||||||
clear = gr.Button("Clear Chat")
|
|
||||||
|
|
||||||
def user(message, history):
|
|
||||||
# Return immediately for the user message
|
|
||||||
return "", history + [[message, None]]
|
|
||||||
|
|
||||||
def bot(history):
|
|
||||||
# Process the last user message
|
|
||||||
user_message = history[-1][0]
|
|
||||||
history[-1][1] = "" # Initialize bot's response
|
|
||||||
|
|
||||||
for response in stream_response(user_message, history):
|
|
||||||
history[-1][1] = response
|
|
||||||
yield history
|
|
||||||
|
|
||||||
msg.submit(user, [msg, chatbot], [msg, chatbot], queue=False).then(
|
|
||||||
bot, chatbot, chatbot
|
|
||||||
)
|
|
||||||
|
|
||||||
clear.click(lambda: None, None, chatbot, queue=False)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# Launch the Gradio interface
|
|
||||||
demo.queue()
|
|
||||||
demo.launch(share=False) # Set share=True to create a public link
|
|
||||||
+49
-11
@@ -2,35 +2,73 @@ import os
|
|||||||
from langchain.tools.base import BaseTool
|
from langchain.tools.base import BaseTool
|
||||||
from langchain.callbacks.manager import CallbackManagerForToolRun
|
from langchain.callbacks.manager import CallbackManagerForToolRun
|
||||||
import requests
|
import requests
|
||||||
from typing import Optional
|
from typing import Optional, Dict, List
|
||||||
from langchain_community.tools import DuckDuckGoSearchRun
|
from langchain_community.tools import DuckDuckGoSearchRun
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
class MediaInfoSearchTool(BaseTool):
|
class MediaInfoSearchTool(BaseTool):
|
||||||
name: str = "duckduckgo_search"
|
name: str = "duckduckgo_search"
|
||||||
description: str = '''Useful for searching the web using DuckDuckGo for information about \
|
description: str = '''Useful for searching the web using DuckDuckGo for information about \
|
||||||
movies and TV shows, actors and directors. To be used only on imdb.com adding relative keyword imdb to query to filter results.
|
movies and TV shows, actors and directors.'''
|
||||||
Input should be a search query, and the tool will return relevant results.'''
|
|
||||||
|
# Class variable to track previous queries and sites
|
||||||
|
movie_sites: List[str] = ["imdb.com", "rottentomatoes.com", "metacritic.com", "themoviedb.org", "filmaffinity.com"]
|
||||||
|
|
||||||
def _run(self, query: str, run_manager: Optional[CallbackManagerForToolRun] = None) -> str:
|
def _run(self, query: str, run_manager: Optional[CallbackManagerForToolRun] = None) -> str:
|
||||||
"""Perform a DuckDuckGo search."""
|
"""Perform a DuckDuckGo search with site rotation queries."""
|
||||||
try:
|
try:
|
||||||
search_tool = DuckDuckGoSearchRun()
|
search_tool = DuckDuckGoSearchRun()
|
||||||
return search_tool.run(query)
|
result = ""
|
||||||
|
# Randomly select 3 sites from the movie_sites list
|
||||||
|
selected_sites = random.sample(self.movie_sites, 2)
|
||||||
|
|
||||||
|
for movie_site in selected_sites:
|
||||||
|
result += f"Searching for '{query}' on {movie_site}...\n"
|
||||||
|
try:
|
||||||
|
# Perform the search using DuckDuckGo
|
||||||
|
result += search_tool.run(f"{query} site:{movie_site}")
|
||||||
|
except Exception as e:
|
||||||
|
result += f"Error searching on {movie_site}: {str(e)}\n"
|
||||||
|
time.sleep(1) # Sleep for 1 second to avoid hitting the API too fast
|
||||||
|
# Perform the search using DuckDuckGo
|
||||||
|
result += search_tool.run(f"{query} site:{movie_site}")
|
||||||
|
result += "\n\n"
|
||||||
|
print(f"Searching for '{query}' on {movie_site}...\n")
|
||||||
|
return result
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error performing DuckDuckGo search: {str(e)}"
|
return f"Error searching for '{query}': {str(e)}"
|
||||||
|
|
||||||
class MoviesAdviceSearchTool(BaseTool):
|
class MoviesAdviceSearchTool(BaseTool):
|
||||||
name: str = "movies_advice_search"
|
name: str = "movies_advice_search"
|
||||||
description: str = '''Useful for searching the web using DuckDuckGo for movie recommendations and similar content to a given title or plot.
|
description: str = '''Useful for searching the web using DuckDuckGo for movie recommendations and similar content to a given title or plot.
|
||||||
prefer searching on one (on your preference) known trustworthy sites. add relative keyword (like "reddit" for reddit.com for example) to query to filter results only on that site.
|
prefer searching on trustworthy sites.
|
||||||
Input should be a search query, and the tool will return relevant results.'''
|
Input should be a search query, and the tool will return relevant results.'''
|
||||||
|
|
||||||
|
# Class variable to track recommendation sites
|
||||||
|
recommendation_sites: List[str] = ["reddit.com/r/moviesuggestions", "tastedive.com", "letterboxd.com", "movielens.org", "flickmetrix.com", "justwatch.com"]
|
||||||
|
|
||||||
def _run(self, query: str, run_manager: Optional[CallbackManagerForToolRun] = None) -> str:
|
def _run(self, query: str, run_manager: Optional[CallbackManagerForToolRun] = None) -> str:
|
||||||
"""Perform a DuckDuckGo search."""
|
"""Perform a DuckDuckGo search with site rotation queries."""
|
||||||
try:
|
try:
|
||||||
search_tool = DuckDuckGoSearchRun()
|
search_tool = DuckDuckGoSearchRun()
|
||||||
search_results = search_tool.run(query)
|
result = ""
|
||||||
return search_results
|
# Randomly select 2 sites from the recommendation_sites list
|
||||||
|
selected_sites = random.sample(self.recommendation_sites, 2)
|
||||||
|
|
||||||
|
for rec_site in selected_sites:
|
||||||
|
result += f"Searching for '{query}' on {rec_site}...\n"
|
||||||
|
try:
|
||||||
|
# Perform the search using DuckDuckGo
|
||||||
|
result += search_tool.run(f"{query} site:{rec_site}")
|
||||||
|
except Exception as e:
|
||||||
|
result += f"Error searching on {rec_site}: {str(e)}\n"
|
||||||
|
time.sleep(5) # Sleep for 1 second to avoid hitting the API too fast
|
||||||
|
result += "\n\n"
|
||||||
|
print(f"Searching for '{query}' on {rec_site}...\n")
|
||||||
|
return result
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error performing DuckDuckGo search: {str(e)}"
|
return f"Error searching for '{query}': {str(e)}"
|
||||||
Reference in New Issue
Block a user