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.
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.
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.
#!/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 -20Step 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.
#!/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")
#!/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)
#!/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.
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.