Wskazówki praktyczne: Opanowanie nowoczesnej demonstracji rekrutacji: wykorzystanie Doclinga i
Krok po kroku instrukcja obsługi Notatek praktycznych: Opanowanie nowoczesnej demonstracji rekrutacji: wykorzystanie Docling oraz umów, weryfikacji i miejsc na kod do wstawienia dla zespołów implementujących ten wzorzec.
Poniższe notatki przedstawiają praktyczny plan działania dotyczący „Opanowania nowoczesnych metod rekrutacji: wykorzystanie Docling i PostgreSQL według Boba do tworzenia lokalnej bazy danych RAG dla kandydatów”. Nacisk kładziony jest na umowy, sprawdzenia oraz miejsca na kod do wstawienia, a nie na motywacyjne aspekty. Podczas przechodzenia przez etap przeglądu, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Trzymaj konfigurację poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całej struktury.
Wprowadzenie
Etap wprowadzenia działa najlepiej, gdy traktuje się go jako mierzalną powierzchnię. Zapisz jeden idealny przypadek, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia działań, zanim rozszerzysz zakres. Zdokumentuj zarówno pomyślny, jak i alternatywny scenariusz działania. Próby ponownych działań, kontrola ludzka oraz obsługa wiadomości nieodebranych stanowią część produktu, a nie elementy dodawane później. Oddziel zasadę dzielenia na fragmenty od zasady wyszukiwania. Zmiana jednej nie powinna zmuszać do przepisywania drugiej, gdy zmieniają się metryki jakości.
Architektura: podejście hybrydowe
Architektura hybrydowa działa najlepiej, gdy traktuje się ją jako mierzalną powierzchnię. Zapisz jeden „złoty” przepis, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia działań, zanim rozszerzysz zakres. Wolno preferować małe, testowalne jednostki zamiast rozbudowanych skryptów. Gdy jakiś krok zawiedzie, awaria powinna wskazywać na konkretną odpowiedzialność, a nie na skomplikowany łańcuch operacji. Rozdziel politykę dzielenia na części od polityki pobierania danych. Zmiana jednej z nich nie powinna zmuszać do przepisywania drugiej, gdy zmieniają się metryki jakości.
Kluczowe komponenty:
Etap „Kluczowe komponenty” funkcjonuje najlepiej, gdy traktowany jest jako mierzalna powierzchnia. Zapisz jeden idealny przepis działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia zmian, zanim rozszerzysz zakres pracy. Traktuj ten etap jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nadaj nazwy poszczególnym elementom, zdefiniuj kryteria sukcesu i odrzucaj ciche, częściowe ukończenie zadań. Oddziel politykę dzielenia na fragmenty od polityki pobierania danych. Zmiana jednej z nich nie powinna zmuszać do przepisywania drugiej, gdy zmieniają się metryki jakości. Etap „Kluczowe komponenty” funkcjonuje najlepiej, gdy traktowany jest jako mierzalna powierzchnia. Zapisz jeden idealny przepis działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia zmian, zanim rozszerzysz zakres pracy. Przechowuj konfigurację poza kodem aplikacji. Pliki środowiskowe, składyce tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą audytować bez konieczności czytania całej struktury.
Techologie użyte w tym projekcie
Dla technologii używanych na tym etapie należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed modyfikacją kodu. Operatorzy powinni móc ponownie uruchomić ten krok na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Należy udokumentować zarówno prawidłowy przebieg procesu, jak i ścieżkę naprawczą. Próby ponownych działań, kontrolne punkty ludzkie oraz obsługa wiadomości błędnych stanowią część produktu, a nie elementy dodawane później. Należy podać fragmenty tekstu, które faktycznie stanowią podstawę odpowiedzi. Bez tych odniesień operatorzy nie będą w stanie odróżnić halucynacji od luki w indeksowaniu.
## Technologies Used
- **[Docling](https://github.com/docling-project/docling)**: Document parsing and chunking
- **[PostgreSQL](https://www.postgresql.org/)**: Database
- **[pgvector](https://github.com/pgvector/pgvector)**: Vector similarity search
- **[Ollama](https://ollama.ai/)**: Local LLM and embeddings
- **[Streamlit](https://streamlit.io/)**: Web interface
- **[Podman](https://podman.io/)**: Container management
Przepływ danych
W etapie The Data Workflow należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić dany krok na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Należy preferować małe, testowalne jednostki zamiast rozbudowanych skryptów. Gdy dany krok zawiedzie, powód awarii powinien wskazywać na konkretną odpowiedzialność, a nie na skomplikowany proces. Należy podawać konkretne fragmenty tekstu, które stanowią podstawę odpowiedzi. Bez tych odniesień operatorzy nie będą w stanie odróżnić fałszywych informacji od braków w indeksowaniu.
1. Dokumentacja przepływu pobierania danych
W fazie 1 Document Ingestion Flow należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić ten krok od znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Traktuj tę fazę jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nazwij poszczególne elementy, zdefiniuj kryteria sukcesu i odrzuć ciche, częściowe ukończenie zadania. Podaj fragmenty tekstu, które faktycznie stanowią podstawę odpowiedzi. Bez tych odniesień operatorzy nie będą w stanie odróżnić halucynacji od luki w indeksowaniu. W fazie 1 Document Ingestion Flow należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić ten krok od znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Przechowuj konfigurację poza kodem aplikacji. Pliki środowiskowe, składysek z danymi poufnymi oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całego pliku.
aph.2. Przepływ zapytań RAG
Gdy przechodzisz przez drugi etap przepływu zapytań RAG, najpierw zapisz specyfikację: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Zdokumentuj zarówno prawidłowy przebieg, jak i ścieżkę naprawczą. Próby ponowne, kontrola przez ludzi oraz obsługa wiadomości błędowych są częścią produktu, a nie elementem późniejszej optymalizacji. Zmierz stopień odzyskiwania informacji na ustalonej serii pytań przed dostosowywaniem promptów. Częste zmiany promptów rzadko naprawiają słabe możliwości wyszukiwania.
Projekt schematu bazy danych
Podczas prace nad etapem projektowania schematu bazy danych najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Wolno preferować małe, testowalne jednostki zamiast rozbudowanych skryptów. Gdy jakiś krok się nie powiedzie, błąd powinien wskazywać na konkretną odpowiedzialność, a nie na skomplikowany proces. Zmierz stopień odzyskiwania informacji na ustalonej serii pytań przed dostosowywaniem promptów. Częste zmiany promptów rzadko naprawiają słabe możliwości wyszukiwania.
CREATE TABLE candidates (
id SERIAL PRIMARY KEY,
firstname VARCHAR(100),
lastname VARCHAR(100),
date_of_birth DATE,
resume_path VARCHAR(255),
created_at TIMESTAMP
);
CREATE TABLE resume_chunks (
id SERIAL PRIMARY KEY,
candidate_id INTEGER REFERENCES candidates(id),
chunk_text TEXT,
chunk_index INTEGER,
embedding vector(384), -- granite-embedding:30m dimension
metadata JSONB,
created_at TIMESTAMP
);
| **Table** | **Column** | **Type** | **Purpose** |
| ----------------- | -------------------------------------------- | ------------------------------ | ----------------------------------------- |
| **Candidates** | `id`, `firstname`, `lastname`, `resume_path` | Serial, Varchar | Stores primary candidate records. |
| **Resume Chunks** | `candidate_id`, `chunk_text`, `embedding` | Integer, Text, **Vector(384)** | Stores the AI-searchable resume segments. |
Struktura projektu
Podczas prace nad etapem struktury projektu najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Traktuj ten etap jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nadaj nazwy poszczególnym elementom, zdefiniuj kryteria sukcesu i odrzuć ciche, częściowe ukończenie zadań. Zmierz stopień przywoływania informacji na ustalonej serii pytań przed dostosowywaniem promptów. Częste zmiany promptów rzadko naprawiają słabe możliwości wyszukiwania informacji. Podczas pracy nad etapem struktury projektu najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Przechowuj konfigurację poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całej struktury.
docling-rag-postgresql/
├── app.py # Streamlit GUI application
├── docker-compose.yml # PostgreSQL setup
├── init.sql # Database initialization
├── requirements.txt # Python dependencies
├── .env.example # Environment variables template
├── README.md # This file
├── Docs/ # Documentation
│ ├── Architecture.md # System architecture with Mermaid diagrams
│ └── QUICKSTART.md # Quick start guide
├── scripts/ # Utility scripts
│ ├── setup.sh # Setup script
│ └── run.sh # Run script
├── resumes/ # Resume files
│ ├── resume1.docx
│ ├── resume2.docx
│ ├── resume3.docx
│ └── resume4.pdf
└── src/ # Source code
├── __init__.py
├── config.py # Configuration management
├── database_service.py # PostgreSQL operations
├── document_processor.py # Docling document processing
├── embedding_service.py # Ollama embeddings
├── rag_service.py # RAG pipeline
└── data_loader.py # Resume loading
Baza danych i LLM
Faza bazy danych i LLM działa najlepiej, gdy traktuje się ją jako mierzalną powierzchnię do analizy. Zapisz jeden idealny przepis działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia działań, zanim rozszerzysz zakres pracy. Zdokumentuj zarówno prawidłowy przebieg operacji, jak i ścieżkę przywracania do stanu poprzedniego. Próby ponownych działań, kontrola przez ludzi oraz obsługa wiadomości nieodebranych stanowią część produktu, a nie elementy dodawane później w celu udoskonalenia. Przydziel budżet tokenów na każdy ruch i sesję. Narzędzia typu agentic intensywnie rozszerzają kontekst; sztywne limity zapobiegają temu, by demonstracje przerodziły się w niespodziewane rachunki.
# Database
DB_HOST=localhost
DB_PORT=5432
DB_NAME=candidates_rag
DB_USER=postgres
DB_PASSWORD=postgres
# Ollama
OLLAMA_HOST=http://localhost:11434
EMBEDDING_MODEL=granite-embedding:30m
LLM_MODEL=granite4:latest
# Application
CHUNK_SIZE=500
CHUNK_OVERLAP=50
TOP_K_RESULTS=5
"""Configuration management for the RAG application."""
import os
from dotenv import load_dotenv
load_dotenv()
class Config:
"""Application configuration."""
# Database Configuration
DB_HOST = os.getenv("DB_HOST", "localhost")
DB_PORT = int(os.getenv("DB_PORT", "5432"))
DB_NAME = os.getenv("DB_NAME", "candidates_rag")
DB_USER = os.getenv("DB_USER", "postgres")
DB_PASSWORD = os.getenv("DB_PASSWORD", "postgres")
# Ollama Configuration
OLLAMA_HOST = os.getenv("OLLAMA_HOST", "http://localhost:11434")
EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "granite-embedding:30m")
LLM_MODEL = os.getenv("LLM_MODEL", "granite4:latest")
# Application Configuration
CHUNK_SIZE = int(os.getenv("CHUNK_SIZE", "500"))
CHUNK_OVERLAP = int(os.getenv("CHUNK_OVERLAP", "50"))
TOP_K_RESULTS = int(os.getenv("TOP_K_RESULTS", "5"))
@property
def database_url(self):
"""Get database connection URL."""
return f"postgresql://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
config = Config()
# Made with Bob
-- Enable pgvector extension
CREATE EXTENSION IF NOT EXISTS vector;
-- Create candidates table
CREATE TABLE IF NOT EXISTS candidates (
id SERIAL PRIMARY KEY,
firstname VARCHAR(100) NOT NULL,
lastname VARCHAR(100) NOT NULL,
date_of_birth DATE NOT NULL,
resume_path VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create resume_chunks table for storing document chunks with embeddings
CREATE TABLE IF NOT EXISTS resume_chunks (
id SERIAL PRIMARY KEY,
candidate_id INTEGER REFERENCES candidates(id) ON DELETE CASCADE,
chunk_text TEXT NOT NULL,
chunk_index INTEGER NOT NULL,
embedding vector(384), -- granite-embedding:30m produces 384-dimensional vectors
metadata JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create index for vector similarity search
CREATE INDEX IF NOT EXISTS resume_chunks_embedding_idx
ON resume_chunks USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
-- Create index for candidate lookups
CREATE INDEX IF NOT EXISTS resume_chunks_candidate_id_idx
ON resume_chunks(candidate_id);
-- Insert sample candidates (will be populated by the application)
INSERT INTO candidates (firstname, lastname, date_of_birth, resume_path) VALUES
('John', 'Smith', '1990-05-15', 'resumes/resume1.docx'),
('Sarah', 'Johnson', '1988-08-22', 'resumes/resume2.docx'),
('Michael', 'Williams', '1992-03-10', 'resumes/resume3.docx'),
('Emily', 'Brown', '1995-11-30', 'resumes/resume4.pdf')
ON CONFLICT DO NOTHING;
-- Made with Bob
Przyjmowanie informacji
Etap pobierania informacji funkcjonuje najlepiej, gdy traktowany jest jako mierzalna powierzchnia. Zapisz jeden idealny przepis transkrypcji, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia działań, zanim rozszerzysz zakres. Wolno preferować małe, testowalne jednostki zamiast rozbudowanych skryptów. Gdy jakiś krok się nie powiedzie, awaria powinna wskazywać na konkretną odpowiedzialność, a nie na skomplikowany łańcuch operacji. Oddziel zasadę dzielenia na fragmenty od zasady pobierania danych. Zmiana jednej nie powinna zmuszać do przepisywania drugiej, gdy zmieniają się metryki jakości.
# data_loader.py
"""Data loader for processing and loading candidate resumes."""
import logging
from pathlib import Path
from typing import List, Dict, Any
from src.database_service import DatabaseService
from src.document_processor import DocumentProcessor
from src.embedding_service import EmbeddingService
logger = logging.getLogger(__name__)
class DataLoader:
"""Load and process candidate resumes into the database."""
def __init__(
self,
db_service: DatabaseService,
doc_processor: DocumentProcessor,
embedding_service: EmbeddingService
):
"""Initialize data loader.
Args:
db_service: Database service instance
doc_processor: Document processor instance
embedding_service: Embedding service instance
"""
self.db_service = db_service
self.doc_processor = doc_processor
self.embedding_service = embedding_service
logger.info("DataLoader initialized")
def load_candidate_resume(self, candidate_id: int) -> bool:
"""Load and process a candidate's resume.
Args:
candidate_id: Candidate ID
Returns:
True if successful, False otherwise
"""
try:
# Get candidate info
candidate = self.db_service.get_candidate_by_id(candidate_id)
if not candidate:
logger.error(f"Candidate {candidate_id} not found")
return False
resume_path = candidate['resume_path']
if not resume_path:
logger.error(f"No resume path for candidate {candidate_id}")
return False
# Check if file exists
if not Path(resume_path).exists():
logger.error(f"Resume file not found: {resume_path}")
return False
logger.info(f"Processing resume for {candidate['firstname']} {candidate['lastname']}")
# Process document
chunks = self.doc_processor.process_document(resume_path)
if not chunks:
logger.warning(f"No chunks extracted from {resume_path}")
return False
# Generate embeddings
chunk_texts = [chunk['text'] for chunk in chunks]
embeddings = self.embedding_service.generate_embeddings(chunk_texts)
# Insert into database
self.db_service.insert_resume_chunks(
candidate_id=candidate_id,
chunks=chunks,
embeddings=embeddings
)
logger.info(f"Successfully loaded resume for candidate {candidate_id}")
return True
except Exception as e:
logger.error(f"Error loading resume for candidate {candidate_id}: {e}")
return False
def load_all_resumes(self) -> Dict[int, bool]:
"""Load all candidate resumes.
Returns:
Dictionary mapping candidate IDs to success status
"""
results = {}
candidates = self.db_service.get_all_candidates()
logger.info(f"Loading resumes for {len(candidates)} candidates")
for candidate in candidates:
candidate_id = candidate['id']
success = self.load_candidate_resume(candidate_id)
results[candidate_id] = success
successful = sum(1 for v in results.values() if v)
logger.info(f"Successfully loaded {successful}/{len(candidates)} resumes")
return results
def reload_candidate_resume(self, candidate_id: int) -> bool:
"""Reload a candidate's resume (delete old chunks and reload).
Args:
candidate_id: Candidate ID
Returns:
True if successful, False otherwise
"""
try:
# Delete existing chunks
self.db_service.delete_candidate_chunks(candidate_id)
# Load resume
return self.load_candidate_resume(candidate_id)
except Exception as e:
logger.error(f"Error reloading resume for candidate {candidate_id}: {e}")
return False
# Made with Bob
# document_processor.py
"""Document processing with Docling."""
import logging
from pathlib import Path
from typing import List, Dict, Any
from docling.document_converter import DocumentConverter
from docling_core.transforms.chunker import HierarchicalChunker
logger = logging.getLogger(__name__)
class DocumentProcessor:
"""Process documents using Docling."""
def __init__(self, chunk_size: int = 500, chunk_overlap: int = 50):
"""Initialize document processor.
Args:
chunk_size: Maximum size of text chunks
chunk_overlap: Overlap between chunks
"""
self.converter = DocumentConverter()
self.chunker = HierarchicalChunker(
max_tokens=chunk_size,
overlap_tokens=chunk_overlap
)
logger.info("DocumentProcessor initialized")
def process_document(self, file_path: str) -> List[Dict[str, Any]]:
"""Process a document and return chunks with metadata.
Args:
file_path: Path to the document file
Returns:
List of dictionaries containing chunk text and metadata
"""
try:
logger.info(f"Processing document: {file_path}")
# Convert document
result = self.converter.convert(file_path)
doc = result.document
# Chunk the document
chunks = list(self.chunker.chunk(doc))
# Extract text and metadata from chunks
processed_chunks = []
for idx, chunk in enumerate(chunks):
chunk_data = {
"text": chunk.text,
"index": idx,
"metadata": {
"source": file_path,
"chunk_index": idx,
"total_chunks": len(chunks)
}
}
processed_chunks.append(chunk_data)
logger.info(f"Processed {len(processed_chunks)} chunks from {file_path}")
return processed_chunks
except Exception as e:
logger.error(f"Error processing document {file_path}: {e}")
raise
def process_multiple_documents(self, file_paths: List[str]) -> Dict[str, List[Dict[str, Any]]]:
"""Process multiple documents.
Args:
file_paths: List of document file paths
Returns:
Dictionary mapping file paths to their processed chunks
"""
results = {}
for file_path in file_paths:
try:
results[file_path] = self.process_document(file_path)
except Exception as e:
logger.error(f"Failed to process {file_path}: {e}")
results[file_path] = []
return results
# Made with Bob
# embedding_service.py
"""Embedding service using Ollama."""
import logging
from typing import List, Optional
import ollama
from src.config import config
logger = logging.getLogger(__name__)
class EmbeddingService:
"""Generate embeddings using Ollama."""
def __init__(self, model: Optional[str] = None, host: Optional[str] = None):
"""Initialize embedding service.
Args:
model: Ollama model name for embeddings
host: Ollama host URL
"""
self.model = model or config.EMBEDDING_MODEL
self.host = host or config.OLLAMA_HOST
self.client = ollama.Client(host=self.host)
logger.info(f"EmbeddingService initialized with model: {self.model}")
def generate_embedding(self, text: str) -> List[float]:
"""Generate embedding for a single text.
Args:
text: Text to embed
Returns:
Embedding vector as list of floats
"""
try:
response = self.client.embeddings(
model=self.model,
prompt=text
)
return response['embedding']
except Exception as e:
logger.error(f"Error generating embedding: {e}")
raise
def generate_embeddings(self, texts: List[str]) -> List[List[float]]:
"""Generate embeddings for multiple texts.
Args:
texts: List of texts to embed
Returns:
List of embedding vectors
"""
embeddings = []
for text in texts:
try:
embedding = self.generate_embedding(text)
embeddings.append(embedding)
except Exception as e:
logger.error(f"Failed to generate embedding for text: {e}")
# Return zero vector on error
embeddings.append([0.0] * 384) # granite-embedding:30m dimension
return embeddings
def test_connection(self) -> bool:
"""Test connection to Ollama service.
Returns:
True if connection successful, False otherwise
"""
try:
# Try to generate a test embedding
self.generate_embedding("test")
logger.info("Successfully connected to Ollama")
return True
except Exception as e:
logger.error(f"Failed to connect to Ollama: {e}")
return False
# Made with Bob
Służba RAG
Etap służby RAG funkcjonuje najlepiej, gdy traktowany jest jako mierzalna powierzchnia. Zapisz jeden idealny przepis transkrypcji, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia działań, zanim rozszerzysz zakres. Traktuj ten etap jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nazwij poszczególne elementy, zdefiniuj kryteria sukcesu i odrzuć ciche, częściowe ukończenie zadań.
# rag_service.py
"""RAG service for retrieval and generation."""
import logging
from typing import List, Dict, Any, Optional
import ollama
from src.config import config
from src.database_service import DatabaseService
from src.embedding_service import EmbeddingService
logger = logging.getLogger(__name__)
class RAGService:
"""Retrieval-Augmented Generation service."""
def __init__(
self,
db_service: DatabaseService,
embedding_service: EmbeddingService,
llm_model: Optional[str] = None,
host: Optional[str] = None
):
"""Initialize RAG service.
Args:
db_service: Database service instance
embedding_service: Embedding service instance
llm_model: Ollama LLM model name
host: Ollama host URL
"""
self.db_service = db_service
self.embedding_service = embedding_service
self.llm_model = llm_model or config.LLM_MODEL
self.host = host or config.OLLAMA_HOST
self.client = ollama.Client(host=self.host)
logger.info(f"RAGService initialized with LLM: {self.llm_model}")
def retrieve_context(
self,
query: str,
top_k: int = None,
candidate_id: Optional[int] = None
) -> List[Dict[str, Any]]:
"""Retrieve relevant context for a query.
Args:
query: User query
top_k: Number of results to retrieve
candidate_id: Optional candidate ID to filter results
Returns:
List of relevant chunks with metadata
"""
if top_k is None:
top_k = config.TOP_K_RESULTS
try:
# Generate query embedding
query_embedding = self.embedding_service.generate_embedding(query)
# Search for similar chunks
results = self.db_service.search_similar_chunks(
query_embedding=query_embedding,
top_k=top_k,
candidate_id=candidate_id
)
logger.info(f"Retrieved {len(results)} chunks for query")
return results
except Exception as e:
logger.error(f"Error retrieving context: {e}")
raise
def generate_response(
self,
query: str,
context_chunks: List[Dict[str, Any]],
system_prompt: Optional[str] = None
) -> str:
"""Generate response using LLM with retrieved context.
Args:
query: User query
context_chunks: Retrieved context chunks
system_prompt: Optional system prompt
Returns:
Generated response
"""
try:
# Build context from chunks
context_parts = []
for i, chunk in enumerate(context_chunks, 1):
candidate_name = f"{chunk['firstname']} {chunk['lastname']}"
similarity = chunk.get('similarity', 0)
context_parts.append(
f"[Context {i} - {candidate_name} (Relevance: {similarity:.2f})]\n{chunk['chunk_text']}"
)
context = "\n\n".join(context_parts)
# Default system prompt
if system_prompt is None:
system_prompt = """You are a helpful AI assistant that answers questions about candidates based on their resumes.
Use only the provided context to answer questions. If the context doesn't contain enough information, say so.
Be specific and cite which candidate the information comes from."""
# Build user prompt
user_prompt = f"""Context from candidate resumes:
{context}
Question: {query}
Please provide a detailed answer based on the context above."""
# Generate response
response = self.client.chat(
model=self.llm_model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
)
return response['message']['content']
except Exception as e:
logger.error(f"Error generating response: {e}")
raise
def chat(
self,
query: str,
candidate_id: Optional[int] = None,
top_k: int = None
) -> Dict[str, Any]:
"""Complete RAG pipeline: retrieve and generate.
Args:
query: User query
candidate_id: Optional candidate ID to filter results
top_k: Number of context chunks to retrieve
Returns:
Dictionary with response and metadata
"""
try:
# Retrieve context
context_chunks = self.retrieve_context(
query=query,
top_k=top_k,
candidate_id=candidate_id
)
if not context_chunks:
return {
"response": "I couldn't find any relevant information in the candidate resumes to answer your question.",
"context_chunks": [],
"query": query
}
# Generate response
response = self.generate_response(
query=query,
context_chunks=context_chunks
)
return {
"response": response,
"context_chunks": context_chunks,
"query": query
}
except Exception as e:
logger.error(f"Error in chat: {e}")
raise
def stream_chat(
self,
query: str,
candidate_id: Optional[int] = None,
top_k: int = None
):
"""Stream RAG response.
Args:
query: User query
candidate_id: Optional candidate ID to filter results
top_k: Number of context chunks to retrieve
Yields:
Response chunks
"""
try:
# Retrieve context
context_chunks = self.retrieve_context(
query=query,
top_k=top_k,
candidate_id=candidate_id
)
if not context_chunks:
yield "I couldn't find any relevant information in the candidate resumes to answer your question."
return
# Build context
context_parts = []
for i, chunk in enumerate(context_chunks, 1):
candidate_name = f"{chunk['firstname']} {chunk['lastname']}"
similarity = chunk.get('similarity', 0)
context_parts.append(
f"[Context {i} - {candidate_name} (Relevance: {similarity:.2f})]\n{chunk['chunk_text']}"
)
context = "\n\n".join(context_parts)
system_prompt = """You are a helpful AI assistant that answers questions about candidates based on their resumes.
Use only the provided context to answer questions. If the context doesn't contain enough information, say so.
Be specific and cite which candidate the information comes from."""
user_prompt = f"""Context from candidate resumes:
{context}
Question: {query}
Please provide a detailed answer based on the context above."""
# Stream response
stream = self.client.chat(
model=self.llm_model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
stream=True
)
for chunk in stream:
if 'message' in chunk and 'content' in chunk['message']:
yield chunk['message']['content']
except Exception as e:
logger.error(f"Error in stream_chat: {e}")
yield f"Error: {str(e)}"
# Made with Bob
Wniosek
Linki
Listwa kontrolna operacyjna
Literatura pokrewna
- Praktyczne notatki: Budowanie nowoczesnego pipeline ISR dla modality kodu w dużych projektach — Szczegółowy przewodnik po Praktycznych notatkach: Budowanie nowoczesnego pipeline ISR dla modality kodu w dużych projektach: umowy, sprawdzenia oraz miejsca na kod do wklejenia dla zespołów stosujących ten wzorzec.
- Praktyczne notatki: Poza hasłkami: Jak actualnie funkcjonuje nowoczesna sztuczna inteligencja (z użyciem LLM) — Szczegółowy przewodnik po Praktycznych notatkach: Poza hasłkami: Jak actualnie funkcjonuje nowoczesna sztuczna inteligencja (z użyciem LLM): umowy, sprawdzenia oraz miejsca na kod do wklejenia dla zespołów stosujących ten wzorzec.