100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Retrieval-Augmented Generation
55 minadvanced

Core RAG Skills Exercise

What You'll Build

In this exercise, you will build a cricket match analysis system that uses Retrieval-Augmented Generation (RAG) to answer complex questions about cricket statistics and historical match data. The system combines a vector database of cricket match records, player statistics, and commentary with a large language model (LLM) to generate contextual answers to queries such as "What was Virat Kohli's performance in bilateral ODIs during 2019?" or "Compare Jasprit Bumrah's bowling economy across different formats."

The RAG pipeline retrieves relevant match documents from a knowledge base, ranks them by relevance, and feeds them to a language model to synthesize accurate, context-aware responses. This architecture demonstrates how RAG improves factual accuracy and reduces hallucination in domain-specific AI systems by grounding responses in verified data rather than relying solely on model parameters.

Analogy🏏Cricket
🏏 Think of it like cricket: Imagine you're a cricket analyst preparing for a match commentary. Before the game, you've collected match scorecards, player performance history, pitch reports, and weather data from the stadium—your 'knowledge base.' When a question comes up during live commentary, like 'Has Rohit Sharma ever scored a century here before?', you don't rely on memory alone (which might be fuzzy or incomplete). Instead, you quickly flip through your indexed filing system (the vector database) to retrieve the most relevant match records from that venue, check Rohit's actual performances there, and synthesize your answer with hard facts. The retrieval step is like scanning your organized files to find the right scorecards; the ranking is like prioritizing the most recent or relevant matches; the augmentation is the language model synthesizing a coherent, authoritative answer from those retrieved facts. Without RAG, you'd guess and risk broadcasting incorrect statistics—with RAG, you ground every answer in verified match data, just as professional analysts do with their research notes during live commentary.

Prerequisites

  • Familiarity with vector embeddings, similarity search, and approximate nearest neighbor algorithms for document retrieval.
  • Understanding of LLM prompt engineering, context windows, and how to structure prompts to guide model responses with retrieved documents.
  • Knowledge of Python async patterns, environment variable management, and API interaction for both embedding models and LLMs.
  • Basic familiarity with JSON/CSV data structures for cricket match records, player statistics, and how to parse domain-specific metadata efficiently.
  • Understanding of evaluation metrics: precision, recall, F1-score for retrieval quality, and BLEU/ROUGE for answer generation quality.

Setup & Project Structure

Your cricket RAG project requires a modular architecture with separate directories for data ingestion, retrieval, and generation. The project structure separates concerns across four layers: a data layer for cricket match JSON documents, a retrieval layer using vector similarity, a ranking and reranking layer to surface the most relevant matches, and a generation layer that prompts the LLM with retrieved context.

Analogy🏏Cricket
🏏 Think of it like cricket: a professional team is not one crowd doing everything — it has a groundstaff preparing the pitch, an analytics unit crunching data, a selection panel picking the eleven, and a captain making the final call on the field. Your RAG project's four layers mirror this division of labour. Just as the groundstaff ingest and prepare the surface, your data layer loads and embeds the cricket match JSON. Just as the analytics unit searches records for patterns, your retrieval layer runs vector similarity to find relevant matches. Just as the selection panel narrows a longlist to the best eleven, your ranking layer reranks candidates for quality. And just as the captain synthesises everything into a match plan, your generation layer prompts the LLM with the retrieved context. The payoff of keeping these roles separate is the same reason clubs don't ask the groundsman to bat at three: each concern can be tested and improved independently without breaking the others.

The system relies on a vector database — either Weaviate or Pinecone — to store embedded match documents, an embedding model such as OpenAI embeddings or Sentence Transformers, and an LLM for response synthesis. Key dependencies include LangChain for orchestration, ChromaDB or Pinecone for vector storage, the Transformers library for embeddings, and the OpenAI or Anthropic SDKs for LLM access.

To keep credentials secure and enable straightforward deployment across environments, all API keys and database credentials should be configured as environment variables rather than hardcoded into the application.

bash
#!/bin/bash
# Project setup for Cricket Match RAG System

mkdir -p cricket_rag_system
cd cricket_rag_system

# Create directory structure
mkdir -p data/{raw_matches,processed}
mkdir -p src/{retrieval,generation,utils}
mkdir -p vectors
mkdir -p results/{answers,metrics}

# Create Python virtual environment
python3 -m venv venv
source venv/bin/activate

# Install dependencies
cat > requirements.txt << 'EOF'
langchain==0.1.0
openai==1.3.0
pinecone-client==2.2.1
chromadb==0.4.0
scipy==1.11.0
python-dotenv==1.0.0
sentence-transformers==2.2.2
pydantic==2.5.0
requests==2.31.0
numpy==1.24.0
pandas==2.1.0
EOF

pip install -r requirements.txt

# Create environment file for API keys
cat > .env.example << 'EOF'
OPENAI_API_KEY=your_openai_api_key
PINCONE_API_KEY=your_pinecone_api_key
PINCONE_ENVIRONMENT=your_pinecone_environment
EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
LLM_MODEL=gpt-4
EOF

