File size: 13,698 Bytes
365d87f
 
 
3d1392a
365d87f
 
3d1392a
365d87f
 
 
 
 
 
 
 
 
3d1392a
365d87f
 
3d1392a
365d87f
3d1392a
 
dfee524
3d1392a
 
 
365d87f
3d1392a
 
365d87f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3d1392a
 
 
 
 
 
 
 
 
 
365d87f
 
 
 
 
 
 
 
3d1392a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364b978
3d1392a
 
 
 
365d87f
3d1392a
 
 
 
 
dfee524
365d87f
3d1392a
 
365d87f
 
 
 
 
 
3d1392a
365d87f
3d1392a
 
 
 
 
 
365d87f
 
 
 
 
 
3d1392a
 
 
 
 
dfee524
 
3d1392a
dfee524
3d1392a
 
 
365d87f
3d1392a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1a673ed
dfee524
 
 
3d1392a
 
 
 
 
364b978
dfee524
3d1392a
 
 
 
 
 
364b978
3d1392a
364b978
 
afb9c30
 
364b978
 
 
 
 
afb9c30
364b978
dfee524
afb9c30
 
364b978
 
afb9c30
 
 
 
 
 
364b978
 
 
 
 
afb9c30
 
 
 
 
364b978
365d87f
3d1392a
 
 
 
 
 
 
dfee524
3d1392a
365d87f
 
dfee524
 
 
 
 
365d87f
 
 
 
3d1392a
365d87f
364b978
3d1392a
 
 
364b978
 
 
 
 
3d1392a
dfee524
364b978
365d87f
 
3d1392a
 
 
 
365d87f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3d1392a
365d87f
3d1392a
365d87f
 
3d1392a
365d87f
 
3d1392a
 
 
 
 
 
 
365d87f
 
 
3d1392a
365d87f
 
3d1392a
 
365d87f
 
 
3d1392a
365d87f
3d1392a
365d87f
 
 
3d1392a
365d87f
 
 
 
 
 
 
dfee524
365d87f
 
 
 
 
 
 
 
 
 
 
 
 
 
3d1392a
364b978
3d1392a
 
 
364b978
 
3d1392a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364b978
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3d1392a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
import asyncio
import logging
import os
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Union

import fire
import pandas as pd
from langchain_openai import ChatOpenAI

from src.config import Config, Provider, load_spreadsheet
from src.config.logging import setup_logging
from src.eval.eval_mlflow import (
    BaseMetric,
    MlflowConfig,
    PageMatch,
    PresentationMatch,
    RAGEvaluatorMlflow,
)
from src.eval.evaluate import LangsmithConfig, RAGEvaluatorLangsmith
from src.rag import ChromaSlideStore, PresentationRetriever
from src.rag.preprocess import RegexQueryPreprocessor
from src.rag.score import (
    BaseScorer,
    ExponentialScorer,
    HyperbolicScorer,
    ScorerFactory,
    ScorerPresets,
)
from src.rag.storage import LLMPresentationRetriever

logger = logging.getLogger(__name__)


class RetrieverType(str, Enum):
    """Available retriever types"""

    BASIC = "basic"  # Basic vector retriever
    LLM = "llm"  # LLM-enhanced retriever


def get_retriever(
    storage: ChromaSlideStore,
    retriever_type: RetrieverType,
    llm: Optional[ChatOpenAI] = None,
) -> Union[PresentationRetriever, LLMPresentationRetriever]:
    """Get appropriate retriever based on type"""
    if retriever_type == RetrieverType.LLM:
        if llm is None:
            raise ValueError("LLM required for LLM-enhanced retriever")
        return LLMPresentationRetriever(storage=storage, llm=llm)
    return PresentationRetriever(storage=storage)


@dataclass
class EvalComponents:
    """Container for evaluation components"""

    llm: ChatOpenAI
    storage: ChromaSlideStore
    retriever: Union[PresentationRetriever, LLMPresentationRetriever]
    scorer_instances: List[BaseScorer]


