File size: 5,078 Bytes
a7cba55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
RAGAS evaluation harness — the "RAGAS evaluation suite" box in the
architecture diagram (Golden dataset · 15 samples · 6 tests, F/R/P/C metrics,
Judge LLM).

Usage:
    python -m evaluation.ragas_eval
    python -m evaluation.ragas_eval --output results.json
"""
from __future__ import annotations

import argparse
import json
import logging
import os
from pathlib import Path
from typing import List

from dotenv import load_dotenv

load_dotenv()
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
logger = logging.getLogger("evaluation.ragas_eval")

GOLDEN_DATASET_PATH = Path(__file__).parent / "golden_dataset.json"


def _run_pipeline_for_eval(question: str) -> dict:
    """Runs the real LangGraph pipeline and extracts what RAGAS needs."""
    import requests
    
    # Try calling the running FastAPI server first to avoid Qdrant local file lock conflicts
    api_url = os.getenv("API_URL", "http://localhost:8000")
    try:
        response = requests.post(
            f"{api_url}/query",
            json={"query": question, "thread_id": f"eval-{abs(hash(question))}"},
            timeout=180
        )
        if response.status_code == 200:
            data = response.json()
            contexts = [s["text_preview"] for s in data.get("sources", [])]
            return {
                "answer": data.get("answer", ""),
                "contexts": contexts or ["(no context retrieved)"],
            }
        else:
            raise RuntimeError(f"API server returned status {response.status_code}: {response.text}")
    except (requests.exceptions.ConnectionError, requests.exceptions.ConnectTimeout):
        logger.warning("API server connection refused. Falling back to direct local execution.")
    except Exception as exc:
        logger.error("API query failed during evaluation: %s", exc)
        raise

    # Fallback to direct Python import/execution if API server is not running
    from core.graph import run_query

    state = run_query(question, thread_id=f"eval-{abs(hash(question))}")
    contexts = [c["text"] for c in state.get("reranked_chunks", [])]
    return {
        "answer": state.get("final_answer", ""),
        "contexts": contexts or ["(no context retrieved)"],
    }


def build_evaluation_dataset(golden_samples: List[dict]):
    """Runs the pipeline for every golden question and assembles a RAGAS-ready dataset."""
    from datasets import Dataset

    questions, answers, contexts, ground_truths = [], [], [], []

    for sample in golden_samples:
        logger.info("Running pipeline for eval question: %s", sample["question"])
        result = _run_pipeline_for_eval(sample["question"])
        questions.append(sample["question"])
        answers.append(result["answer"])
        contexts.append(result["contexts"])
        ground_truths.append(sample["ground_truth"])

    return Dataset.from_dict(
        {
            "question": questions,
            "answer": answers,
            "contexts": contexts,
            "ground_truth": ground_truths,
        }
    )


def run_evaluation(golden_dataset_path: Path = GOLDEN_DATASET_PATH) -> dict:
    from ragas import evaluate
    from ragas.metrics import (
        answer_relevancy,
        context_precision,
        context_recall,
        faithfulness,
    )

    golden_samples = json.loads(golden_dataset_path.read_text())
    dataset = build_evaluation_dataset(golden_samples)

    from langchain_groq import ChatGroq
    from langchain_huggingface import HuggingFaceEmbeddings

    groq_llm = ChatGroq(
        model=os.getenv("GROQ_PRIMARY_MODEL", "llama-3.3-70b-versatile"),
        api_key=os.getenv("GROQ_API_KEY"),
    )
    embeddings = HuggingFaceEmbeddings(
        model_name=os.getenv("EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2")
    )

    from ragas.run_config import RunConfig

    rate_friendly_config = RunConfig(
        max_workers=2,
        max_retries=20,
        timeout=180
    )

    logger.info("Running RAGAS metrics: faithfulness, answer_relevancy, context_precision, context_recall")
    result = evaluate(
        dataset,
        metrics=[faithfulness, answer_relevancy, context_precision, context_recall],
        llm=groq_llm,
        embeddings=embeddings,
        run_config=rate_friendly_config,
    )

    scores = result.to_pandas().mean(numeric_only=True).to_dict()
    logger.info("RAGAS results: %s", scores)
    return scores


def main() -> None:
    parser = argparse.ArgumentParser(description="Run RAGAS evaluation against the golden dataset.")
    parser.add_argument("--dataset", type=str, default=str(GOLDEN_DATASET_PATH))
    parser.add_argument("--output", type=str, default="evaluation/results.json")
    args = parser.parse_args()

    scores = run_evaluation(Path(args.dataset))

    output_path = Path(args.output)
    output_path.parent.mkdir(parents=True, exist_ok=True)
    output_path.write_text(json.dumps(scores, indent=2))
    logger.info("Wrote evaluation results to %s", output_path)


if __name__ == "__main__":
    main()