echo "Cricket RAG System setup complete!"
echo "Project structure:"
tree -L 3 . 2>/dev/null || find . -type d | head -20

Step 1 — Foundation

Step 1 establishes the data ingestion pipeline by loading cricket match records from JSON files and creating embeddings using a sentence transformer model. This foundation layer transforms raw match documents into vectorized representations that can be stored and searched within a vector database. Each match record — containing metadata such as teams, date, venue, result, player statistics, and commentary excerpts — is embedded into a dense vector space where semantic similarity can be measured.

To implement this step, you will create a MatchDocument class to standardize document structure, implement a batch embedding function to efficiently vectorize documents at scale, and validate embeddings to ensure dimensional consistency before storage. This step is critical because the quality of your vector representations directly determines retrieval accuracy — poorly generated embeddings lead to irrelevant retrieved documents, which in turn degrade the quality of the final generated answers.

Analogy🏏Cricket
🏏 Think of it like cricket: Before a cricket tournament, the organizers must catalog every match—they assign each match a unique identity card that captures essential information: teams, date, venue, winning margin, key player performances, and significant moments. This is like your MatchDocument structure. Then they assign each match a numeric 'fingerprint' (embedding) based on its characteristics—two similar matches (like India vs Pakistan in bilateral ODI at the same venue) get similar fingerprints, while different matches (India vs USA in T20) get distinct fingerprints. Just as scorecards use numerical encoding to store all match data compactly, embeddings compress match semantics into dense vectors. The batch embedding process is like creating a centralized digital archive of all match fingerprints rather than computing them one-by-one, which saves time during tournament season. Without this foundational fingerprinting step, you couldn't quickly find 'all matches where Rohit Sharma scored a century under similar conditions'—you'd have to read every scorecard manually.
python
#!/usr/bin/env python3
# Step 1: Foundation - Data Ingestion & Embedding Pipeline
# Demonstrates: Catalog cricket matches → Create embeddings → Store in retrieval system

import json
import os
from typing import List, Dict, Any
from dataclasses import dataclass
from datetime import datetime
import numpy as np
from sentence_transformers import SentenceTransformer
from pydantic import BaseModel, Field
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# ============================================================================
# 1. DATA MODELS - Like cricket scorecard cataloging system
# ============================================================================

class MatchDocument(BaseModel):
    """Represents a cricket match - the identity card with essential information"""
    match_id: str = Field(..., description="Unique match identifier")
    tournament_name: str
    teams: List[str] = Field(..., min_items=2, max_items=2)
    match_date: str
    venue: str
    match_format: str  # ODI, T20, Test
    winning_team: str
    winning_margin: str
    key_performers: List[str]
    significant_moments: str
    match_summary: str
    
    def get_cricket_fingerprint_text(self) -> str:
        """Create text representation for embedding (numeric fingerprint)"""
        return f"""
        Match: {self.teams[0]} vs {self.teams[1]}
        Tournament: {self.tournament_name}
        Format: {self.match_format}
        Venue: {self.venue}
        Date: {self.match_date}
        Winner: {self.winning_team} by {self.winning_margin}
        Star Players: {', '.join(self.key_performers)}
        Key Moments: {self.significant_moments}
        Summary: {self.match_summary}
        """


class EmbeddedMatch(BaseModel):
    """Match with its numeric fingerprint (embedding)"""
    match_document: MatchDocument
    embedding_vector: List[float] = Field(..., description="Numeric fingerprint")
    embedding_model: str


# ============================================================================
# 2. EMBEDDING PIPELINE - Creating numeric fingerprints
# ============================================================================

class CricketEmbeddingPipeline:
    """
    Converts cricket match text into numeric fingerprints.
    Similar matches (India vs Pakistan at same venue)  Similar fingerprints
    Different matches (India vs USA)  Different fingerprints
    """
    
    def __init__(self, model_name: str = "all-MiniLM-L6-v2"):
        """Initialize the embedding model"""
        logger.info(f"🏏 Loading embedding model: {model_name}")
        self.embedding_model = SentenceTransformer(model_name)
        self.model_name = model_name
    
    def create_match_embedding(self, match_doc: MatchDocument) -> np.ndarray:
        """
        Create a numeric fingerprint for a match.
        The fingerprint captures: teams, venue, format, key performances, outcomes
        """
        match_fingerprint_text = match_doc.get_cricket_fingerprint_text()
        embedding_vector = self.embedding_model.encode(match_fingerprint_text)
        return embedding_vector
    
    def embed_match_catalog(self, matches: List[MatchDocument]) -> List[EmbeddedMatch]:
        """
        Catalog multiple matches with their fingerprints.
        Like assigning identity cards to every match in the tournament.
        """
        embedded_matches = []
        
        for match in matches:
            logger.info(f"📊 Creating fingerprint for: {match.teams[0]} vs {match.teams[1]}")
            vector = self.create_match_embedding(match)
            
            embedded_match = EmbeddedMatch(
                match_document=match,
                embedding_vector=vector.tolist(),
                embedding_model=self.model_name
            )
            embedded_matches.append(embedded_match)
        
        return embedded_matches


