From 5b73cc74ab49077b2e699f9ce36afec97a9fe1ba Mon Sep 17 00:00:00 2001 From: Sri Charan Thoutam Date: Wed, 12 Feb 2025 00:47:27 +0530 Subject: [PATCH 1/3] AI Blog Search Project added! --- ai_agent_tutorials/ai_blog_search/README.md | 56 +++ ai_agent_tutorials/ai_blog_search/app.py | 373 ++++++++++++++++++ .../ai_blog_search/requirements.txt | 10 + 3 files changed, 439 insertions(+) create mode 100644 ai_agent_tutorials/ai_blog_search/README.md create mode 100644 ai_agent_tutorials/ai_blog_search/app.py create mode 100644 ai_agent_tutorials/ai_blog_search/requirements.txt diff --git a/ai_agent_tutorials/ai_blog_search/README.md b/ai_agent_tutorials/ai_blog_search/README.md new file mode 100644 index 0000000..8a6dcb5 --- /dev/null +++ b/ai_agent_tutorials/ai_blog_search/README.md @@ -0,0 +1,56 @@ +# Agentic RAG with LangGraph: AI Blog Search + +## Overview +AI Blog Search is an Agentic RAG application designed to enhance information retrieval from AI-related blog posts. This system leverages LangChain, LangGraph, and Google's Gemini model to fetch, process, and analyze blog content, providing users with accurate and contextually relevant answers. + +## LangGraph Workflow +![LangGraph-Workflow](https://github.com/user-attachments/assets/07d8a6b5-f1ef-4b7e-b47a-4f14a192bd8a) + +## Demo +https://github.com/user-attachments/assets/cee07380-d3dc-45f4-ad26-7d944ba9c32b + +## Features +- **Document Retrieval:** Uses Qdrant as a vector database to store and retrieve blog content based on embeddings. +- **Agentic Query Processing:** Uses an AI-powered agent to determine whether a query should be rewritten, answered, or require more retrieval. +- **Relevance Assessment:** Implements an automated relevance grading system using Google's Gemini model. +- **Query Refinement:** Enhances poorly structured queries for better retrieval results. +- **Streamlit UI:** Provides a user-friendly interface for entering blog URLs, queries and retrieving insightful responses. +- **Graph-Based Workflow:** Implements a structured state graph using LangGraph for efficient decision-making. + +## Technologies Used +- **Programming Language**: [Python 3.10+](https://www.python.org/downloads/release/python-31011/) +- **Framework**: [LangChain](https://www.langchain.com/) and [LangGraph](https://langchain-ai.github.io/langgraph/tutorials/introduction/) +- **Database**: [Qdrant](https://qdrant.tech/) +- **Models**: + - Embeddings: [Google Gemini API (embedding-001)](https://ai.google.dev/gemini-api/docs/embeddings) + - Chat: [Google Gemini API (gemini-1.5-pro)](https://ai.google.dev/gemini-api/docs/models/gemini#gemini-1.5-pro) +- **Blogs Loader**: [Langchain WebBaseLoader](https://python.langchain.com/docs/integrations/document_loaders/web_base/) +- **Document Splitter**: [RecursiveCharacterTextSplitter](https://python.langchain.com/v0.1/docs/modules/data_connection/document_transformers/recursive_text_splitter/) +- **User Interface (UI)**: [Streamlit](https://docs.streamlit.io/) + +## Requirements +1. **Install Dependencies**: + ```bash + pip install -r requirements.txt + ``` + +2. **Run the Application**: + ```bash + streamlit run app.py + ``` + +3. **Use the Application**: + - Paste your Google API Key in the sidebar. + - Paste the blog link. + - Enter your query about the blog post. + +## :mailbox: Connect With Me +handshake gif + +

+ codewithcharan + __mr.__.unique + codewithcharan +

