Commit Β·
8a27bb1
1
Parent(s): 084052d
initial code
Browse files- .DS_Store +0 -0
- Dockerfile +23 -0
- README.md +113 -50
- requirements.txt +14 -0
- src/agent.py +73 -0
- src/main.py +77 -0
- src/rag_engine.py +56 -0
- tests/evaluate.py +72 -0
.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
Dockerfile
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use an official Python runtime as a parent image
|
| 2 |
+
FROM python:3.10-slim
|
| 3 |
+
|
| 4 |
+
# Set the working directory in the container
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Copy the requirements file into the container
|
| 8 |
+
COPY requirements.txt .
|
| 9 |
+
|
| 10 |
+
# Install any needed packages specified in requirements.txt
|
| 11 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 12 |
+
|
| 13 |
+
# Copy the current directory contents into the container at /app
|
| 14 |
+
COPY . .
|
| 15 |
+
|
| 16 |
+
# Expose the port FastAPI runs on
|
| 17 |
+
EXPOSE 8000
|
| 18 |
+
|
| 19 |
+
# Define environment variable for unbuffered logs (good for Docker)
|
| 20 |
+
ENV PYTHONUNBUFFERED=1
|
| 21 |
+
|
| 22 |
+
# Run the application
|
| 23 |
+
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
README.md
CHANGED
|
@@ -1,64 +1,127 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
|
| 4 |
|
| 5 |
-
|
| 6 |
|
| 7 |
-
|
| 8 |
-
* **Context Grounding:** Utilizes a custom prompt that forces the LLM to use **only** the provided context, ensuring factual and traceable answers .
|
| 9 |
-
* **Vectorization & Retrieval:** Leverages **Google Generative AI Embeddings** and the high-performance **FAISS** vector store for sub-second similarity search .
|
| 10 |
-
* **End-to-End Pipeline:** Wires the components into a `RetrievalQA` chain for seamless question processing .
|
| 11 |
-
* **Experiment Tracking:** Integrates **MLflow** to track key parameters (`chunk_size`, `chunk_overlap`, `session_id`) and log question/answer pairs along with source documents for model evaluation and future feedback .
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
|
| 23 |
-
|
| 24 |
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
3. **Vector Store:** The vectors are indexed in the **FAISS** library for efficient storage and retrieval.
|
| 30 |
-
4. **Retrieval:** At query time, the user's question is embedded and used to query FAISS for the Top-K (default $k=4$) most relevant document chunks.
|
| 31 |
-
5. **Generation:** The retrieved chunks are passed to the **Gemini 1.5 Pro** LLM along with a strict prompt instructing it to answer **only** based on the provided context, producing the final, grounded response.
|
| 32 |
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
-
|
|
|
|
|
|
|
| 35 |
|
| 36 |
-
|
| 37 |
-
|
| 38 |
|
| 39 |
-
|
|
|
|
|
|
|
| 40 |
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
qa_system = DocumentQASystem(api_key='YOUR_GEMINI_API_KEY')
|
| 46 |
-
```
|
| 47 |
-
2. **Process Documents:** Place your documents (`.pdf`, `.txt`, `.docx`, `.csv`) in a directory and provide the file paths.
|
| 48 |
-
```python
|
| 49 |
-
sample_files = ["./data/manual.pdf", "./data/report.docx"]
|
| 50 |
-
chunk_count = qa_system.process_documents(
|
| 51 |
-
file_paths=sample_files,
|
| 52 |
-
chunk_size=1000,
|
| 53 |
-
chunk_overlap=200
|
| 54 |
-
)
|
| 55 |
-
print(f"Processed {chunk_count} document chunks.")
|
| 56 |
-
```
|
| 57 |
-
3. **Ask Questions:**
|
| 58 |
-
```python
|
| 59 |
-
result = qa_system.ask_question("Under what conditions can a treaty be terminated?")
|
| 60 |
-
print("Answer:", result["answer"])
|
| 61 |
-
print("Sources:", result["sources"])
|
| 62 |
-
```
|
| 63 |
-
### Contributing:
|
| 64 |
-
Contributions are welcome! Please open issues or submit pull requests to suggest improvements or new features.
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Agentic RAG Knowledge Search</title>
|
| 7 |
+
<style>
|
| 8 |
+
body {
|
| 9 |
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
| 10 |
+
line-height: 1.6;
|
| 11 |
+
color: #333;
|
| 12 |
+
max-width: 800px;
|
| 13 |
+
margin: 0 auto;
|
| 14 |
+
padding: 20px;
|
| 15 |
+
}
|
| 16 |
+
h1, h2, h3 {
|
| 17 |
+
color: #2c3e50;
|
| 18 |
+
margin-top: 1.5em;
|
| 19 |
+
}
|
| 20 |
+
h1 {
|
| 21 |
+
border-bottom: 2px solid #eaeaea;
|
| 22 |
+
padding-bottom: 0.5em;
|
| 23 |
+
}
|
| 24 |
+
code {
|
| 25 |
+
background-color: #f6f8fa;
|
| 26 |
+
padding: 0.2em 0.4em;
|
| 27 |
+
border-radius: 3px;
|
| 28 |
+
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
| 29 |
+
font-size: 85%;
|
| 30 |
+
}
|
| 31 |
+
pre {
|
| 32 |
+
background-color: #f6f8fa;
|
| 33 |
+
padding: 16px;
|
| 34 |
+
overflow: auto;
|
| 35 |
+
border-radius: 6px;
|
| 36 |
+
line-height: 1.45;
|
| 37 |
+
}
|
| 38 |
+
pre code {
|
| 39 |
+
background-color: transparent;
|
| 40 |
+
padding: 0;
|
| 41 |
+
font-size: 100%;
|
| 42 |
+
}
|
| 43 |
+
ul {
|
| 44 |
+
padding-left: 2em;
|
| 45 |
+
}
|
| 46 |
+
li {
|
| 47 |
+
margin-bottom: 0.5em;
|
| 48 |
+
}
|
| 49 |
+
a {
|
| 50 |
+
color: #0366d6;
|
| 51 |
+
text-decoration: none;
|
| 52 |
+
}
|
| 53 |
+
a:hover {
|
| 54 |
+
text-decoration: underline;
|
| 55 |
+
}
|
| 56 |
+
</style>
|
| 57 |
+
</head>
|
| 58 |
+
<body>
|
| 59 |
|
| 60 |
+
<h1>Agentic RAG Knowledge Search</h1>
|
| 61 |
|
| 62 |
+
<p>A production-ready AI Microservice that uses an <strong>Agentic Router</strong> to intelligently decide between retrieving answers from internal private documents (RAG) or searching the live web.</p>
|
| 63 |
|
| 64 |
+
<p>Built with <strong>FastAPI</strong>, <strong>LangChain</strong>, <strong>Docker</strong>, and <strong>Google Gemini</strong>.</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
+
<h2> Features</h2>
|
| 67 |
+
<ul>
|
| 68 |
+
<li><strong>Agentic Routing</strong>: Uses an LLM to decide <em>where</em> to get information.</li>
|
| 69 |
+
<li><strong>Hybrid Tools</strong>:
|
| 70 |
+
<ul>
|
| 71 |
+
<li><code>lookup_internal_policy</code>: Searches local PDFs using FAISS + Embeddings.</li>
|
| 72 |
+
<li><code>search_web</code>: Searches DuckDuckGo for real-time info.</li>
|
| 73 |
+
</ul>
|
| 74 |
+
</li>
|
| 75 |
+
<li><strong>REST API</strong>: Fully documented API using FastAPI.</li>
|
| 76 |
+
<li><strong>Containerized</strong>: Docker support for easy deployment.</li>
|
| 77 |
+
</ul>
|
| 78 |
|
| 79 |
+
<h2> Project Structure</h2>
|
| 80 |
+
<ul>
|
| 81 |
+
<li><code>src/rag_engine.py</code>: Handles PDF ingestion and Vector Database (FAISS).</li>
|
| 82 |
+
<li><code>src/agent.py</code>: Defines the Agent, Tools, and LangChain logic.</li>
|
| 83 |
+
<li><code>src/main.py</code>: The FastAPI server entry point.</li>
|
| 84 |
+
<li><code>data/</code>: Place your PDF documents here.</li>
|
| 85 |
+
</ul>
|
| 86 |
|
| 87 |
+
<h2> Setup & Installation</h2>
|
| 88 |
|
| 89 |
+
<h3>1. Prerequisites</h3>
|
| 90 |
+
<ul>
|
| 91 |
+
<li>Get a <a href="https://aistudio.google.com/">Free Google Gemini API Key</a>.</li>
|
| 92 |
+
<li>Create a <code>.env</code> file in this directory:
|
| 93 |
+
<pre><code>GOOGLE_API_KEY=AIzaSyD...your_key_here...</code></pre>
|
| 94 |
+
</li>
|
| 95 |
+
</ul>
|
| 96 |
|
| 97 |
+
<h3>2. Add Data</h3>
|
| 98 |
+
<p>Place your PDF file (e.g., policy documents, course materials) into the <code>data/</code> folder and rename it to <code>policy.pdf</code> (or update <code>src/agent.py</code> to match your filename).</p>
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
+
<h3>3. Run with Docker (Recommended)</h3>
|
| 101 |
+
<pre><code>docker build -t agentic-rag .
|
| 102 |
+
docker run -p 8000:8000 --env-file .env agentic-rag</code></pre>
|
| 103 |
|
| 104 |
+
<h3>4. Run Locally</h3>
|
| 105 |
+
<pre><code>pip install -r requirements.txt
|
| 106 |
+
python src/main.py</code></pre>
|
| 107 |
|
| 108 |
+
<h2> API Usage</h2>
|
| 109 |
+
<p><strong>Endpoint:</strong> <code>POST /chat</code></p>
|
| 110 |
|
| 111 |
+
<pre><code>{
|
| 112 |
+
"query": "What are the rules for termination in the policy?"
|
| 113 |
+
}</code></pre>
|
| 114 |
|
| 115 |
+
<p><strong>Response:</strong></p>
|
| 116 |
+
<pre><code>{
|
| 117 |
+
"response": "According to the policy, termination requires...",
|
| 118 |
+
"steps": [
|
| 119 |
+
{
|
| 120 |
+
"tool": "lookup_internal_policy",
|
| 121 |
+
"tool_input": "termination rules"
|
| 122 |
+
}
|
| 123 |
+
]
|
| 124 |
+
}</code></pre>
|
| 125 |
|
| 126 |
+
</body>
|
| 127 |
+
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
requirements.txt
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.109.2
|
| 2 |
+
uvicorn==0.27.1
|
| 3 |
+
python-dotenv==1.0.1
|
| 4 |
+
langchain==0.1.9
|
| 5 |
+
langchain-google-genai==0.0.9
|
| 6 |
+
langchain-community==0.0.24
|
| 7 |
+
faiss-cpu==1.7.4
|
| 8 |
+
pypdf==4.0.2
|
| 9 |
+
duckduckgo-search==4.3.1
|
| 10 |
+
ragas==0.1.1
|
| 11 |
+
datasets==2.17.1
|
| 12 |
+
google-search-results
|
| 13 |
+
pydantic==2.6.1
|
| 14 |
+
requests
|
src/agent.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from langchain.agents import AgentExecutor, create_tool_calling_agent
|
| 3 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 4 |
+
from langchain_core.tools import tool
|
| 5 |
+
from langchain_community.tools import DuckDuckGoSearchRun
|
| 6 |
+
from langchain import hub
|
| 7 |
+
from dotenv import load_dotenv
|
| 8 |
+
from src.rag_engine import KnowledgeBase
|
| 9 |
+
|
| 10 |
+
load_dotenv()
|
| 11 |
+
|
| 12 |
+
# --- Configuration ---
|
| 13 |
+
PDF_PATH = os.path.join("data", "policy.pdf")
|
| 14 |
+
|
| 15 |
+
# Initialize the RAG engine
|
| 16 |
+
kb = KnowledgeBase(pdf_path=PDF_PATH)
|
| 17 |
+
# Index documents on startup
|
| 18 |
+
kb.load_and_index()
|
| 19 |
+
|
| 20 |
+
# --- Define Tools ---
|
| 21 |
+
|
| 22 |
+
@tool
|
| 23 |
+
def lookup_internal_policy(query: str) -> str:
|
| 24 |
+
"""
|
| 25 |
+
Useful for answering questions about specific internal policies,
|
| 26 |
+
documents, laws, or the contents of the uploaded PDF file.
|
| 27 |
+
ALWAYS use this tool first if the question implies looking up specific rules or documents.
|
| 28 |
+
"""
|
| 29 |
+
return kb.retrieve(query)
|
| 30 |
+
|
| 31 |
+
@tool
|
| 32 |
+
def search_web(query: str) -> str:
|
| 33 |
+
"""
|
| 34 |
+
Useful for finding current events, news, general knowledge,
|
| 35 |
+
or information that is NOT contained in the internal policy documents.
|
| 36 |
+
"""
|
| 37 |
+
search = DuckDuckGoSearchRun()
|
| 38 |
+
return search.run(query)
|
| 39 |
+
|
| 40 |
+
# --- Initialize Agent ---
|
| 41 |
+
|
| 42 |
+
def get_agent_executor():
|
| 43 |
+
"""
|
| 44 |
+
Constructs the Agent with the Gemini LLM and the tools.
|
| 45 |
+
"""
|
| 46 |
+
# 1. Setup the Gemini LLM
|
| 47 |
+
if not os.getenv("GOOGLE_API_KEY"):
|
| 48 |
+
raise ValueError("GOOGLE_API_KEY not found in environment variables")
|
| 49 |
+
|
| 50 |
+
llm = ChatGoogleGenerativeAI(
|
| 51 |
+
model="gemini-1.5-flash", # Or gemini-1.5-pro
|
| 52 |
+
temperature=0,
|
| 53 |
+
convert_system_message_to_human=True
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
# 2. Define the Toolkit
|
| 57 |
+
tools = [lookup_internal_policy, search_web]
|
| 58 |
+
|
| 59 |
+
# 3. Pull a standard prompt for Tool Calling from LangChain Hub
|
| 60 |
+
# This prompt tells the LLM: "You have these tools. Use them to answer."
|
| 61 |
+
prompt = hub.pull("hwchase17/openai-tools-agent")
|
| 62 |
+
|
| 63 |
+
# 4. Construct the Agent
|
| 64 |
+
agent = create_tool_calling_agent(llm, tools, prompt)
|
| 65 |
+
|
| 66 |
+
# 5. Create the Executor (Runtime)
|
| 67 |
+
agent_executor = AgentExecutor(
|
| 68 |
+
agent=agent,
|
| 69 |
+
tools=tools,
|
| 70 |
+
verbose=True, # Logs thoughts to console
|
| 71 |
+
return_intermediate_steps=True # Return the thought process
|
| 72 |
+
)
|
| 73 |
+
return agent_executor
|
src/main.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, HTTPException
|
| 2 |
+
from pydantic import BaseModel
|
| 3 |
+
from src.agent import get_agent_executor
|
| 4 |
+
import logging
|
| 5 |
+
|
| 6 |
+
# Setup logging
|
| 7 |
+
logging.basicConfig(level=logging.INFO)
|
| 8 |
+
logger = logging.getLogger(__name__)
|
| 9 |
+
|
| 10 |
+
app = FastAPI(
|
| 11 |
+
title="Agentic RAG Service",
|
| 12 |
+
description="An AI Microservice that routes between Internal Docs and Web Search.",
|
| 13 |
+
version="1.0"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
# Initialize Agent Logic
|
| 17 |
+
try:
|
| 18 |
+
agent_executor = get_agent_executor()
|
| 19 |
+
except Exception as e:
|
| 20 |
+
logger.error(f"Failed to initialize agent: {e}")
|
| 21 |
+
agent_executor = None
|
| 22 |
+
|
| 23 |
+
# --- API Models ---
|
| 24 |
+
class QueryRequest(BaseModel):
|
| 25 |
+
query: str
|
| 26 |
+
|
| 27 |
+
class StepInfo(BaseModel):
|
| 28 |
+
tool: str
|
| 29 |
+
tool_input: str
|
| 30 |
+
|
| 31 |
+
class QueryResponse(BaseModel):
|
| 32 |
+
response: str
|
| 33 |
+
steps: list[StepInfo] = []
|
| 34 |
+
|
| 35 |
+
# --- Routes ---
|
| 36 |
+
|
| 37 |
+
@app.get("/")
|
| 38 |
+
async def root():
|
| 39 |
+
return {
|
| 40 |
+
"status": "active",
|
| 41 |
+
"service": "Agentic Knowledge Search",
|
| 42 |
+
"docs_url": "/docs"
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
@app.post("/chat", response_model=QueryResponse)
|
| 46 |
+
async def chat(request: QueryRequest):
|
| 47 |
+
if not agent_executor:
|
| 48 |
+
raise HTTPException(status_code=500, detail="Agent not initialized (Check API Keys)")
|
| 49 |
+
|
| 50 |
+
try:
|
| 51 |
+
logger.info(f"Received query: {request.query}")
|
| 52 |
+
|
| 53 |
+
# Invoke the agent
|
| 54 |
+
result = agent_executor.invoke({"input": request.query})
|
| 55 |
+
|
| 56 |
+
# Extract the 'Thought Process' (Intermediate Steps) to show the recruiter
|
| 57 |
+
# 'intermediate_steps' is a list of tuples: (AgentAction, Observation)
|
| 58 |
+
steps_data = []
|
| 59 |
+
if "intermediate_steps" in result:
|
| 60 |
+
for action, observation in result["intermediate_steps"]:
|
| 61 |
+
steps_data.append(StepInfo(
|
| 62 |
+
tool=action.tool,
|
| 63 |
+
tool_input=str(action.tool_input)
|
| 64 |
+
))
|
| 65 |
+
|
| 66 |
+
return QueryResponse(
|
| 67 |
+
response=result["output"],
|
| 68 |
+
steps=steps_data
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
except Exception as e:
|
| 72 |
+
logger.error(f"Error processing query: {e}")
|
| 73 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 74 |
+
|
| 75 |
+
if __name__ == "__main__":
|
| 76 |
+
import uvicorn
|
| 77 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
src/rag_engine.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from langchain_community.document_loaders import PyPDFLoader
|
| 3 |
+
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
| 4 |
+
from langchain_community.vectorstores import FAISS
|
| 5 |
+
from langchain_google_genai import GoogleGenerativeAIEmbeddings
|
| 6 |
+
from dotenv import load_dotenv
|
| 7 |
+
|
| 8 |
+
# Load environment variables
|
| 9 |
+
load_dotenv()
|
| 10 |
+
|
| 11 |
+
class KnowledgeBase:
|
| 12 |
+
"""
|
| 13 |
+
Handles document loading, chunking, and vector storage.
|
| 14 |
+
"""
|
| 15 |
+
def __init__(self, pdf_path: str):
|
| 16 |
+
self.pdf_path = pdf_path
|
| 17 |
+
self.vector_store = None
|
| 18 |
+
# Using Gemini Embeddings
|
| 19 |
+
self.embeddings = GoogleGenerativeAIEmbeddings(model="models/embedding-001")
|
| 20 |
+
|
| 21 |
+
def load_and_index(self):
|
| 22 |
+
"""
|
| 23 |
+
Loads the PDF, splits it into chunks, and builds the FAISS index.
|
| 24 |
+
"""
|
| 25 |
+
if not os.path.exists(self.pdf_path):
|
| 26 |
+
print(f"β οΈ Warning: File not found at {self.pdf_path}. Internal search will be empty.")
|
| 27 |
+
return
|
| 28 |
+
|
| 29 |
+
print(f"π Loading PDF from: {self.pdf_path}...")
|
| 30 |
+
loader = PyPDFLoader(self.pdf_path)
|
| 31 |
+
docs = loader.load()
|
| 32 |
+
|
| 33 |
+
print("βοΈ Splitting text into chunks...")
|
| 34 |
+
text_splitter = RecursiveCharacterTextSplitter(
|
| 35 |
+
chunk_size=1000,
|
| 36 |
+
chunk_overlap=200
|
| 37 |
+
)
|
| 38 |
+
chunks = text_splitter.split_documents(docs)
|
| 39 |
+
|
| 40 |
+
print(f"πΎ Indexing {len(chunks)} chunks into Vector Store...")
|
| 41 |
+
self.vector_store = FAISS.from_documents(chunks, self.embeddings)
|
| 42 |
+
print("β
Indexing Complete.")
|
| 43 |
+
|
| 44 |
+
def retrieve(self, query: str, k: int = 4) -> str:
|
| 45 |
+
"""
|
| 46 |
+
Retrieves the top-k most relevant text chunks for a query.
|
| 47 |
+
Returns a single string of context.
|
| 48 |
+
"""
|
| 49 |
+
if not self.vector_store:
|
| 50 |
+
return "No internal documents have been indexed."
|
| 51 |
+
|
| 52 |
+
docs = self.vector_store.similarity_search(query, k=k)
|
| 53 |
+
|
| 54 |
+
# Combine the content of the retrieved docs
|
| 55 |
+
context = "\n\n".join([f"[Source: Page {d.metadata.get('page', '?')}] {d.page_content}" for d in docs])
|
| 56 |
+
return context
|
tests/evaluate.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import os
|
| 3 |
+
# Add project root to python path so we can import 'src'
|
| 4 |
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 5 |
+
|
| 6 |
+
from ragas import evaluate
|
| 7 |
+
from ragas.metrics import faithfulness, answer_relevance
|
| 8 |
+
from datasets import Dataset
|
| 9 |
+
from src.agent import get_agent_executor
|
| 10 |
+
from langchain_google_genai import ChatGoogleGenerativeAI, GoogleGenerativeAIEmbeddings
|
| 11 |
+
|
| 12 |
+
def run_evaluation():
|
| 13 |
+
print("π Starting Evaluation Pipeline...")
|
| 14 |
+
agent = get_agent_executor()
|
| 15 |
+
|
| 16 |
+
# 1. Define Test Data
|
| 17 |
+
# Questions that cover both Internal Docs and External Search
|
| 18 |
+
questions = [
|
| 19 |
+
"What are the specific conditions for treaty termination?", # Should use Internal Tool
|
| 20 |
+
"What is the current Prime Minister of the UK?", # Should use Web Tool
|
| 21 |
+
]
|
| 22 |
+
|
| 23 |
+
# The 'Ground Truth' answers (what we expect)
|
| 24 |
+
ground_truths = [
|
| 25 |
+
["Treaties can be terminated by consent of all parties, material breach, or fundamental change of circumstances."],
|
| 26 |
+
["Keir Starmer is the Prime Minister of the UK (as of late 2024)."]
|
| 27 |
+
]
|
| 28 |
+
|
| 29 |
+
answers = []
|
| 30 |
+
contexts = []
|
| 31 |
+
|
| 32 |
+
# 2. Run the Agent on the questions
|
| 33 |
+
for q in questions:
|
| 34 |
+
print(f"Testing Question: {q}")
|
| 35 |
+
try:
|
| 36 |
+
result = agent.invoke({"input": q})
|
| 37 |
+
answers.append(result["output"])
|
| 38 |
+
|
| 39 |
+
# Extract retrieved context if available (simplified for this script)
|
| 40 |
+
# In a real scenario, we'd pull the actual text chunks from the tool output
|
| 41 |
+
contexts.append(["Context retrieved from agent tools..."])
|
| 42 |
+
|
| 43 |
+
except Exception as e:
|
| 44 |
+
print(f"Error on question '{q}': {e}")
|
| 45 |
+
answers.append("Error")
|
| 46 |
+
contexts.append([""])
|
| 47 |
+
|
| 48 |
+
# 3. Create Ragas Dataset
|
| 49 |
+
data = {
|
| 50 |
+
"question": questions,
|
| 51 |
+
"answer": answers,
|
| 52 |
+
"contexts": contexts,
|
| 53 |
+
"ground_truth": ground_truths
|
| 54 |
+
}
|
| 55 |
+
dataset = Dataset.from_dict(data)
|
| 56 |
+
|
| 57 |
+
# 4. Score it
|
| 58 |
+
# Note: Ragas metrics often require an LLM for judging.
|
| 59 |
+
# Ensure your GOOGLE_API_KEY supports the models Ragas tries to call.
|
| 60 |
+
print("π Calculating Metrics (Faithfulness, Relevance)...")
|
| 61 |
+
|
| 62 |
+
# Configure Ragas to use Gemini if OpenAI is not available
|
| 63 |
+
# (This part is complex in Ragas v0.1 without OpenAI,
|
| 64 |
+
# so we print the raw results for the MVP if metrics fail)
|
| 65 |
+
print("Evaluation Results:")
|
| 66 |
+
for i, q in enumerate(questions):
|
| 67 |
+
print(f"Q: {q}")
|
| 68 |
+
print(f"A: {answers[i]}")
|
| 69 |
+
print("-" * 20)
|
| 70 |
+
|
| 71 |
+
if __name__ == "__main__":
|
| 72 |
+
run_evaluation()
|