# ============================================================================
# 3. SAMPLE DATA - Cricket match catalog
# ============================================================================

CRICKET_MATCH_CATALOG = [
    MatchDocument(
        match_id="IND_PAK_ODI_001",
        tournament_name="ICC Cricket World Cup 2023",
        teams=["India", "Pakistan"],
        match_date="2023-10-14",
        venue="MCG, Melbourne",
        match_format="ODI",
        winning_team="India",
        winning_margin="6 wickets",
        key_performers=["Virat Kohli", "Jasprit Bumrah", "Babar Azam"],
        significant_moments="Bumrah 2/43, Kohli batting masterclass in chase",
        match_summary="India successfully chased Pakistan's total in a thrilling encounter at MCG"
    ),
    MatchDocument(
        match_id="IND_PAK_T20_001",
        tournament_name="T20 World Cup 2024",
        teams=["India", "Pakistan"],
        match_date="2024-06-09",
        venue="MCG, Melbourne",
        match_format="T20",
        winning_team="Pakistan",
        winning_margin="4 runs",
        key_performers=["Rohit Sharma", "Mohammad Rizwan", "Naseem Shah"],
        significant_moments="Dramatic finish, Pakistan bowled brilliantly in death overs",
        match_summary="Pakistan narrowly defeated India in a high-octane T20 clash"
    ),
    MatchDocument(
        match_id="IND_USA_T20_001",
        tournament_name="T20 World Cup 2024",
        teams=["India", "USA"],
        match_date="2024-06-12",
        venue="Nassau County International Cricket Stadium, New York",
        match_format="T20",
        winning_team="India",
        winning_margin="6 runs",
        key_performers=["Suryakumar Yadav", "Jasprit Bumrah", "Aaron Jones"],
        significant_moments="India won in Super Over, Bumrah's final over was decisive",
        match_summary="India beat USA in a Super Over thriller in New York"
    ),
    MatchDocument(
        match_id="AUS_ENG_TEST_001",
        tournament_name="Ashes Series 2023",
        teams=["Australia", "England"],
        match_date="2023-11-30",
        venue="The Oval, London",
        match_format="Test",
        winning_team="Australia",
        winning_margin="275 runs",
        key_performers=["Steve Smith", "Stuart Broad", "Nathan Lyon"],
        significant_moments="Smith's century, Lyon took 6 wickets in innings",
        match_summary="Australia dominates England in final Ashes Test"
    ),
    MatchDocument(
        match_id="IND_AUS_ODI_001",
        tournament_name="ICC Cricket World Cup 2023",
        teams=["India", "Australia"],
        match_date="2023-10-08",
        venue="MCG, Melbourne",
        match_format="ODI",
        winning_team="India",
        winning_margin="6 wickets",
        key_performers=["Virat Kohli", "Josh Hazlewood", "Siraj"],
        significant_moments="Kohli's aggressive 86, Siraj's early breakthroughs",
        match_summary="India overcomes Australian bowling attack in World Cup"
    )
]


# ============================================================================
# 4. DEMONSTRATION - Running the RAG ingestion pipeline
# ============================================================================

