from __future__ import annotations import math import time import typing as t import numpy as np import pytest from langchain_core.outputs import Generation, LLMResult from langchain_core.prompt_values import StringPromptValue as PromptValue from ragas._analytics import EvaluationEvent from ragas.llms.base import BaseRagasLLM class EchoLLM(BaseRagasLLM): def generate_text( # type: ignore self, prompt: PromptValue, ) -> LLMResult: return LLMResult(generations=[[Generation(text=prompt.to_string())]]) async def agenerate_text( # type: ignore self, prompt: PromptValue, ) -> LLMResult: return LLMResult(generations=[[Generation(text=prompt.to_string())]]) def is_finished(self, response: LLMResult) -> bool: return True def test_debug_tracking_flag(monkeypatch): import os from ragas._analytics import RAGAS_DEBUG_TRACKING monkeypatch.setenv(RAGAS_DEBUG_TRACKING, "true") assert os.environ.get(RAGAS_DEBUG_TRACKING, "").lower() == "true" def test_base_event(): from ragas._analytics import BaseEvent be = BaseEvent(event_type="evaluation") assert isinstance(be.model_dump().get("event_type"), str) assert isinstance(be.model_dump().get("user_id"), str) def test_evaluation_event(): from ragas._analytics import EvaluationEvent evaluation_event = EvaluationEvent( event_type="evaluation", metrics=["harmfulness"], num_rows=1, language="english", evaluation_type="SINGLE_TURN", ) payload = evaluation_event.model_dump() assert isinstance(payload.get("user_id"), str) assert isinstance(payload.get("evaluation_type"), str) assert isinstance(payload.get("metrics"), list) def setup_user_id_filepath(tmp_path, monkeypatch): # setup def user_data_dir_patch(appname, roaming=True) -> str: return str(tmp_path / appname) import ragas._analytics from ragas._analytics import USER_DATA_DIR_NAME monkeypatch.setattr(ragas._analytics, "user_data_dir", user_data_dir_patch) userid_filepath = tmp_path / USER_DATA_DIR_NAME / "uuid.json" return userid_filepath def test_write_to_file(tmp_path, monkeypatch): userid_filepath = setup_user_id_filepath(tmp_path, monkeypatch) # check if file created if not existing assert not userid_filepath.exists() import json from ragas._analytics import get_userid # clear LRU cache since its created in setup for the above test get_userid.cache_clear() userid = get_userid() assert userid_filepath.exists() with open(userid_filepath, "r") as f: assert userid == json.load(f)["userid"] assert not (tmp_path / "uuid.json").exists() # del file and check if LRU cache is working userid_filepath.unlink() assert not userid_filepath.exists() userid_cached = get_userid() assert userid == userid_cached def test_load_userid_from_json_file(tmp_path, monkeypatch): userid_filepath = setup_user_id_filepath(tmp_path, monkeypatch) assert not userid_filepath.exists() # create uuid.json file userid_filepath.parent.mkdir(parents=True, exist_ok=True) with open(userid_filepath, "w") as f: import json json.dump({"userid": "test-userid"}, f) from ragas._analytics import get_userid # clear LRU cache since its created in setup for the above test get_userid.cache_clear() assert get_userid() == "test-userid" def test_testset_generation_tracking(monkeypatch): import ragas._analytics as analyticsmodule from ragas._analytics import TestsetGenerationEvent, track from ragas.testset.synthesizers import default_query_distribution distributions = default_query_distribution(llm=EchoLLM()) testset_event_payload = TestsetGenerationEvent( event_type="testset_generation", evolution_names=[e.name for e, _ in distributions], evolution_percentages=[p for _, p in distributions], num_rows=10, language="english", ) assert testset_event_payload.model_dump()["evolution_names"] == [ "single_hop_specific_query_synthesizer", "multi_hop_abstract_query_synthesizer", "multi_hop_specific_query_synthesizer", ] assert all( np.isclose( testset_event_payload.model_dump()["evolution_percentages"], [ 0.33, 0.33, 0.33, ], atol=0.01, ).tolist() ) # just in the case you actually want to check if tracking is working in the # dashboard if False: monkeypatch.setattr(analyticsmodule, "do_not_track", lambda: False) monkeypatch.setattr(analyticsmodule, "_usage_event_debugging", lambda: False) track(testset_event_payload) def test_was_completed(monkeypatch): from ragas._analytics import IsCompleteEvent, track_was_completed event_properties_list: t.List[IsCompleteEvent] = [] def echo_track(event_properties): event_properties_list.append(event_properties) monkeypatch.setattr("ragas._analytics.track", echo_track) @track_was_completed def test(raise_error=True): if raise_error: raise ValueError("test") else: pass with pytest.raises(ValueError): test(raise_error=True) assert event_properties_list[-1].event_type == "test" assert event_properties_list[-1].is_completed is False test(raise_error=False) assert event_properties_list[-1].event_type == "test" assert event_properties_list[-1].is_completed is True evaluation_events_and_num_rows = [ ( # 5 same events [ EvaluationEvent( event_type="evaluation", metrics=["harmfulness"], num_rows=1, evaluation_type="SINGLE_TURN", language="english", ) for _ in range(5) ], [5], ), ( # 5 different events with different metrics [ EvaluationEvent( event_type="evaluation", metrics=[f"harmfulness_{i}"], num_rows=1, evaluation_type="SINGLE_TURN", language="english", ) for i in range(5) ], [1, 1, 1, 1, 1], ), ( # 5 different events with different num_rows but 2 group of metrics [ EvaluationEvent( metrics=["harmfulness"], num_rows=1, evaluation_type="SINGLE_TURN", language="english", ) for i in range(10) ] + [ EvaluationEvent( event_type="evaluation", metrics=["accuracy"], num_rows=1, evaluation_type="SINGLE_TURN", language="english", ) for i in range(5) ], [10, 5], ), ] @pytest.mark.parametrize( "evaluation_events, expected_num_rows_set", evaluation_events_and_num_rows ) def test_analytics_batcher_join_evaluation_events( monkeypatch, evaluation_events, expected_num_rows_set ): """ Test if the batcher joins the evaluation events correctly """ from ragas._analytics import AnalyticsBatcher batcher = AnalyticsBatcher() joined_events = batcher._join_evaluation_events(evaluation_events) assert len(joined_events) == len(expected_num_rows_set) assert sorted(e.num_rows for e in joined_events) == sorted(expected_num_rows_set) @pytest.mark.skip(reason="This test is flaky and needs to be fixed") @pytest.mark.parametrize( "evaluation_events, expected_num_rows_set", evaluation_events_and_num_rows ) def test_analytics_batcher_flush(monkeypatch, evaluation_events, expected_num_rows_set): """ Test if the batcher flushes the events correctly """ from ragas._analytics import AnalyticsBatcher FLUSH_INTERVAL = 0.3 BATCH_SIZE = 5 batcher = AnalyticsBatcher(batch_size=BATCH_SIZE, flush_interval=FLUSH_INTERVAL) # Use a list to hold the counter so it can be modified in the nested function flush_mock_call_count = [0] def flush_mock(): # Access the list and modify its first element flush_mock_call_count[0] += 1 batcher.buffer = [] batcher.last_flush_time = time.time() monkeypatch.setattr(batcher, "flush", flush_mock) for event in evaluation_events[:-1]: batcher.add_evaluation(event) # Access the counter using flush_mock_call_count[0] time.sleep(FLUSH_INTERVAL + 0.1) batcher.add_evaluation(evaluation_events[-1]) assert flush_mock_call_count[0] == math.ceil( sum(expected_num_rows_set) / BATCH_SIZE )