Spaces:
Sleeping
Sleeping
Commit ·
6c58cf4
0
Parent(s):
Initial deployment to Hugging Face Spaces
Browse files- .dockerignore +40 -0
- Dockerfile +17 -0
- README.md +47 -0
- app.py +56 -0
- project/__init__.py +0 -0
- project/config/config.yaml +20 -0
- project/exception/__init__.py +0 -0
- project/exception/except.py +26 -0
- project/logger/__init__.py +0 -0
- project/logger/logging.py +21 -0
- project/model/__init__.py +0 -0
- project/model/reranking.py +61 -0
- project/model/retriever.py +93 -0
- project/pipeline/__init__.py +0 -0
- project/pipeline/agents.py +214 -0
- project/pipeline/rag.py +77 -0
- project/prompts/__init__.py +0 -0
- project/prompts/prompt_template.py +66 -0
- project/source/__init__.py +0 -0
- project/source/data_preparation.py +95 -0
- project/utils/__init__.py +0 -0
- project/utils/config_loader.py +19 -0
- project/utils/model_loader.py +61 -0
- requirements.txt +20 -0
- static/styles.css +217 -0
- templates/index.html +82 -0
.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>
|