def main():
    """Demonstrate the cricket match cataloging and embedding pipeline"""
    
    print("\n" + "="*80)
    print("🏏 CRICKET MATCH CATALOG & EMBEDDING PIPELINE 🏏")
    print("="*80)
    
    # Step 1: Initialize the embedding pipeline
    print("\n📚 Step 1: Initializing Embedding Pipeline...")
    pipeline = CricketEmbeddingPipeline(model_name="all-MiniLM-L6-v2")
    
    # Step 2: Create embeddings (numeric fingerprints) for all matches
    print("\n🔢 Step 2: Creating Numeric Fingerprints for Each Match...")
    embedded_matches = pipeline.embed_match_catalog(CRICKET_MATCH_CATALOG)
    
    # Step 3: Display results and verify similarity
    print("\n📊 Step 3: Analyzing Match Fingerprints...")
    print(f"Total matches cataloged: {len(embedded_matches)}")
    
    # Show embedding dimensions
    embedding_dim = len(embedded_matches[0].embedding_vector)
    print(f"Each fingerprint has {embedding_dim} dimensions")
    
    # Calculate and display similarities between similar vs different matches
    print("\n🔍 Step 4: Similarity Analysis (Cosine Similarity)...")
    
    def cosine_similarity(vec1: List[float], vec2: List[float]) -> float:
        """Calculate cosine similarity between two embedding vectors"""
        v1 = np.array(vec1)
        v2 = np.array(vec2)
        return float(np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2)))
    
    # Compare India vs Pakistan matches (similar)
    sim_similar = cosine_similarity(
        embedded_matches[0].embedding_vector,  # India vs Pakistan ODI
        embedded_matches[1].embedding_vector   # India vs Pakistan T20
    )
    print(f"📍 India vs Pakistan (ODI) ↔ India vs Pakistan (T20): {sim_similar:.4f}")
    print("   → HIGH similarity (same teams, same venue)")
    
    # Compare India vs Pakistan with India vs USA (different)
    sim_different = cosine_similarity(
        embedded_matches[0].embedding_vector,  # India vs Pakistan ODI
        embedded_matches[2].embedding_vector   # India vs USA T20
    )
    print(f"📍 India vs Pakistan (ODI) ↔ India vs USA (T20): {sim_different:.4f}")
    print("   → LOWER similarity (different opponents)")
    
    # Compare India vs USA with India vs Australia (different tournaments/formats)
    sim_different2 = cosine_similarity(
        embedded_matches[2].embedding_vector,  # India vs USA T20
        embedded_matches[4].embedding_vector   # India vs Australia ODI
    )
    print(f"📍 India vs USA (T20) ↔ India vs Australia (ODI): {sim_different2:.4f}")
    print("   → LOWER similarity (different formats and opponents)")
    
    # Step 4: Display sample embedded match
    print("\n💾 Step 5: Sample Embedded Match Document...")
    sample_match = embedded_matches[0]
    print(f"Match ID: {sample_match.match_document.match_id}")
    print(f"Teams: {sample_match.match_document.teams}")
    print(f"Venue: {sample_match.match_document.venue}")
    print(f"Format: {sample_match.match_document.match_format}")
    print(f"Winner: {sample_match.match_document.winning_team}")
    print(f"Embedding Model: {sample_match.embedding_model}")
    print(f"Fingerprint (first 10 dimensions): {sample_match.embedding_vector[:10]}")
    
    # Step 5: Store ingestion metadata
    print("\n✅ Step 6: Ingestion Complete - Ready for Retrieval!")
    print(f"   • {len(embedded_matches)} matches cataloged")
    print(f"   • Each match has a {embedding_dim}-dimensional fingerprint")
    print(f"   • Fingerprints enable fast semantic search")
    print(f"   • Similar matches can be found by comparing fingerprints")
    
    return embedded_matches


if __name__ == "__main__":
    embedded_catalog = main()
    print("\n" + "="*80)
    print("🏏 Catalog ready for Retrieval-Augmented Generation pipeline 🏏")
    print("="*80 + "\n")
python
#!/usr/bin/env python3
# Step 2: Core Logic - Retrieval & Ranking
# src/retrieval/retriever.py

import numpy as np
import json
from typing import List, Tuple, Dict, Optional
from dataclasses import dataclass, asdict
from sentence_transformers import SentenceTransformer, CrossEncoder
from scipy.spatial.distance import cosine
from datetime import datetime
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

@dataclass
class RetrievedMatch:
    """Represents a retrieved cricket match document with relevance score"""
    match_id: str
    teams: str
    venue: str
    match_date: str
    winning_margin: str
    key_performer: str
    relevance_score: float
    embedding: Optional[List[float]] = None

@dataclass
class MatchDocument:
    """Original match document structure - the identity card for each cricket match"""
    match_id: str
    teams: str  # e.g., "India vs Pakistan"
    venue: str  # e.g., "Lahore Stadium"
    match_date: str  # e.g., "2023-12-25"
    format: str  # ODI, T20, Test
    winning_margin: str  # e.g., "7 wickets"
    key_performer: str  # e.g., "Rohit Sharma"
    significant_moments: str  # Match highlights
    batting_first_team_score: int
    embedding: Optional[np.ndarray] = None

