Abeshith commited on
Commit
6c58cf4
·
0 Parent(s):

Initial deployment to Hugging Face Spaces

Browse files
.dockerignore ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+ .Python
6
+ build/
7
+ develop-eggs/
8
+ dist/
9
+ downloads/
10
+ eggs/
11
+ .eggs/
12
+ lib/
13
+ lib64/
14
+ parts/
15
+ sdist/
16
+ var/
17
+ wheels/
18
+ *.egg-info/
19
+ .installed.cfg
20
+ *.egg
21
+
22
+ .env
23
+ .venv
24
+ env/
25
+ venv/
26
+ ENV/
27
+ env.bak/
28
+ venv.bak/
29
+
30
+ logs/
31
+ *.log
32
+
33
+ .DS_Store
34
+ .vscode/
35
+ .idea/
36
+
37
+ faiss_index/
38
+ *.faiss
39
+
40
+ workflow.png
Dockerfile ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY pyproject.toml ./
6
+ COPY requirements.txt* ./
7
+
8
+ RUN pip install --no-cache-dir uv && \
9
+ uv pip install --system --no-cache -r pyproject.toml
10
+
11
+ COPY . .
12
+
13
+ EXPOSE 8000
14
+
15
+ ENV PYTHONUNBUFFERED=1
16
+
17
+ CMD ["python", "app.py"]
README.md ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: RAG Project - Learn with Transformers
3
+ emoji: 🤖
4
+ colorFrom: purple
5
+ colorTo: blue
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ # RAG Project - Learn with Transformers
11
+
12
+ A production-ready Corrective Retrieval-Augmented Generation (CRAG) system built with LangChain, LangGraph, and FastAPI.
13
+
14
+ ## Features
15
+
16
+ - **Intelligent Document Grading**: LLM evaluates retrieved documents for relevance
17
+ - **Query Transformation**: Rewrites queries for better retrieval
18
+ - **Web Search Fallback**: Tavily API integration when local docs insufficient
19
+ - **Advanced Retrieval**: FAISS + FastEmbed + FlashRank reranking
20
+ - **Agent Workflow**: LangGraph state machine with conditional routing
21
+
22
+ ## Tech Stack
23
+
24
+ - **LLM**: Groq (openai/gpt-oss-120b)
25
+ - **Embeddings**: FastEmbed (BAAI/bge-small-en-v1.5)
26
+ - **Vector Store**: FAISS
27
+ - **Reranker**: FlashRank (rank-T5-flan)
28
+ - **Framework**: LangChain 0.3.x + LangGraph
29
+ - **Web**: FastAPI + Uvicorn
30
+
31
+ ## Environment Variables
32
+
33
+ Required secrets (set in Space Settings):
34
+ - `GROQ_API_KEY`
35
+ - `GOOGLE_API_KEY`
36
+ - `LANGSMITH_API_KEY` (optional)
37
+ - `TAVILY_API_KEY` (optional)
38
+
39
+ ## How It Works
40
+
41
+ 1. User query → FAISS retrieval with MMR
42
+ 2. FlashRank reranking
43
+ 3. LLM grades document relevance
44
+ 4. If poor quality → Transform query + Web search
45
+ 5. Generate answer with Groq LLM
46
+
47
+ The app will automatically download "Attention Is All You Need" paper from ArXiv on first run.
app.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Request, Form
2
+ from fastapi.responses import HTMLResponse
3
+ from fastapi.staticfiles import StaticFiles
4
+ from fastapi.templating import Jinja2Templates
5
+ from project.pipeline.agents import AgentWorkflow
6
+ from project.logger.logging import get_logger
7
+ import uvicorn
8
+
9
+ logger = get_logger(__name__)
10
+
11
+ app = FastAPI(title="Learn with Transformers")
12
+
13
+ app.mount("/static", StaticFiles(directory="static"), name="static")
14
+ templates = Jinja2Templates(directory="templates")
15
+
16
+ agent = None
17
+
18
+
19
+ @app.on_event("startup")
20
+ async def startup_event():
21
+ global agent
22
+ logger.info("Initializing RAG pipeline...")
23
+ agent = AgentWorkflow()
24
+ agent.setup(use_attention_paper=True)
25
+ logger.info("RAG pipeline ready")
26
+
27
+
28
+ @app.get("/", response_class=HTMLResponse)
29
+ async def home(request: Request):
30
+ return templates.TemplateResponse("index.html", {"request": request})
31
+
32
+
33
+ @app.post("/search", response_class=HTMLResponse)
34
+ async def search(request: Request, query: str = Form(...)):
35
+ if not query.strip():
36
+ return templates.TemplateResponse(
37
+ "index.html",
38
+ {"request": request, "error": "Please enter a question"}
39
+ )
40
+
41
+ try:
42
+ answer = agent.run(query)
43
+ return templates.TemplateResponse(
44
+ "index.html",
45
+ {"request": request, "query": query, "answer": answer}
46
+ )
47
+ except Exception as e:
48
+ logger.error(f"Search failed: {str(e)}")
49
+ return templates.TemplateResponse(
50
+ "index.html",
51
+ {"request": request, "error": f"Error: {str(e)}"}
52
+ )
53
+
54
+
55
+ if __name__ == "__main__":
56
+ uvicorn.run(app, host="0.0.0.0", port=8000)
project/__init__.py ADDED
File without changes
project/config/config.yaml ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ embedding_model:
2
+ provider: "fastembedding"
3
+ model_name : "BAAI/bge-small-en-v1.5"
4
+
5
+ retriever:
6
+ search_type: "mmr"
7
+ top_k: 3
8
+
9
+ llm:
10
+ provider: "langchain_groq"
11
+ model: "openai/gpt-oss-120b"
12
+ temperature: 0.1
13
+ max_tokens: 2048
14
+
15
+ reranker:
16
+ model_name: "rank-T5-flan"
17
+ top_k: 3
18
+ cache_dir: null
19
+
20
+
project/exception/__init__.py ADDED
File without changes
project/exception/except.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import traceback
3
+ from typing import Optional
4
+
5
+
6
+ class CustomException(Exception):
7
+ def __init__(self, error_message: str, error_detail: Optional[sys.exc_info] = None):
8
+ super().__init__(error_message)
9
+ self.error_message = error_message
10
+
11
+ if error_detail:
12
+ _, _, exc_tb = error_detail
13
+ self.error_message = self._get_detailed_error_message(error_message, exc_tb)
14
+
15
+ def _get_detailed_error_message(self, error_message: str, exc_tb) -> str:
16
+ file_name = exc_tb.tb_frame.f_code.co_filename
17
+ line_number = exc_tb.tb_lineno
18
+
19
+ return f"Error in [{file_name}] at line [{line_number}]: {error_message}"
20
+
21
+ def __str__(self):
22
+ return self.error_message
23
+
24
+ def __repr__(self):
25
+ return f"{self.__class__.__name__}('{self.error_message}')"
26
+
project/logger/__init__.py ADDED
File without changes
project/logger/logging.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ from datetime import datetime
4
+
5
+ LOG_DIR = "logs"
6
+ os.makedirs(LOG_DIR, exist_ok=True)
7
+
8
+ LOG_FILE = os.path.join(LOG_DIR, f"{datetime.now().strftime('%Y_%m_%d')}.log")
9
+
10
+ logging.basicConfig(
11
+ level=logging.INFO,
12
+ format="[%(asctime)s] %(levelname)s - %(name)s - %(message)s",
13
+ datefmt="%Y-%m-%d %H:%M:%S",
14
+ handlers=[
15
+ logging.FileHandler(LOG_FILE),
16
+ logging.StreamHandler()
17
+ ]
18
+ )
19
+
20
+ def get_logger(name: str) -> logging.Logger:
21
+ return logging.getLogger(name)
project/model/__init__.py ADDED
File without changes
project/model/reranking.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List
2
+ from langchain.schema import Document
3
+ from flashrank.Ranker import Ranker, RerankRequest
4
+ from project.utils.config_loader import load_config
5
+ from project.logger.logging import get_logger
6
+
7
+ logger = get_logger(__name__)
8
+
9
+
10
+ class DocumentReranker:
11
+
12
+ def __init__(self, config_path: str = None):
13
+ self.config = load_config(config_path)
14
+ reranker_config = self.config.get('reranker', {})
15
+ model_name = reranker_config.get('model_name', 'rank-T5-flan')
16
+ cache_dir = reranker_config.get('cache_dir')
17
+ self.top_k = reranker_config.get('top_k', 3)
18
+
19
+ if cache_dir:
20
+ self.ranker = Ranker(model_name=model_name, cache_dir=cache_dir)
21
+ else:
22
+ self.ranker = Ranker(model_name=model_name)
23
+
24
+ logger.info(f"FlashRank reranker initialized with model: {model_name}")
25
+
26
+ def rerank(
27
+ self,
28
+ query: str,
29
+ documents: List[Document],
30
+ top_k: int = None
31
+ ) -> List[Document]:
32
+
33
+ if top_k is None:
34
+ top_k = self.top_k
35
+
36
+ if not documents:
37
+ logger.warning("No documents to rerank")
38
+ return []
39
+
40
+ passages = [
41
+ {
42
+ "id": i,
43
+ "text": doc.page_content,
44
+ "meta": doc.metadata
45
+ }
46
+ for i, doc in enumerate(documents)
47
+ ]
48
+
49
+ rerank_request = RerankRequest(query=query, passages=passages)
50
+
51
+ results = self.ranker.rerank(rerank_request)
52
+
53
+ reranked_docs = []
54
+ for result in results[:top_k]:
55
+ doc_idx = result["id"]
56
+ original_doc = documents[doc_idx]
57
+ original_doc.metadata["rerank_score"] = result["score"]
58
+ reranked_docs.append(original_doc)
59
+
60
+ logger.info(f"Reranked {len(documents)} documents, returning top {len(reranked_docs)}")
61
+ return reranked_docs
project/model/retriever.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Optional
2
+ from langchain.schema import Document
3
+ from langchain_community.vectorstores import FAISS
4
+ from langchain.chains.query_constructor.base import AttributeInfo
5
+ from langchain.retrievers.self_query.base import SelfQueryRetriever
6
+ from project.utils.model_loader import ModelLoader
7
+ from project.utils.config_loader import load_config
8
+ from project.logger.logging import get_logger
9
+
10
+ logger = get_logger(__name__)
11
+
12
+ class DocumentRetriever:
13
+ def __init__(self, config_path: str = None):
14
+ self.config = load_config(config_path)
15
+ self.model_loader = ModelLoader(config_path)
16
+ self.embeddings = self.model_loader.load_embeddings()
17
+ self.llm = self.model_loader.load_llm()
18
+ self.vectorstore = None
19
+ self.retriever = None
20
+ logger.info("DocumentRetriever initialized")
21
+
22
+ def create_vectorstore(self, documents: List[Document]) -> FAISS:
23
+ self.vectorstore = FAISS.from_documents(documents, self.embeddings)
24
+ logger.info(f"Vector store created with {len(documents)} documents")
25
+ return self.vectorstore
26
+
27
+ def setup_self_query_retriever(
28
+ self,
29
+ document_content_description: str = "Research papers and technical documents",
30
+ metadata_field_info: Optional[List[AttributeInfo]] = None
31
+ ):
32
+ if self.vectorstore is None:
33
+ raise ValueError("Vector store not initialized. Call create_vectorstore first.")
34
+
35
+ if metadata_field_info is None:
36
+ metadata_field_info = [
37
+ AttributeInfo(
38
+ name="source",
39
+ description="The source file or document name",
40
+ type="string"
41
+ ),
42
+ AttributeInfo(
43
+ name="page",
44
+ description="The page number in the document",
45
+ type="integer"
46
+ )
47
+ ]
48
+
49
+ retriever_config = self.config.get('retriever', {})
50
+
51
+ self.retriever = SelfQueryRetriever.from_llm(
52
+ llm=self.llm,
53
+ vectorstore=self.vectorstore,
54
+ document_contents=document_content_description,
55
+ metadata_field_info=metadata_field_info,
56
+ search_kwargs={
57
+ 'k': retriever_config.get('top_k', 3)
58
+ },
59
+ enable_limit=True
60
+ )
61
+
62
+ logger.info("Self-query retriever configured")
63
+ return self.retriever
64
+
65
+ def retrieve(self, query: str) -> List[Document]:
66
+ if self.retriever is None:
67
+ raise ValueError("Retriever not initialized. Call setup_self_query_retriever first.")
68
+
69
+ documents = self.retriever.invoke(query)
70
+ logger.info(f"Retrieved {len(documents)} documents for query")
71
+ return documents
72
+
73
+ def get_base_retriever(self):
74
+ if self.vectorstore is None:
75
+ raise ValueError("Vector store not initialized.")
76
+
77
+ retriever_config = self.config.get('retriever', {})
78
+ search_type = retriever_config.get('search_type', 'similarity')
79
+ top_k = retriever_config.get('top_k', 3)
80
+
81
+ if search_type == 'mmr':
82
+ self.retriever = self.vectorstore.as_retriever(
83
+ search_type='mmr',
84
+ search_kwargs={'k': top_k, 'fetch_k': top_k * 2}
85
+ )
86
+ else:
87
+ self.retriever = self.vectorstore.as_retriever(
88
+ search_type='similarity',
89
+ search_kwargs={'k': top_k}
90
+ )
91
+
92
+ logger.info(f"Base retriever configured with {search_type} search")
93
+ return self.retriever
project/pipeline/__init__.py ADDED
File without changes
project/pipeline/agents.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import List, Literal
3
+ from typing_extensions import TypedDict
4
+ from pydantic import BaseModel, Field
5
+ from langchain.schema import Document
6
+ from langchain_core.output_parsers import StrOutputParser
7
+ from langgraph.graph import END, StateGraph, START
8
+ from project.pipeline.rag import RAGPipeline
9
+ from project.utils.model_loader import ModelLoader
10
+ from project.prompts.prompt_template import ROUTER_PROMPT, WEB_SEARCH_PROMPT
11
+ from project.logger.logging import get_logger
12
+
13
+ logger = get_logger(__name__)
14
+
15
+
16
+ class GradeDocuments(BaseModel):
17
+ binary_score: str = Field(description="Documents are relevant to the question, 'yes' or 'no'")
18
+
19
+
20
+ class GraphState(TypedDict):
21
+ question: str
22
+ generation: str
23
+ web_search: str
24
+ documents: List[str]
25
+
26
+
27
+ class AgentWorkflow:
28
+
29
+ def __init__(self, config_path: str = None):
30
+ self.config_path = config_path
31
+ self.model_loader = ModelLoader(config_path)
32
+ self.llm = self.model_loader.load_llm()
33
+ self.rag_pipeline = RAGPipeline(config_path)
34
+ self.web_search_tool = None
35
+ self._setup_web_search()
36
+ self.workflow = None
37
+ self.app = None
38
+ self._setup_graders()
39
+ logger.info("AgentWorkflow initialized")
40
+
41
+ def _setup_web_search(self):
42
+ tavily_key = os.getenv("TAVILY_API_KEY")
43
+ if tavily_key:
44
+ try:
45
+ from langchain_community.tools.tavily_search import TavilySearchResults
46
+ self.web_search_tool = TavilySearchResults(k=3)
47
+ logger.info("Web search tool initialized")
48
+ except Exception as e:
49
+ logger.warning(f"Could not initialize web search: {str(e)}")
50
+ self.web_search_tool = None
51
+ else:
52
+ logger.warning("TAVILY_API_KEY not found, web search disabled")
53
+
54
+ def _setup_graders(self):
55
+ grade_prompt = """You are a grader assessing relevance of a retrieved document to a user question.
56
+ If the document contains keywords or semantic meaning related to the question, grade it as relevant.
57
+ Give ONLY a binary score 'yes' or 'no' to indicate whether the document is relevant to the question.
58
+
59
+ Retrieved document: {document}
60
+ User question: {question}
61
+
62
+ Answer (yes or no):"""
63
+
64
+ self.grade_prompt_text = grade_prompt
65
+ self.retrieval_grader = self.llm | StrOutputParser()
66
+
67
+ rewrite_prompt = """You are a question re-writer that converts an input question to a better version optimized for web search.
68
+ Look at the input and try to reason about the underlying semantic intent/meaning.
69
+ Provide only the improved question without any explanation.
70
+
71
+ Initial question: {question}
72
+
73
+ Improved question:"""
74
+
75
+ self.rewrite_prompt_text = rewrite_prompt
76
+ self.question_rewriter = self.llm | StrOutputParser()
77
+
78
+ def setup(self, pdf_path: str = None, use_attention_paper: bool = True):
79
+ self.rag_pipeline.setup(pdf_path=pdf_path, use_attention_paper=use_attention_paper)
80
+ self._build_graph()
81
+ logger.info("Agent workflow setup complete")
82
+
83
+ def retrieve(self, state: GraphState):
84
+ logger.info("---RETRIEVE---")
85
+ question = state["question"]
86
+ documents = self.rag_pipeline.retriever.invoke(question)
87
+ return {"documents": documents, "question": question}
88
+
89
+ def grade_documents(self, state: GraphState):
90
+ logger.info("---CHECK DOCUMENT RELEVANCE TO QUESTION---")
91
+ question = state["question"]
92
+ documents = state["documents"]
93
+
94
+ filtered_docs = []
95
+ web_search = "No"
96
+
97
+ for d in documents:
98
+ prompt_filled = self.grade_prompt_text.format(
99
+ document=d.page_content[:500],
100
+ question=question
101
+ )
102
+ score = self.retrieval_grader.invoke(prompt_filled)
103
+ grade = score.strip().lower()
104
+
105
+ if "yes" in grade:
106
+ logger.info("---GRADE: DOCUMENT RELEVANT---")
107
+ filtered_docs.append(d)
108
+ else:
109
+ logger.info("---GRADE: DOCUMENT NOT RELEVANT---")
110
+ web_search = "Yes"
111
+
112
+ return {"documents": filtered_docs, "question": question, "web_search": web_search}
113
+
114
+ def generate(self, state: GraphState):
115
+ logger.info("---GENERATE---")
116
+ question = state["question"]
117
+ documents = state["documents"]
118
+
119
+ generation = self.rag_pipeline.chain.invoke({"question": question})
120
+ return {"documents": documents, "question": question, "generation": generation}
121
+
122
+ def transform_query(self, state: GraphState):
123
+ logger.info("---TRANSFORM QUERY---")
124
+ question = state["question"]
125
+ documents = state["documents"]
126
+
127
+ prompt_filled = self.rewrite_prompt_text.format(question=question)
128
+ better_question = self.question_rewriter.invoke(prompt_filled)
129
+
130
+ return {"documents": documents, "question": better_question}
131
+
132
+ def web_search(self, state: GraphState):
133
+ logger.info("---WEB SEARCH---")
134
+ question = state["question"]
135
+ documents = state["documents"]
136
+
137
+ if self.web_search_tool is None:
138
+ logger.warning("Web search tool not available, skipping")
139
+ return {"documents": documents, "question": question}
140
+
141
+ try:
142
+ response = self.web_search_tool.invoke({"query": question})
143
+
144
+ if not response:
145
+ logger.warning("No results from web search")
146
+ return {"documents": documents, "question": question}
147
+
148
+ web_results = "\n".join([d["content"] for d in response if "content" in d])
149
+ web_doc = Document(page_content=web_results)
150
+ documents.append(web_doc)
151
+ except Exception as e:
152
+ logger.error(f"Web search failed: {str(e)}")
153
+
154
+ return {"documents": documents, "question": question}
155
+
156
+ def decide_to_generate(self, state: GraphState) -> Literal["transform_query", "generate"]:
157
+ logger.info("---ASSESS GRADED DOCUMENTS---")
158
+ documents = state.get("documents", [])
159
+
160
+ if len(documents) == 0:
161
+ logger.info("---DECISION: NO RELEVANT DOCUMENTS, TRANSFORM QUERY---")
162
+ return "transform_query"
163
+ else:
164
+ logger.info("---DECISION: RELEVANT DOCUMENTS FOUND, GENERATE---")
165
+ return "generate"
166
+
167
+ def _build_graph(self):
168
+ workflow = StateGraph(GraphState)
169
+
170
+ workflow.add_node("retrieve", self.retrieve)
171
+ workflow.add_node("grade_documents", self.grade_documents)
172
+ workflow.add_node("generate", self.generate)
173
+ workflow.add_node("transform_query", self.transform_query)
174
+ workflow.add_node("web_search", self.web_search)
175
+
176
+ workflow.add_edge(START, "retrieve")
177
+ workflow.add_edge("retrieve", "grade_documents")
178
+ workflow.add_conditional_edges(
179
+ "grade_documents",
180
+ self.decide_to_generate,
181
+ {
182
+ "transform_query": "transform_query",
183
+ "generate": "generate",
184
+ },
185
+ )
186
+ workflow.add_edge("transform_query", "web_search")
187
+ workflow.add_edge("web_search", "generate")
188
+ workflow.add_edge("generate", END)
189
+
190
+ self.app = workflow.compile()
191
+ logger.info("LangGraph workflow compiled")
192
+
193
+ def save_graph(self, output_path: str = "workflow.png"):
194
+ try:
195
+ from IPython.display import Image
196
+ graph_image = self.app.get_graph().draw_mermaid_png()
197
+ with open(output_path, "wb") as f:
198
+ f.write(graph_image)
199
+ logger.info(f"Workflow graph saved to {output_path}")
200
+ except Exception as e:
201
+ logger.error(f"Failed to save graph: {str(e)}")
202
+
203
+ def run(self, question: str) -> str:
204
+ if self.app is None:
205
+ raise ValueError("Workflow not setup. Call setup() first.")
206
+
207
+ inputs = {"question": question}
208
+
209
+ for output in self.app.stream(inputs):
210
+ for key, value in output.items():
211
+ logger.info(f"Node '{key}' completed")
212
+
213
+ final_generation = value.get("generation", "No answer generated")
214
+ return final_generation
project/pipeline/rag.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Dict, Any
2
+ from langchain.schema import Document
3
+ from langchain_core.output_parsers import StrOutputParser
4
+ from langchain_core.runnables import RunnablePassthrough
5
+ from project.source.data_preparation import DataPreparation
6
+ from project.model.retriever import DocumentRetriever
7
+ from project.model.reranking import DocumentReranker
8
+ from project.utils.model_loader import ModelLoader
9
+ from project.prompts.prompt_template import RAG_PROMPT
10
+ from project.logger.logging import get_logger
11
+
12
+ logger = get_logger(__name__)
13
+
14
+
15
+ class RAGPipeline:
16
+
17
+ def __init__(self, config_path: str = None):
18
+ self.config_path = config_path
19
+ self.model_loader = ModelLoader(config_path)
20
+ self.llm = self.model_loader.load_llm()
21
+ self.data_prep = DataPreparation()
22
+ self.retriever_module = DocumentRetriever(config_path)
23
+ self.reranker = DocumentReranker(config_path)
24
+ self.chain = None
25
+ self.retriever = None
26
+ logger.info("RAGPipeline initialized")
27
+
28
+ def setup(self, pdf_path: str = None, use_attention_paper: bool = True):
29
+ chunks = self.data_prep.prepare_documents(
30
+ pdf_path=pdf_path,
31
+ use_attention_paper=use_attention_paper
32
+ )
33
+
34
+ self.retriever_module.create_vectorstore(chunks)
35
+ self.retriever = self.retriever_module.get_base_retriever()
36
+
37
+ self._build_chain()
38
+ logger.info("RAG pipeline setup complete")
39
+
40
+ def _retrieve_and_rerank(self, query: str) -> List[Document]:
41
+ retrieved_docs = self.retriever.invoke(query)
42
+ reranked_docs = self.reranker.rerank(query, retrieved_docs)
43
+ return reranked_docs
44
+
45
+ def _format_docs(self, docs: List[Document]) -> str:
46
+ return "\n\n".join([
47
+ f"Document {i+1}:\n{doc.page_content}"
48
+ for i, doc in enumerate(docs)
49
+ ])
50
+
51
+ def _build_chain(self):
52
+ self.chain = (
53
+ {
54
+ "context": lambda x: self._format_docs(
55
+ self._retrieve_and_rerank(x["question"])
56
+ ),
57
+ "question": lambda x: x["question"]
58
+ }
59
+ | RAG_PROMPT
60
+ | self.llm
61
+ | StrOutputParser()
62
+ )
63
+ logger.info("RAG chain built successfully")
64
+
65
+ def invoke(self, query: str) -> str:
66
+ if self.chain is None:
67
+ raise ValueError("Pipeline not setup. Call setup() first.")
68
+
69
+ response = self.chain.invoke({"question": query})
70
+ logger.info(f"Query processed successfully")
71
+ return response
72
+
73
+ def get_retrieved_documents(self, query: str) -> List[Document]:
74
+ if self.retriever is None:
75
+ raise ValueError("Pipeline not setup. Call setup() first.")
76
+
77
+ return self._retrieve_and_rerank(query)
project/prompts/__init__.py ADDED
File without changes
project/prompts/prompt_template.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_core.prompts import ChatPromptTemplate
2
+
3
+
4
+ RAG_PROMPT = ChatPromptTemplate.from_messages([
5
+ ("system", """You are an expert AI assistant specializing in analyzing and answering questions about research papers and technical documents.
6
+
7
+ Your task is to provide accurate, detailed, and well-structured answers based solely on the provided context.
8
+
9
+ Guidelines:
10
+ - Answer questions using ONLY the information from the provided context
11
+ - If the answer cannot be found in the context, clearly state "I cannot find this information in the provided documents"
12
+ - Provide specific details, citations, and examples when available in the context
13
+ - Structure your responses clearly with proper formatting
14
+ - If the context contains relevant equations, formulas, or technical details, include them in your answer
15
+ - Be concise but comprehensive
16
+ - Maintain technical accuracy
17
+
18
+ Context:
19
+ {context}
20
+
21
+ Question: {question}
22
+
23
+ Answer:"""),
24
+ ])
25
+
26
+
27
+ ROUTER_PROMPT = ChatPromptTemplate.from_messages([
28
+ ("system", """You are an intelligent routing assistant that determines the best source to answer a user's question.
29
+
30
+ Analyze the user's question and decide:
31
+ 1. "rag" - If the question is about technical documents, research papers, or specific domain knowledge that would be in stored documents
32
+ 2. "websearch" - If the question requires current information, recent events, real-time data, or general knowledge not in documents
33
+
34
+ Your response must be either "rag" or "websearch" only.
35
+
36
+ Examples:
37
+ - "What is the attention mechanism in transformers?" → rag
38
+ - "What's the weather today?" → websearch
39
+ - "Explain the architecture described in the paper" → rag
40
+ - "Who won the latest Nobel Prize?" → websearch
41
+
42
+ Question: {question}
43
+
44
+ Route:"""),
45
+ ])
46
+
47
+
48
+ WEB_SEARCH_PROMPT = ChatPromptTemplate.from_messages([
49
+ ("system", """You are a helpful AI assistant that provides accurate and up-to-date information using web search results.
50
+
51
+ Your task is to answer the user's question based on the search results provided.
52
+
53
+ Guidelines:
54
+ - Synthesize information from multiple search results
55
+ - Provide accurate and current information
56
+ - Cite sources when relevant
57
+ - If search results are insufficient, acknowledge limitations
58
+ - Structure your response clearly
59
+
60
+ Search Results:
61
+ {search_results}
62
+
63
+ Question: {question}
64
+
65
+ Answer:"""),
66
+ ])
project/source/__init__.py ADDED
File without changes
project/source/data_preparation.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from pathlib import Path
3
+ from typing import List, Optional
4
+ from langchain_community.document_loaders import PyPDFLoader, ArxivLoader
5
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
6
+ from langchain.schema import Document
7
+ from project.logger.logging import get_logger
8
+
9
+ logger = get_logger(__name__)
10
+
11
+ class DataPreparation:
12
+ def __init__(
13
+ self,
14
+ data_dir: str = "data",
15
+ chunk_size: int = 1000,
16
+ chunk_overlap: int = 200
17
+ ):
18
+ self.data_dir = Path(data_dir)
19
+ self.data_dir.mkdir(exist_ok=True)
20
+
21
+ self.text_splitter = RecursiveCharacterTextSplitter(
22
+ chunk_size=chunk_size,
23
+ chunk_overlap=chunk_overlap,
24
+ length_function=len,
25
+ separators=["\n\n", "\n", " ", ""]
26
+ )
27
+ logger.info(f"DataPreparation initialized with chunk_size={chunk_size}")
28
+
29
+ def load_attention_paper(self, arxiv_id: str = "1706.03762") -> List[Document]:
30
+ pdf_path = self.data_dir / "attention-is-all-you-need.pdf"
31
+
32
+ if pdf_path.exists():
33
+ logger.info(f"Loading PDF from local file: {pdf_path}")
34
+ return self._load_pdf(str(pdf_path))
35
+
36
+ logger.info(f"PDF not found locally. Downloading from ArXiv: {arxiv_id}")
37
+ try:
38
+ loader = ArxivLoader(query=arxiv_id, load_max_docs=1)
39
+ documents = loader.load()
40
+ if documents:
41
+ logger.info(f"Successfully downloaded paper from ArXiv")
42
+ return documents
43
+ else:
44
+ raise ValueError("No documents returned from ArXiv")
45
+
46
+ except Exception as e:
47
+ logger.error(f"Failed to download from ArXiv: {str(e)}")
48
+ raise
49
+
50
+ def _load_pdf(self, pdf_path: str) -> List[Document]:
51
+ try:
52
+ loader = PyPDFLoader(pdf_path)
53
+ documents = loader.load()
54
+ logger.info(f"Loaded {len(documents)} pages from PDF")
55
+ return documents
56
+ except Exception as e:
57
+ logger.error(f"Failed to load PDF: {str(e)}")
58
+ raise
59
+
60
+ def load_custom_pdf(self, pdf_path: str) -> List[Document]:
61
+ if not Path(pdf_path).exists():
62
+ raise FileNotFoundError(f"PDF not found: {pdf_path}")
63
+
64
+ return self._load_pdf(pdf_path)
65
+
66
+ def split_documents(self, documents: List[Document]) -> List[Document]:
67
+ try:
68
+ chunks = self.text_splitter.split_documents(documents)
69
+ logger.info(f"Split documents into {len(chunks)} chunks")
70
+ return chunks
71
+ except Exception as e:
72
+ logger.error(f"Failed to split documents: {str(e)}")
73
+ raise
74
+
75
+ def prepare_documents(
76
+ self,
77
+ pdf_path: Optional[str] = None,
78
+ use_attention_paper: bool = True
79
+ ) -> List[Document]:
80
+ try:
81
+ if pdf_path:
82
+ documents = self.load_custom_pdf(pdf_path)
83
+ elif use_attention_paper:
84
+ documents = self.load_attention_paper()
85
+ else:
86
+ raise ValueError("Either provide pdf_path or set use_attention_paper=True")
87
+
88
+ chunks = self.split_documents(documents)
89
+
90
+ logger.info(f"Document preparation complete: {len(chunks)} chunks ready")
91
+ return chunks
92
+
93
+ except Exception as e:
94
+ logger.error(f"Document preparation failed: {str(e)}")
95
+ raise
project/utils/__init__.py ADDED
File without changes
project/utils/config_loader.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import yaml
3
+ from typing import Dict, Any
4
+
5
+
6
+ def load_config(config_path: str = None) -> Dict[str, Any]:
7
+ if config_path is None:
8
+ base_dir = Path(__file__).parent.parent
9
+ config_path = base_dir / "config" / "config.yaml"
10
+ else:
11
+ config_path = Path(config_path)
12
+
13
+ if not config_path.exists():
14
+ raise FileNotFoundError(f"Config file not found: {config_path}")
15
+
16
+ with open(config_path, 'r', encoding='utf-8') as file:
17
+ config = yaml.safe_load(file)
18
+
19
+ return config
project/utils/model_loader.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import Any
3
+ from dotenv import load_dotenv
4
+ from langchain_groq import ChatGroq
5
+ from langchain_community.embeddings import FastEmbedEmbeddings
6
+ from project.utils.config_loader import load_config
7
+ from project.logger.logging import get_logger
8
+
9
+ logger = get_logger(__name__)
10
+
11
+
12
+ class ModelLoader:
13
+ def __init__(self, config_path: str = None):
14
+ load_dotenv()
15
+ self.config = load_config(config_path)
16
+ self._load_api_keys()
17
+ logger.info("ModelLoader initialized")
18
+
19
+ def _load_api_keys(self):
20
+ groq_key = os.getenv('GROQ_API_KEY')
21
+
22
+ if groq_key:
23
+ os.environ['GROQ_API_KEY'] = groq_key
24
+ logger.info("GROQ API key loaded")
25
+
26
+
27
+ def load_llm(self) -> Any:
28
+ llm_config = self.config.get('llm', {})
29
+ provider = llm_config.get('provider', 'langchain_groq')
30
+
31
+ try:
32
+ if provider == 'langchain_groq':
33
+ model = ChatGroq(
34
+ model=llm_config.get('model', 'openai/gpt-oss-20b'),
35
+ temperature=llm_config.get('temperature', 0.1),
36
+ max_tokens=llm_config.get('max_tokens', 2048)
37
+ )
38
+ logger.info(f"Loaded Groq LLM: {llm_config.get('model')}")
39
+ return model
40
+ else:
41
+ raise ValueError(f"Unsupported LLM provider: {provider}")
42
+ except Exception as e:
43
+ logger.error(f"Failed to load LLM: {str(e)}")
44
+ raise
45
+
46
+ def load_embeddings(self) -> Any:
47
+ embed_config = self.config.get('embedding_model', {})
48
+ provider = embed_config.get('provider', 'fastembedding')
49
+
50
+ try:
51
+ if provider == 'fastembedding':
52
+ embeddings = FastEmbedEmbeddings(
53
+ model_name=embed_config.get('model_name', 'BAAI/bge-small-en-v1.5')
54
+ )
55
+ logger.info(f"Loaded FastEmbed: {embed_config.get('model_name')}")
56
+ return embeddings
57
+ else:
58
+ raise ValueError(f"Unsupported embedding provider: {provider}")
59
+ except Exception as e:
60
+ logger.error(f"Failed to load embeddings: {str(e)}")
61
+ raise
requirements.txt ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ arxiv
2
+ chromadb
3
+ faiss-cpu
4
+ fastembed
5
+ fastapi
6
+ flashrank
7
+ google-generativeai
8
+ jinja2
9
+ langchain
10
+ langchain-chroma
11
+ langchain-community
12
+ langchain-google-genai
13
+ langchain-groq
14
+ langgraph
15
+ pypdf
16
+ python-dotenv
17
+ python-multipart
18
+ rapidocr-onnxruntime
19
+ tiktoken
20
+ uvicorn
static/styles.css ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
9
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
10
+ min-height: 100vh;
11
+ display: flex;
12
+ justify-content: center;
13
+ align-items: center;
14
+ padding: 20px;
15
+ }
16
+
17
+ .container {
18
+ max-width: 900px;
19
+ width: 100%;
20
+ background: white;
21
+ border-radius: 20px;
22
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
23
+ overflow: hidden;
24
+ }
25
+
26
+ header {
27
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
28
+ color: white;
29
+ padding: 60px 40px;
30
+ }
31
+
32
+ .header-content {
33
+ display: grid;
34
+ grid-template-columns: 1fr auto;
35
+ gap: 40px;
36
+ align-items: center;
37
+ }
38
+
39
+ .header-text {
40
+ text-align: left;
41
+ }
42
+
43
+ .header-image {
44
+ display: flex;
45
+ justify-content: center;
46
+ align-items: center;
47
+ }
48
+
49
+ .header-image img {
50
+ max-width: 220px;
51
+ height: auto;
52
+ border-radius: 10px;
53
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
54
+ background: white;
55
+ padding: 10px;
56
+ }
57
+
58
+ .main-title {
59
+ font-size: 3.5rem;
60
+ font-weight: 800;
61
+ letter-spacing: 2px;
62
+ margin-bottom: 15px;
63
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
64
+ }
65
+
66
+ .subtitle {
67
+ font-size: 1.2rem;
68
+ opacity: 0.9;
69
+ font-weight: 300;
70
+ }
71
+
72
+ main {
73
+ padding: 40px;
74
+ }
75
+
76
+ .search-form {
77
+ margin-bottom: 30px;
78
+ }
79
+
80
+ .search-box {
81
+ display: flex;
82
+ gap: 10px;
83
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
84
+ border-radius: 50px;
85
+ overflow: hidden;
86
+ }
87
+
88
+ .search-input {
89
+ flex: 1;
90
+ padding: 18px 30px;
91
+ border: 2px solid #e0e0e0;
92
+ border-radius: 50px 0 0 50px;
93
+ font-size: 1.1rem;
94
+ outline: none;
95
+ transition: border-color 0.3s;
96
+ }
97
+
98
+ .search-input:focus {
99
+ border-color: #667eea;
100
+ }
101
+
102
+ .search-button {
103
+ padding: 18px 40px;
104
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
105
+ color: white;
106
+ border: none;
107
+ border-radius: 0 50px 50px 0;
108
+ font-size: 1.1rem;
109
+ font-weight: 600;
110
+ cursor: pointer;
111
+ transition: transform 0.2s;
112
+ }
113
+
114
+ .search-button:hover {
115
+ transform: scale(1.05);
116
+ }
117
+
118
+ .search-button:active {
119
+ transform: scale(0.95);
120
+ }
121
+
122
+ .error-box {
123
+ background: #fee;
124
+ border-left: 4px solid #f44;
125
+ padding: 15px 20px;
126
+ border-radius: 5px;
127
+ color: #c33;
128
+ margin-bottom: 20px;
129
+ }
130
+
131
+ .results {
132
+ margin-top: 30px;
133
+ }
134
+
135
+ .question-box {
136
+ background: #f8f9fa;
137
+ padding: 20px;
138
+ border-radius: 10px;
139
+ margin-bottom: 20px;
140
+ border-left: 4px solid #667eea;
141
+ }
142
+
143
+ .question-box h3 {
144
+ color: #667eea;
145
+ margin-bottom: 10px;
146
+ font-size: 1.2rem;
147
+ }
148
+
149
+ .question-box p {
150
+ font-size: 1.1rem;
151
+ color: #333;
152
+ }
153
+
154
+ .answer-box {
155
+ background: #ffffff;
156
+ padding: 25px;
157
+ border-radius: 10px;
158
+ border: 2px solid #e0e0e0;
159
+ }
160
+
161
+ .answer-box h3 {
162
+ color: #764ba2;
163
+ margin-bottom: 15px;
164
+ font-size: 1.2rem;
165
+ }
166
+
167
+ .answer-content {
168
+ line-height: 1.8;
169
+ color: #444;
170
+ font-size: 1rem;
171
+ white-space: pre-wrap;
172
+ }
173
+
174
+ footer {
175
+ background: #f8f9fa;
176
+ text-align: center;
177
+ padding: 20px;
178
+ color: #666;
179
+ font-size: 0.9rem;
180
+ }
181
+
182
+ @media (max-width: 768px) {
183
+ .main-title {
184
+ font-size: 2.5rem;
185
+ }
186
+
187
+ .header-content {
188
+ grid-template-columns: 1fr;
189
+ gap: 20px;
190
+ }
191
+
192
+ .header-text {
193
+ text-align: center;
194
+ }
195
+
196
+ .header-image img {
197
+ max-width: 200px;
198
+ }
199
+
200
+ .search-box {
201
+ flex-direction: column;
202
+ }
203
+
204
+ .search-input,
205
+ .search-button {
206
+ border-radius: 50px;
207
+ border: 2px solid #e0e0e0;
208
+ }
209
+
210
+ header {
211
+ padding: 40px 20px;
212
+ }
213
+
214
+ main {
215
+ padding: 20px;
216
+ }
217
+ }
templates/index.html ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>RAG PROJECT PIPELINE</title>
7
+ <link rel="stylesheet" href="/static/styles.css">
8
+ <script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
9
+ <script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
10
+ <script>
11
+ window.MathJax = {
12
+ tex: {
13
+ inlineMath: [['$', '$'], ['\\(', '\\)']],
14
+ displayMath: [['$$', '$$'], ['\\[', '\\]']],
15
+ processEscapes: true
16
+ }
17
+ };
18
+ </script>
19
+ </head>
20
+ <body>
21
+ <div class="container">
22
+ <header>
23
+ <div class="header-content">
24
+ <div class="header-text">
25
+ <h1 class="main-title">LEARN ABOUT TRANSFORMERS</h1>
26
+ <p class="subtitle">Ask questions about the "Attention Is All You Need Research Paper AKA Transformers."</p>
27
+ </div>
28
+ <div class="header-image">
29
+ <img src="https://i.postimg.cc/gjtCYK7R/transformer-architecture-converted.png" alt="Transformer Architecture" />
30
+ </div>
31
+ </div>
32
+ </header>
33
+
34
+ <main>
35
+ <form method="post" action="/search" class="search-form">
36
+ <div class="search-box">
37
+ <input
38
+ type="text"
39
+ name="query"
40
+ placeholder="What is the attention mechanism?"
41
+ class="search-input"
42
+ value="{{ query or '' }}"
43
+ autofocus
44
+ >
45
+ <button type="submit" class="search-button">Search</button>
46
+ </div>
47
+ </form>
48
+
49
+ {% if error %}
50
+ <div class="error-box">
51
+ <p>{{ error }}</p>
52
+ </div>
53
+ {% endif %}
54
+
55
+ {% if query and answer %}
56
+ <div class="results">
57
+ <div class="question-box">
58
+ <h3>Question:</h3>
59
+ <p>{{ query }}</p>
60
+ </div>
61
+
62
+ <div class="answer-box">
63
+ <h3>Answer:</h3>
64
+ <div class="answer-content">{{ answer | safe }}</div>
65
+ </div>
66
+ </div>
67
+ {% endif %}
68
+ </main>
69
+
70
+ <footer>
71
+ <p>Powered by RAG Pipeline with Corrective RAG (CRAG)</p>
72
+ </footer>
73
+ </div>
74
+
75
+ <script>
76
+ // Trigger MathJax rendering after page load
77
+ if (window.MathJax) {
78
+ MathJax.typesetPromise();
79
+ }
80
+ </script>
81
+ </body>
82
+ </html>