Arif commited on
Commit
4722db8
·
0 Parent(s):

Create portfolio project for generative ai. Project is started.

Browse files
.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