chore: import upstream snapshot with attribution
Ruff / Ruff (push) Has been cancelled
Test / Core Tests (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.10) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.11) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.12) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.13) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.9) (push) Has been cancelled
Test / Full Coverage (Python 3.11) (push) Has been cancelled
Test / Core Provider Tests (OpenAI) (push) Has been cancelled
Test / Core Provider Tests (Anthropic) (push) Has been cancelled
Test / Core Provider Tests (Google) (push) Has been cancelled
Test / Core Provider Tests (Other) (push) Has been cancelled
Test / Anthropic Tests (push) Has been cancelled
Test / Gemini Tests (push) Has been cancelled
Test / Google GenAI Tests (push) Has been cancelled
Test / Vertex AI Tests (push) Has been cancelled
Test / OpenAI Tests (push) Has been cancelled
Test / Writer Tests (push) Has been cancelled
Test / Auto Client Tests (push) Has been cancelled
ty / type-check (push) Has been cancelled
Ruff / Ruff (push) Has been cancelled
Test / Core Tests (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.10) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.11) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.12) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.13) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.9) (push) Has been cancelled
Test / Full Coverage (Python 3.11) (push) Has been cancelled
Test / Core Provider Tests (OpenAI) (push) Has been cancelled
Test / Core Provider Tests (Anthropic) (push) Has been cancelled
Test / Core Provider Tests (Google) (push) Has been cancelled
Test / Core Provider Tests (Other) (push) Has been cancelled
Test / Anthropic Tests (push) Has been cancelled
Test / Gemini Tests (push) Has been cancelled
Test / Google GenAI Tests (push) Has been cancelled
Test / Vertex AI Tests (push) Has been cancelled
Test / OpenAI Tests (push) Has been cancelled
Test / Writer Tests (push) Has been cancelled
Test / Auto Client Tests (push) Has been cancelled
ty / type-check (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user