Upload 5 files
Browse files- Dockerfile +22 -0
- agent.py +98 -0
- main.py +56 -0
- requirements.txt +18 -0
- utils.py +86 -0
Dockerfile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM PYTHON:3.13.3
|
| 2 |
+
|
| 3 |
+
# Set the working directory
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
# Copy the requirements file into the container
|
| 6 |
+
COPY requirements.txt /app/requirements.txt
|
| 7 |
+
# Install the required packages
|
| 8 |
+
RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
|
| 9 |
+
|
| 10 |
+
RUN useradd user
|
| 11 |
+
|
| 12 |
+
USER user
|
| 13 |
+
|
| 14 |
+
ENV HOME=/home/user \
|
| 15 |
+
PATH=/home/user/.local/bin:$PATH
|
| 16 |
+
|
| 17 |
+
WORKDIR $HOME/app
|
| 18 |
+
|
| 19 |
+
COPY --chown=user . $HOME/app
|
| 20 |
+
|
| 21 |
+
# Expose the port the app runs on
|
| 22 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0", "--port", "7680", "--reload"]
|
agent.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import os
|
| 3 |
+
from typing import TypedDict, Annotated, List
|
| 4 |
+
from langchain_core.messages import BaseMessage, ToolMessage
|
| 5 |
+
from langchain_core.prompts import ChatPromptTemplate
|
| 6 |
+
from langchain_core.output_parsers import StrOutputParser
|
| 7 |
+
from langchain_core.tools import tool
|
| 8 |
+
from langchain_groq import ChatGroq
|
| 9 |
+
from langgraph.graph import StateGraph, END
|
| 10 |
+
from langgraph.prebuilt import ToolNode
|
| 11 |
+
from utils import process_documents
|
| 12 |
+
from dotenv import load_dotenv
|
| 13 |
+
|
| 14 |
+
load_dotenv()
|
| 15 |
+
|
| 16 |
+
# The retriever will be set at runtime from the FastAPI endpoint
|
| 17 |
+
retriever_for_tool = None
|
| 18 |
+
|
| 19 |
+
class AgentState(TypedDict):
|
| 20 |
+
messages: Annotated[List[BaseMessage], lambda x, y: x + y]
|
| 21 |
+
question: str
|
| 22 |
+
answer: str
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
# This setter will be called from FastAPI to set the retriever for each request
|
| 26 |
+
def set_retriever_for_tool(retriever):
|
| 27 |
+
global retriever_for_tool
|
| 28 |
+
retriever_for_tool = retriever
|
| 29 |
+
|
| 30 |
+
@tool
|
| 31 |
+
def retrieve_clauses(query: str) -> str:
|
| 32 |
+
"""
|
| 33 |
+
Searches the policy document for relevant clauses based on a user's query.
|
| 34 |
+
Use this tool to find information within the document to answer a question.
|
| 35 |
+
"""
|
| 36 |
+
global retriever_for_tool
|
| 37 |
+
if retriever_for_tool is None:
|
| 38 |
+
return "No retriever set. Please process the document first."
|
| 39 |
+
retrieved_docs = retriever_for_tool.invoke(query)
|
| 40 |
+
context = "\n\n---\n\n".join([doc.page_content for doc in retrieved_docs])
|
| 41 |
+
return context
|
| 42 |
+
|
| 43 |
+
llm = ChatGroq(model="meta-llama/llama-4-scout-17b-16e-instruct", temperature=0, max_tokens=1024)
|
| 44 |
+
tools = [retrieve_clauses]
|
| 45 |
+
llm_with_tools = llm.bind_tools(tools)
|
| 46 |
+
|
| 47 |
+
final_answer_prompt = ChatPromptTemplate.from_template(
|
| 48 |
+
"""
|
| 49 |
+
You are an expert in policy and contract analysis. Your task is to answer the user's question in a complete, self-contained sentence, using only the provided context. If the context does not contain the answer, say so explicitly.
|
| 50 |
+
|
| 51 |
+
Always provide a full, context-rich answer, not just a short phrase or fragment. Your answer should be clear, precise, and directly address the user's question, restating key details as needed for clarity.
|
| 52 |
+
|
| 53 |
+
Do NOT answer with just a number, time period, or short phrase. Instead, write a full sentence that would make sense as a stand-alone answer in a JSON API response.
|
| 54 |
+
|
| 55 |
+
Context from the document:
|
| 56 |
+
---
|
| 57 |
+
{context}
|
| 58 |
+
---
|
| 59 |
+
|
| 60 |
+
User's Question: {question}
|
| 61 |
+
|
| 62 |
+
Your Answer:
|
| 63 |
+
"""
|
| 64 |
+
)
|
| 65 |
+
string_parser = StrOutputParser()
|
| 66 |
+
generation_chain = final_answer_prompt | llm | string_parser
|
| 67 |
+
|
| 68 |
+
def call_model(state: AgentState):
|
| 69 |
+
messages = state["messages"]
|
| 70 |
+
response = llm_with_tools.invoke(messages)
|
| 71 |
+
return {"messages": [response]}
|
| 72 |
+
|
| 73 |
+
def should_continue(state: AgentState):
|
| 74 |
+
if state["messages"][-1].tool_calls:
|
| 75 |
+
return "continue"
|
| 76 |
+
return "end"
|
| 77 |
+
|
| 78 |
+
def generate_final_answer(state: AgentState):
|
| 79 |
+
question = state["question"]
|
| 80 |
+
context = ""
|
| 81 |
+
for msg in reversed(state["messages"]):
|
| 82 |
+
if isinstance(msg, ToolMessage):
|
| 83 |
+
context = msg.content
|
| 84 |
+
break
|
| 85 |
+
response = generation_chain.invoke({"context": context, "question": question})
|
| 86 |
+
return {"answer": response}
|
| 87 |
+
|
| 88 |
+
workflow = StateGraph(AgentState)
|
| 89 |
+
workflow.add_node("agent", call_model)
|
| 90 |
+
workflow.add_node("action", ToolNode(tools))
|
| 91 |
+
workflow.add_node("final_answer", generate_final_answer)
|
| 92 |
+
|
| 93 |
+
workflow.set_entry_point("agent")
|
| 94 |
+
workflow.add_conditional_edges("agent", should_continue, {"continue": "action", "end": "final_answer"})
|
| 95 |
+
workflow.add_edge("action", "agent")
|
| 96 |
+
workflow.add_edge("final_answer", END)
|
| 97 |
+
|
| 98 |
+
agent_app = workflow.compile()
|
main.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import warnings
|
| 2 |
+
warnings.filterwarnings("ignore", category=FutureWarning)
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
from typing import List
|
| 6 |
+
from fastapi import FastAPI, Depends, HTTPException, status
|
| 7 |
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
| 8 |
+
from pydantic import BaseModel, HttpUrl
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
from agent import agent_app, set_retriever_for_tool
|
| 11 |
+
|
| 12 |
+
load_dotenv()
|
| 13 |
+
|
| 14 |
+
EXPECTED_TOKEN = os.getenv("BEARER_TOKEN")
|
| 15 |
+
http_bearer_scheme = HTTPBearer()
|
| 16 |
+
|
| 17 |
+
async def verify_token(credentials: HTTPAuthorizationCredentials = Depends(http_bearer_scheme)):
|
| 18 |
+
if not credentials or credentials.scheme != "Bearer":
|
| 19 |
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid authentication scheme.")
|
| 20 |
+
if credentials.credentials != EXPECTED_TOKEN:
|
| 21 |
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token.")
|
| 22 |
+
return credentials.credentials
|
| 23 |
+
|
| 24 |
+
class QueryRequest(BaseModel):
|
| 25 |
+
documents: HttpUrl
|
| 26 |
+
questions: List[str]
|
| 27 |
+
|
| 28 |
+
class QueryResponse(BaseModel):
|
| 29 |
+
answers: List[str]
|
| 30 |
+
|
| 31 |
+
app = FastAPI(
|
| 32 |
+
title="Intelligent Query-Retrieval System",
|
| 33 |
+
description="An API for processing documents and answering questions using a LangGraph ReAct agent.",
|
| 34 |
+
version="1.0.0"
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
@app.post("/hackrx/run", response_model=QueryResponse, tags=["Query"])
|
| 38 |
+
async def run_submission(request: QueryRequest, token: str = Depends(verify_token)):
|
| 39 |
+
try:
|
| 40 |
+
# Process the document ONCE per request
|
| 41 |
+
from utils import process_documents
|
| 42 |
+
vector_store = process_documents(str(request.documents))
|
| 43 |
+
retriever = vector_store.as_retriever(search_kwargs={"k": 5})
|
| 44 |
+
set_retriever_for_tool(retriever)
|
| 45 |
+
responses = []
|
| 46 |
+
for question in request.questions:
|
| 47 |
+
final_state = agent_app.invoke({
|
| 48 |
+
"question": question,
|
| 49 |
+
"messages": [("user", question)]
|
| 50 |
+
})
|
| 51 |
+
answer_str = final_state.get("answer", "Error: Could not generate an answer.")
|
| 52 |
+
responses.append(answer_str)
|
| 53 |
+
return QueryResponse(answers=responses)
|
| 54 |
+
except Exception as e:
|
| 55 |
+
print(f"An error occurred: {e}")
|
| 56 |
+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
requirements.txt
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
python-dotenv
|
| 2 |
+
fastapi
|
| 3 |
+
uvicorn[standard]
|
| 4 |
+
pydantic
|
| 5 |
+
pinecone
|
| 6 |
+
langchain-pinecone
|
| 7 |
+
langchain-core
|
| 8 |
+
langchain
|
| 9 |
+
langchain-groq
|
| 10 |
+
requests
|
| 11 |
+
langchain-community
|
| 12 |
+
ipykernel
|
| 13 |
+
langgraph
|
| 14 |
+
langchain-huggingface
|
| 15 |
+
python-docx
|
| 16 |
+
sentence-transformers
|
| 17 |
+
pypdf
|
| 18 |
+
tiktoken
|
utils.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import requests
|
| 3 |
+
import tempfile
|
| 4 |
+
from typing import List
|
| 5 |
+
from langchain_community.document_loaders import PyPDFLoader
|
| 6 |
+
from langchain_huggingface import HuggingFaceEmbeddings
|
| 7 |
+
from langchain_pinecone import PineconeVectorStore
|
| 8 |
+
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
| 9 |
+
from langchain_core.documents import Document
|
| 10 |
+
from langchain_core.tools import Tool
|
| 11 |
+
from pinecone import Pinecone, ServerlessSpec
|
| 12 |
+
from docx import Document as DocxDocument
|
| 13 |
+
from dotenv import load_dotenv
|
| 14 |
+
|
| 15 |
+
load_dotenv()
|
| 16 |
+
|
| 17 |
+
PINECONE_API_KEY = os.getenv("PINECONE_API_KEY")
|
| 18 |
+
PINECONE_INDEX_NAME = os.getenv("document-processor-index-1") or "policy-index"
|
| 19 |
+
|
| 20 |
+
pc = Pinecone(api_key=PINECONE_API_KEY)
|
| 21 |
+
embeddings_model = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
|
| 22 |
+
|
| 23 |
+
def download_file(url: str) -> str:
|
| 24 |
+
try:
|
| 25 |
+
response = requests.get(url)
|
| 26 |
+
response.raise_for_status()
|
| 27 |
+
suffix = ".pdf" if ".pdf" in url else ".docx"
|
| 28 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as temp_file:
|
| 29 |
+
temp_file.write(response.content)
|
| 30 |
+
return temp_file.name
|
| 31 |
+
except requests.RequestException as e:
|
| 32 |
+
print(f"Error downloading file from {url}: {e}")
|
| 33 |
+
raise
|
| 34 |
+
|
| 35 |
+
def load_docx_documents(file_path: str) -> List[Document]:
|
| 36 |
+
doc = DocxDocument(file_path)
|
| 37 |
+
full_text = [para.text for para in doc.paragraphs if para.text.strip()]
|
| 38 |
+
content = "\n".join(full_text)
|
| 39 |
+
return [Document(page_content=content)]
|
| 40 |
+
|
| 41 |
+
def process_documents(document_url: str) -> PineconeVectorStore:
|
| 42 |
+
print("Starting data ingestion pipeline...")
|
| 43 |
+
temp_file_path = download_file(document_url)
|
| 44 |
+
|
| 45 |
+
if temp_file_path.endswith(".pdf"):
|
| 46 |
+
loader = PyPDFLoader(temp_file_path)
|
| 47 |
+
docs = loader.load()
|
| 48 |
+
elif temp_file_path.endswith(".docx"):
|
| 49 |
+
docs = load_docx_documents(temp_file_path)
|
| 50 |
+
else:
|
| 51 |
+
raise ValueError("Unsupported document format provided.")
|
| 52 |
+
|
| 53 |
+
print(f"Loaded {len(docs)} document pages.")
|
| 54 |
+
|
| 55 |
+
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
|
| 56 |
+
chunked_docs = text_splitter.split_documents(docs)
|
| 57 |
+
print(f"Split document into {len(chunked_docs)} chunks.")
|
| 58 |
+
|
| 59 |
+
if PINECONE_INDEX_NAME not in pc.list_indexes().names():
|
| 60 |
+
print(f"Creating new Pinecone index: {PINECONE_INDEX_NAME}")
|
| 61 |
+
pc.create_index(
|
| 62 |
+
name=PINECONE_INDEX_NAME,
|
| 63 |
+
dimension=384,
|
| 64 |
+
metric="cosine",
|
| 65 |
+
spec=ServerlessSpec(cloud="aws", region="us-east-1")
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
print("Upserting document chunks to Pinecone...")
|
| 69 |
+
vector_store = PineconeVectorStore.from_documents(
|
| 70 |
+
chunked_docs, embeddings_model, index_name=PINECONE_INDEX_NAME
|
| 71 |
+
)
|
| 72 |
+
print("Document processing complete.")
|
| 73 |
+
|
| 74 |
+
os.remove(temp_file_path)
|
| 75 |
+
return vector_store
|
| 76 |
+
|
| 77 |
+
def create_retriever_tool(vector_store: PineconeVectorStore) -> Tool:
|
| 78 |
+
retriever = vector_store.as_retriever(search_kwargs={"k": 5})
|
| 79 |
+
def retrieve_context(query: str) -> str:
|
| 80 |
+
retrieved_docs = retriever.invoke(query)
|
| 81 |
+
return "\n\n---\n\n".join([doc.page_content for doc in retrieved_docs])
|
| 82 |
+
return Tool(
|
| 83 |
+
name="document_retriever",
|
| 84 |
+
func=retrieve_context,
|
| 85 |
+
description="Use this tool to retrieve relevant context from the policy document to answer a user's question."
|
| 86 |
+
)
|