class CricketRAGRetriever:
    """
    Core RAG retriever that uses embeddings (numeric fingerprints) 
    to find similar cricket matches based on query characteristics
    """
    
    def __init__(self, embedding_model: str = "all-MiniLM-L6-v2", 
                 ranker_model: str = "cross-encoder/ms-marco-MiniLM-L-6-v2"):
        """Initialize retriever with embedding and ranking models"""
        logger.info("🏏 Initializing Cricket RAG Retriever...")
        self.embedder = SentenceTransformer(embedding_model)
        self.ranker = CrossEncoder(ranker_model)
        self.match_database: List[MatchDocument] = []
        self.embeddings_index: Dict[str, np.ndarray] = {}
        logger.info("✅ Retriever initialized successfully")
    
    def index_cricket_matches(self, match_documents: List[MatchDocument]) -> None:
        """
        Index cricket match documents by creating numeric fingerprints (embeddings)
        Similar matches get similar fingerprints, different matches get distinct ones
        """
        logger.info(f"🏏 Indexing {len(match_documents)} cricket matches...")
        self.match_database = match_documents
        
        for match in match_documents:
            # Create a text summary of the match - this becomes the "characteristics"
            match_summary = f"{match.teams} at {match.venue} ({match.match_date}). " \
                          f"Format: {match.format}. Winner: {match.winning_margin}. " \
                          f"Key player: {match.key_performer}. {match.significant_moments}"
            
            # Generate embedding (numeric fingerprint) for this match
            match_embedding = self.embedder.encode(match_summary, convert_to_numpy=True)
            
            # Store embedding with match
            match.embedding = match_embedding
            self.embeddings_index[match.match_id] = match_embedding
            
            logger.info(f"  ✓ Indexed match {match.match_id}: {match.teams}")
        
        logger.info(f"✅ Indexed {len(self.match_database)} matches successfully")
    
    def retrieve_similar_matches(self, query: str, top_k: int = 5) -> List[RetrievedMatch]:
        """
        Retrieve most similar matches using embedding similarity search
        Like finding other India vs Pakistan matches at similar venues
        """
        logger.info(f"🏏 Retrieving similar matches for query: '{query}'")
        
        # Step 1: Convert query to embedding (same fingerprint logic)
        query_embedding = self.embedder.encode(query, convert_to_numpy=True)
        
        # Step 2: Calculate similarity scores using cosine distance
        similarity_scores: List[Tuple[str, float]] = []
        
        for match in self.match_database:
            # Cosine similarity: 0 = completely different, 1 = identical fingerprints
            distance = cosine(query_embedding, match.embedding)
            similarity_score = 1 - distance  # Convert distance to similarity
            similarity_scores.append((match.match_id, similarity_score))
        
        # Step 3: Sort by relevance and get top_k
        similarity_scores.sort(key=lambda x: x[1], reverse=True)
        top_matches = similarity_scores[:top_k]
        
        logger.info(f"📊 Found {len(top_matches)} candidate matches")
        
        # Step 4: Retrieve full match documents for top candidates
        retrieved_matches = []
        for match_id, score in top_matches:
            match = next((m for m in self.match_database if m.match_id == match_id), None)
            if match:
                retrieved = RetrievedMatch(
                    match_id=match.match_id,
                    teams=match.teams,
                    venue=match.venue,
                    match_date=match.match_date,
                    winning_margin=match.winning_margin,
                    key_performer=match.key_performer,
                    relevance_score=float(score),
                    embedding=match.embedding.tolist() if match.embedding is not None else None
                )
                retrieved_matches.append(retrieved)
        
        return retrieved_matches
    
    def rerank_matches(self, query: str, candidates: List[RetrievedMatch]) -> List[RetrievedMatch]:
        """
        Re-rank retrieved matches using a cross-encoder model
        This fine-tunes the ranking by considering query-document pairs together
        """
        logger.info(f"♻️  Re-ranking {len(candidates)} candidates...")
        
        # Prepare pairs of (query, match_summary) for cross-encoder
        match_summaries = [
            f"{c.teams} at {c.venue} ({c.match_date}). "
            f"Winner: {c.winning_margin}. Key player: {c.key_performer}"
            for c in candidates
        ]
        
        pairs = [[query, summary] for summary in match_summaries]
        
        # Get reranking scores from cross-encoder
        rerank_scores = self.ranker.predict(pairs)
        
        # Update relevance scores with reranked values
        for candidate, score in zip(candidates, rerank_scores):
            candidate.relevance_score = float(score)
        
        # Sort by new scores
        candidates.sort(key=lambda x: x.relevance_score, reverse=True)
        
        logger.info("✅ Re-ranking complete")
        return candidates
    
    def hybrid_retrieve(self, query: str, top_k: int = 5, rerank: bool = True) -> List[RetrievedMatch]:
        """
        Hybrid retrieval: Combine embedding-based retrieval with optional reranking
        This is the complete RAG pipeline for cricket match retrieval
        """
        logger.info("🏏 Starting hybrid retrieval pipeline...")
        
        # Phase 1: Fast embedding-based retrieval
        candidates = self.retrieve_similar_matches(query, top_k=top_k*2)
        
        # Phase 2: Optional precision reranking
        if rerank and len(candidates) > 0:
            candidates = self.rerank_matches(query, candidates)
        
        # Return top_k final results
        final_results = candidates[:top_k]
        
        logger.info(f"🎯 Final results: {len(final_results)} matches retrieved")
        return final_results


# ============================================================================
# EXAMPLE USAGE: Cricket Match Retrieval System
# ============================================================================

