Harnessing Agentic RAG and Graph-Based Metadata Filtering for Enhanced Information Retrieval

SHUBHAM NAGAR
Mind and Machine
Published in
4 min readMay 17, 2024

The landscape of information retrieval is constantly evolving, driven by the need for more sophisticated and accurate methods. Two notable advancements in this area are Agentic Retrieval-Augmented Generation (RAG) and graph-based metadata filtering. By integrating these technologies, we can create powerful systems capable of handling complex queries and delivering highly relevant results. This article explores these concepts and demonstrates how to implement them in a practical use case.

Introduction to Agentic RAG

Native RAG

Native RAG is a prevalent approach where a user query is processed through a pipeline involving retrieval, reranking, synthesis, and generation of a response. This method combines retrieval and generation techniques to provide accurate and contextually relevant answers.

Agentic RAG

Agentic RAG takes this concept further by employing an agent-based architecture for question answering over multiple documents. It supports complex tasks requiring planning, multi-step reasoning, tool use, and learning over time. The key components of Agentic RAG include:

  • Document Agents: Each document has a dedicated agent capable of answering questions and summarizing content.
  • Meta-Agent: A top-level agent that manages all document agents, orchestrating their interactions and integrating their outputs to generate comprehensive responses.

Benefits of Agentic RAG

  • Autonomy: Agents act independently to retrieve, process, and generate information.
  • Adaptability: The system can adjust strategies based on new data and changing contexts.
  • Proactivity: Agents can anticipate needs and take preemptive actions to achieve goals.

Enhancing RAG with Graph-Based Metadata Filtering

Graph-based metadata filtering optimizes vector retrieval by leveraging structured data to narrow search results. This method is particularly useful for sorting information based on specific criteria, such as dates or categories.

Implementation Overview

Using Neo4j and LangChain, we can implement graph-based metadata filtering to enhance RAG applications. The following example demonstrates how to combine these technologies to retrieve information based on structured and unstructured data.

Sample Code Implementation

Setting Up the Environment

First, we need to define the necessary credentials and establish connections to Neo4j and OpenAI.

import os
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.graph import Neo4jGraph
from langchain.vector import Neo4jVector

# Set environment variables
os.environ["OPENAI_API_KEY"] = "your_openai_api_key"
os.environ["NEO4J_URI"] = "neo4j+s://demo.neo4jlabs.com"
os.environ["NEO4J_USERNAME"] = "companies"
os.environ["NEO4J_PASSWORD"] = "companies"
os.environ["NEO4J_DATABASE"] = "companies"

# Initialize embeddings and graph connection
embeddings = OpenAIEmbeddings()
graph = Neo4jGraph()
vector_index = Neo4jVector.from_existing_index(embeddings, index_name="news")

Defining the Query Function

The following function dynamically generates a Cypher query based on user input and retrieves relevant information from the Neo4j database.

def get_organization_news(topic=None, organization=None, country=None, sentiment=None):
# Use vector index if no pre-filtering parameters are specified
if topic and not organization and not country and not sentiment:
return vector_index.similarity_search(topic)

base_query = "CYPHER runtime = parallel parallelRuntimeSupport=all MATCH (c:Chunk)<-[:HAS_CHUNK]-(a:Article) WHERE "
where_queries = []
params = {"k": 5} # Number of text chunks to retrieve

# Add organization filter
if organization:
candidates = get_candidates(organization)
if len(candidates) > 1:
return f"Ask a follow-up question to clarify the organization. Available options: {candidates}"
where_queries.append("EXISTS {(a)-[:MENTIONS]->(:Organization {name: $organization})}")
params["organization"] = candidates[0]

# Add country filter
if country:
where_queries.append("EXISTS {(a)-[:MENTIONS]->(:Organization)-[:IN_CITY]->()-[:IN_COUNTRY]->(:Country {name: $country})}")
params["country"] = country

# Add sentiment filter
if sentiment:
sentiment_value = 0.5 if sentiment == "positive" else -0.5
where_queries.append("a.sentiment > $sentiment" if sentiment == "positive" else "a.sentiment < $sentiment")
params["sentiment"] = sentiment_value

# Add vector similarity search
if topic:
vector_snippet = " WITH c, a, vector.similarity.cosine(c.embedding, $embedding) AS score ORDER BY score DESC LIMIT toInteger($k) "
params["embedding"] = embeddings.embed_query(topic)
else:
vector_snippet = " WITH c, a ORDER BY a.date DESC LIMIT toInteger($k) "

# Construct final query
complete_query = base_query + " AND ".join(where_queries) + vector_snippet + "RETURN '#title ' + a.title + '\n#date ' + toString(a.date) + '\n#text ' + c.text AS output"
data = graph.query(complete_query, params)

# Remove embedding from params for printing
params.pop('embedding', None)
return "###Article: ".join([el["output"] for el in data])

def get_candidates(organization):
# Example implementation to retrieve organization candidates
return ["Neo4j"] if organization.lower() == "neo4j" else []

Wrapping the Function in an Agent Tool

We can then wrap this function in an agent tool to allow an LLM to use it dynamically.

from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.agents import AgentExecutor, OpenAIFunctionsAgentOutputParser
from langchain.tools import BaseTool

class NewsInput(BaseModel):
topic: Optional[str]
organization: Optional[str]
country: Optional[str]
sentiment: Optional[str]

class NewsTool(BaseTool):
name = "NewsInformation"
description = "Find relevant information in the news."
args_schema = NewsInput

def _run(self, topic=None, organization=None, country=None, sentiment=None, run_manager=None):
return get_organization_news(topic, organization, country, sentiment)

# Define the agent executor
llm = ChatOpenAI(temperature=0, model="gpt-4-turbo", streaming=True)
tools = [NewsTool()]
llm_with_tools = llm.bind(functions=[format_tool_to_openai_function(t) for t in tools])

prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful assistant that finds information about news and organizations."),
MessagesPlaceholder(variable_name="chat_history"),
("user", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
])

agent = ({"input": lambda x: x["input"], "chat_history": lambda x: x.get("chat_history", []), "agent_scratchpad": lambda x: x.get("intermediate_steps", [])} | prompt | llm_with_tools | OpenAIFunctionsAgentOutputParser())

agent_executor = AgentExecutor(agent=agent, tools=tools)

Testing the Implementation

Finally, we can test the implementation with different inputs to ensure it retrieves the desired information.

result = agent_executor.invoke({"input": "What are some positive news regarding Neo4j?"})
print(result)

result = agent_executor.invoke({"input": "What are some of the latest negative news about employee happiness for companies from France?"})
print(result)

Conclusion

By combining Agentic RAG and graph-based metadata filtering, we can create sophisticated information retrieval systems that handle complex queries and deliver highly relevant results. This integration enhances the capabilities of RAG applications, making them more adaptable, autonomous, and proactive. The example provided demonstrates how to implement these technologies using LangChain and Neo4j, offering a practical guide for developing advanced retrieval systems.

--

--

SHUBHAM NAGAR
Mind and Machine

Brussels-based blockchain/AI expert. Specializes in data analytics, data modeling, AI & Automation. Passionate about books, food, and writing on Medium.