wuenlp_tools.utils.batch_prompting
Warning: This is fully AI generated and not checked at all
1""" 2Warning: This is fully AI generated and not checked at all 3""" 4 5from __future__ import annotations 6 7import json 8import os 9import pickle 10import tempfile 11import time 12import uuid 13from typing import TYPE_CHECKING 14 15import requests 16from loguru import logger 17from openai import NOT_GIVEN, OpenAI 18 19from wuenlp_tools.keys import OPENAI_API_KEY 20 21from . import prompting 22from .model_cost import get_cost 23from .prompting import YesNo, YesNoReason 24 25if TYPE_CHECKING: 26 from .prompting import LLM, LLMArchitecture, YesNoLLM 27 28logger.warning("This file is fully AI generated and not checked at all!") 29 30 31class BatchRequest: 32 """Represents a single request in a batch.""" 33 34 def __init__( 35 self, 36 custom_id: str, 37 prompt: str, 38 system_prompt: str = "", 39 endpoint: str = "/v1/chat/completions", 40 ): 41 self.custom_id = custom_id 42 self.prompt = prompt 43 self.system_prompt = system_prompt 44 self.endpoint = endpoint 45 46 def to_jsonl_line(self, model: str, seed=NOT_GIVEN) -> str: 47 """Convert to JSONL format for batch processing.""" 48 system_role = "system" if not model.startswith(("o3", "o4")) else "developer" 49 body = { 50 "model": model, 51 "messages": [ 52 {"role": system_role, "content": self.system_prompt}, 53 {"role": "user", "content": self.prompt}, 54 ], 55 } 56 if seed != NOT_GIVEN: 57 body["seed"] = seed 58 59 request = { 60 "custom_id": self.custom_id, 61 "method": "POST", 62 "url": self.endpoint, 63 "body": body, 64 } 65 return json.dumps(request) 66 67 68class BatchLLM: 69 """LLM class that supports OpenAI's Batch API for asynchronous processing.""" 70 71 def __init__( 72 self, 73 model: LLMArchitecture, 74 system_prompt: str, 75 seed=NOT_GIVEN, 76 endpoint: str = "/v1/chat/completions", 77 ): 78 if not model.openai: 79 raise ValueError("Batch API is only supported for OpenAI models") 80 81 self.openai = OpenAI(api_key=OPENAI_API_KEY) 82 self.model = model 83 self.system_prompt = system_prompt 84 self.seed = seed 85 self.endpoint = endpoint 86 self.requests = [] 87 88 def add_request(self, prompt: str, custom_id: str = None) -> str: 89 """Add a request to the batch. Returns the custom_id.""" 90 if custom_id is None: 91 custom_id = str(uuid.uuid4()) 92 93 # Truncate prompt if too long 94 model_max_chars = self.model.estimated_num_characters 95 if model_max_chars > -1 and len(prompt) > model_max_chars: 96 prompt = prompt[:model_max_chars] 97 logger.warning( 98 f"Truncated prompt to {model_max_chars} characters for request {custom_id}" 99 ) 100 101 request = BatchRequest(custom_id, prompt, self.system_prompt, self.endpoint) 102 self.requests.append(request) 103 return custom_id 104 105 def clear_requests(self): 106 """Clear all pending requests.""" 107 self.requests = [] 108 109 def create_batch_file(self) -> str: 110 """Create a temporary JSONL file with all requests.""" 111 if not self.requests: 112 raise ValueError("No requests to process") 113 114 # Create temporary file 115 with tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False) as f: 116 for request in self.requests: 117 f.write(request.to_jsonl_line(self.model.model, self.seed) + "\n") 118 temp_file_path = f.name 119 120 logger.info( 121 f"Created batch file with {len(self.requests)} requests: {temp_file_path}" 122 ) 123 return temp_file_path 124 125 def submit_batch( 126 self, completion_window: str = "24h", metadata: dict = None 127 ) -> str: 128 """Submit the batch for processing. Returns batch_id.""" 129 if not self.requests: 130 raise ValueError("No requests to submit") 131 132 # Create and upload batch file 133 batch_file_path = self.create_batch_file() 134 135 try: 136 # Upload file 137 with open(batch_file_path, "rb") as f: 138 batch_input_file = self.openai.files.create(file=f, purpose="batch") 139 140 # Create batch 141 batch = self.openai.batches.create( 142 input_file_id=batch_input_file.id, 143 endpoint=self.endpoint, 144 completion_window=completion_window, 145 metadata=metadata or {}, 146 ) 147 148 logger.info( 149 f"Submitted batch {batch.id} with {len(self.requests)} requests" 150 ) 151 return batch.id 152 153 finally: 154 # Clean up temporary file 155 try: 156 os.unlink(batch_file_path) 157 except OSError: 158 pass 159 160 def get_batch_status(self, batch_id: str) -> dict: 161 """Get the status of a batch.""" 162 batch = self.openai.batches.retrieve(batch_id) 163 return { 164 "id": batch.id, 165 "status": batch.status, 166 "request_counts": batch.request_counts, 167 "created_at": batch.created_at, 168 "completed_at": batch.completed_at, 169 "failed_at": batch.failed_at, 170 "expires_at": batch.expires_at, 171 "output_file_id": batch.output_file_id, 172 "error_file_id": batch.error_file_id, 173 } 174 175 def wait_for_completion(self, batch_id: str, poll_interval: int = 60) -> dict: 176 """Wait for batch to complete, polling at specified interval.""" 177 logger.info(f"Waiting for batch {batch_id} to complete...") 178 179 while True: 180 status = self.get_batch_status(batch_id) 181 logger.debug(f"Batch {batch_id} status: {status['status']}") 182 183 if status["status"] in ["completed", "failed", "expired", "cancelled"]: 184 logger.info( 185 f"Batch {batch_id} finished with status: {status['status']}" 186 ) 187 return status 188 189 time.sleep(poll_interval) 190 191 def get_results(self, batch_id: str) -> dict: 192 """Retrieve results from a completed batch.""" 193 status = self.get_batch_status(batch_id) 194 195 if status["status"] != "completed": 196 raise ValueError( 197 f"Batch {batch_id} is not completed (status: {status['status']})" 198 ) 199 200 if not status["output_file_id"]: 201 raise ValueError(f"No output file available for batch {batch_id}") 202 203 # Download results 204 file_response = self.openai.files.content(status["output_file_id"]) 205 results_text = file_response.text 206 207 # Parse results and map by custom_id 208 results = {} 209 errors = {} 210 211 for line in results_text.strip().split("\n"): 212 if not line: 213 continue 214 215 result = json.loads(line) 216 custom_id = result["custom_id"] 217 218 if result.get("error"): 219 errors[custom_id] = result["error"] 220 else: 221 response_body = result["response"]["body"] 222 content = response_body["choices"][0]["message"]["content"] 223 results[custom_id] = { 224 "content": content, 225 "usage": response_body.get("usage", {}), 226 "model": response_body.get("model"), 227 "finish_reason": response_body["choices"][0].get("finish_reason"), 228 } 229 230 # Get error results if any 231 if status.get("error_file_id"): 232 error_response = self.openai.files.content(status["error_file_id"]) 233 error_text = error_response.text 234 235 for line in error_text.strip().split("\n"): 236 if not line: 237 continue 238 239 error_result = json.loads(line) 240 custom_id = error_result["custom_id"] 241 errors[custom_id] = error_result["error"] 242 243 # Calculate total cost 244 total_cost = 0 245 for custom_id, result in results.items(): 246 if "usage" in result: 247 # Create a mock response object for cost calculation 248 class MockResponse: 249 def __init__(self, usage_dict): 250 self.usage = type("Usage", (), usage_dict)() 251 252 mock_response = MockResponse(result["usage"]) 253 cost = get_cost(mock_response, self.model.model) 254 total_cost += cost 255 256 # Update global run cost 257 prompting.RUN_COST += total_cost 258 259 logger.info( 260 f"Retrieved {len(results)} successful results and {len(errors)} errors from batch {batch_id}" 261 ) 262 logger.info(f"Batch cost: {total_cost:.4f} €") 263 264 return { 265 "results": results, 266 "errors": errors, 267 "total_cost": total_cost, 268 "status": status, 269 } 270 271 def cancel_batch(self, batch_id: str): 272 """Cancel a running batch.""" 273 batch = self.openai.batches.cancel(batch_id) 274 logger.info(f"Cancelled batch {batch_id}") 275 return batch 276 277 def list_batches(self, limit: int = 20): 278 """List all batches.""" 279 return self.openai.batches.list(limit=limit) 280 281 def process_batch( 282 self, 283 prompts: list, 284 custom_ids: list = None, 285 completion_window: str = "24h", 286 metadata: dict = None, 287 wait: bool = True, 288 poll_interval: int = 60, 289 ) -> dict: 290 """Complete batch workflow: add requests, submit, wait, and return results.""" 291 # Clear any existing requests 292 self.clear_requests() 293 294 # Add all requests 295 if custom_ids is None: 296 custom_ids = [None] * len(prompts) 297 298 added_ids = [] 299 for prompt, custom_id in zip(prompts, custom_ids): 300 added_id = self.add_request(prompt, custom_id) 301 added_ids.append(added_id) 302 303 # Submit batch 304 batch_id = self.submit_batch(completion_window, metadata) 305 306 if not wait: 307 return {"batch_id": batch_id, "custom_ids": added_ids} 308 309 # Wait for completion and return results 310 self.wait_for_completion(batch_id, poll_interval) 311 return self.get_results(batch_id) 312 313 def submit_batch_async( 314 self, 315 prompts: list, 316 custom_ids: list = None, 317 completion_window: str = "24h", 318 metadata: dict = None, 319 state_file: str = None, 320 ) -> dict: 321 """Submit batch without waiting, optionally save state to file.""" 322 # Clear any existing requests 323 self.clear_requests() 324 325 # Add all requests 326 if custom_ids is None: 327 custom_ids = [None] * len(prompts) 328 329 added_ids = [] 330 for prompt, custom_id in zip(prompts, custom_ids): 331 added_id = self.add_request(prompt, custom_id) 332 added_ids.append(added_id) 333 334 # Submit batch 335 batch_id = self.submit_batch(completion_window, metadata) 336 337 result = {"batch_id": batch_id, "custom_ids": added_ids} 338 339 # Optionally save state 340 if state_file: 341 self.save_batch_state(batch_id, state_file) 342 343 return result 344 345 def check_and_retrieve_results(self, batch_id: str) -> dict: 346 """Check batch status and retrieve results if completed.""" 347 status = self.get_batch_status(batch_id) 348 349 if status["status"] in ["completed", "failed", "expired", "cancelled"]: 350 if status["status"] == "completed": 351 return self.get_results(batch_id) 352 else: 353 return { 354 "status": status["status"], 355 "error": f"Batch finished with status: {status['status']}", 356 "results": {}, 357 "errors": {}, 358 } 359 else: 360 return { 361 "status": status["status"], 362 "pending": True, 363 "results": {}, 364 "errors": {}, 365 } 366 367 def save_batch_state(self, batch_id: str, state_file: str): 368 """Save batch state to file for later retrieval.""" 369 state = { 370 "batch_id": batch_id, 371 "timestamp": time.time(), 372 "client_config": {"api_key": self.client.api_key, "model": self.model}, 373 } 374 375 os.makedirs(os.path.dirname(state_file), exist_ok=True) 376 with open(state_file, "wb") as f: 377 pickle.dump(state, f) 378 379 logger.info(f"Batch state saved to {state_file}") 380 381 def load_batch_state(self, state_file: str) -> dict: 382 """Load batch state from file.""" 383 with open(state_file, "rb") as f: 384 state = pickle.load(f) 385 386 logger.info(f"Batch state loaded from {state_file}") 387 return state 388 389 390class BatchLLMAdapter: 391 """Adapter that converts any LLM instance to use batch processing.""" 392 393 def __init__(self, llm_instance: LLM): 394 if not llm_instance.model.openai: 395 raise ValueError("Batch API is only supported for OpenAI models") 396 397 self.original_llm = llm_instance 398 self.batch_llm = BatchLLM( 399 model=llm_instance.model, 400 system_prompt=llm_instance.system_prompt, 401 seed=llm_instance.seed, 402 endpoint="/v1/chat/completions", 403 ) 404 405 def add_request(self, prompt: str, custom_id: str = None) -> str: 406 """Add a request to the batch.""" 407 return self.batch_llm.add_request(prompt, custom_id) 408 409 def clear_requests(self): 410 """Clear all pending requests.""" 411 self.batch_llm.clear_requests() 412 413 def submit_batch( 414 self, completion_window: str = "24h", metadata: dict = None 415 ) -> str: 416 """Submit the batch for processing.""" 417 return self.batch_llm.submit_batch(completion_window, metadata) 418 419 def get_batch_status(self, batch_id: str) -> dict: 420 """Get the status of a batch.""" 421 return self.batch_llm.get_batch_status(batch_id) 422 423 def wait_for_completion(self, batch_id: str, poll_interval: int = 60) -> dict: 424 """Wait for batch to complete.""" 425 return self.batch_llm.wait_for_completion(batch_id, poll_interval) 426 427 def cancel_batch(self, batch_id: str): 428 """Cancel a running batch.""" 429 return self.batch_llm.cancel_batch(batch_id) 430 431 def list_batches(self, limit: int = 20): 432 """List all batches.""" 433 return self.batch_llm.list_batches(limit) 434 435 def get_results(self, batch_id: str) -> dict: 436 """Retrieve results from a completed batch with post-processing.""" 437 raw_results = self.batch_llm.get_results(batch_id) 438 439 # Apply post-processing based on original LLM type 440 processed_results = {} 441 processed_errors = raw_results["errors"] 442 443 for custom_id, result in raw_results["results"].items(): 444 try: 445 processed_result = self._post_process_result(result["content"]) 446 processed_results[custom_id] = { 447 **result, 448 "content": result["content"], 449 "processed": processed_result, 450 } 451 except Exception as e: 452 logger.warning(f"Post-processing failed for {custom_id}: {e}") 453 processed_errors[custom_id] = { 454 "message": f"Post-processing failed: {e}" 455 } 456 457 return {**raw_results, "results": processed_results, "errors": processed_errors} 458 459 def _post_process_result(self, content: str): 460 """Apply the same post-processing logic as the original LLM.""" 461 # Import YesNoLLM at runtime to avoid circular imports 462 from .prompting import YesNoLLM 463 464 if isinstance(self.original_llm, YesNoLLM): 465 return self._post_process_yes_no(content) 466 else: 467 # For other LLM types, return content as-is 468 return content 469 470 def _post_process_yes_no(self, content: str): 471 """Post-process results for YesNoLLM.""" 472 473 if self.original_llm.model.openai: 474 # For OpenAI models, structured output should already be parsed 475 try: 476 parsed = json.loads(content) if isinstance(content, str) else content 477 if self.original_llm.reason: 478 return YesNoReason(**parsed) 479 else: 480 return YesNo(**parsed) 481 except: 482 pass 483 484 # Fallback to original parsing logic 485 content_lower = content.lower().strip("*").strip() 486 487 if self.original_llm.is_positive( 488 content_lower 489 ) or self.original_llm.is_negative(content_lower): 490 answer = self.original_llm.is_positive(content_lower) 491 if self.original_llm.reason: 492 return YesNoReason(answer=answer, reason=content) 493 else: 494 return YesNo(answer=answer) 495 else: 496 raise ValueError(f"Could not parse yes/no answer from: {content}") 497 498 def process_batch( 499 self, 500 prompts: list, 501 custom_ids: list = None, 502 completion_window: str = "24h", 503 metadata: dict = None, 504 wait: bool = True, 505 poll_interval: int = 60, 506 ) -> dict: 507 """Complete batch workflow with post-processing.""" 508 # Add post_prompt to each prompt if needed 509 processed_prompts = [] 510 for prompt in prompts: 511 full_prompt = prompt + (self.original_llm.post_prompt or "") 512 processed_prompts.append(full_prompt) 513 514 # Use the underlying batch LLM to process 515 raw_results = self.batch_llm.process_batch( 516 processed_prompts, 517 custom_ids, 518 completion_window, 519 metadata, 520 wait, 521 poll_interval, 522 ) 523 524 if not wait: 525 return raw_results 526 527 # Apply post-processing to results 528 processed_results = {} 529 processed_errors = raw_results["errors"] 530 531 for custom_id, result in raw_results["results"].items(): 532 try: 533 processed_result = self._post_process_result(result["content"]) 534 processed_results[custom_id] = { 535 **result, 536 "content": result["content"], 537 "processed": processed_result, 538 } 539 except Exception as e: 540 logger.warning(f"Post-processing failed for {custom_id}: {e}") 541 processed_errors[custom_id] = { 542 "message": f"Post-processing failed: {e}" 543 } 544 545 return {**raw_results, "results": processed_results, "errors": processed_errors} 546 547 def submit_batch_async( 548 self, 549 prompts: list, 550 custom_ids: list = None, 551 completion_window: str = "24h", 552 metadata: dict = None, 553 state_file: str = None, 554 ) -> dict: 555 """Submit batch without waiting, with post-processing support.""" 556 # Add post_prompt to each prompt if needed 557 processed_prompts = [] 558 for prompt in prompts: 559 full_prompt = prompt + (self.original_llm.post_prompt or "") 560 processed_prompts.append(full_prompt) 561 562 # Submit via underlying batch LLM 563 return self.batch_llm.submit_batch_async( 564 processed_prompts, 565 custom_ids, 566 completion_window, 567 metadata, 568 state_file, 569 ) 570 571 def check_and_retrieve_results(self, batch_id: str) -> dict: 572 """Check batch status and retrieve results with post-processing.""" 573 raw_results = self.batch_llm.check_and_retrieve_results(batch_id) 574 575 # If not completed or has error, return as-is 576 if raw_results.get("pending") or raw_results.get("error"): 577 return raw_results 578 579 # Apply post-processing to completed results 580 if raw_results.get("results"): 581 processed_results = {} 582 processed_errors = raw_results["errors"] 583 584 for custom_id, result in raw_results["results"].items(): 585 try: 586 processed_result = self._post_process_result(result["content"]) 587 processed_results[custom_id] = { 588 **result, 589 "content": result["content"], 590 "processed": processed_result, 591 } 592 except Exception as e: 593 logger.warning(f"Post-processing failed for {custom_id}: {e}") 594 processed_errors[custom_id] = { 595 "message": f"Post-processing failed: {e}" 596 } 597 598 return { 599 **raw_results, 600 "results": processed_results, 601 "errors": processed_errors, 602 } 603 604 return raw_results 605 606 def save_batch_state(self, batch_id: str, state_file: str): 607 """Save batch state to file.""" 608 return self.batch_llm.save_batch_state(batch_id, state_file) 609 610 def load_batch_state(self, state_file: str) -> dict: 611 """Load batch state from file.""" 612 return self.batch_llm.load_batch_state(state_file) 613 614 615def make_batch_llm(llm: LLM) -> BatchLLMAdapter: 616 """Factory function to convert any LLM instance to batch processing. 617 618 Args: 619 llm: Any LLM instance (LLM, SentenceInContextLLM, YesNoLLM, etc.) 620 621 Returns: 622 BatchLLMAdapter that preserves the original LLM's behavior 623 624 Usage: 625 # Convert existing LLM to batch version 626 yes_no_llm = YesNoLLM(gpt4mini, "Is this positive?", reason=True) 627 batch_yes_no = make_batch_llm(yes_no_llm) 628 629 # Process multiple prompts 630 results = batch_yes_no.process_batch([ 631 "I love this!", 632 "This is terrible.", 633 "It's okay I guess." 634 ]) 635 636 # Access processed results 637 for custom_id, result in results["results"].items(): 638 print(f"{custom_id}: {result['processed']}") # YesNoReason objects 639 """ 640 return BatchLLMAdapter(llm)
32class BatchRequest: 33 """Represents a single request in a batch.""" 34 35 def __init__( 36 self, 37 custom_id: str, 38 prompt: str, 39 system_prompt: str = "", 40 endpoint: str = "/v1/chat/completions", 41 ): 42 self.custom_id = custom_id 43 self.prompt = prompt 44 self.system_prompt = system_prompt 45 self.endpoint = endpoint 46 47 def to_jsonl_line(self, model: str, seed=NOT_GIVEN) -> str: 48 """Convert to JSONL format for batch processing.""" 49 system_role = "system" if not model.startswith(("o3", "o4")) else "developer" 50 body = { 51 "model": model, 52 "messages": [ 53 {"role": system_role, "content": self.system_prompt}, 54 {"role": "user", "content": self.prompt}, 55 ], 56 } 57 if seed != NOT_GIVEN: 58 body["seed"] = seed 59 60 request = { 61 "custom_id": self.custom_id, 62 "method": "POST", 63 "url": self.endpoint, 64 "body": body, 65 } 66 return json.dumps(request)
Represents a single request in a batch.
47 def to_jsonl_line(self, model: str, seed=NOT_GIVEN) -> str: 48 """Convert to JSONL format for batch processing.""" 49 system_role = "system" if not model.startswith(("o3", "o4")) else "developer" 50 body = { 51 "model": model, 52 "messages": [ 53 {"role": system_role, "content": self.system_prompt}, 54 {"role": "user", "content": self.prompt}, 55 ], 56 } 57 if seed != NOT_GIVEN: 58 body["seed"] = seed 59 60 request = { 61 "custom_id": self.custom_id, 62 "method": "POST", 63 "url": self.endpoint, 64 "body": body, 65 } 66 return json.dumps(request)
Convert to JSONL format for batch processing.
69class BatchLLM: 70 """LLM class that supports OpenAI's Batch API for asynchronous processing.""" 71 72 def __init__( 73 self, 74 model: LLMArchitecture, 75 system_prompt: str, 76 seed=NOT_GIVEN, 77 endpoint: str = "/v1/chat/completions", 78 ): 79 if not model.openai: 80 raise ValueError("Batch API is only supported for OpenAI models") 81 82 self.openai = OpenAI(api_key=OPENAI_API_KEY) 83 self.model = model 84 self.system_prompt = system_prompt 85 self.seed = seed 86 self.endpoint = endpoint 87 self.requests = [] 88 89 def add_request(self, prompt: str, custom_id: str = None) -> str: 90 """Add a request to the batch. Returns the custom_id.""" 91 if custom_id is None: 92 custom_id = str(uuid.uuid4()) 93 94 # Truncate prompt if too long 95 model_max_chars = self.model.estimated_num_characters 96 if model_max_chars > -1 and len(prompt) > model_max_chars: 97 prompt = prompt[:model_max_chars] 98 logger.warning( 99 f"Truncated prompt to {model_max_chars} characters for request {custom_id}" 100 ) 101 102 request = BatchRequest(custom_id, prompt, self.system_prompt, self.endpoint) 103 self.requests.append(request) 104 return custom_id 105 106 def clear_requests(self): 107 """Clear all pending requests.""" 108 self.requests = [] 109 110 def create_batch_file(self) -> str: 111 """Create a temporary JSONL file with all requests.""" 112 if not self.requests: 113 raise ValueError("No requests to process") 114 115 # Create temporary file 116 with tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False) as f: 117 for request in self.requests: 118 f.write(request.to_jsonl_line(self.model.model, self.seed) + "\n") 119 temp_file_path = f.name 120 121 logger.info( 122 f"Created batch file with {len(self.requests)} requests: {temp_file_path}" 123 ) 124 return temp_file_path 125 126 def submit_batch( 127 self, completion_window: str = "24h", metadata: dict = None 128 ) -> str: 129 """Submit the batch for processing. Returns batch_id.""" 130 if not self.requests: 131 raise ValueError("No requests to submit") 132 133 # Create and upload batch file 134 batch_file_path = self.create_batch_file() 135 136 try: 137 # Upload file 138 with open(batch_file_path, "rb") as f: 139 batch_input_file = self.openai.files.create(file=f, purpose="batch") 140 141 # Create batch 142 batch = self.openai.batches.create( 143 input_file_id=batch_input_file.id, 144 endpoint=self.endpoint, 145 completion_window=completion_window, 146 metadata=metadata or {}, 147 ) 148 149 logger.info( 150 f"Submitted batch {batch.id} with {len(self.requests)} requests" 151 ) 152 return batch.id 153 154 finally: 155 # Clean up temporary file 156 try: 157 os.unlink(batch_file_path) 158 except OSError: 159 pass 160 161 def get_batch_status(self, batch_id: str) -> dict: 162 """Get the status of a batch.""" 163 batch = self.openai.batches.retrieve(batch_id) 164 return { 165 "id": batch.id, 166 "status": batch.status, 167 "request_counts": batch.request_counts, 168 "created_at": batch.created_at, 169 "completed_at": batch.completed_at, 170 "failed_at": batch.failed_at, 171 "expires_at": batch.expires_at, 172 "output_file_id": batch.output_file_id, 173 "error_file_id": batch.error_file_id, 174 } 175 176 def wait_for_completion(self, batch_id: str, poll_interval: int = 60) -> dict: 177 """Wait for batch to complete, polling at specified interval.""" 178 logger.info(f"Waiting for batch {batch_id} to complete...") 179 180 while True: 181 status = self.get_batch_status(batch_id) 182 logger.debug(f"Batch {batch_id} status: {status['status']}") 183 184 if status["status"] in ["completed", "failed", "expired", "cancelled"]: 185 logger.info( 186 f"Batch {batch_id} finished with status: {status['status']}" 187 ) 188 return status 189 190 time.sleep(poll_interval) 191 192 def get_results(self, batch_id: str) -> dict: 193 """Retrieve results from a completed batch.""" 194 status = self.get_batch_status(batch_id) 195 196 if status["status"] != "completed": 197 raise ValueError( 198 f"Batch {batch_id} is not completed (status: {status['status']})" 199 ) 200 201 if not status["output_file_id"]: 202 raise ValueError(f"No output file available for batch {batch_id}") 203 204 # Download results 205 file_response = self.openai.files.content(status["output_file_id"]) 206 results_text = file_response.text 207 208 # Parse results and map by custom_id 209 results = {} 210 errors = {} 211 212 for line in results_text.strip().split("\n"): 213 if not line: 214 continue 215 216 result = json.loads(line) 217 custom_id = result["custom_id"] 218 219 if result.get("error"): 220 errors[custom_id] = result["error"] 221 else: 222 response_body = result["response"]["body"] 223 content = response_body["choices"][0]["message"]["content"] 224 results[custom_id] = { 225 "content": content, 226 "usage": response_body.get("usage", {}), 227 "model": response_body.get("model"), 228 "finish_reason": response_body["choices"][0].get("finish_reason"), 229 } 230 231 # Get error results if any 232 if status.get("error_file_id"): 233 error_response = self.openai.files.content(status["error_file_id"]) 234 error_text = error_response.text 235 236 for line in error_text.strip().split("\n"): 237 if not line: 238 continue 239 240 error_result = json.loads(line) 241 custom_id = error_result["custom_id"] 242 errors[custom_id] = error_result["error"] 243 244 # Calculate total cost 245 total_cost = 0 246 for custom_id, result in results.items(): 247 if "usage" in result: 248 # Create a mock response object for cost calculation 249 class MockResponse: 250 def __init__(self, usage_dict): 251 self.usage = type("Usage", (), usage_dict)() 252 253 mock_response = MockResponse(result["usage"]) 254 cost = get_cost(mock_response, self.model.model) 255 total_cost += cost 256 257 # Update global run cost 258 prompting.RUN_COST += total_cost 259 260 logger.info( 261 f"Retrieved {len(results)} successful results and {len(errors)} errors from batch {batch_id}" 262 ) 263 logger.info(f"Batch cost: {total_cost:.4f} €") 264 265 return { 266 "results": results, 267 "errors": errors, 268 "total_cost": total_cost, 269 "status": status, 270 } 271 272 def cancel_batch(self, batch_id: str): 273 """Cancel a running batch.""" 274 batch = self.openai.batches.cancel(batch_id) 275 logger.info(f"Cancelled batch {batch_id}") 276 return batch 277 278 def list_batches(self, limit: int = 20): 279 """List all batches.""" 280 return self.openai.batches.list(limit=limit) 281 282 def process_batch( 283 self, 284 prompts: list, 285 custom_ids: list = None, 286 completion_window: str = "24h", 287 metadata: dict = None, 288 wait: bool = True, 289 poll_interval: int = 60, 290 ) -> dict: 291 """Complete batch workflow: add requests, submit, wait, and return results.""" 292 # Clear any existing requests 293 self.clear_requests() 294 295 # Add all requests 296 if custom_ids is None: 297 custom_ids = [None] * len(prompts) 298 299 added_ids = [] 300 for prompt, custom_id in zip(prompts, custom_ids): 301 added_id = self.add_request(prompt, custom_id) 302 added_ids.append(added_id) 303 304 # Submit batch 305 batch_id = self.submit_batch(completion_window, metadata) 306 307 if not wait: 308 return {"batch_id": batch_id, "custom_ids": added_ids} 309 310 # Wait for completion and return results 311 self.wait_for_completion(batch_id, poll_interval) 312 return self.get_results(batch_id) 313 314 def submit_batch_async( 315 self, 316 prompts: list, 317 custom_ids: list = None, 318 completion_window: str = "24h", 319 metadata: dict = None, 320 state_file: str = None, 321 ) -> dict: 322 """Submit batch without waiting, optionally save state to file.""" 323 # Clear any existing requests 324 self.clear_requests() 325 326 # Add all requests 327 if custom_ids is None: 328 custom_ids = [None] * len(prompts) 329 330 added_ids = [] 331 for prompt, custom_id in zip(prompts, custom_ids): 332 added_id = self.add_request(prompt, custom_id) 333 added_ids.append(added_id) 334 335 # Submit batch 336 batch_id = self.submit_batch(completion_window, metadata) 337 338 result = {"batch_id": batch_id, "custom_ids": added_ids} 339 340 # Optionally save state 341 if state_file: 342 self.save_batch_state(batch_id, state_file) 343 344 return result 345 346 def check_and_retrieve_results(self, batch_id: str) -> dict: 347 """Check batch status and retrieve results if completed.""" 348 status = self.get_batch_status(batch_id) 349 350 if status["status"] in ["completed", "failed", "expired", "cancelled"]: 351 if status["status"] == "completed": 352 return self.get_results(batch_id) 353 else: 354 return { 355 "status": status["status"], 356 "error": f"Batch finished with status: {status['status']}", 357 "results": {}, 358 "errors": {}, 359 } 360 else: 361 return { 362 "status": status["status"], 363 "pending": True, 364 "results": {}, 365 "errors": {}, 366 } 367 368 def save_batch_state(self, batch_id: str, state_file: str): 369 """Save batch state to file for later retrieval.""" 370 state = { 371 "batch_id": batch_id, 372 "timestamp": time.time(), 373 "client_config": {"api_key": self.client.api_key, "model": self.model}, 374 } 375 376 os.makedirs(os.path.dirname(state_file), exist_ok=True) 377 with open(state_file, "wb") as f: 378 pickle.dump(state, f) 379 380 logger.info(f"Batch state saved to {state_file}") 381 382 def load_batch_state(self, state_file: str) -> dict: 383 """Load batch state from file.""" 384 with open(state_file, "rb") as f: 385 state = pickle.load(f) 386 387 logger.info(f"Batch state loaded from {state_file}") 388 return state
LLM class that supports OpenAI's Batch API for asynchronous processing.
72 def __init__( 73 self, 74 model: LLMArchitecture, 75 system_prompt: str, 76 seed=NOT_GIVEN, 77 endpoint: str = "/v1/chat/completions", 78 ): 79 if not model.openai: 80 raise ValueError("Batch API is only supported for OpenAI models") 81 82 self.openai = OpenAI(api_key=OPENAI_API_KEY) 83 self.model = model 84 self.system_prompt = system_prompt 85 self.seed = seed 86 self.endpoint = endpoint 87 self.requests = []
89 def add_request(self, prompt: str, custom_id: str = None) -> str: 90 """Add a request to the batch. Returns the custom_id.""" 91 if custom_id is None: 92 custom_id = str(uuid.uuid4()) 93 94 # Truncate prompt if too long 95 model_max_chars = self.model.estimated_num_characters 96 if model_max_chars > -1 and len(prompt) > model_max_chars: 97 prompt = prompt[:model_max_chars] 98 logger.warning( 99 f"Truncated prompt to {model_max_chars} characters for request {custom_id}" 100 ) 101 102 request = BatchRequest(custom_id, prompt, self.system_prompt, self.endpoint) 103 self.requests.append(request) 104 return custom_id
Add a request to the batch. Returns the custom_id.
110 def create_batch_file(self) -> str: 111 """Create a temporary JSONL file with all requests.""" 112 if not self.requests: 113 raise ValueError("No requests to process") 114 115 # Create temporary file 116 with tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False) as f: 117 for request in self.requests: 118 f.write(request.to_jsonl_line(self.model.model, self.seed) + "\n") 119 temp_file_path = f.name 120 121 logger.info( 122 f"Created batch file with {len(self.requests)} requests: {temp_file_path}" 123 ) 124 return temp_file_path
Create a temporary JSONL file with all requests.
126 def submit_batch( 127 self, completion_window: str = "24h", metadata: dict = None 128 ) -> str: 129 """Submit the batch for processing. Returns batch_id.""" 130 if not self.requests: 131 raise ValueError("No requests to submit") 132 133 # Create and upload batch file 134 batch_file_path = self.create_batch_file() 135 136 try: 137 # Upload file 138 with open(batch_file_path, "rb") as f: 139 batch_input_file = self.openai.files.create(file=f, purpose="batch") 140 141 # Create batch 142 batch = self.openai.batches.create( 143 input_file_id=batch_input_file.id, 144 endpoint=self.endpoint, 145 completion_window=completion_window, 146 metadata=metadata or {}, 147 ) 148 149 logger.info( 150 f"Submitted batch {batch.id} with {len(self.requests)} requests" 151 ) 152 return batch.id 153 154 finally: 155 # Clean up temporary file 156 try: 157 os.unlink(batch_file_path) 158 except OSError: 159 pass
Submit the batch for processing. Returns batch_id.
161 def get_batch_status(self, batch_id: str) -> dict: 162 """Get the status of a batch.""" 163 batch = self.openai.batches.retrieve(batch_id) 164 return { 165 "id": batch.id, 166 "status": batch.status, 167 "request_counts": batch.request_counts, 168 "created_at": batch.created_at, 169 "completed_at": batch.completed_at, 170 "failed_at": batch.failed_at, 171 "expires_at": batch.expires_at, 172 "output_file_id": batch.output_file_id, 173 "error_file_id": batch.error_file_id, 174 }
Get the status of a batch.
176 def wait_for_completion(self, batch_id: str, poll_interval: int = 60) -> dict: 177 """Wait for batch to complete, polling at specified interval.""" 178 logger.info(f"Waiting for batch {batch_id} to complete...") 179 180 while True: 181 status = self.get_batch_status(batch_id) 182 logger.debug(f"Batch {batch_id} status: {status['status']}") 183 184 if status["status"] in ["completed", "failed", "expired", "cancelled"]: 185 logger.info( 186 f"Batch {batch_id} finished with status: {status['status']}" 187 ) 188 return status 189 190 time.sleep(poll_interval)
Wait for batch to complete, polling at specified interval.
192 def get_results(self, batch_id: str) -> dict: 193 """Retrieve results from a completed batch.""" 194 status = self.get_batch_status(batch_id) 195 196 if status["status"] != "completed": 197 raise ValueError( 198 f"Batch {batch_id} is not completed (status: {status['status']})" 199 ) 200 201 if not status["output_file_id"]: 202 raise ValueError(f"No output file available for batch {batch_id}") 203 204 # Download results 205 file_response = self.openai.files.content(status["output_file_id"]) 206 results_text = file_response.text 207 208 # Parse results and map by custom_id 209 results = {} 210 errors = {} 211 212 for line in results_text.strip().split("\n"): 213 if not line: 214 continue 215 216 result = json.loads(line) 217 custom_id = result["custom_id"] 218 219 if result.get("error"): 220 errors[custom_id] = result["error"] 221 else: 222 response_body = result["response"]["body"] 223 content = response_body["choices"][0]["message"]["content"] 224 results[custom_id] = { 225 "content": content, 226 "usage": response_body.get("usage", {}), 227 "model": response_body.get("model"), 228 "finish_reason": response_body["choices"][0].get("finish_reason"), 229 } 230 231 # Get error results if any 232 if status.get("error_file_id"): 233 error_response = self.openai.files.content(status["error_file_id"]) 234 error_text = error_response.text 235 236 for line in error_text.strip().split("\n"): 237 if not line: 238 continue 239 240 error_result = json.loads(line) 241 custom_id = error_result["custom_id"] 242 errors[custom_id] = error_result["error"] 243 244 # Calculate total cost 245 total_cost = 0 246 for custom_id, result in results.items(): 247 if "usage" in result: 248 # Create a mock response object for cost calculation 249 class MockResponse: 250 def __init__(self, usage_dict): 251 self.usage = type("Usage", (), usage_dict)() 252 253 mock_response = MockResponse(result["usage"]) 254 cost = get_cost(mock_response, self.model.model) 255 total_cost += cost 256 257 # Update global run cost 258 prompting.RUN_COST += total_cost 259 260 logger.info( 261 f"Retrieved {len(results)} successful results and {len(errors)} errors from batch {batch_id}" 262 ) 263 logger.info(f"Batch cost: {total_cost:.4f} €") 264 265 return { 266 "results": results, 267 "errors": errors, 268 "total_cost": total_cost, 269 "status": status, 270 }
Retrieve results from a completed batch.
272 def cancel_batch(self, batch_id: str): 273 """Cancel a running batch.""" 274 batch = self.openai.batches.cancel(batch_id) 275 logger.info(f"Cancelled batch {batch_id}") 276 return batch
Cancel a running batch.
278 def list_batches(self, limit: int = 20): 279 """List all batches.""" 280 return self.openai.batches.list(limit=limit)
List all batches.
282 def process_batch( 283 self, 284 prompts: list, 285 custom_ids: list = None, 286 completion_window: str = "24h", 287 metadata: dict = None, 288 wait: bool = True, 289 poll_interval: int = 60, 290 ) -> dict: 291 """Complete batch workflow: add requests, submit, wait, and return results.""" 292 # Clear any existing requests 293 self.clear_requests() 294 295 # Add all requests 296 if custom_ids is None: 297 custom_ids = [None] * len(prompts) 298 299 added_ids = [] 300 for prompt, custom_id in zip(prompts, custom_ids): 301 added_id = self.add_request(prompt, custom_id) 302 added_ids.append(added_id) 303 304 # Submit batch 305 batch_id = self.submit_batch(completion_window, metadata) 306 307 if not wait: 308 return {"batch_id": batch_id, "custom_ids": added_ids} 309 310 # Wait for completion and return results 311 self.wait_for_completion(batch_id, poll_interval) 312 return self.get_results(batch_id)
Complete batch workflow: add requests, submit, wait, and return results.
314 def submit_batch_async( 315 self, 316 prompts: list, 317 custom_ids: list = None, 318 completion_window: str = "24h", 319 metadata: dict = None, 320 state_file: str = None, 321 ) -> dict: 322 """Submit batch without waiting, optionally save state to file.""" 323 # Clear any existing requests 324 self.clear_requests() 325 326 # Add all requests 327 if custom_ids is None: 328 custom_ids = [None] * len(prompts) 329 330 added_ids = [] 331 for prompt, custom_id in zip(prompts, custom_ids): 332 added_id = self.add_request(prompt, custom_id) 333 added_ids.append(added_id) 334 335 # Submit batch 336 batch_id = self.submit_batch(completion_window, metadata) 337 338 result = {"batch_id": batch_id, "custom_ids": added_ids} 339 340 # Optionally save state 341 if state_file: 342 self.save_batch_state(batch_id, state_file) 343 344 return result
Submit batch without waiting, optionally save state to file.
346 def check_and_retrieve_results(self, batch_id: str) -> dict: 347 """Check batch status and retrieve results if completed.""" 348 status = self.get_batch_status(batch_id) 349 350 if status["status"] in ["completed", "failed", "expired", "cancelled"]: 351 if status["status"] == "completed": 352 return self.get_results(batch_id) 353 else: 354 return { 355 "status": status["status"], 356 "error": f"Batch finished with status: {status['status']}", 357 "results": {}, 358 "errors": {}, 359 } 360 else: 361 return { 362 "status": status["status"], 363 "pending": True, 364 "results": {}, 365 "errors": {}, 366 }
Check batch status and retrieve results if completed.
368 def save_batch_state(self, batch_id: str, state_file: str): 369 """Save batch state to file for later retrieval.""" 370 state = { 371 "batch_id": batch_id, 372 "timestamp": time.time(), 373 "client_config": {"api_key": self.client.api_key, "model": self.model}, 374 } 375 376 os.makedirs(os.path.dirname(state_file), exist_ok=True) 377 with open(state_file, "wb") as f: 378 pickle.dump(state, f) 379 380 logger.info(f"Batch state saved to {state_file}")
Save batch state to file for later retrieval.
382 def load_batch_state(self, state_file: str) -> dict: 383 """Load batch state from file.""" 384 with open(state_file, "rb") as f: 385 state = pickle.load(f) 386 387 logger.info(f"Batch state loaded from {state_file}") 388 return state
Load batch state from file.
391class BatchLLMAdapter: 392 """Adapter that converts any LLM instance to use batch processing.""" 393 394 def __init__(self, llm_instance: LLM): 395 if not llm_instance.model.openai: 396 raise ValueError("Batch API is only supported for OpenAI models") 397 398 self.original_llm = llm_instance 399 self.batch_llm = BatchLLM( 400 model=llm_instance.model, 401 system_prompt=llm_instance.system_prompt, 402 seed=llm_instance.seed, 403 endpoint="/v1/chat/completions", 404 ) 405 406 def add_request(self, prompt: str, custom_id: str = None) -> str: 407 """Add a request to the batch.""" 408 return self.batch_llm.add_request(prompt, custom_id) 409 410 def clear_requests(self): 411 """Clear all pending requests.""" 412 self.batch_llm.clear_requests() 413 414 def submit_batch( 415 self, completion_window: str = "24h", metadata: dict = None 416 ) -> str: 417 """Submit the batch for processing.""" 418 return self.batch_llm.submit_batch(completion_window, metadata) 419 420 def get_batch_status(self, batch_id: str) -> dict: 421 """Get the status of a batch.""" 422 return self.batch_llm.get_batch_status(batch_id) 423 424 def wait_for_completion(self, batch_id: str, poll_interval: int = 60) -> dict: 425 """Wait for batch to complete.""" 426 return self.batch_llm.wait_for_completion(batch_id, poll_interval) 427 428 def cancel_batch(self, batch_id: str): 429 """Cancel a running batch.""" 430 return self.batch_llm.cancel_batch(batch_id) 431 432 def list_batches(self, limit: int = 20): 433 """List all batches.""" 434 return self.batch_llm.list_batches(limit) 435 436 def get_results(self, batch_id: str) -> dict: 437 """Retrieve results from a completed batch with post-processing.""" 438 raw_results = self.batch_llm.get_results(batch_id) 439 440 # Apply post-processing based on original LLM type 441 processed_results = {} 442 processed_errors = raw_results["errors"] 443 444 for custom_id, result in raw_results["results"].items(): 445 try: 446 processed_result = self._post_process_result(result["content"]) 447 processed_results[custom_id] = { 448 **result, 449 "content": result["content"], 450 "processed": processed_result, 451 } 452 except Exception as e: 453 logger.warning(f"Post-processing failed for {custom_id}: {e}") 454 processed_errors[custom_id] = { 455 "message": f"Post-processing failed: {e}" 456 } 457 458 return {**raw_results, "results": processed_results, "errors": processed_errors} 459 460 def _post_process_result(self, content: str): 461 """Apply the same post-processing logic as the original LLM.""" 462 # Import YesNoLLM at runtime to avoid circular imports 463 from .prompting import YesNoLLM 464 465 if isinstance(self.original_llm, YesNoLLM): 466 return self._post_process_yes_no(content) 467 else: 468 # For other LLM types, return content as-is 469 return content 470 471 def _post_process_yes_no(self, content: str): 472 """Post-process results for YesNoLLM.""" 473 474 if self.original_llm.model.openai: 475 # For OpenAI models, structured output should already be parsed 476 try: 477 parsed = json.loads(content) if isinstance(content, str) else content 478 if self.original_llm.reason: 479 return YesNoReason(**parsed) 480 else: 481 return YesNo(**parsed) 482 except: 483 pass 484 485 # Fallback to original parsing logic 486 content_lower = content.lower().strip("*").strip() 487 488 if self.original_llm.is_positive( 489 content_lower 490 ) or self.original_llm.is_negative(content_lower): 491 answer = self.original_llm.is_positive(content_lower) 492 if self.original_llm.reason: 493 return YesNoReason(answer=answer, reason=content) 494 else: 495 return YesNo(answer=answer) 496 else: 497 raise ValueError(f"Could not parse yes/no answer from: {content}") 498 499 def process_batch( 500 self, 501 prompts: list, 502 custom_ids: list = None, 503 completion_window: str = "24h", 504 metadata: dict = None, 505 wait: bool = True, 506 poll_interval: int = 60, 507 ) -> dict: 508 """Complete batch workflow with post-processing.""" 509 # Add post_prompt to each prompt if needed 510 processed_prompts = [] 511 for prompt in prompts: 512 full_prompt = prompt + (self.original_llm.post_prompt or "") 513 processed_prompts.append(full_prompt) 514 515 # Use the underlying batch LLM to process 516 raw_results = self.batch_llm.process_batch( 517 processed_prompts, 518 custom_ids, 519 completion_window, 520 metadata, 521 wait, 522 poll_interval, 523 ) 524 525 if not wait: 526 return raw_results 527 528 # Apply post-processing to results 529 processed_results = {} 530 processed_errors = raw_results["errors"] 531 532 for custom_id, result in raw_results["results"].items(): 533 try: 534 processed_result = self._post_process_result(result["content"]) 535 processed_results[custom_id] = { 536 **result, 537 "content": result["content"], 538 "processed": processed_result, 539 } 540 except Exception as e: 541 logger.warning(f"Post-processing failed for {custom_id}: {e}") 542 processed_errors[custom_id] = { 543 "message": f"Post-processing failed: {e}" 544 } 545 546 return {**raw_results, "results": processed_results, "errors": processed_errors} 547 548 def submit_batch_async( 549 self, 550 prompts: list, 551 custom_ids: list = None, 552 completion_window: str = "24h", 553 metadata: dict = None, 554 state_file: str = None, 555 ) -> dict: 556 """Submit batch without waiting, with post-processing support.""" 557 # Add post_prompt to each prompt if needed 558 processed_prompts = [] 559 for prompt in prompts: 560 full_prompt = prompt + (self.original_llm.post_prompt or "") 561 processed_prompts.append(full_prompt) 562 563 # Submit via underlying batch LLM 564 return self.batch_llm.submit_batch_async( 565 processed_prompts, 566 custom_ids, 567 completion_window, 568 metadata, 569 state_file, 570 ) 571 572 def check_and_retrieve_results(self, batch_id: str) -> dict: 573 """Check batch status and retrieve results with post-processing.""" 574 raw_results = self.batch_llm.check_and_retrieve_results(batch_id) 575 576 # If not completed or has error, return as-is 577 if raw_results.get("pending") or raw_results.get("error"): 578 return raw_results 579 580 # Apply post-processing to completed results 581 if raw_results.get("results"): 582 processed_results = {} 583 processed_errors = raw_results["errors"] 584 585 for custom_id, result in raw_results["results"].items(): 586 try: 587 processed_result = self._post_process_result(result["content"]) 588 processed_results[custom_id] = { 589 **result, 590 "content": result["content"], 591 "processed": processed_result, 592 } 593 except Exception as e: 594 logger.warning(f"Post-processing failed for {custom_id}: {e}") 595 processed_errors[custom_id] = { 596 "message": f"Post-processing failed: {e}" 597 } 598 599 return { 600 **raw_results, 601 "results": processed_results, 602 "errors": processed_errors, 603 } 604 605 return raw_results 606 607 def save_batch_state(self, batch_id: str, state_file: str): 608 """Save batch state to file.""" 609 return self.batch_llm.save_batch_state(batch_id, state_file) 610 611 def load_batch_state(self, state_file: str) -> dict: 612 """Load batch state from file.""" 613 return self.batch_llm.load_batch_state(state_file)
Adapter that converts any LLM instance to use batch processing.
394 def __init__(self, llm_instance: LLM): 395 if not llm_instance.model.openai: 396 raise ValueError("Batch API is only supported for OpenAI models") 397 398 self.original_llm = llm_instance 399 self.batch_llm = BatchLLM( 400 model=llm_instance.model, 401 system_prompt=llm_instance.system_prompt, 402 seed=llm_instance.seed, 403 endpoint="/v1/chat/completions", 404 )
406 def add_request(self, prompt: str, custom_id: str = None) -> str: 407 """Add a request to the batch.""" 408 return self.batch_llm.add_request(prompt, custom_id)
Add a request to the batch.
410 def clear_requests(self): 411 """Clear all pending requests.""" 412 self.batch_llm.clear_requests()
Clear all pending requests.
414 def submit_batch( 415 self, completion_window: str = "24h", metadata: dict = None 416 ) -> str: 417 """Submit the batch for processing.""" 418 return self.batch_llm.submit_batch(completion_window, metadata)
Submit the batch for processing.
420 def get_batch_status(self, batch_id: str) -> dict: 421 """Get the status of a batch.""" 422 return self.batch_llm.get_batch_status(batch_id)
Get the status of a batch.
424 def wait_for_completion(self, batch_id: str, poll_interval: int = 60) -> dict: 425 """Wait for batch to complete.""" 426 return self.batch_llm.wait_for_completion(batch_id, poll_interval)
Wait for batch to complete.
428 def cancel_batch(self, batch_id: str): 429 """Cancel a running batch.""" 430 return self.batch_llm.cancel_batch(batch_id)
Cancel a running batch.
432 def list_batches(self, limit: int = 20): 433 """List all batches.""" 434 return self.batch_llm.list_batches(limit)
List all batches.
436 def get_results(self, batch_id: str) -> dict: 437 """Retrieve results from a completed batch with post-processing.""" 438 raw_results = self.batch_llm.get_results(batch_id) 439 440 # Apply post-processing based on original LLM type 441 processed_results = {} 442 processed_errors = raw_results["errors"] 443 444 for custom_id, result in raw_results["results"].items(): 445 try: 446 processed_result = self._post_process_result(result["content"]) 447 processed_results[custom_id] = { 448 **result, 449 "content": result["content"], 450 "processed": processed_result, 451 } 452 except Exception as e: 453 logger.warning(f"Post-processing failed for {custom_id}: {e}") 454 processed_errors[custom_id] = { 455 "message": f"Post-processing failed: {e}" 456 } 457 458 return {**raw_results, "results": processed_results, "errors": processed_errors}
Retrieve results from a completed batch with post-processing.
499 def process_batch( 500 self, 501 prompts: list, 502 custom_ids: list = None, 503 completion_window: str = "24h", 504 metadata: dict = None, 505 wait: bool = True, 506 poll_interval: int = 60, 507 ) -> dict: 508 """Complete batch workflow with post-processing.""" 509 # Add post_prompt to each prompt if needed 510 processed_prompts = [] 511 for prompt in prompts: 512 full_prompt = prompt + (self.original_llm.post_prompt or "") 513 processed_prompts.append(full_prompt) 514 515 # Use the underlying batch LLM to process 516 raw_results = self.batch_llm.process_batch( 517 processed_prompts, 518 custom_ids, 519 completion_window, 520 metadata, 521 wait, 522 poll_interval, 523 ) 524 525 if not wait: 526 return raw_results 527 528 # Apply post-processing to results 529 processed_results = {} 530 processed_errors = raw_results["errors"] 531 532 for custom_id, result in raw_results["results"].items(): 533 try: 534 processed_result = self._post_process_result(result["content"]) 535 processed_results[custom_id] = { 536 **result, 537 "content": result["content"], 538 "processed": processed_result, 539 } 540 except Exception as e: 541 logger.warning(f"Post-processing failed for {custom_id}: {e}") 542 processed_errors[custom_id] = { 543 "message": f"Post-processing failed: {e}" 544 } 545 546 return {**raw_results, "results": processed_results, "errors": processed_errors}
Complete batch workflow with post-processing.
548 def submit_batch_async( 549 self, 550 prompts: list, 551 custom_ids: list = None, 552 completion_window: str = "24h", 553 metadata: dict = None, 554 state_file: str = None, 555 ) -> dict: 556 """Submit batch without waiting, with post-processing support.""" 557 # Add post_prompt to each prompt if needed 558 processed_prompts = [] 559 for prompt in prompts: 560 full_prompt = prompt + (self.original_llm.post_prompt or "") 561 processed_prompts.append(full_prompt) 562 563 # Submit via underlying batch LLM 564 return self.batch_llm.submit_batch_async( 565 processed_prompts, 566 custom_ids, 567 completion_window, 568 metadata, 569 state_file, 570 )
Submit batch without waiting, with post-processing support.
572 def check_and_retrieve_results(self, batch_id: str) -> dict: 573 """Check batch status and retrieve results with post-processing.""" 574 raw_results = self.batch_llm.check_and_retrieve_results(batch_id) 575 576 # If not completed or has error, return as-is 577 if raw_results.get("pending") or raw_results.get("error"): 578 return raw_results 579 580 # Apply post-processing to completed results 581 if raw_results.get("results"): 582 processed_results = {} 583 processed_errors = raw_results["errors"] 584 585 for custom_id, result in raw_results["results"].items(): 586 try: 587 processed_result = self._post_process_result(result["content"]) 588 processed_results[custom_id] = { 589 **result, 590 "content": result["content"], 591 "processed": processed_result, 592 } 593 except Exception as e: 594 logger.warning(f"Post-processing failed for {custom_id}: {e}") 595 processed_errors[custom_id] = { 596 "message": f"Post-processing failed: {e}" 597 } 598 599 return { 600 **raw_results, 601 "results": processed_results, 602 "errors": processed_errors, 603 } 604 605 return raw_results
Check batch status and retrieve results with post-processing.
616def make_batch_llm(llm: LLM) -> BatchLLMAdapter: 617 """Factory function to convert any LLM instance to batch processing. 618 619 Args: 620 llm: Any LLM instance (LLM, SentenceInContextLLM, YesNoLLM, etc.) 621 622 Returns: 623 BatchLLMAdapter that preserves the original LLM's behavior 624 625 Usage: 626 # Convert existing LLM to batch version 627 yes_no_llm = YesNoLLM(gpt4mini, "Is this positive?", reason=True) 628 batch_yes_no = make_batch_llm(yes_no_llm) 629 630 # Process multiple prompts 631 results = batch_yes_no.process_batch([ 632 "I love this!", 633 "This is terrible.", 634 "It's okay I guess." 635 ]) 636 637 # Access processed results 638 for custom_id, result in results["results"].items(): 639 print(f"{custom_id}: {result['processed']}") # YesNoReason objects 640 """ 641 return BatchLLMAdapter(llm)
Factory function to convert any LLM instance to batch processing.
Args: llm: Any LLM instance (LLM, SentenceInContextLLM, YesNoLLM, etc.)
Returns: BatchLLMAdapter that preserves the original LLM's behavior
Usage: # Convert existing LLM to batch version yes_no_llm = YesNoLLM(gpt4mini, "Is this positive?", reason=True) batch_yes_no = make_batch_llm(yes_no_llm)
# Process multiple prompts
results = batch_yes_no.process_batch([
"I love this!",
"This is terrible.",
"It's okay I guess."
])
# Access processed results
for custom_id, result in results["results"].items():
print(f"{custom_id}: {result['processed']}") # YesNoReason objects