+ + \ No newline at end of file diff --git a/ai_agent_tutorials/ai_blog_search/app.py b/ai_agent_tutorials/ai_blog_search/app.py new file mode 100644 index 0000000..1bbc925 --- /dev/null +++ b/ai_agent_tutorials/ai_blog_search/app.py @@ -0,0 +1,373 @@ +from langchain_google_genai import GoogleGenerativeAIEmbeddings +from langchain_qdrant import QdrantVectorStore +from qdrant_client import QdrantClient +from uuid import uuid4 +from langchain_community.document_loaders import WebBaseLoader +from langchain_text_splitters import RecursiveCharacterTextSplitter +from langchain.tools.retriever import create_retriever_tool + +from typing import Annotated, Literal, Sequence +from typing_extensions import TypedDict +from functools import partial + +from langchain import hub +from langchain_core.messages import BaseMessage, HumanMessage +from langgraph.graph.message import add_messages +from langchain_core.output_parsers import StrOutputParser +from langchain_core.prompts import PromptTemplate +from langchain_google_genai import ChatGoogleGenerativeAI + +from pydantic import BaseModel, Field + +from langgraph.graph import END, StateGraph, START +from langgraph.prebuilt import ToolNode, tools_condition + +import streamlit as st + +st.set_page_config(page_title="AI Blog Search", page_icon=":mag_right:") +st.header(":blue[Agentic RAG with LangGraph:] :green[AI Blog Search]") + +# Initialize session state variables if they don't exist +if 'qdrant_host' not in st.session_state: + st.session_state.qdrant_host = "" +if 'qdrant_api_key' not in st.session_state: + st.session_state.qdrant_api_key = "" +if 'gemini_api_key' not in st.session_state: + st.session_state.gemini_api_key = "" + +def set_sidebar(): + """Setup sidebar for API keys and configuration.""" + with st.sidebar: + st.subheader("API Configuration") + + qdrant_host = st.text_input("Enter your Qdrant Host URL:", type="password") + qdrant_api_key = st.text_input("Enter your Qdrant API key:", type="password") + gemini_api_key = st.text_input("Enter your Gemini API key:", type="password") + + if st.button("Done"): + if qdrant_host and qdrant_api_key and gemini_api_key: + st.session_state.qdrant_host = qdrant_host + st.session_state.qdrant_api_key = qdrant_api_key + st.session_state.gemini_api_key = gemini_api_key + st.success("API keys saved!") + else: + st.warning("Please fill all API fields") + +def initialize_components(): + """Initialize components that require API keys""" + if not all([st.session_state.qdrant_host, + st.session_state.qdrant_api_key, + st.session_state.gemini_api_key]): + return None, None, None + + try: + # Initialize embedding model with API key + embedding_model = GoogleGenerativeAIEmbeddings( + model="models/embedding-001", + google_api_key=st.session_state.gemini_api_key + ) + + # Initialize Qdrant client + client = QdrantClient( + st.session_state.qdrant_host, + api_key=st.session_state.qdrant_api_key + ) + + # Initialize vector store + db = QdrantVectorStore( + client=client, + collection_name="qdrant_db", + embedding=embedding_model + ) + + return embedding_model, client, db + + except Exception as e: + st.error(f"Initialization error: {str(e)}") + return None, None, None + +class AgentState(TypedDict): + messages: Annotated[Sequence[BaseMessage], add_messages] + +# Edges +## Check Relevance +def grade_documents(state) -> Literal["generate", "rewrite"]: + """ + Determines whether the retrieved documents are relevant to the question. + + Args: + state (messages): The current state + + Returns: + str: A decision for whether the documents are relevant or not + """ + + print("---CHECK RELEVANCE---") + + # Data model + class grade(BaseModel): + """Binary score for relevance check.""" + + binary_score: str = Field(description="Relevance score 'yes' or 'no'") + + # LLM + model = ChatGoogleGenerativeAI(api_key=st.session_state.gemini_api_key, temperature=0, model="gemini-1.5-pro", streaming=True) + + # LLM with tool and validation + llm_with_tool = model.with_structured_output(grade) + + # Prompt + prompt = PromptTemplate( + template="""You are a grader assessing relevance of a retrieved document to a user question. \n + Here is the retrieved document: \n\n {context} \n\n + Here is the user question: {question} \n + If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \n + Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.""", + input_variables=["context", "question"], + ) + + # Chain + chain = prompt | llm_with_tool + + messages = state["messages"] + last_message = messages[-1] + + question = messages[0].content + docs = last_message.content + + scored_result = chain.invoke({"question": question, "context": docs}) + + score = scored_result.binary_score + + if score == "yes": + print("---DECISION: DOCS RELEVANT---") + return "generate" + + else: + print("---DECISION: DOCS NOT RELEVANT---") + print(score) + return "rewrite" + +# Nodes +## agent node +def agent(state, tools): + """ + Invokes the agent model to generate a response based on the current state. Given + the question, it will decide to retrieve using the retriever tool, or simply end. + + Args: + state (messages): The current state + + Returns: + dict: The updated state with the agent response appended to messages + """ + print("---CALL AGENT---") + messages = state["messages"] + model = ChatGoogleGenerativeAI(api_key=st.session_state.gemini_api_key, temperature=0, streaming=True, model="gemini-1.5-pro") + model = model.bind_tools(tools) + response = model.invoke(messages) + + # We return a list, because this will get added to the existing list + return {"messages": [response]} + +## rewrite node +def rewrite(state): + """ + Transform the query to produce a better question. + + Args: + state (messages): The current state + + Returns: + dict: The updated state with re-phrased question + """ + + print("---TRANSFORM QUERY---") + messages = state["messages"] + question = messages[0].content + + msg = [ + HumanMessage( + content=f""" \n + Look at the input and try to reason about the underlying semantic intent / meaning. \n + Here is the initial question: + \n ------- \n + {question} + \n ------- \n + Formulate an improved question: """, + ) + ] + + # Grader + model = ChatGoogleGenerativeAI(api_key=st.session_state.gemini_api_key, temperature=0, model="gemini-1.5-pro", streaming=True) + response = model.invoke(msg) + return {"messages": [response]} + +## generate node +def generate(state): + """ + Generate answer + + Args: + state (messages): The current state + + Returns: + dict: The updated state with re-phrased question + """ + print("---GENERATE---") + messages = state["messages"] + question = messages[0].content + last_message = messages[-1] + + docs = last_message.content + + # Initialize a Chat Prompt Template + prompt_template = hub.pull("rlm/rag-prompt") + + # Initialize a Generator (i.e. Chat Model) + chat_model = ChatGoogleGenerativeAI(api_key=st.session_state.gemini_api_key, model="gemini-1.5-pro", temperature=0, streaming=True) + + # Initialize a Output Parser + output_parser = StrOutputParser() + + # RAG Chain + rag_chain = prompt_template | chat_model | output_parser + + response = rag_chain.invoke({"context": docs, "question": question}) + + return {"messages": [response]} + +# graph function +def get_graph(retriever_tool): + tools = [retriever_tool] # Create tools list here + + # Define a new graph + workflow = StateGraph(AgentState) + + # Use partial to pass tools to the agent function + workflow.add_node("agent", partial(agent, tools=tools)) + + # Rest of the graph setup remains the same + retrieve = ToolNode(tools) + workflow.add_node("retrieve", retrieve) + workflow.add_node("rewrite", rewrite) # Re-writing the question + workflow.add_node( + "generate", generate + ) # Generating a response after we know the documents are relevant + # Call agent node to decide to retrieve or not + workflow.add_edge(START, "agent") + + # Decide whether to retrieve + workflow.add_conditional_edges( + "agent", + # Assess agent decision + tools_condition, + { + # Translate the condition outputs to nodes in our graph + "tools": "retrieve", + END: END, + }, + ) + + # Edges taken after the `action` node is called. + workflow.add_conditional_edges( + "retrieve", + # Assess agent decision + grade_documents, + ) + workflow.add_edge("generate", END) + workflow.add_edge("rewrite", "agent") + + # Compile + graph = workflow.compile() + + return graph + +def generate_message(graph, inputs): + generated_message = "" + + for output in graph.stream(inputs): + for key, value in output.items(): + if key == "generate" and isinstance(value, dict): + generated_message = value.get("messages", [""])[0] + + return generated_message + +def add_documents_to_qdrant(url, db): + try: + docs = WebBaseLoader(url).load() + text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder( + chunk_size=100, chunk_overlap=50 + ) + doc_chunks = text_splitter.split_documents(docs) + uuids = [str(uuid4()) for _ in range(len(doc_chunks))] + db.add_documents(documents=doc_chunks, ids=uuids) + return True + except Exception as e: + st.error(f"Error adding documents: {str(e)}") + return False + +def main(): + set_sidebar() + + # Check if API keys are set + if not all([st.session_state.qdrant_host, + st.session_state.qdrant_api_key, + st.session_state.gemini_api_key]): + st.warning("Please configure your API keys in the sidebar first") + return + + # Initialize components + embedding_model, client, db = initialize_components() + if not all([embedding_model, client, db]): + return + + # Initialize retriever and tools + retriever = db.as_retriever(search_type="similarity", search_kwargs={"k": 5}) + retriever_tool = create_retriever_tool( + retriever, + "retrieve_blog_posts", + "Search and return information about blog posts on LLMs, LLM agents, prompt engineering, and adversarial attacks on LLMs.", + ) + tools = [retriever_tool] + + # URL input section + url = st.text_input( + ":link: Paste the blog link:", + placeholder="e.g., https://lilianweng.github.io/posts/2023-06-23-agent/" + ) + if st.button("Enter URL"): + if url: + with st.spinner("Processing documents..."): + if add_documents_to_qdrant(url, db): + st.success("Documents added successfully!") + else: + st.error("Failed to add documents") + else: + st.warning("Please enter a URL") + + # Query section + graph = get_graph(retriever_tool) + query = st.text_area( + ":bulb: Enter your query about the blog post:", + placeholder="e.g., What does Lilian Weng say about the types of agent memory?" + ) + + if st.button("Submit Query"): + if not query: + st.warning("Please enter a query") + return + + inputs = {"messages": [HumanMessage(content=query)]} + with st.spinner("Generating response..."): + try: + response = generate_message(graph, inputs) + st.write(response) + except Exception as e: + st.error(f"Error generating response: {str(e)}") + + st.markdown("---") + st.write("Built with :blue-background[LangChain] | :blue-background[LangGraph] by [Charan](https://www.linkedin.com/in/codewithcharan/)") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/ai_agent_tutorials/ai_blog_search/requirements.txt b/ai_agent_tutorials/ai_blog_search/requirements.txt new file mode 100644 index 0000000..82e15a7 --- /dev/null +++ b/ai_agent_tutorials/ai_blog_search/requirements.txt @@ -0,0 +1,10 @@ +langchain +langgraph +langchainhub +langchain-community +langchain-google-genai +langchain-qdrant +langchain-text-splitters +tiktoken +beautifulsoup4 +python-dotenv \ No newline at end of file From 2691e174b6bbf94195354af78bd4cbf35c548325 Mon Sep 17 00:00:00 2001 From: Sri Charan Thoutam Date: Wed, 12 Feb 2025 00:57:44 +0530 Subject: [PATCH 2/3] Replaced ChromaDB with Qdrant and upgraded the model to Gemini 2.0 Flash --- ai_agent_tutorials/ai_blog_search/README.md | 2 +- ai_agent_tutorials/ai_blog_search/app.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ai_agent_tutorials/ai_blog_search/README.md b/ai_agent_tutorials/ai_blog_search/README.md index 8a6dcb5..abf2d34 100644 --- a/ai_agent_tutorials/ai_blog_search/README.md +++ b/ai_agent_tutorials/ai_blog_search/README.md @@ -23,7 +23,7 @@ https://github.com/user-attachments/assets/cee07380-d3dc-45f4-ad26-7d944ba9c32b - **Database**: [Qdrant](https://qdrant.tech/) - **Models**: - Embeddings: [Google Gemini API (embedding-001)](https://ai.google.dev/gemini-api/docs/embeddings) - - Chat: [Google Gemini API (gemini-1.5-pro)](https://ai.google.dev/gemini-api/docs/models/gemini#gemini-1.5-pro) + - Chat: [Google Gemini API (gemini-2.0-flash)](https://ai.google.dev/gemini-api/docs/models/gemini#gemini-2.0-flash) - **Blogs Loader**: [Langchain WebBaseLoader](https://python.langchain.com/docs/integrations/document_loaders/web_base/) - **Document Splitter**: [RecursiveCharacterTextSplitter](https://python.langchain.com/v0.1/docs/modules/data_connection/document_transformers/recursive_text_splitter/) - **User Interface (UI)**: [Streamlit](https://docs.streamlit.io/) diff --git a/ai_agent_tutorials/ai_blog_search/app.py b/ai_agent_tutorials/ai_blog_search/app.py index 1bbc925..36006a4 100644 --- a/ai_agent_tutorials/ai_blog_search/app.py +++ b/ai_agent_tutorials/ai_blog_search/app.py @@ -111,7 +111,7 @@ def grade_documents(state) -> Literal["generate", "rewrite"]: binary_score: str = Field(description="Relevance score 'yes' or 'no'") # LLM - model = ChatGoogleGenerativeAI(api_key=st.session_state.gemini_api_key, temperature=0, model="gemini-1.5-pro", streaming=True) + model = ChatGoogleGenerativeAI(api_key=st.session_state.gemini_api_key, temperature=0, model="gemini-2.0-flash", streaming=True) # LLM with tool and validation llm_with_tool = model.with_structured_output(grade) @@ -163,7 +163,7 @@ def agent(state, tools): """ print("---CALL AGENT---") messages = state["messages"] - model = ChatGoogleGenerativeAI(api_key=st.session_state.gemini_api_key, temperature=0, streaming=True, model="gemini-1.5-pro") + model = ChatGoogleGenerativeAI(api_key=st.session_state.gemini_api_key, temperature=0, streaming=True, model="gemini-2.0-flash") model = model.bind_tools(tools) response = model.invoke(messages) @@ -199,7 +199,7 @@ def rewrite(state): ] # Grader - model = ChatGoogleGenerativeAI(api_key=st.session_state.gemini_api_key, temperature=0, model="gemini-1.5-pro", streaming=True) + model = ChatGoogleGenerativeAI(api_key=st.session_state.gemini_api_key, temperature=0, model="gemini-2.0-flash", streaming=True) response = model.invoke(msg) return {"messages": [response]} @@ -225,7 +225,7 @@ def generate(state): prompt_template = hub.pull("rlm/rag-prompt") # Initialize a Generator (i.e. Chat Model) - chat_model = ChatGoogleGenerativeAI(api_key=st.session_state.gemini_api_key, model="gemini-1.5-pro", temperature=0, streaming=True) + chat_model = ChatGoogleGenerativeAI(api_key=st.session_state.gemini_api_key, model="gemini-2.0-flash", temperature=0, streaming=True) # Initialize a Output Parser output_parser = StrOutputParser() From e0a9ba0362682b995d04108824d8a29d00e2d0f8 Mon Sep 17 00:00:00 2001 From: Sri Charan Thoutam Date: Sun, 16 Feb 2025 14:24:17 +0530 Subject: [PATCH 3/3] Moved AI Blog Search tutorial under rag_tutorials section --- {ai_agent_tutorials => rag_tutorials}/ai_blog_search/README.md | 0 {ai_agent_tutorials => rag_tutorials}/ai_blog_search/app.py | 0 .../ai_blog_search/requirements.txt | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename {ai_agent_tutorials => rag_tutorials}/ai_blog_search/README.md (100%) rename {ai_agent_tutorials => rag_tutorials}/ai_blog_search/app.py (100%) rename {ai_agent_tutorials => rag_tutorials}/ai_blog_search/requirements.txt (100%) diff --git a/ai_agent_tutorials/ai_blog_search/README.md b/rag_tutorials/ai_blog_search/README.md similarity index 100% rename from ai_agent_tutorials/ai_blog_search/README.md rename to rag_tutorials/ai_blog_search/README.md diff --git a/ai_agent_tutorials/ai_blog_search/app.py b/rag_tutorials/ai_blog_search/app.py similarity index 100% rename from ai_agent_tutorials/ai_blog_search/app.py rename to rag_tutorials/ai_blog_search/app.py diff --git a/ai_agent_tutorials/ai_blog_search/requirements.txt b/rag_tutorials/ai_blog_search/requirements.txt similarity index 100% rename from ai_agent_tutorials/ai_blog_search/requirements.txt rename to rag_tutorials/ai_blog_search/requirements.txt