diff --git a/rag_tutorials/gemini_agentic_rag/README.md b/rag_tutorials/gemini_agentic_rag/README.md new file mode 100644 index 0000000..0042f11 --- /dev/null +++ b/rag_tutorials/gemini_agentic_rag/README.md @@ -0,0 +1,90 @@ +# Agentic RAG with Gemini Thinking ⌘G + +A RAG Agentic system built with the new Gemini 2.0 Flash Thinking model and gemini-exp-1206, Qdrant for vector storage, and Agno (phidata prev) for agent orchestration. This application features intelligent query rewriting, document processing, and web search fallback capabilities to provide comprehensive AI-powered responses. + +## Features + +- **Document Processing** + - PDF document upload and processing + - Web page content extraction + - Automatic text chunking and embedding + - Vector storage in Qdrant cloud + +- **Intelligent Querying** + - Query rewriting for better retrieval + - RAG-based document retrieval + - Similarity search with threshold filtering + - Automatic fallback to web search + - Source attribution for answers + +- **Advanced Capabilities** + - Exa AI web search integration + - Custom domain filtering for web search + - Context-aware response generation + - Chat history management + - Query reformulation agent + +- **Model Specific Features** + - Gemini Thinking 2.0 Flash for chat and reasoning + - Gemini Embedding model for vector embeddings + - Agno Agent framework for orchestration + - Streamlit-based interactive interface + +## Prerequisites + +### 1. Google API Key +1. Go to [Google AI Studio](https://aistudio.google.com/apikey) +2. Sign up or log in to your account +3. Create a new API key + +### 2. Qdrant Cloud Setup +1. Visit [Qdrant Cloud](https://cloud.qdrant.io/) +2. Create an account or sign in +3. Create a new cluster +4. Get your credentials: + - Qdrant API Key: Found in API Keys section + - Qdrant URL: Your cluster URL (format: `https://xxx-xxx.cloud.qdrant.io`) + +### 3. Exa AI API Key (Optional) +1. Visit [Exa AI](https://exa.ai) +2. Sign up for an account +3. Generate an API key for web search capabilities + +## How to Run + +1. Clone the repository: +```bash +git clone https://github.com/Shubhamsaboo/awesome-llm-apps.git +cd rag_tutorials/gemini_agentic_rag +``` + +2. Install dependencies: +```bash +pip install -r requirements.txt +``` + +3. Run the application: +```bash +streamlit run agentic_rag_gemini.py +``` + +## Usage + +1. Configure API keys in the sidebar: + - Enter your Google API key + - Add Qdrant credentials + - (Optional) Add Exa AI key for web search + +2. Upload documents: + - Use the file uploader for PDFs + - Enter URLs for web content + +3. Ask questions: + - Type your query in the chat interface + - View rewritten queries and sources + - See web search results when relevant + +4. Manage your session: + - Clear chat history as needed + - Configure web search domains + - Monitor processed documents diff --git a/rag_tutorials/gemini_agentic_rag/agentic_rag_gemini.py b/rag_tutorials/gemini_agentic_rag/agentic_rag_gemini.py new file mode 100644 index 0000000..dac188f --- /dev/null +++ b/rag_tutorials/gemini_agentic_rag/agentic_rag_gemini.py @@ -0,0 +1,407 @@ +import os +import tempfile +from datetime import datetime +from typing import List + +import streamlit as st +import google.generativeai as genai +import bs4 +from agno.agent import Agent +from agno.models.google import Gemini +from langchain_community.document_loaders import PyPDFLoader, WebBaseLoader +from langchain.text_splitter import RecursiveCharacterTextSplitter +from langchain_qdrant import QdrantVectorStore +from qdrant_client import QdrantClient +from qdrant_client.models import Distance, VectorParams +from langchain_core.embeddings import Embeddings +from agno.tools.exa import ExaTools + + +# Custom Classes +class GeminiEmbedder(Embeddings): + def __init__(self, model_name="models/text-embedding-004"): + genai.configure(api_key=st.session_state.google_api_key) + self.model = model_name + + def embed_documents(self, texts: List[str]) -> List[List[float]]: + return [self.embed_query(text) for text in texts] + + def embed_query(self, text: str) -> List[float]: + response = genai.embed_content( + model=self.model, + content=text, + task_type="retrieval_document" + ) + return response['embedding'] + + +# Constants +COLLECTION_NAME = "gemini-thinking-agent-agno" + + +# Streamlit App Initialization +st.title("🤖 Agentic RAG with Gemini Thinking and Agno") + +# Session State Initialization +if 'google_api_key' not in st.session_state: + st.session_state.google_api_key = "" +if 'qdrant_api_key' not in st.session_state: + st.session_state.qdrant_api_key = "" +if 'qdrant_url' not in st.session_state: + st.session_state.qdrant_url = "" +if 'vector_store' not in st.session_state: + st.session_state.vector_store = None +if 'processed_documents' not in st.session_state: + st.session_state.processed_documents = [] +if 'history' not in st.session_state: + st.session_state.history = [] +if 'exa_api_key' not in st.session_state: + st.session_state.exa_api_key = "" +if 'use_web_search' not in st.session_state: + st.session_state.use_web_search = False + + +# Sidebar Configuration +st.sidebar.header("🔑 API Configuration") +google_api_key = st.sidebar.text_input("Google API Key", type="password", value=st.session_state.google_api_key) +qdrant_api_key = st.sidebar.text_input("Qdrant API Key", type="password", value=st.session_state.qdrant_api_key) +qdrant_url = st.sidebar.text_input("Qdrant URL", + placeholder="https://your-cluster.cloud.qdrant.io:6333", + value=st.session_state.qdrant_url) + +# Clear Chat Button +if st.sidebar.button("đŸ—‘ī¸ Clear Chat History"): + st.session_state.history = [] + st.rerun() + +# Update session state +st.session_state.google_api_key = google_api_key +st.session_state.qdrant_api_key = qdrant_api_key +st.session_state.qdrant_url = qdrant_url + +# Add in the sidebar configuration section, after the existing API inputs +st.sidebar.header("🌐 Web Search Configuration") +st.session_state.use_web_search = st.sidebar.checkbox("Enable Web Search Fallback", value=st.session_state.use_web_search) + +if st.session_state.use_web_search: + exa_api_key = st.sidebar.text_input( + "Exa AI API Key", + type="password", + value=st.session_state.exa_api_key, + help="Required for web search fallback when no relevant documents are found" + ) + st.session_state.exa_api_key = exa_api_key + + # Optional domain filtering + default_domains = ["arxiv.org", "wikipedia.org", "github.com", "medium.com"] + custom_domains = st.sidebar.text_input( + "Custom domains (comma-separated)", + value=",".join(default_domains), + help="Enter domains to search from, e.g.: arxiv.org,wikipedia.org" + ) + search_domains = [d.strip() for d in custom_domains.split(",") if d.strip()] + + +# Utility Functions +def init_qdrant(): + """Initialize Qdrant client with configured settings.""" + if not all([st.session_state.qdrant_api_key, st.session_state.qdrant_url]): + return None + try: + return QdrantClient( + url=st.session_state.qdrant_url, + api_key=st.session_state.qdrant_api_key, + timeout=60 + ) + except Exception as e: + st.error(f"🔴 Qdrant connection failed: {str(e)}") + return None + + +def get_web_search_results(query: str) -> str: + """Perform web search using Exa AI and return formatted results.""" + try: + exa_agent = Agent( + name="Web Search Agent", + tools=[ExaTools( + api_key=st.session_state.exa_api_key, + include_domains=search_domains + )], + show_tool_calls=True + ) + response = exa_agent.run(f"Search for the query: {query}") + return response.content + except Exception as e: + st.error(f"🌐 Web search error: {str(e)}") + return "" + + +# Document Processing Functions +def process_pdf(file) -> List: + """Process PDF file and add source metadata.""" + try: + with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp_file: + tmp_file.write(file.getvalue()) + loader = PyPDFLoader(tmp_file.name) + documents = loader.load() + + # Add source metadata + for doc in documents: + doc.metadata.update({ + "source_type": "pdf", + "file_name": file.name, + "timestamp": datetime.now().isoformat() + }) + + text_splitter = RecursiveCharacterTextSplitter( + chunk_size=1000, + chunk_overlap=200 + ) + return text_splitter.split_documents(documents) + except Exception as e: + st.error(f"📄 PDF processing error: {str(e)}") + return [] + + +def process_web(url: str) -> List: + """Process web URL and add source metadata.""" + try: + loader = WebBaseLoader( + web_paths=(url,), + bs_kwargs=dict( + parse_only=bs4.SoupStrainer( + class_=("post-content", "post-title", "post-header", "content", "main") + ) + ) + ) + documents = loader.load() + + # Add source metadata + for doc in documents: + doc.metadata.update({ + "source_type": "url", + "url": url, + "timestamp": datetime.now().isoformat() + }) + + text_splitter = RecursiveCharacterTextSplitter( + chunk_size=1000, + chunk_overlap=200 + ) + return text_splitter.split_documents(documents) + except Exception as e: + st.error(f"🌐 Web processing error: {str(e)}") + return [] + + +# Vector Store Management +def create_vector_store(client, texts): + """Create and initialize vector store with documents.""" + try: + # Create collection if needed + try: + client.create_collection( + collection_name=COLLECTION_NAME, + vectors_config=VectorParams( + size=768, # Gemini embedding-004 dimension + distance=Distance.COSINE + ) + ) + st.success(f"📚 Created new collection: {COLLECTION_NAME}") + except Exception as e: + if "already exists" not in str(e).lower(): + raise e + + # Initialize vector store + vector_store = QdrantVectorStore( + client=client, + collection_name=COLLECTION_NAME, + embedding=GeminiEmbedder() + ) + + # Add documents + with st.spinner('📤 Uploading documents to Qdrant...'): + vector_store.add_documents(texts) + st.success("✅ Documents stored successfully!") + return vector_store + + except Exception as e: + st.error(f"🔴 Vector store error: {str(e)}") + return None + + +# Add this after the GeminiEmbedder class +def get_query_rewriter_agent() -> Agent: + """Initialize a query rewriting agent.""" + return Agent( + name="Query Rewriter", + model=Gemini(id="gemini-exp-1206"), + instructions="""You are an expert at reformulating questions to be more precise and detailed. + Your task is to: + 1. Analyze the user's question + 2. Judge the query first, if you think it is clear enough, just return the same query + 3. Take the user's question and rewrite it to be more specific and search-friendly + 3. Rewrite it to be more specific and search-friendly (RAG and Web Search) + 4. Expand any acronyms or technical terms + 5. Return ONLY the rewritten query without explanations + + Example: + User: "What is DQN?" + Rewritten: "Explain Deep Q-Networks (DQN), including their architecture, how they combine Q-learning with neural networks, their key innovations like experience replay and target networks, and their applications in reinforcement learning for complex environments" + + Example 2: + User: "What's the price?" + Rewritten: "What is the current market price of the product we discussed in our previous conversation about electric vehicles, specifically the Tesla Model 3?" + + Example 3: + User: "AWS costs too much" + Rewritten: "What are the effective strategies and best practices for optimizing and reducing AWS cloud infrastructure costs, including resource management and cost-saving features?" + """, + show_tool_calls=False, + markdown=True, + ) + + +# Main Application Flow +if st.session_state.google_api_key: + os.environ["GOOGLE_API_KEY"] = st.session_state.google_api_key + genai.configure(api_key=st.session_state.google_api_key) + + qdrant_client = init_qdrant() + + # File/URL Upload Section + st.sidebar.header("📁 Data Upload") + uploaded_file = st.sidebar.file_uploader("Upload PDF", type=["pdf"]) + web_url = st.sidebar.text_input("Or enter URL") + + # Process documents + if uploaded_file: + file_name = uploaded_file.name + if file_name not in st.session_state.processed_documents: + with st.spinner('Processing PDF...'): + texts = process_pdf(uploaded_file) + if texts and qdrant_client: + if st.session_state.vector_store: + st.session_state.vector_store.add_documents(texts) + else: + st.session_state.vector_store = create_vector_store(qdrant_client, texts) + st.session_state.processed_documents.append(file_name) + st.success(f"✅ Added PDF: {file_name}") + + if web_url: + if web_url not in st.session_state.processed_documents: + with st.spinner('Processing URL...'): + texts = process_web(web_url) + if texts and qdrant_client: + if st.session_state.vector_store: + st.session_state.vector_store.add_documents(texts) + else: + st.session_state.vector_store = create_vector_store(qdrant_client, texts) + st.session_state.processed_documents.append(web_url) + st.success(f"✅ Added URL: {web_url}") + + # Display sources in sidebar + if st.session_state.processed_documents: + st.sidebar.header("📚 Processed Sources") + for source in st.session_state.processed_documents: + if source.endswith('.pdf'): + st.sidebar.text(f"📄 {source}") + else: + st.sidebar.text(f"🌐 {source}") + + # Initialize Agent + agent = Agent( + name="Gemini RAG Agent", + model=Gemini(id="gemini-2.0-flash-thinking-exp-01-21"), + instructions="You are an Intelligent Agent. You are an elite specialist and an expert in all fields. Answer user's questions clearly, if any document is added, Use retrieved documents to answer questions accurately.", + show_tool_calls=True, + markdown=True, + ) + + # Chat Interface + # Display chat messages + for msg in st.session_state.history: + with st.chat_message(msg["role"]): + st.write(msg["content"]) + + # Handle user input + if prompt := st.chat_input("Ask about your documents..."): + # Add user message to history + st.session_state.history.append({"role": "user", "content": prompt}) + with st.chat_message("user"): + st.write(prompt) + + # Rewrite query for better retrieval + with st.spinner("🤔 Reformulating query..."): + try: + query_rewriter = get_query_rewriter_agent() + rewritten_query = query_rewriter.run(f"Rewrite this query: {prompt}").content + + with st.expander("🔄 See rewritten query"): + st.write(f"Original: {prompt}") + st.write(f"Rewritten: {rewritten_query}") + except Exception as e: + st.error(f"❌ Error rewriting query: {str(e)}") + rewritten_query = prompt # Fallback to original query + + # Retrieve relevant documents using rewritten query + context = "" + if st.session_state.vector_store: + retriever = st.session_state.vector_store.as_retriever( + search_type="similarity_score_threshold", + search_kwargs={"k": 5, "score_threshold": 0.7} + ) + docs = retriever.invoke(rewritten_query) # Use rewritten query + context = "\n\n".join([d.page_content for d in docs]) + + # Generate response + with st.spinner("🤖 Thinking..."): + try: + # Check if we have relevant documents + if context: + full_prompt = f"Context: {context}\n\nOriginal Question: {prompt}\nRewritten Question: {rewritten_query}" + # If no relevant documents and web search is enabled + elif st.session_state.use_web_search and st.session_state.exa_api_key: + with st.spinner("🔍 Searching the web..."): + web_results = get_web_search_results(rewritten_query) + if web_results: + full_prompt = f"Web Search Results: {web_results}\n\nOriginal Question: {prompt}\nRewritten Question: {rewritten_query}" + st.info("â„šī¸ No relevant documents found in the database. Using web search results.") + else: + full_prompt = f"Original Question: {prompt}\nRewritten Question: {rewritten_query}" + else: + full_prompt = f"Original Question: {prompt}\nRewritten Question: {rewritten_query}" + if not context: + st.info("â„šī¸ No relevant documents found in the database.") + + response = agent.run(full_prompt) + + # Add assistant response to history + st.session_state.history.append({ + "role": "assistant", + "content": response.content + }) + + with st.chat_message("assistant"): + st.write(response.content) + + # Show sources if available + if st.session_state.vector_store and docs: + with st.expander("🔍 See document sources"): + for i, doc in enumerate(docs, 1): + source_type = doc.metadata.get("source_type", "unknown") + source_icon = "📄" if source_type == "pdf" else "🌐" + source_name = doc.metadata.get("file_name" if source_type == "pdf" else "url", "unknown") + st.write(f"{source_icon} Source {i} from {source_name}:") + st.write(f"{doc.page_content[:200]}...") + + # Show web search results if used + elif 'web_results' in locals() and web_results: + with st.expander("🌐 See web search results"): + st.write(web_results) + + except Exception as e: + st.error(f"❌ Error generating response: {str(e)}") + +else: + st.warning("âš ī¸ Please enter your Google API Key to continue") \ No newline at end of file diff --git a/rag_tutorials/gemini_agentic_rag/requirements.txt b/rag_tutorials/gemini_agentic_rag/requirements.txt new file mode 100644 index 0000000..1ac2d62 --- /dev/null +++ b/rag_tutorials/gemini_agentic_rag/requirements.txt @@ -0,0 +1,6 @@ +agno +exa==0.5.26 +qdrant-client==1.12.1 +langchain-qdrant==0.2.0 +langchain-community==0.3.13 +streamlit==1.41.1 \ No newline at end of file