Spaces:
Configuration error
Configuration error
File size: 9,584 Bytes
0004cda | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 | import os
import sys
import json
import time
import urllib.request
import urllib.error
from typing import List, Dict
# Reconfigure stdout for UTF-8 to support Windows console emoji printing
if sys.platform.startswith("win"):
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
# Add the current directory to sys.path so we can import from app
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
try:
from app import answer_question, search_archives
except ImportError as e:
print(f"Error importing app backend: {e}")
sys.exit(1)
# ==============================================================================
# GOLDEN EVALUATION DATASET
# ==============================================================================
# A representative set of critical doctrinal questions to run automated metrics on.
GOLDEN_DATASET = [
{
"query": "what are the seven seals?",
"expected_topics": ["mysteries of God", "Lamb opening the seals", "white horse", "red horse"]
},
{
"query": "what are the seven thunders?",
"expected_topics": ["mysteries", "unwritten", "seventh seal", "revelation"]
},
{
"query": "What is the relationship between the Serpent's Seed and Cain?",
"expected_topics": ["serpent seed", "Eve", "Cain", "lineage", "Satan"]
},
{
"query": "Summarize the seven church ages.",
"expected_topics": ["Ephesus", "Smyrna", "Pergamos", "Thyatira", "Sardis", "Philadelphia", "Laodicea"]
}
]
# ==============================================================================
# LLM-AS-A-JUDGE METRIC EVALUATORS
# ==============================================================================
def call_gemini_judge(prompt: str) -> str:
"""Helper to query the Gemini API to act as an objective evaluation judge with automatic retry backoff."""
api_key = os.getenv("GOOGLE_API_KEY")
if not api_key:
return "ERROR: GOOGLE_API_KEY not set."
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key={api_key}"
headers = {"Content-Type": "application/json"}
payload = {
"contents": [{"parts": [{"text": prompt}]}],
"generationConfig": {"temperature": 0.0} # Temp 0.0 for deterministic evaluation scoring!
}
max_retries = 5
backoff = 10.0
for attempt in range(max_retries):
req = urllib.request.Request(
url,
data=json.dumps(payload).encode("utf-8"),
headers=headers,
method="POST"
)
try:
with urllib.request.urlopen(req) as response:
res = json.loads(response.read().decode("utf-8"))
return res["candidates"][0]["content"]["parts"][0]["text"].strip()
except urllib.error.HTTPError as e:
if e.code == 429:
print(f" [RATE LIMIT 429] Judge hit Gemini rate limits. Retrying in {backoff}s (Attempt {attempt+1}/{max_retries})...")
time.sleep(backoff)
backoff *= 2.0
else:
return f"ERROR: judge API call failed with HTTP {e.code}: {e.reason}"
except Exception as e:
return f"ERROR: judge API call failed: {e}"
return "ERROR: Exceeded maximum API retry limit due to continuous 429 Rate Limits."
def evaluate_faithfulness(answer: str, retrieved_contexts: List[str]) -> float:
"""Evaluates Groundedness/Faithfulness: Is the answer supported *only* by the context?"""
context_text = "\n---\n".join(retrieved_contexts)
prompt = f"""
You are an expert AI system evaluation judge. Your task is to score the FAITHFULNESS (groundedness) of a generated answer based ONLY on the provided retrieved context paragraphs.
RETRIVED CONTEXT:
{context_text}
GENERATED ANSWER:
{answer}
INSTRUCTIONS:
- Analyze the generated answer. Identify every factual claim made in it.
- Check if each claim is directly supported by the retrieved context.
- Rate the FAITHFULNESS on a scale of 0.0 to 1.0:
* 1.0: All claims in the answer are fully grounded and supported by the context.
* 0.7 - 0.9: Most claims are supported, but there are minor ungrounded additions or extensions.
* 0.4 - 0.6: Significant claims in the answer are not found in the context (hallucinations).
* 0.0 - 0.3: The answer has no grounding in the context or directly contradicts it.
Respond with EXACTLY a single floating-point number representing the score. Do not write any other text or reasoning.
"""
result = call_gemini_judge(prompt)
try:
return float(result.strip())
except ValueError:
# Fallback parsing
for token in result.split():
try:
return float(token)
except ValueError:
continue
return 0.5
def evaluate_answer_relevance(query: str, answer: str) -> float:
"""Evaluates Answer Relevance: Does the generated answer address the user query?"""
prompt = f"""
You are an expert AI system evaluation judge. Your task is to score the ANSWER RELEVANCE of a generated response based on the user's initial query.
USER QUERY:
{query}
GENERATED ANSWER:
{answer}
INSTRUCTIONS:
- Score how directly the answer addresses the user query.
- Rate the RELEVANCE on a scale of 0.0 to 1.0:
* 1.0: The answer directly, fully, and clearly answers the user query.
* 0.7 - 0.9: The answer addresses the query, but misses a small aspect or includes unnecessary extra details.
* 0.4 - 0.6: The answer is partially relevant but misses the core question or is vague.
* 0.0 - 0.3: The answer is completely off-topic or fails to address the question.
Respond with EXACTLY a single floating-point number representing the score. Do not write any other text or reasoning.
"""
result = call_gemini_judge(prompt)
try:
return float(result.strip())
except ValueError:
for token in result.split():
try:
return float(token)
except ValueError:
continue
return 0.5
def evaluate_context_recall(expected_topics: List[str], retrieved_contexts: List[str]) -> float:
"""Evaluates Context Recall: Did retrieval capture the key doctrinal topics expected?"""
context_merged = " ".join(retrieved_contexts).lower()
hits = 0
for topic in expected_topics:
if topic.lower() in context_merged:
hits += 1
return hits / len(expected_topics) if expected_topics else 1.0
# ==============================================================================
# MAIN RUNNER
# ==============================================================================
def main():
print("=" * 80)
print("๐ STARTING AUTOMATED RAG EVALUATION SUITE FOR THE 7TH HANDLE")
print(f"Dataset Size: {len(GOLDEN_DATASET)} golden doctrinal test queries")
print("=" * 80)
results = []
for idx, item in enumerate(GOLDEN_DATASET, 1):
query = item["query"]
expected_topics = item["expected_topics"]
print(f"\n[{idx}/{len(GOLDEN_DATASET)}] Evaluating Query: '{query}'...")
# 1. Run RAG Pipeline
start_time = time.perf_counter()
response = answer_question(query, answer_mode="Strict Mode")
latency = time.perf_counter() - start_time
answer = response.get("answer", "")
sources = response.get("source_documents", [])
contexts = [doc.page_content for doc in sources]
# 2. Run Evaluators
faithfulness = evaluate_faithfulness(answer, contexts)
relevance = evaluate_answer_relevance(query, answer)
recall = evaluate_context_recall(expected_topics, contexts)
print(f" -> Latency: {latency:.2f}s | Source Count: {len(sources)}")
print(f" -> Faithfulness (Groundedness): {faithfulness:.2f}")
print(f" -> Answer Relevance: {relevance:.2f}")
print(f" -> Context Recall: {recall:.2f}")
results.append({
"query": query,
"latency": latency,
"faithfulness": faithfulness,
"relevance": relevance,
"recall": recall
})
# Rate limit safety sleep
time.sleep(1.0)
# ==============================================================================
# RENDER REPORT TABLE
# ==============================================================================
print("\n" + "=" * 80)
print("๐ FINAL RAG EVALUATION METRICS REPORT")
print("=" * 80)
print(f"{'Query':<45} | {'Latency':<7} | {'Faithful':<8} | {'Relevance':<9} | {'Recall':<6}")
print("-" * 80)
avg_latency = 0.0
avg_faithfulness = 0.0
avg_relevance = 0.0
avg_recall = 0.0
for res in results:
q_short = res["query"] if len(res["query"]) <= 45 else res["query"][:42] + "..."
print(f"{q_short:<45} | {res['latency']:5.2f}s | {res['faithfulness']:8.2f} | {res['relevance']:9.2f} | {res['recall']:6.2f}")
avg_latency += res["latency"]
avg_faithfulness += res["faithfulness"]
avg_relevance += res["relevance"]
avg_recall += res["recall"]
n = len(results)
print("-" * 80)
print(f"{'OVERALL AVERAGE':<45} | {avg_latency/n:5.2f}s | {avg_faithfulness/n:8.2f} | {avg_relevance/n:9.2f} | {avg_recall/n:6.2f}")
print("=" * 80)
print("๐ Evaluation suite execution completed successfully!")
if __name__ == "__main__":
main()
|