if __name__ == "__main__":
    logger.info("\n" + "="*70)
    logger.info("CRICKET MATCH RAG SYSTEM - Core Retrieval Demonstration")
    logger.info("="*70 + "\n")
    
    # Create sample cricket match documents (like scorecard catalog)
    cricket_matches = [
        MatchDocument(
            match_id="IND_PAK_001",
            teams="India vs Pakistan",
            venue="Lahore Stadium",
            match_date="2023-12-25",
            format="ODI",
            winning_margin="7 wickets",
            key_performer="Rohit Sharma",
            significant_moments="Rohit Sharma scored 95 runs. Jasprit Bumrah took 3 wickets.",
            batting_first_team_score=285
        ),
        MatchDocument(
            match_id="IND_USA_001",
            teams="India vs USA",
            venue="New York Stadium",
            match_date="2024-06-15",
            format="T20",
            winning_margin="110 runs",
            key_performer="Virat Kohli",
            significant_moments="Kohli smashed 82 runs. Pace attack dominated.",
            batting_first_team_score=198
        ),
        MatchDocument(
            match_id="IND_AUS_001",
            teams="India vs Australia",
            venue="MCG Melbourne",
            match_date="2023-10-08",
            format="ODI",
            winning_margin="6 wickets",
            key_performer="KL Rahul",
            significant_moments="KL Rahul's 78 guided chase. Bumrah's spell was economical.",
            batting_first_team_score=312
        ),
        MatchDocument(
            match_id="PAK_AUS_001",
            teams="Pakistan vs Australia",
            venue="Karachi Stadium",
            match_date="2024-01-10",
            format="Test",
            winning_margin="Innings and 40 runs",
            key_performer="Naseem Shah",
            significant_moments="Naseem took 7 wickets. Strong bowling performance.",
            batting_first_team_score=456
        ),
        MatchDocument(
            match_id="IND_PAK_002",
            teams="India vs Pakistan",
            venue="Islamabad Stadium",
            match_date="2024-02-20",
            format="T20",
            winning_margin="8 runs",
            key_performer="Jasprit Bumrah",
            significant_moments="Bumrah's final over heroics. India defended 165.",
            batting_first_team_score=165
        ),
    ]
    
    # Initialize retriever and index matches
    retriever = CricketRAGRetriever()
    retriever.index_cricket_matches(cricket_matches)
    
    # Example Query 1: Search for India vs Pakistan matches
    logger.info("\n🔍 QUERY 1: India vs Pakistan bilateral match\n")
    results_1 = retriever.hybrid_retrieve(
        query="India vs Pakistan bilateral cricket match",
        top_k=3,
        rerank=True
    )
    
    logger.info("\n📋 Top Retrieved Matches:")
    for i, match in enumerate(results_1, 1):
        logger.info(f"\n  {i}. {match.teams}")
        logger.info(f"     Venue: {match.venue}")
        logger.info(f"     Date: {match.match_date}")
        logger.info(f"     Winner: {match.winning_margin}")
        logger.info(f"     Key Player: {match.key_performer}")
        logger.info(f"     📊 Relevance Score: {match.relevance_score:.4f}")
    
    # Example Query 2: Search for matches with strong bowling performances
    logger.info("\n\n🔍 QUERY 2: Cricket match with outstanding bowling performance\n")
    results_2 = retriever.hybrid_retrieve(
        query="bowling performance wickets take wickets pitcher",
        top_k=3,
        rerank=True
    )
    
    logger.info("\n📋 Top Retrieved Matches:")
    for i, match in enumerate(results_2, 1):
        logger.info(f"\n  {i}. {match.teams}")
        logger.info(f"     Venue: {match.venue}")
        logger.info(f"     Date: {match.match_date}")
        logger.info(f"     Key Player: {match.key_performer}")
        logger.info(f"     📊 Relevance Score: {match.relevance_score:.4f}")
    
    logger.info("\n" + "="*70)
    logger.info("✅ Core RAG Skills Demonstration Complete!")
    logger.info("="*70)
Analogy🏏Cricket
🏏 Think of it like cricket: You've retrieved the most relevant match scorecards and player statistics (Step 2), but now you need to synthesize them into a coherent, authoritative analysis for a broadcast or article. You're a cricket commentator who receives those retrieved scorecards and must craft a narrative explanation: 'Here's why Rohit Sharma performed better against Australia in 2023'—pulling from actual numbers in the scorecards, citing specific matches, and connecting dots across multiple games. The prompt template is like an editorial template: 'Start with the player's overall performance, cite 2-3 specific matches, highlight key statistics, conclude with trends.' The LLM is like an experienced analyst who reads these scorecards and synthesizes them into fluent commentary—but we must fact-check that the commentary doesn't invent statistics ('Rohit averaged 150 in 2023' when scorecards show 89). Citation tracking ensures every claim in the answer points back to a source scorecard. Post-processing extracts tables of statistics directly from the data, so we generate precise tables rather than narrative prose when precision is needed. This validation step is critical because unchecked LLMs can 'hallucinate' statistics; with RAG and fact-checking, every answer is grounded in verified match data.
python
#!/usr/bin/env python3
# Step 3: Integration & Enhancement - RAG Generation Pipeline
# src/generation/generator.py

import json
from typing import List, Dict, Tuple, Optional
from dataclasses import dataclass, asdict
from datetime import datetime
import logging
import re
from openai import OpenAI

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

@dataclass
class Citation:
    """Represents a citation to a source match."""
    match_id: str
    teams: str
    date: str
    venue: str

@dataclass
class GeneratedAnswer:
    """Represents the final RAG-generated answer."""
    query: str
    answer: str
    citations: List[Citation]
    source_matches: List[str]  # match_ids used
    retrieved_count: int
    generation_time_ms: float
    confidence_score: float

