Spaces:
Sleeping
Sleeping
Arif
commited on
Commit
·
4722db8
0
Parent(s):
Create portfolio project for generative ai. Project is started.
Browse files- .gitignore +11 -0
- .python-version +1 -0
- README.md +0 -0
- app/__init__.py +0 -0
- app/api/__init__.py +0 -0
- app/api/dependencies.py +0 -0
- app/api/routes.py +0 -0
- app/config.py +27 -0
- app/core/__init__.py +0 -0
- app/core/embeddings.py +17 -0
- app/core/llm.py +43 -0
- app/core/vector_store.py +59 -0
- app/main.py +102 -0
- app/models/__init__.py +0 -0
- app/models/schemas.py +26 -0
- app/services/__init__.py +0 -0
- app/services/document_processor.py +64 -0
- app/services/rag_chain.py +67 -0
- app/services/retriever.py +0 -0
- main.py +6 -0
- pyproject.toml +27 -0
- scripts/ingest_documents.py +0 -0
- scripts/setup_qdrant.py +0 -0
- tests/__init__.py +0 -0
- tests/test_rag.py +0 -0
- uv.lock +0 -0
.gitignore
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python-generated files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[oc]
|
| 4 |
+
build/
|
| 5 |
+
dist/
|
| 6 |
+
wheels/
|
| 7 |
+
*.egg-info
|
| 8 |
+
|
| 9 |
+
# Virtual environments
|
| 10 |
+
.venv
|
| 11 |
+
.env
|
.python-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
3.10
|
README.md
ADDED
|
File without changes
|
app/__init__.py
ADDED
|
File without changes
|
app/api/__init__.py
ADDED
|
File without changes
|
app/api/dependencies.py
ADDED
|
File without changes
|
app/api/routes.py
ADDED
|
File without changes
|
app/config.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic_settings import BaseSettings
|
| 2 |
+
from functools import lru_cache
|
| 3 |
+
|
| 4 |
+
class Settings(BaseSettings):
|
| 5 |
+
# Ollama
|
| 6 |
+
ollama_base_url: str = "http://localhost:11434"
|
| 7 |
+
ollama_model: str = "llama3.1"
|
| 8 |
+
|
| 9 |
+
# Qdrant
|
| 10 |
+
qdrant_host: str = "localhost"
|
| 11 |
+
qdrant_port: int = 6333
|
| 12 |
+
qdrant_collection_name: str = "documents"
|
| 13 |
+
|
| 14 |
+
# Embeddings
|
| 15 |
+
embedding_model: str = "sentence-transformers/all-MiniLM-L6-v2"
|
| 16 |
+
embedding_dimension: int = 384
|
| 17 |
+
|
| 18 |
+
# App
|
| 19 |
+
app_host: str = "0.0.0.0"
|
| 20 |
+
app_port: int = 8000
|
| 21 |
+
|
| 22 |
+
class Config:
|
| 23 |
+
env_file = ".env"
|
| 24 |
+
|
| 25 |
+
@lru_cache()
|
| 26 |
+
def get_settings() -> Settings:
|
| 27 |
+
return Settings()
|
app/core/__init__.py
ADDED
|
File without changes
|
app/core/embeddings.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sentence_transformers import SentenceTransformer
|
| 2 |
+
from typing import List
|
| 3 |
+
|
| 4 |
+
class EmbeddingGenerator:
|
| 5 |
+
def __init__(self, model_name: str):
|
| 6 |
+
self.model = SentenceTransformer(model_name)
|
| 7 |
+
self.dimension = self.model.get_sentence_embedding_dimension()
|
| 8 |
+
|
| 9 |
+
def generate(self, texts: List[str]) -> List[List[float]]:
|
| 10 |
+
"""Generate embeddings for a list of texts"""
|
| 11 |
+
embeddings = self.model.encode(texts, convert_to_numpy=True)
|
| 12 |
+
return embeddings.tolist()
|
| 13 |
+
|
| 14 |
+
def generate_single(self, text: str) -> List[float]:
|
| 15 |
+
"""Generate embedding for a single text"""
|
| 16 |
+
embedding = self.model.encode([text], convert_to_numpy=True)
|
| 17 |
+
return embedding[0].tolist()
|
app/core/llm.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain_ollama import ChatOllama
|
| 2 |
+
try:
|
| 3 |
+
from langchain.prompts import PromptTemplate
|
| 4 |
+
except ImportError:
|
| 5 |
+
from langchain_core.prompts import PromptTemplate
|
| 6 |
+
|
| 7 |
+
from langchain_core.output_parsers import StrOutputParser
|
| 8 |
+
|
| 9 |
+
class OllamaLLM:
|
| 10 |
+
def __init__(self, base_url: str, model: str):
|
| 11 |
+
self.llm = ChatOllama(
|
| 12 |
+
base_url=base_url,
|
| 13 |
+
model=model,
|
| 14 |
+
temperature=0.2, # Lower for more factual responses
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
# RAG-specific prompt template
|
| 18 |
+
self.prompt_template = PromptTemplate(
|
| 19 |
+
template="""You are a helpful AI assistant. Use the following context to answer the question accurately and concisely.
|
| 20 |
+
|
| 21 |
+
Context:
|
| 22 |
+
{context}
|
| 23 |
+
|
| 24 |
+
Question: {question}
|
| 25 |
+
|
| 26 |
+
Instructions:
|
| 27 |
+
- Answer based ONLY on the provided context
|
| 28 |
+
- If the answer is not in the context, say "I don't have enough information to answer that"
|
| 29 |
+
- Keep your answer clear and concise (max 3-5 sentences)
|
| 30 |
+
- Cite specific parts of the context when relevant
|
| 31 |
+
|
| 32 |
+
Answer:""",
|
| 33 |
+
input_variables=["context", "question"]
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
self.chain = self.prompt_template | self.llm | StrOutputParser()
|
| 37 |
+
|
| 38 |
+
def generate(self, question: str, context: str) -> str:
|
| 39 |
+
"""Generate answer using RAG context"""
|
| 40 |
+
return self.chain.invoke({
|
| 41 |
+
"question": question,
|
| 42 |
+
"context": context
|
| 43 |
+
})
|
app/core/vector_store.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from qdrant_client import QdrantClient
|
| 2 |
+
from qdrant_client.models import Distance, VectorParams, PointStruct
|
| 3 |
+
from typing import List, Dict
|
| 4 |
+
import uuid
|
| 5 |
+
|
| 6 |
+
class VectorStore:
|
| 7 |
+
def __init__(self, host: str, port: int, collection_name: str, vector_size: int):
|
| 8 |
+
self.client = QdrantClient(host=host, port=port)
|
| 9 |
+
self.collection_name = collection_name
|
| 10 |
+
self.vector_size = vector_size
|
| 11 |
+
self._ensure_collection()
|
| 12 |
+
|
| 13 |
+
def _ensure_collection(self):
|
| 14 |
+
"""Create collection if it doesn't exist"""
|
| 15 |
+
collections = self.client.get_collections().collections
|
| 16 |
+
collection_names = [col.name for col in collections]
|
| 17 |
+
|
| 18 |
+
if self.collection_name not in collection_names:
|
| 19 |
+
self.client.create_collection(
|
| 20 |
+
collection_name=self.collection_name,
|
| 21 |
+
vectors_config=VectorParams(
|
| 22 |
+
size=self.vector_size,
|
| 23 |
+
distance=Distance.COSINE
|
| 24 |
+
)
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
def add_documents(self, texts: List[str], embeddings: List[List[float]],
|
| 28 |
+
metadata: List[Dict] = None):
|
| 29 |
+
"""Add documents to vector store"""
|
| 30 |
+
points = []
|
| 31 |
+
for idx, (text, embedding) in enumerate(zip(texts, embeddings)):
|
| 32 |
+
point_id = str(uuid.uuid4())
|
| 33 |
+
payload = {"text": text}
|
| 34 |
+
if metadata and idx < len(metadata):
|
| 35 |
+
payload.update(metadata[idx])
|
| 36 |
+
|
| 37 |
+
points.append(
|
| 38 |
+
PointStruct(
|
| 39 |
+
id=point_id,
|
| 40 |
+
vector=embedding,
|
| 41 |
+
payload=payload
|
| 42 |
+
)
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
self.client.upsert(
|
| 46 |
+
collection_name=self.collection_name,
|
| 47 |
+
points=points
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
def search(self, query_embedding: List[float], limit: int = 5,
|
| 51 |
+
score_threshold: float = 0.7):
|
| 52 |
+
"""Search for similar documents"""
|
| 53 |
+
results = self.client.search(
|
| 54 |
+
collection_name=self.collection_name,
|
| 55 |
+
query_vector=query_embedding,
|
| 56 |
+
limit=limit,
|
| 57 |
+
score_threshold=score_threshold
|
| 58 |
+
)
|
| 59 |
+
return results
|
app/main.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, UploadFile, File, HTTPException
|
| 2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
+
from app.config import get_settings
|
| 4 |
+
from app.core.embeddings import EmbeddingGenerator
|
| 5 |
+
from app.core.vector_store import VectorStore
|
| 6 |
+
from app.core.llm import OllamaLLM
|
| 7 |
+
from app.services.document_processor import DocumentProcessor
|
| 8 |
+
from app.services.rag_chain import RAGChain
|
| 9 |
+
from app.models.schemas import QueryRequest, QueryResponse, IngestResponse
|
| 10 |
+
import tempfile
|
| 11 |
+
import os
|
| 12 |
+
|
| 13 |
+
# Initialize FastAPI app
|
| 14 |
+
app = FastAPI(
|
| 15 |
+
title="RAG Portfolio Project",
|
| 16 |
+
description="Production-grade Retrieval-Augmented Generation system",
|
| 17 |
+
version="1.0.0"
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
# Add CORS middleware
|
| 21 |
+
app.add_middleware(
|
| 22 |
+
CORSMiddleware,
|
| 23 |
+
allow_origins=["*"],
|
| 24 |
+
allow_credentials=True,
|
| 25 |
+
allow_methods=["*"],
|
| 26 |
+
allow_headers=["*"],
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
# Initialize components
|
| 30 |
+
settings = get_settings()
|
| 31 |
+
|
| 32 |
+
embedding_generator = EmbeddingGenerator(settings.embedding_model)
|
| 33 |
+
vector_store = VectorStore(
|
| 34 |
+
host=settings.qdrant_host,
|
| 35 |
+
port=settings.qdrant_port,
|
| 36 |
+
collection_name=settings.qdrant_collection_name,
|
| 37 |
+
vector_size=embedding_generator.dimension
|
| 38 |
+
)
|
| 39 |
+
llm = OllamaLLM(settings.ollama_base_url, settings.ollama_model)
|
| 40 |
+
document_processor = DocumentProcessor()
|
| 41 |
+
|
| 42 |
+
rag_chain = RAGChain(embedding_generator, vector_store, llm)
|
| 43 |
+
|
| 44 |
+
@app.get("/")
|
| 45 |
+
async def root():
|
| 46 |
+
return {
|
| 47 |
+
"message": "RAG Portfolio Project API",
|
| 48 |
+
"status": "running",
|
| 49 |
+
"docs": "/docs"
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
@app.get("/health")
|
| 53 |
+
async def health_check():
|
| 54 |
+
return {"status": "healthy", "ollama_connected": True}
|
| 55 |
+
|
| 56 |
+
@app.post("/ingest/file", response_model=IngestResponse)
|
| 57 |
+
async def ingest_file(file: UploadFile = File(...)):
|
| 58 |
+
"""Upload and ingest a document into the RAG system"""
|
| 59 |
+
try:
|
| 60 |
+
# Save uploaded file temporarily
|
| 61 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=file.filename) as tmp:
|
| 62 |
+
content = await file.read()
|
| 63 |
+
tmp.write(content)
|
| 64 |
+
tmp_path = tmp.name
|
| 65 |
+
|
| 66 |
+
# Process document
|
| 67 |
+
chunks = document_processor.process_document(tmp_path)
|
| 68 |
+
|
| 69 |
+
# Ingest into RAG system
|
| 70 |
+
result = rag_chain.ingest_documents(chunks)
|
| 71 |
+
|
| 72 |
+
# Clean up
|
| 73 |
+
os.unlink(tmp_path)
|
| 74 |
+
|
| 75 |
+
return IngestResponse(**result, message=f"Successfully ingested {file.filename}")
|
| 76 |
+
|
| 77 |
+
except Exception as e:
|
| 78 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 79 |
+
|
| 80 |
+
@app.post("/query", response_model=QueryResponse)
|
| 81 |
+
async def query(request: QueryRequest):
|
| 82 |
+
"""Query the RAG system"""
|
| 83 |
+
try:
|
| 84 |
+
result = rag_chain.query(request.question, request.top_k)
|
| 85 |
+
return QueryResponse(**result)
|
| 86 |
+
|
| 87 |
+
except Exception as e:
|
| 88 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 89 |
+
|
| 90 |
+
@app.delete("/reset")
|
| 91 |
+
async def reset_collection():
|
| 92 |
+
"""Reset the vector collection (delete all documents)"""
|
| 93 |
+
try:
|
| 94 |
+
vector_store.client.delete_collection(settings.qdrant_collection_name)
|
| 95 |
+
vector_store._ensure_collection()
|
| 96 |
+
return {"status": "success", "message": "Collection reset successfully"}
|
| 97 |
+
except Exception as e:
|
| 98 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 99 |
+
|
| 100 |
+
if __name__ == "__main__":
|
| 101 |
+
import uvicorn
|
| 102 |
+
uvicorn.run(app, host=settings.app_host, port=settings.app_port)
|
app/models/__init__.py
ADDED
|
File without changes
|
app/models/schemas.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel
|
| 2 |
+
from typing import List, Optional
|
| 3 |
+
|
| 4 |
+
class DocumentUpload(BaseModel):
|
| 5 |
+
filename: str
|
| 6 |
+
content: str
|
| 7 |
+
|
| 8 |
+
class QueryRequest(BaseModel):
|
| 9 |
+
question: str
|
| 10 |
+
top_k: Optional[int] = 5
|
| 11 |
+
|
| 12 |
+
class SourceInfo(BaseModel):
|
| 13 |
+
source: str
|
| 14 |
+
score: float
|
| 15 |
+
chunk_index: int
|
| 16 |
+
|
| 17 |
+
class QueryResponse(BaseModel):
|
| 18 |
+
question: str
|
| 19 |
+
answer: str
|
| 20 |
+
sources: List[SourceInfo]
|
| 21 |
+
context_used: int
|
| 22 |
+
|
| 23 |
+
class IngestResponse(BaseModel):
|
| 24 |
+
status: str
|
| 25 |
+
documents_ingested: int
|
| 26 |
+
message: Optional[str] = None
|
app/services/__init__.py
ADDED
|
File without changes
|
app/services/document_processor.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Dict
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import pypdf
|
| 4 |
+
from docx import Document
|
| 5 |
+
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class DocumentProcessor:
|
| 9 |
+
def __init__(self, chunk_size: int = 500, chunk_overlap: int = 50):
|
| 10 |
+
self.text_splitter = RecursiveCharacterTextSplitter(
|
| 11 |
+
chunk_size=chunk_size,
|
| 12 |
+
chunk_overlap=chunk_overlap,
|
| 13 |
+
separators=["\n\n", "\n", ".", "!", "?", ",", " ", ""]
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
def load_pdf(self, file_path: str) -> str:
|
| 17 |
+
"""Load text from PDF"""
|
| 18 |
+
with open(file_path, 'rb') as file:
|
| 19 |
+
reader = pypdf.PdfReader(file)
|
| 20 |
+
text = ""
|
| 21 |
+
for page in reader.pages:
|
| 22 |
+
text += page.extract_text()
|
| 23 |
+
return text
|
| 24 |
+
|
| 25 |
+
def load_docx(self, file_path: str) -> str:
|
| 26 |
+
"""Load text from DOCX"""
|
| 27 |
+
doc = Document(file_path)
|
| 28 |
+
return "\n".join([paragraph.text for paragraph in doc.paragraphs])
|
| 29 |
+
|
| 30 |
+
def load_txt(self, file_path: str) -> str:
|
| 31 |
+
"""Load text from TXT"""
|
| 32 |
+
with open(file_path, 'r', encoding='utf-8') as file:
|
| 33 |
+
return file.read()
|
| 34 |
+
|
| 35 |
+
def process_document(self, file_path: str) -> List[Dict]:
|
| 36 |
+
"""Process document and return chunks with metadata"""
|
| 37 |
+
path = Path(file_path)
|
| 38 |
+
|
| 39 |
+
# Load based on extension
|
| 40 |
+
if path.suffix == '.pdf':
|
| 41 |
+
text = self.load_pdf(file_path)
|
| 42 |
+
elif path.suffix == '.docx':
|
| 43 |
+
text = self.load_docx(file_path)
|
| 44 |
+
elif path.suffix == '.txt':
|
| 45 |
+
text = self.load_txt(file_path)
|
| 46 |
+
else:
|
| 47 |
+
raise ValueError(f"Unsupported file type: {path.suffix}")
|
| 48 |
+
|
| 49 |
+
# Split into chunks
|
| 50 |
+
chunks = self.text_splitter.split_text(text)
|
| 51 |
+
|
| 52 |
+
# Add metadata
|
| 53 |
+
chunk_data = []
|
| 54 |
+
for idx, chunk in enumerate(chunks):
|
| 55 |
+
chunk_data.append({
|
| 56 |
+
"text": chunk,
|
| 57 |
+
"metadata": {
|
| 58 |
+
"source": path.name,
|
| 59 |
+
"chunk_index": idx,
|
| 60 |
+
"total_chunks": len(chunks)
|
| 61 |
+
}
|
| 62 |
+
})
|
| 63 |
+
|
| 64 |
+
return chunk_data
|
app/services/rag_chain.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.core.embeddings import EmbeddingGenerator
|
| 2 |
+
from app.core.vector_store import VectorStore
|
| 3 |
+
from app.core.llm import OllamaLLM
|
| 4 |
+
from typing import List, Dict
|
| 5 |
+
|
| 6 |
+
class RAGChain:
|
| 7 |
+
def __init__(
|
| 8 |
+
self,
|
| 9 |
+
embedding_generator: EmbeddingGenerator,
|
| 10 |
+
vector_store: VectorStore,
|
| 11 |
+
llm: OllamaLLM
|
| 12 |
+
):
|
| 13 |
+
self.embedding_generator = embedding_generator
|
| 14 |
+
self.vector_store = vector_store
|
| 15 |
+
self.llm = llm
|
| 16 |
+
|
| 17 |
+
def ingest_documents(self, documents: List[Dict]):
|
| 18 |
+
"""Ingest documents into vector store"""
|
| 19 |
+
texts = [doc["text"] for doc in documents]
|
| 20 |
+
metadata = [doc["metadata"] for doc in documents]
|
| 21 |
+
|
| 22 |
+
# Generate embeddings
|
| 23 |
+
embeddings = self.embedding_generator.generate(texts)
|
| 24 |
+
|
| 25 |
+
# Store in vector database
|
| 26 |
+
self.vector_store.add_documents(texts, embeddings, metadata)
|
| 27 |
+
|
| 28 |
+
return {"status": "success", "documents_ingested": len(documents)}
|
| 29 |
+
|
| 30 |
+
def query(self, question: str, top_k: int = 5) -> Dict:
|
| 31 |
+
"""Query the RAG system"""
|
| 32 |
+
# Generate query embedding
|
| 33 |
+
query_embedding = self.embedding_generator.generate_single(question)
|
| 34 |
+
|
| 35 |
+
# Retrieve relevant documents
|
| 36 |
+
search_results = self.vector_store.search(
|
| 37 |
+
query_embedding,
|
| 38 |
+
limit=top_k,
|
| 39 |
+
score_threshold=0.6
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
# Format context from retrieved documents
|
| 43 |
+
context_parts = []
|
| 44 |
+
sources = []
|
| 45 |
+
|
| 46 |
+
for result in search_results:
|
| 47 |
+
context_parts.append(result.payload["text"])
|
| 48 |
+
sources.append({
|
| 49 |
+
"source": result.payload.get("source", "unknown"),
|
| 50 |
+
"score": result.score,
|
| 51 |
+
"chunk_index": result.payload.get("chunk_index", 0)
|
| 52 |
+
})
|
| 53 |
+
|
| 54 |
+
context = "\n\n".join(context_parts)
|
| 55 |
+
|
| 56 |
+
# Generate answer using LLM
|
| 57 |
+
if not context:
|
| 58 |
+
answer = "I don't have any relevant information to answer this question."
|
| 59 |
+
else:
|
| 60 |
+
answer = self.llm.generate(question, context)
|
| 61 |
+
|
| 62 |
+
return {
|
| 63 |
+
"question": question,
|
| 64 |
+
"answer": answer,
|
| 65 |
+
"sources": sources,
|
| 66 |
+
"context_used": len(search_results)
|
| 67 |
+
}
|
app/services/retriever.py
ADDED
|
File without changes
|
main.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def main():
|
| 2 |
+
print("Hello from generative-ai-portfolio-project!")
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
main()
|
pyproject.toml
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "generative-ai-portfolio-project"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "Add your description here"
|
| 5 |
+
readme = "README.md"
|
| 6 |
+
requires-python = ">=3.10"
|
| 7 |
+
dependencies = [
|
| 8 |
+
"fastapi>=0.119.1",
|
| 9 |
+
"langchain>=1.0.2",
|
| 10 |
+
"langchain-community>=0.4",
|
| 11 |
+
"langchain-ollama>=1.0.0",
|
| 12 |
+
"langchain-text-splitters>=1.0.0",
|
| 13 |
+
"pypdf>=6.1.3",
|
| 14 |
+
"python-docx>=1.2.0",
|
| 15 |
+
"python-multipart>=0.0.20",
|
| 16 |
+
"qdrant-client>=1.15.1",
|
| 17 |
+
"sentence-transformers>=5.1.2",
|
| 18 |
+
"unstructured>=0.18.15",
|
| 19 |
+
"uvicorn>=0.38.0",
|
| 20 |
+
]
|
| 21 |
+
|
| 22 |
+
[dependency-groups]
|
| 23 |
+
dev = [
|
| 24 |
+
"black>=25.9.0",
|
| 25 |
+
"pytest>=8.4.2",
|
| 26 |
+
"ruff>=0.14.1",
|
| 27 |
+
]
|
scripts/ingest_documents.py
ADDED
|
File without changes
|
scripts/setup_qdrant.py
ADDED
|
File without changes
|
tests/__init__.py
ADDED
|
File without changes
|
tests/test_rag.py
ADDED
|
File without changes
|
uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|