1+ import datetime
2+ import json
3+ import os
4+ import types
5+
16import pytest
27
8+ from bespokelabs .curator .llm .prompt_formatter import PromptFormatter
9+ from bespokelabs .curator .request_processor .config import OnlineRequestProcessorConfig
310from bespokelabs .curator .request_processor .online .anthropic_online_request_processor import AnthropicOnlineRequestProcessor
11+ from bespokelabs .curator .request_processor .online .openai_online_request_processor import OpenAIOnlineRequestProcessor
12+ from bespokelabs .curator .types .generic_request import GenericRequest
13+ from bespokelabs .curator .types .generic_response import GenericResponse
414
515
616@pytest .mark .asyncio
@@ -18,3 +28,97 @@ async def test_aiohttp_connector_trusts_env_proxy():
1828
1929 async with processor .aiohttp_connector (tcp_limit = 1 ) as session :
2030 assert session .trust_env is True
31+
32+
33+ _QUESTIONS = ["q0" , "q1" , "q2" ]
34+ _REJECTED = "q1"
35+
36+
37+ class _ViewerClient :
38+ async def stream_response (self , response_data , idx ):
39+ pass
40+
41+
42+ def _parse (row , response ):
43+ return {"question" : row ["question" ], "answer" : response }
44+
45+
46+ def _parse_raises_on_rejected_row (row , response ):
47+ if row ["question" ] == _REJECTED :
48+ raise KeyError ("bug in parse_func" )
49+ return _parse (row , response )
50+
51+
52+ def _parse_filters_out_rejected_row (row , response ):
53+ if row ["question" ] == _REJECTED :
54+ return []
55+ return _parse (row , response )
56+
57+
58+ def _online_processor (working_dir , parse_func ):
59+ # append_generic_response and the cache helpers only need these attributes, so skip __init__,
60+ # which makes a network call to read rate limits.
61+ processor = object .__new__ (OpenAIOnlineRequestProcessor )
62+ processor .config = OnlineRequestProcessorConfig (model = "gpt-4o-mini" )
63+ processor .working_dir = str (working_dir )
64+ processor .prompt_formatter = PromptFormatter (model_name = "gpt-4o-mini" , prompt_func = lambda row : row ["question" ], parse_func = parse_func )
65+ processor .viewer_client = _ViewerClient ()
66+ return processor
67+
68+
69+ async def _record_api_responses (processor , working_dir ):
70+ """Write the request file and record an API response for every request, as an online run does."""
71+ tracker = types .SimpleNamespace (num_parsed_responses = 0 )
72+ now = datetime .datetime .now ()
73+ with open (os .path .join (working_dir , "requests_0.jsonl" ), "w" ) as requests_file :
74+ for idx , question in enumerate (_QUESTIONS ):
75+ request = GenericRequest (
76+ model = "gpt-4o-mini" ,
77+ messages = [{"role" : "user" , "content" : question }],
78+ original_row = {"question" : question },
79+ original_row_idx = idx ,
80+ )
81+ requests_file .write (json .dumps (request .model_dump (), default = str ) + "\n " )
82+ response = GenericResponse (
83+ response_message = f"answer to { question } " ,
84+ raw_response = {},
85+ generic_request = request ,
86+ created_at = now ,
87+ finished_at = now ,
88+ )
89+ await processor .append_generic_response (tracker , response , os .path .join (working_dir , "responses_0.jsonl" ))
90+ return tracker
91+
92+
93+ @pytest .mark .asyncio
94+ @pytest .mark .parametrize (
95+ "parse_func" ,
96+ [_parse_raises_on_rejected_row , _parse_filters_out_rejected_row ],
97+ ids = ["parse_func_raises" , "parse_func_filters_out_row" ],
98+ )
99+ async def test_response_rejected_by_parse_func_is_not_requested_again_on_resume (tmp_path , parse_func ):
100+ processor = _online_processor (tmp_path , parse_func )
101+ tracker = await _record_api_responses (processor , tmp_path )
102+
103+ completed_request_ids , _ = processor .validate_existing_response_file (str (tmp_path / "responses_0.jsonl" ))
104+
105+ assert completed_request_ids == {0 , 1 , 2 }
106+ assert tracker .num_parsed_responses == 2
107+
108+
109+ @pytest .mark .asyncio
110+ async def test_dataset_is_rebuilt_from_cached_responses_after_parse_func_is_fixed (tmp_path ):
111+ await _record_api_responses (_online_processor (tmp_path , _parse_raises_on_rejected_row ), tmp_path )
112+
113+ dataset = _online_processor (tmp_path , _parse ).create_dataset_files ("fixed_parse_func_hash" )
114+
115+ assert dataset ["answer" ] == [f"answer to { question } " for question in _QUESTIONS ]
116+
117+
118+ @pytest .mark .asyncio
119+ async def test_rows_filtered_out_by_parse_func_do_not_fail_the_run (tmp_path ):
120+ await _record_api_responses (_online_processor (tmp_path , _parse_filters_out_rejected_row ), tmp_path )
121+
122+ dataset = _online_processor (tmp_path , _parse_filters_out_rejected_row ).create_dataset_files ("parse_func_hash" )
123+
124+ assert dataset ["question" ] == ["q0" , "q2" ]
0 commit comments