class CricketRAGGenerator:
    """Generates cricket analysis answers using retrieved context."""
    
    def __init__(self, api_key: str, model: str = "gpt-4"):
        """Initialize LLM client for answer generation."""
        logger.info(f"Initializing RAG Generator with model: {model}")
        self.client = OpenAI(api_key=api_key)
        self.model = model
        self.conversation_history = []
    
    def format_context(
        self,
        retrieved_matches: List[Dict],
        max_context_length: int = 3000
    ) -> str:
        """Format retrieved matches into LLM context."""
        context_lines = ["## Retrieved Match Context\n"]
        total_length = 0
        
        for i, match in enumerate(retrieved_matches, 1):
            match_block = f"""
### Match {i}: {match['match_id']}
- **Teams**: {match['teams'][0]} vs {match['teams'][1]}
- **Date**: {match['match_date']}
- **Venue**: {match['venue']}
- **Format**: {match['format']}
- **Result**: {match['result']}
- **Winning Margin**: {match.get('winning_margin', 'N/A')}

**Player Statistics**:
"""
            for player, stats in match.get('player_stats', {}).items():
                stats_str = ", ".join([f"{k}: {v}" for k, v in stats.items()])
                match_block += f"- {player}: {stats_str}\n"
            
            match_block += f"\n**Key Moments**: {', '.join(match.get('key_moments', []))}\n"
            
            if total_length + len(match_block) > max_context_length:
                logger.warning(f"Context limit reached at match {i}")
                break
            
            context_lines.append(match_block)
            total_length += len(match_block)
        
        return "\n".join(context_lines)
    
    def build_prompt(
        self,
        query: str,
        context: str,
        system_instruction: Optional[str] = None
    ) -> Tuple[str, str]:
        """Build system and user prompts for LLM."""
        if system_instruction is None:
            system_instruction = """You are an expert cricket analyst with deep knowledge of cricket statistics, 
player performances, and historical matches. Your task is to answer questions about cricket based on the 
retrieved match data provided. 

Instructions:
1. Answer the question thoroughly using only information from the retrieved matches.
2. If the retrieved context doesn't contain relevant information, say so explicitly.
3. Cite specific match IDs and dates when referencing data.
4. Never invent statistics or claims not supported by the context.
5. Highlight any trends or patterns you notice across multiple matches.
6. Be concise but comprehensive."""
        
        user_prompt = f"""{context}

## Your Question
{query}

Please provide a detailed answer based on the retrieved match data above. Make sure to cite the matches you're referencing."""
        
        return system_instruction, user_prompt
    
    def generate_answer(
        self,
        query: str,
        retrieved_matches: List[Dict],
        temperature: float = 0.7
    ) -> GeneratedAnswer:
        """Generate RAG answer using LLM with retrieved context."""
        import time
        start_time = time.time()
        
        logger.info(f"Generating answer for: {query[:60]}")
        logger.info(f"Using {len(retrieved_matches)} retrieved matches")
        
        # Format context
        context = self.format_context(retrieved_matches)
        
        # Build prompts
        system_prompt, user_prompt = self.build_prompt(query, context)
        
        # Call LLM
        logger.info(f"Calling {self.model}...")
        response = self.client.chat.completions.create(
            model=self.model,
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": user_prompt}
            ],
            temperature=temperature,
            max_tokens=1500
        )
        
        answer_text = response.choices[0].message.content
        generation_time = (time.time() - start_time) * 1000
        
        # Extract citations (match IDs mentioned in answer)
        citations = self._extract_citations(answer_text, retrieved_matches)
        
        # Calculate confidence score based on citation coverage
        confidence = self._calculate_confidence(answer_text, citations)
        
        logger.info(f"Generated answer in {generation_time:.1f}ms")
        logger.info(f"Found {len(citations)} citations")
        
        return GeneratedAnswer(
            query=query,
            answer=answer_text,
            citations=citations,
            source_matches=[m['match_id'] for m in retrieved_matches],
            retrieved_count=len(retrieved_matches),
            generation_time_ms=generation_time,
            confidence_score=confidence
        )
    
    def _extract_citations(
        self,
        answer_text: str,
        source_matches: List[Dict]
    ) -> List[Citation]:
        """Extract match IDs and match details from generated answer."""
        citations = []
        match_id_pattern = r'([A-Z]{3}_[A-Z]{3}_[A-Z0-9_]+)'
        
        found_match_ids = set(re.findall(match_id_pattern, answer_text))
        
        for match_id in found_match_ids:
            for match in source_matches:
                if match.get('match_id') == match_id:
                    citations.append(Citation(
                        match_id=match_id,
                        teams=f"{match['teams'][0]} vs {match['teams'][1]}",
                        date=match.get('match_date', 'N/A'),
                        venue=match.get('venue', 'N/A')
                    ))
                    break
        
        return citations
    
    def _calculate_confidence(self, answer_text: str, citations: List[Citation]) -> float:
        """Calculate confidence score based on answer grounding."""
        if not answer_text:
            return 0.0
        
        # Base confidence on citation count and answer length
        citation_score = min(len(citations) / 3.0, 1.0)  # Reward up to 3 citations
        length_score = min(len(answer_text) / 500.0, 1.0)  # Reward comprehensive answers
        
        # Check for uncertainty language
        uncertainty_phrases = ['might', 'could', 'unclear', 'uncertain', 'insufficient data']
        uncertainty_count = sum(1 for phrase in uncertainty_phrases if phrase in answer_text.lower())
        uncertainty_penalty = uncertainty_count * 0.1
        
        confidence = (0.6 * citation_score + 0.4 * length_score) - uncertainty_penalty
        return max(0.0, min(1.0, confidence))
    
    def validate_answer(
        self,
        answer: GeneratedAnswer,
        strict_mode: bool = True
    ) -> Dict[str, any]:
        """Validate answer for factual grounding and hallucinations."""
        logger.info(f"Validating answer...")
        
        validation_result = {
            "is_valid": True,
            "issues": [],
            "citation_ratio": len(answer.citations) / max(1, answer.retrieved_count),
            "confidence_score": answer.confidence_score
        }
        
        # Check minimum citations
        if len(answer.citations) < 1 and strict_mode:
            validation_result["is_valid"] = False
            validation_result["issues"].append(
                "Answer has no citations to source matches"
            )
        
        # Check confidence threshold
        if answer.confidence_score < 0.4 and strict_mode:
            validation_result["is_valid"] = False
            validation_result["issues"].append(
                f"Low confidence score: {answer.confidence_score:.2f}"
            )
        
        # Check answer length
        if len(answer.answer) < 100:
            validation_result["issues"].append(
                "Answer may be too brief"
            )
        
        logger.info(f"Validation result: {'✓ VALID' if validation_result['is_valid'] else '✗ INVALID'}")
        return validation_result

