Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:36:38 +08:00

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