areen09 commited on
Commit
d9cd5c8
·
verified ·
1 Parent(s): 1ffdd60

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile +22 -0
  2. agent.py +98 -0
  3. main.py +56 -0
  4. requirements.txt +18 -0
  5. 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
+ )