# Example usage
if __name__ == "__main__":
    import os
    from dotenv import load_dotenv
    
    load_dotenv()
    api_key = os.getenv("OPENAI_API_KEY")
    
    # Initialize generator
    generator = CricketRAGGenerator(api_key=api_key, model="gpt-4")
    
    # Sample retrieved matches
    sample_retrieved = [
        {
            "match_id": "IND_AUS_ODI_2023_01",
            "teams": ["India", "Australia"],
            "match_date": "2023-03-15",
            "venue": "MCG, Melbourne",
            "format": "ODI",
            "result": "India won by 6 runs",
            "winning_margin": "6 runs",
            "player_stats": {
                "Rohit Sharma": {"runs": 119, "balls": 127, "fours": 8},
                "Virat Kohli": {"runs": 85, "balls": 98},
                "Jasprit Bumrah": {"wickets": 3, "runs_conceded": 42}
            },
            "key_moments": ["Rohit century", "Bumrah's death bowling"]
        }
    ]
    
    # Generate answer
    query = "How did Rohit Sharma perform against Australia in 2023?"
    
    answer = generator.generate_answer(
        query=query,
        retrieved_matches=sample_retrieved
    )
    
    print(f"\n{'='*60}")
    print(f"Question: {answer.query}")
    print(f"{'='*60}")
    print(f"\nAnswer:\n{answer.answer}")
    print(f"\nCitations: {len(answer.citations)}")
    for citation in answer.citations:
        print(f"  - {citation.match_id}: {citation.teams} on {citation.date}")
    print(f"\nConfidence Score: {answer.confidence_score:.2f}")
    print(f"Generation Time: {answer.generation_time_ms:.1f}ms")
    
    # Validate
    validation = generator.validate_answer(answer, strict_mode=False)
    print(f"\nValidation: {'✓ VALID' if validation['is_valid'] else '✗ INVALID'}")
    if validation["issues"]:
        print("Issues:")
        for issue in validation["issues"]:
            print(f"  - {issue}")

Step 4 — Testing & Verification

Step 4 runs the complete end-to-end RAG pipeline on sample cricket queries to verify correct behavior across all stages. The test harness executes ingestion (loading and embedding match data), retrieval (searching for relevant matches), ranking (reranking for quality), and generation (LLM synthesis) in sequence, logging intermediate results at each step.

Analogy🏏Cricket
🏏 Think of it like cricket: before a plan is trusted, a team plays a full warm-up fixture that runs every phase together — openers see the new ball, bowlers find rhythm, fielders hold catches — to expose faults while they are cheap to fix. Your Step 4 is that fixture. Just as a practice game exercises the whole team in sequence rather than in isolated nets, the test harness runs ingestion, retrieval, ranking, and generation end to end on sample cricket queries. Just as coaches log how each phase performed to spot the weak link, the harness logs intermediate results at every stage so you can see exactly where behaviour drifts. Passing individual nets never guarantees a winning eleven — only the full fixture reveals how the parts combine. The payoff is match-ready confidence: a pipeline proven to move a query cleanly from raw match data all the way to a grounded answer before it faces real users.

During validation, the harness measures latency, confirms that retrieved matches are relevant, checks that generated answers cite their sources, and verifies confidence scores. The expected output includes the original query, ranked retrieved matches with similarity scores, the final generated answer with citations, and validation results confirming that the answer is grounded in source data without hallucination.

Lesson 10 of 35
0% complete