class EvaluationCLI:
    """CLI for RAG evaluation pipeline"""

    def __init__(self):
        """Initialize CLI with logging setup"""
        setup_logging(logger, Path("logs"))
        self.config = Config()

    def _get_scorers(self, scorers: List[str]) -> List[BaseScorer]:
        """Get scorer instances from specifications

        Args:
            scorers: List of scorer specifications. Each item can be:
                - Preset name: "default", "weighted", "all"
                - Scorer spec: "min", "hyperbolic_k2.0_p3.0", etc

        Returns:
            List of configured scorer instances
        """
        scorer_specs = []

        # Process each specification
        for spec in scorers:
            if hasattr(ScorerPresets, spec.upper()):
                scorer_specs.extend(getattr(ScorerPresets, spec.upper()))
            else:
                scorer_specs.append(spec)

        # Create scorer instances
        scorer_instances = ScorerFactory.parse_scorer_specs(scorer_specs)

        if not scorer_instances:
            logger.warning("No valid scorers specified, using default")
            scorer_instances = [ScorerFactory.create_default()]
        else:
            logger.info(f"Using scorers: {[s.id for s in scorer_instances]}")

        return scorer_instances

    def _initialize_components(
        self,
        retriever: str,
        provider: str,
        model_name: Optional[str],
        collection: str,
        scorers: List[str],
        preprocessing: Optional[str] = None,
        temperature: float = 0.2,
    ) -> EvalComponents:
        """Initialize common evaluation components

        Args:
            retriever: Retriever type ('basic' or 'llm')
            provider: Model provider ('vsegpt' or 'openai')
            model_name: Optional specific model name
            collection: ChromaDB collection name
            scorers: List of scorer specifications
            temperature: Model temperature

        Returns:
            Configured evaluation components

        Raises:
            ValueError: If invalid retriever type or provider specified
        """
        try:
            retriever_type = RetrieverType(retriever.lower())
            provider = Provider(provider.lower())
        except ValueError as e:
            logger.error(f"Invalid parameter: {str(e)}")
            raise

        # Initialize components
        llm = self.config.model_config.get_llm(provider, model_name, temperature)
        embeddings = self.config.embedding_config.get_embeddings(provider)
        query_preprocessor = {"regex": RegexQueryPreprocessor()}.get(preprocessing) if preprocessing else None

        storage = ChromaSlideStore(
            collection_name=collection, embedding_model=embeddings, query_preprocessor=query_preprocessor
        )

        logger.info(f"Initialized storage collection: {collection}")

        # Get scorer instances
        scorer_instances = self._get_scorers(scorers)

        # Configure retriever
        retriever_instance = get_retriever(storage, retriever_type, llm)

        return EvalComponents(
            llm=llm,
            storage=storage,
            retriever=retriever_instance,
            scorer_instances=scorer_instances,
        )

    def mlflow(
        self,
        retriever: str = "basic",
        n_query_results: int = 70,
        n_contexts: int = -1,
        n_pages: int = -1,
        preprocessing: str = "regex",
        provider: str = "vsegpt",
        model_name: Optional[str] = None,
        collection: str = "pres1",
        experiment: str = "PresRetrieve_eval",
        scorers: List[str] = ["default"],
        metrics: List[str] = ["basic"],
        n_judge_contexts: int = 8,
        n_questions: int = -1,
        max_concurrent: int = 8,
        rate_limit_timeout: float = -1,
        temperature: float = 0.2,
        spread_id: Optional[str] = None,
        sheet_id: Optional[str] = None,
        write_to_google: bool = False,
    ) -> None:
        """Run evaluation pipeline with MLflow tracking.

        Key Arguments:
            scorers: List of scorer specifications for ranking results
                Options:
                    - Presets: 'default', 'all', 'weightedall', 'hyperbolic', 'exponential', 'step', 'linear'
                    - Individual: 'min', 'hyperbolic_k2.0_p3.0'
                Default: ['default']

            metrics: List of evaluation metrics to use
                Options:
                    - Presets: 'basic', 'llm', 'all'
                    - Individual: 'presentationmatch', 'presentationfound', 'pagematch',
                                'pagefound', 'presentationcount', 'llmrelevance'
                Default: ['basic']

            n_query_results: Number of results to fetch from vector store (default: 50)
            n_contexts: Number of contexts per presentation, -1 for unlimited (default: -1)
            n_pages: Number of pages per presentation, -1 for unlimited (default: -1)
            n_judge_contexts: Number of contexts for LLM evaluation (default: 8)
            preprocessing: Query preprocessing type ('regex' or None) (default: 'regex')
            rate_limit_timeout: Delay between API calls in seconds, -1 to disable (default: -1)

        Examples:
            # Basic evaluation with default settings
            python -m src.run_evaluation mlflow

            # Custom scoring and metrics
            python -m src.run_evaluation mlflow \
                --scorers=[min,hyperbolic_k2.0_p3.0] \
                --metrics=[basic,llmrelevance] \
                --n_query_results=100
        """
        try:
            # Initialize components
            components = self._initialize_components(
                retriever=retriever,
                provider=provider,
                model_name=model_name,
                collection=collection,
                scorers=scorers,
                preprocessing=preprocessing,
                temperature=temperature,
            )

            # Set attributes
            components.retriever.n_query_results = n_query_results
            components.retriever.n_contexts = n_contexts
            components.retriever.n_pages = n_pages

            # Setup evaluation config
            db_path = self.config.navigator.eval_runs / "mlruns.db"
            artifacts_path = self.config.navigator.eval_artifacts

            eval_config = MlflowConfig(
                experiment_name=experiment,
                metrics=metrics,
                scorers=components.scorer_instances,
                retriever=components.retriever,
                metric_args=dict(
                    rate_limit_timeout=(
                        rate_limit_timeout or 1.05
                        if provider == Provider.VSEGPT
                        else -1.0
                    )
                ),
                n_judge_contexts=n_judge_contexts,
                write_to_google=write_to_google,
            )

            evaluator = RAGEvaluatorMlflow(
                config=eval_config,
                llm=components.llm,
                max_concurrent=max_concurrent,
            )

            # Load and process questions
            spreadsheet_id = spread_id or os.getenv("BENCHMARK_SPREADSHEET_ID")
            if spreadsheet_id is None:
                raise ValueError("No spreadsheet ID provided")

            questions_df = evaluator.load_questions_from_sheet(
                spreadsheet_id, gid=sheet_id
            )
            logger.info(f"Loaded {len(questions_df)} questions")

            if n_questions > 0:
                questions_df = questions_df.sample(n_questions).reset_index()
                logger.info(f"Selected {len(questions_df)} random questions")

            evaluator.run_evaluation(questions_df)
            logger.info("MLflow evaluation completed successfully")

        except Exception as e:
            logger.error("MLflow evaluation failed", exc_info=True)
            raise

    def langsmith(
        self,
        retriever: str = "basic",
        provider: str = "vsegpt",
        model_name: Optional[str] = None,
        collection: str = "pres1",
        dataset: str = "RAG_test",
        experiment_prefix: Optional[str] = None,
        scorers: List[str] = ["default"],
        n_questions: int = -1,
        max_concurrent: int = 5,
        temperature: float = 0.2,
    ) -> None:
        """Run LangSmith-based evaluation pipeline"""
        try:
            # Initialize components
            components = self._initialize_components(
                retriever=retriever,
                provider=provider,
                model_name=model_name,
                collection=collection,
                scorers=scorers,
                temperature=temperature,
            )

            # Configure evaluation
            langsmith_config = LangsmithConfig(
                dataset_name=dataset,
                experiment_prefix=experiment_prefix,
                retriever=components.retriever,
                scorers=components.scorer_instances,
                max_concurrency=max_concurrent,
            )

            evaluator = RAGEvaluatorLangsmith(
                config=langsmith_config,
                llm=components.llm,
            )

            # Load and process questions
            sheet_id = os.getenv("BENCHMARK_SPREADSHEET_ID")
            questions_df = evaluator.load_questions_from_sheet(sheet_id)
            logger.info(f"Loaded {len(questions_df)} questions")

            if n_questions > 0:
                questions_df = questions_df.sample(n_questions).reset_index()
                logger.info(f"Selected {len(questions_df)} random questions")

            evaluator.run_evaluation()
            logger.info("LangSmith evaluation completed successfully")

        except Exception as e:
            logger.error("LangSmith evaluation failed", exc_info=True)
            raise


