97e91a83f3
Ruff / Ruff (push) Waiting to run
Test / Core Tests (push) Waiting to run
Test / Offline Coverage Tests (Python 3.10) (push) Waiting to run
Test / Offline Coverage Tests (Python 3.11) (push) Waiting to run
Test / Offline Coverage Tests (Python 3.12) (push) Waiting to run
Test / Offline Coverage Tests (Python 3.13) (push) Waiting to run
Test / Offline Coverage Tests (Python 3.9) (push) Waiting to run
Test / Full Coverage (Python 3.11) (push) Waiting to run
Test / Core Provider Tests (OpenAI) (push) Blocked by required conditions
Test / Core Provider Tests (Anthropic) (push) Blocked by required conditions
Test / Core Provider Tests (Google) (push) Blocked by required conditions
Test / Core Provider Tests (Other) (push) Blocked by required conditions
Test / Anthropic Tests (push) Blocked by required conditions
Test / Gemini Tests (push) Blocked by required conditions
Test / Google GenAI Tests (push) Blocked by required conditions
Test / Vertex AI Tests (push) Blocked by required conditions
Test / OpenAI Tests (push) Blocked by required conditions
Test / Writer Tests (push) Blocked by required conditions
Test / Auto Client Tests (push) Blocked by required conditions
ty / type-check (push) Waiting to run
292 lines
11 KiB
Python
292 lines
11 KiB
Python
"""
|
|
Batch processor for unified batch processing across providers.
|
|
|
|
This module contains the BatchProcessor class that provides a unified interface
|
|
for batch processing across different LLM providers.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
from typing import Any, Generic
|
|
import json
|
|
import os
|
|
import io
|
|
from .models import BatchResult, BatchSuccess, BatchError, BatchJobInfo, T
|
|
from .request import BatchRequest
|
|
from .providers import get_provider
|
|
|
|
|
|
class BatchProcessor(Generic[T]):
|
|
"""Unified batch processor that works across all providers"""
|
|
|
|
def __init__(self, model: str, response_model: type[T]):
|
|
self.model = model
|
|
self.response_model = response_model
|
|
|
|
# Parse provider from model string
|
|
try:
|
|
self.provider_name, self.model_name = model.split("/", 1)
|
|
except ValueError as err:
|
|
raise ValueError(
|
|
'Model string must be in format "provider/model-name" '
|
|
'(e.g. "openai/gpt-4" or "anthropic/claude-3-sonnet")'
|
|
) from err
|
|
|
|
# Get the batch provider instance
|
|
self.provider = get_provider(self.provider_name)
|
|
|
|
def create_batch_from_messages(
|
|
self,
|
|
messages_list: list[list[dict[str, Any]]],
|
|
file_path: str | None = None,
|
|
max_tokens: int | None = 1000,
|
|
temperature: float | None = 0.1,
|
|
) -> str | io.BytesIO:
|
|
"""Create batch file from list of message conversations
|
|
|
|
Args:
|
|
messages_list: List of message conversations, each as a list of message dicts
|
|
file_path: Path to save the batch request file. If None, returns BytesIO buffer
|
|
max_tokens: Maximum tokens per request
|
|
temperature: Temperature for generation
|
|
|
|
Returns:
|
|
The file path where the batch was saved, or BytesIO buffer if file_path is None
|
|
"""
|
|
if file_path is not None:
|
|
if os.path.exists(file_path):
|
|
os.remove(file_path)
|
|
|
|
batch_requests = []
|
|
for i, messages in enumerate(messages_list):
|
|
batch_request = BatchRequest[T](
|
|
custom_id=f"request-{i}",
|
|
messages=messages,
|
|
response_model=self.response_model,
|
|
model=self.model_name,
|
|
max_tokens=max_tokens,
|
|
temperature=temperature,
|
|
)
|
|
batch_request.save_to_file(file_path, self.provider_name)
|
|
batch_requests.append(batch_request)
|
|
|
|
print(f"Created batch file {file_path} with {len(batch_requests)} requests")
|
|
return file_path
|
|
# Create BytesIO buffer - caller is responsible for cleanup
|
|
buffer = io.BytesIO()
|
|
batch_requests = []
|
|
for i, messages in enumerate(messages_list):
|
|
batch_request = BatchRequest[T](
|
|
custom_id=f"request-{i}",
|
|
messages=messages,
|
|
response_model=self.response_model,
|
|
model=self.model_name,
|
|
max_tokens=max_tokens,
|
|
temperature=temperature,
|
|
)
|
|
batch_request.save_to_file(buffer, self.provider_name)
|
|
batch_requests.append(batch_request)
|
|
|
|
print(f"Created batch buffer with {len(batch_requests)} requests")
|
|
buffer.seek(0) # Reset buffer position for reading
|
|
return buffer
|
|
|
|
def submit_batch(
|
|
self,
|
|
file_path_or_buffer: str | io.BytesIO,
|
|
metadata: dict[str, Any] | None = None,
|
|
**kwargs,
|
|
) -> str:
|
|
"""Submit batch job to the provider and return job ID
|
|
|
|
Args:
|
|
file_path_or_buffer: Path to the batch request file or BytesIO buffer
|
|
metadata: Optional metadata to attach to the batch job
|
|
**kwargs: Additional provider-specific arguments
|
|
"""
|
|
if metadata is None:
|
|
metadata = {"description": "Instructor batch job"}
|
|
|
|
return self.provider.submit_batch(
|
|
file_path_or_buffer, metadata=metadata, **kwargs
|
|
)
|
|
|
|
def get_batch_status(self, batch_id: str) -> dict[str, Any]:
|
|
"""Get batch job status from the provider"""
|
|
return self.provider.get_status(batch_id)
|
|
|
|
def retrieve_results(self, batch_id: str) -> list[BatchResult]:
|
|
"""Retrieve and parse batch results from the provider"""
|
|
results_content = self.provider.retrieve_results(batch_id)
|
|
return self.parse_results(results_content)
|
|
|
|
def list_batches(self, limit: int = 10) -> list[BatchJobInfo]:
|
|
"""List batch jobs for the current provider
|
|
|
|
Args:
|
|
limit: Maximum number of batch jobs to return
|
|
|
|
Returns:
|
|
List of BatchJobInfo objects with normalized batch information
|
|
"""
|
|
return self.provider.list_batches(limit)
|
|
|
|
def get_results(
|
|
self, batch_id: str, file_path: str | None = None
|
|
) -> list[BatchResult]:
|
|
"""Get batch results, optionally saving raw results to a file
|
|
|
|
Args:
|
|
batch_id: The batch job ID
|
|
file_path: Optional file path to save raw results. If provided,
|
|
raw results will be saved to this file. If not provided,
|
|
results are only kept in memory.
|
|
|
|
Returns:
|
|
List of BatchResult objects (BatchSuccess[T] or BatchError)
|
|
"""
|
|
# Retrieve results directly to memory
|
|
results_content = self.retrieve_results(batch_id)
|
|
|
|
# If file path is provided, save raw results to file
|
|
if file_path is not None:
|
|
self.provider.download_results(batch_id, file_path)
|
|
|
|
return results_content
|
|
|
|
def cancel_batch(self, batch_id: str) -> dict[str, Any]:
|
|
"""Cancel a batch job
|
|
|
|
Args:
|
|
batch_id: The batch job ID to cancel
|
|
|
|
Returns:
|
|
Dict containing the cancelled batch information
|
|
"""
|
|
return self.provider.cancel_batch(batch_id)
|
|
|
|
def delete_batch(self, batch_id: str) -> dict[str, Any]:
|
|
"""Delete a batch job (only available for completed batches)
|
|
|
|
Args:
|
|
batch_id: The batch job ID to delete
|
|
|
|
Returns:
|
|
Dict containing the deletion confirmation
|
|
"""
|
|
return self.provider.delete_batch(batch_id)
|
|
|
|
def parse_results(self, results_content: str) -> list[BatchResult]:
|
|
"""Parse batch results from content string into Maybe-like results with custom_id tracking"""
|
|
results: list[BatchResult] = []
|
|
|
|
lines = results_content.strip().split("\n")
|
|
for line in lines:
|
|
if not line.strip():
|
|
continue
|
|
|
|
try:
|
|
data = json.loads(line)
|
|
custom_id = data.get("custom_id", "unknown")
|
|
extracted_data = self._extract_from_response(data)
|
|
|
|
if extracted_data:
|
|
try:
|
|
# Parse into response model
|
|
result = self.response_model(**extracted_data)
|
|
batch_result = BatchSuccess[T](
|
|
custom_id=custom_id, result=result
|
|
)
|
|
results.append(batch_result)
|
|
except Exception as e:
|
|
error_result = BatchError(
|
|
custom_id=custom_id,
|
|
error_type="parsing_error",
|
|
error_message=f"Failed to parse into {self.response_model.__name__}: {e}",
|
|
raw_data=extracted_data,
|
|
)
|
|
results.append(error_result)
|
|
else:
|
|
# Check if this is a provider error response
|
|
error_message = "Unknown error"
|
|
error_type = "extraction_error"
|
|
|
|
if self.provider_name == "anthropic" and "result" in data:
|
|
result = data["result"]
|
|
if result.get("type") == "error":
|
|
error_info = result.get("error", {})
|
|
if isinstance(error_info, dict) and "error" in error_info:
|
|
error_details = error_info["error"]
|
|
error_message = error_details.get(
|
|
"message", "Unknown Anthropic error"
|
|
)
|
|
error_type = error_details.get(
|
|
"type", "anthropic_error"
|
|
)
|
|
else:
|
|
error_message = str(error_info)
|
|
error_type = "anthropic_error"
|
|
|
|
error_result = BatchError(
|
|
custom_id=custom_id,
|
|
error_type=error_type,
|
|
error_message=error_message,
|
|
raw_data=data,
|
|
)
|
|
results.append(error_result)
|
|
|
|
except Exception as e:
|
|
error_result = BatchError(
|
|
custom_id="unknown",
|
|
error_type="json_parse_error",
|
|
error_message=f"Failed to parse JSON: {e}",
|
|
raw_data={"raw_line": line},
|
|
)
|
|
results.append(error_result)
|
|
|
|
return results
|
|
|
|
def _extract_from_response(self, data: dict[str, Any]) -> dict[str, Any] | None:
|
|
"""Extract structured data from provider-specific response format"""
|
|
try:
|
|
if self.provider_name == "openai":
|
|
# OpenAI JSON schema response
|
|
content = data["response"]["body"]["choices"][0]["message"]["content"]
|
|
return json.loads(content)
|
|
|
|
if self.provider_name == "anthropic":
|
|
# Anthropic batch response format
|
|
if "result" not in data:
|
|
return None
|
|
|
|
result = data["result"]
|
|
|
|
# Check if result is an error
|
|
if result.get("type") == "error":
|
|
# Return None to indicate error, let caller handle
|
|
return None
|
|
|
|
# Handle successful message result
|
|
if result.get("type") == "succeeded" and "message" in result:
|
|
content = result["message"]["content"]
|
|
if isinstance(content, list) and len(content) > 0:
|
|
# Try tool_use first
|
|
for item in content:
|
|
if item.get("type") == "tool_use":
|
|
return item.get("input", {})
|
|
|
|
# Fallback to text content and parse JSON
|
|
for item in content:
|
|
if item.get("type") == "text":
|
|
text = item.get("text", "")
|
|
try:
|
|
return json.loads(text)
|
|
except json.JSONDecodeError:
|
|
continue
|
|
|
|
return None
|
|
|
|
except Exception:
|
|
return None
|
|
|
|
return None
|