def main():
    """Entry point for Fire CLI"""
    fire.Fire(EvaluationCLI)


if __name__ == "__main__":
    main()


"""
EXAMPLES



# Basic MLflow evaluation with default settings
python -m src.run_evaluation mlflow

# MLflow with specific scorer combinations
python -m src.run_evaluation mlflow \
    --scorers=[min,hyperbolic_k2.0_p3.0]

# MLflow with preset scorer configurations
python -m src.run_evaluation mlflow \
    --scorers=[default,weighted]

# MLflow with LLM-enhanced retrieval
python -m src.run_evaluation mlflow \
    --retriever=llm \
    --scorers=[exponential_a0.7_w1.7_s2.8] \
    --provider=openai \
    --model-name=gpt-4 \
    --temperature=0.1

# MLflow with limited questions and custom experiment name
python -m src.run_evaluation mlflow \
    --n-questions=20 \
    --experiment=custom_experiment \
    --max-concurrent=3

# MLflow with specific spreadsheet
python -m src.run_evaluation mlflow \
    --spread-id=your_spreadsheet_id \
    --sheet-id=your_sheet_id

# My extended command
poetry run python -m src.run_evaluation mlflow \
          --retriever="basic" \
          --provider="vsegpt" \
          --scorers=["min", "exponential"] \
          --metrics=[basic] \
          --max_concurrent=5 \
          --model_name="openai/gpt-4o-mini" \
          --collection="pres_45" \
          --experiment="PresRetrieve_45" \
          --n_questions=3 \
          --temperature=0.2 \
          --sheet_id="1636334554" \
          --write_to_google=true


# Basic LangSmith evaluation
python -m src.run_evaluation langsmith

# LangSmith with custom configuration
python -m src.run_evaluation langsmith \
    --retriever=llm \
    --scorers=[default,exponential_a0.7_w1.7_s2.8] \
    --dataset=custom_dataset \
    --experiment-prefix=test_run \
    --n-questions=10

# LangSmith with VSE-GPT provider
python -m src.run_evaluation langsmith \
    --provider=vsegpt \
    --model-name=custom_model \
    --max-concurrent=2
"""