참고소스 수정본
This commit is contained in:
186
참고/instructor-main/instructor/batch/__init__.py
Normal file
186
참고/instructor-main/instructor/batch/__init__.py
Normal file
@@ -0,0 +1,186 @@
|
||||
"""
|
||||
Unified Batch Processing API for Multiple Providers
|
||||
|
||||
This module provides a unified interface for batch processing across OpenAI and Anthropic
|
||||
providers. The API uses a Maybe/Result-like pattern with custom_id
|
||||
tracking for type-safe handling of batch results.
|
||||
|
||||
Supported Providers:
|
||||
- OpenAI: 50% cost savings on batch requests
|
||||
- Anthropic: 50% cost savings on batch requests (Message Batches API)
|
||||
|
||||
Features:
|
||||
- Type-safe Maybe/Result pattern for handling successes and errors
|
||||
- Custom ID tracking for correlating results to original requests
|
||||
- Unified interface across all providers
|
||||
- Helper functions for filtering and extracting results
|
||||
|
||||
Example usage:
|
||||
from instructor.batch import BatchProcessor, filter_successful, extract_results
|
||||
from pydantic import BaseModel
|
||||
|
||||
class User(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
|
||||
processor = BatchProcessor("openai/gpt-4o-mini", User)
|
||||
batch_id = processor.submit_batch("requests.jsonl")
|
||||
|
||||
# Results are BatchSuccess[T] | BatchError union types
|
||||
all_results = processor.retrieve_results(batch_id)
|
||||
successful_results = filter_successful(all_results)
|
||||
extracted_users = extract_results(all_results)
|
||||
|
||||
Documentation:
|
||||
- OpenAI Batch API: https://platform.openai.com/docs/guides/batch
|
||||
- Anthropic Message Batches: https://docs.anthropic.com/en/api/creating-message-batches
|
||||
"""
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
# Import all public symbols from the modules
|
||||
from .models import (
|
||||
BatchSuccess,
|
||||
BatchError,
|
||||
BatchStatus,
|
||||
BatchTimestamps,
|
||||
BatchRequestCounts,
|
||||
BatchErrorInfo,
|
||||
BatchFiles,
|
||||
BatchJobInfo,
|
||||
BatchResult,
|
||||
T,
|
||||
)
|
||||
from .utils import (
|
||||
filter_successful,
|
||||
filter_errors,
|
||||
extract_results,
|
||||
get_results_by_custom_id,
|
||||
)
|
||||
from .request import (
|
||||
BatchRequest,
|
||||
Function,
|
||||
Tool,
|
||||
RequestBody,
|
||||
BatchModel,
|
||||
)
|
||||
from .processor import BatchProcessor
|
||||
|
||||
|
||||
class BatchJob:
|
||||
"""Legacy BatchJob class for backward compatibility"""
|
||||
|
||||
@classmethod
|
||||
def parse_from_file(
|
||||
cls, file_path: str, response_model: type[T]
|
||||
) -> tuple[list[T], list[dict[Any, Any]]]:
|
||||
with open(file_path) as file:
|
||||
content = file.read()
|
||||
return cls.parse_from_string(content, response_model)
|
||||
|
||||
@classmethod
|
||||
def parse_from_string(
|
||||
cls, content: str, response_model: type[T]
|
||||
) -> tuple[list[T], list[dict[Any, Any]]]:
|
||||
"""Enhanced parser that works with all providers using JSON schema"""
|
||||
import json
|
||||
|
||||
res: list[T] = []
|
||||
error_objs: list[dict[Any, Any]] = []
|
||||
|
||||
lines = content.strip().split("\n")
|
||||
for line in lines:
|
||||
if not line.strip():
|
||||
continue
|
||||
|
||||
try:
|
||||
data = json.loads(line)
|
||||
extracted_data = cls._extract_structured_data(data)
|
||||
|
||||
if extracted_data:
|
||||
try:
|
||||
result = response_model(**extracted_data)
|
||||
res.append(result)
|
||||
except Exception:
|
||||
error_objs.append(data)
|
||||
else:
|
||||
error_objs.append(data)
|
||||
|
||||
except Exception:
|
||||
error_objs.append({"error": "Failed to parse JSON", "raw_line": line})
|
||||
|
||||
return res, error_objs
|
||||
|
||||
@classmethod
|
||||
def _extract_structured_data(cls, data: dict[str, Any]) -> Optional[dict[str, Any]]:
|
||||
"""Extract structured data from various provider response formats"""
|
||||
import json
|
||||
|
||||
try:
|
||||
# Try OpenAI JSON schema format first
|
||||
if "response" in data and "body" in data["response"]:
|
||||
choices = data["response"]["body"].get("choices", [])
|
||||
if choices:
|
||||
message = choices[0].get("message", {})
|
||||
|
||||
# JSON schema response
|
||||
if "content" in message:
|
||||
content = message["content"]
|
||||
if isinstance(content, str):
|
||||
return json.loads(content)
|
||||
|
||||
# Tool calls (legacy)
|
||||
if "tool_calls" in message:
|
||||
tool_call = message["tool_calls"][0]
|
||||
return json.loads(tool_call["function"]["arguments"])
|
||||
|
||||
# Try Anthropic format
|
||||
if "result" in data and "message" in data["result"]:
|
||||
content = data["result"]["message"]["content"]
|
||||
if isinstance(content, list) and len(content) > 0:
|
||||
# Tool use response
|
||||
for item in content:
|
||||
if item.get("type") == "tool_use":
|
||||
return item.get("input", {})
|
||||
# Text response with JSON
|
||||
for item in content:
|
||||
if item.get("type") == "text":
|
||||
text = item.get("text", "")
|
||||
return json.loads(text)
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# Define what gets exported when someone does "from instructor.batch import *"
|
||||
__all__ = [
|
||||
# Core types
|
||||
"T",
|
||||
"BatchResult",
|
||||
# Models
|
||||
"BatchSuccess",
|
||||
"BatchError",
|
||||
"BatchStatus",
|
||||
"BatchTimestamps",
|
||||
"BatchRequestCounts",
|
||||
"BatchErrorInfo",
|
||||
"BatchFiles",
|
||||
"BatchJobInfo",
|
||||
# Utility functions
|
||||
"filter_successful",
|
||||
"filter_errors",
|
||||
"extract_results",
|
||||
"get_results_by_custom_id",
|
||||
# Request models
|
||||
"BatchRequest",
|
||||
"Function",
|
||||
"Tool",
|
||||
"RequestBody",
|
||||
"BatchModel",
|
||||
# Main processor
|
||||
"BatchProcessor",
|
||||
# Legacy
|
||||
"BatchJob",
|
||||
]
|
||||
293
참고/instructor-main/instructor/batch/models.py
Normal file
293
참고/instructor-main/instructor/batch/models.py
Normal file
@@ -0,0 +1,293 @@
|
||||
"""
|
||||
Data models and types for batch processing.
|
||||
|
||||
This module contains all the Pydantic models, enums, and type definitions
|
||||
used throughout the batch processing system.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import Any, Union, TypeVar, Generic
|
||||
from typing_extensions import TypeAlias
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
|
||||
T = TypeVar("T", bound=BaseModel)
|
||||
|
||||
|
||||
class BatchSuccess(BaseModel, Generic[T]):
|
||||
"""Successful batch result with custom_id"""
|
||||
|
||||
custom_id: str
|
||||
result: T
|
||||
success: bool = True
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
|
||||
class BatchError(BaseModel):
|
||||
"""Error information for failed batch requests"""
|
||||
|
||||
custom_id: str
|
||||
error_type: str
|
||||
error_message: str
|
||||
success: bool = False
|
||||
raw_data: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class BatchStatus(str, Enum):
|
||||
"""Normalized batch status across providers"""
|
||||
|
||||
PENDING = "pending"
|
||||
PROCESSING = "processing"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
CANCELLED = "cancelled"
|
||||
EXPIRED = "expired"
|
||||
|
||||
|
||||
class BatchTimestamps(BaseModel):
|
||||
"""Comprehensive timestamp tracking"""
|
||||
|
||||
created_at: datetime | None = None
|
||||
started_at: datetime | None = None # in_progress_at, processing start
|
||||
completed_at: datetime | None = None # completed_at, ended_at
|
||||
failed_at: datetime | None = None
|
||||
cancelled_at: datetime | None = None
|
||||
expired_at: datetime | None = None
|
||||
expires_at: datetime | None = None
|
||||
|
||||
|
||||
class BatchRequestCounts(BaseModel):
|
||||
"""Unified request counts across providers"""
|
||||
|
||||
total: int | None = None
|
||||
|
||||
# OpenAI fields
|
||||
completed: int | None = None
|
||||
failed: int | None = None
|
||||
|
||||
# Anthropic fields
|
||||
processing: int | None = None
|
||||
succeeded: int | None = None
|
||||
errored: int | None = None
|
||||
cancelled: int | None = None
|
||||
expired: int | None = None
|
||||
|
||||
|
||||
class BatchErrorInfo(BaseModel):
|
||||
"""Batch-level error information"""
|
||||
|
||||
error_type: str | None = None
|
||||
error_message: str | None = None
|
||||
error_code: str | None = None
|
||||
|
||||
|
||||
class BatchFiles(BaseModel):
|
||||
"""File references for batch job"""
|
||||
|
||||
input_file_id: str | None = None
|
||||
output_file_id: str | None = None
|
||||
error_file_id: str | None = None
|
||||
results_url: str | None = None # Anthropic
|
||||
|
||||
|
||||
class BatchJobInfo(BaseModel):
|
||||
"""Enhanced unified batch job information with comprehensive provider support"""
|
||||
|
||||
# Core identifiers
|
||||
id: str
|
||||
provider: str
|
||||
|
||||
# Status information
|
||||
status: BatchStatus
|
||||
raw_status: str # Original provider status
|
||||
|
||||
# Timing information
|
||||
timestamps: BatchTimestamps
|
||||
|
||||
# Request tracking
|
||||
request_counts: BatchRequestCounts
|
||||
|
||||
# File references
|
||||
files: BatchFiles
|
||||
|
||||
# Error information
|
||||
error: BatchErrorInfo | None = None
|
||||
|
||||
# Provider-specific data
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
raw_data: dict[str, Any] | None = None
|
||||
|
||||
# Additional fields
|
||||
model: str | None = None
|
||||
endpoint: str | None = None
|
||||
completion_window: str | None = None
|
||||
|
||||
@classmethod
|
||||
def from_openai(cls, batch_data: dict[str, Any]) -> BatchJobInfo:
|
||||
"""Create from OpenAI batch response"""
|
||||
# Normalize status
|
||||
status_map = {
|
||||
"validating": BatchStatus.PENDING,
|
||||
"in_progress": BatchStatus.PROCESSING,
|
||||
"finalizing": BatchStatus.PROCESSING,
|
||||
"completed": BatchStatus.COMPLETED,
|
||||
"failed": BatchStatus.FAILED,
|
||||
"expired": BatchStatus.EXPIRED,
|
||||
"cancelled": BatchStatus.CANCELLED,
|
||||
"cancelling": BatchStatus.CANCELLED,
|
||||
}
|
||||
|
||||
# Parse timestamps
|
||||
timestamps = BatchTimestamps(
|
||||
created_at=(
|
||||
datetime.fromtimestamp(batch_data["created_at"], tz=timezone.utc)
|
||||
if batch_data.get("created_at")
|
||||
else None
|
||||
),
|
||||
started_at=(
|
||||
datetime.fromtimestamp(batch_data["in_progress_at"], tz=timezone.utc)
|
||||
if batch_data.get("in_progress_at")
|
||||
else None
|
||||
),
|
||||
completed_at=(
|
||||
datetime.fromtimestamp(batch_data["completed_at"], tz=timezone.utc)
|
||||
if batch_data.get("completed_at")
|
||||
else None
|
||||
),
|
||||
failed_at=(
|
||||
datetime.fromtimestamp(batch_data["failed_at"], tz=timezone.utc)
|
||||
if batch_data.get("failed_at")
|
||||
else None
|
||||
),
|
||||
cancelled_at=(
|
||||
datetime.fromtimestamp(batch_data["cancelled_at"], tz=timezone.utc)
|
||||
if batch_data.get("cancelled_at")
|
||||
else None
|
||||
),
|
||||
expired_at=(
|
||||
datetime.fromtimestamp(batch_data["expired_at"], tz=timezone.utc)
|
||||
if batch_data.get("expired_at")
|
||||
else None
|
||||
),
|
||||
expires_at=(
|
||||
datetime.fromtimestamp(batch_data["expires_at"], tz=timezone.utc)
|
||||
if batch_data.get("expires_at")
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
# Parse request counts
|
||||
request_counts_data = batch_data.get("request_counts", {})
|
||||
request_counts = BatchRequestCounts(
|
||||
total=request_counts_data.get("total"),
|
||||
completed=request_counts_data.get("completed"),
|
||||
failed=request_counts_data.get("failed"),
|
||||
)
|
||||
|
||||
# Parse files
|
||||
files = BatchFiles(
|
||||
input_file_id=batch_data.get("input_file_id"),
|
||||
output_file_id=batch_data.get("output_file_id"),
|
||||
error_file_id=batch_data.get("error_file_id"),
|
||||
)
|
||||
|
||||
# Parse error information
|
||||
error = None
|
||||
if batch_data.get("errors"):
|
||||
error_data = batch_data["errors"]
|
||||
error = BatchErrorInfo(
|
||||
error_type=error_data.get("type"),
|
||||
error_message=error_data.get("message"),
|
||||
error_code=error_data.get("code"),
|
||||
)
|
||||
|
||||
return cls(
|
||||
id=batch_data["id"],
|
||||
provider="openai",
|
||||
status=status_map.get(batch_data["status"], BatchStatus.PENDING),
|
||||
raw_status=batch_data["status"],
|
||||
timestamps=timestamps,
|
||||
request_counts=request_counts,
|
||||
files=files,
|
||||
error=error,
|
||||
metadata=batch_data.get("metadata", {}),
|
||||
raw_data=batch_data,
|
||||
endpoint=batch_data.get("endpoint"),
|
||||
completion_window=batch_data.get("completion_window"),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_anthropic(cls, batch_data: dict[str, Any]) -> BatchJobInfo:
|
||||
"""Create from Anthropic batch response"""
|
||||
# Normalize status
|
||||
status_map = {
|
||||
"in_progress": BatchStatus.PROCESSING,
|
||||
"ended": BatchStatus.COMPLETED,
|
||||
"failed": BatchStatus.FAILED,
|
||||
"cancelled": BatchStatus.CANCELLED,
|
||||
"expired": BatchStatus.EXPIRED,
|
||||
}
|
||||
|
||||
# Parse timestamps
|
||||
def parse_iso_timestamp(timestamp_value):
|
||||
if not timestamp_value:
|
||||
return None
|
||||
try:
|
||||
# Handle different timestamp format variations
|
||||
if isinstance(timestamp_value, datetime):
|
||||
return timestamp_value
|
||||
elif isinstance(timestamp_value, str):
|
||||
return datetime.fromisoformat(
|
||||
timestamp_value.replace("Z", "+00:00")
|
||||
)
|
||||
else:
|
||||
return None
|
||||
except (ValueError, AttributeError):
|
||||
return None
|
||||
|
||||
timestamps = BatchTimestamps(
|
||||
created_at=parse_iso_timestamp(batch_data.get("created_at")),
|
||||
started_at=parse_iso_timestamp(
|
||||
batch_data.get("created_at")
|
||||
), # Anthropic doesn't provide started_at, use created_at
|
||||
cancelled_at=parse_iso_timestamp(batch_data.get("cancel_initiated_at")),
|
||||
completed_at=parse_iso_timestamp(batch_data.get("ended_at")),
|
||||
expires_at=parse_iso_timestamp(batch_data.get("expires_at")),
|
||||
)
|
||||
|
||||
# Parse request counts
|
||||
request_counts_data = batch_data.get("request_counts", {})
|
||||
request_counts = BatchRequestCounts(
|
||||
processing=request_counts_data.get("processing"),
|
||||
succeeded=request_counts_data.get("succeeded"),
|
||||
errored=request_counts_data.get("errored"),
|
||||
cancelled=request_counts_data.get(
|
||||
"canceled"
|
||||
), # Note: Anthropic uses "canceled"
|
||||
expired=request_counts_data.get("expired"),
|
||||
total=request_counts_data.get("processing", 0)
|
||||
+ request_counts_data.get("succeeded", 0)
|
||||
+ request_counts_data.get("errored", 0),
|
||||
)
|
||||
|
||||
# Parse files
|
||||
files = BatchFiles(
|
||||
results_url=batch_data.get("results_url"),
|
||||
)
|
||||
|
||||
return cls(
|
||||
id=batch_data["id"],
|
||||
provider="anthropic",
|
||||
status=status_map.get(batch_data["processing_status"], BatchStatus.PENDING),
|
||||
raw_status=batch_data["processing_status"],
|
||||
timestamps=timestamps,
|
||||
request_counts=request_counts,
|
||||
files=files,
|
||||
raw_data=batch_data,
|
||||
)
|
||||
|
||||
|
||||
# Union type for batch results - like a Maybe/Result type
|
||||
BatchResult: TypeAlias = Union[BatchSuccess[T], BatchError] # type: ignore
|
||||
292
참고/instructor-main/instructor/batch/processor.py
Normal file
292
참고/instructor-main/instructor/batch/processor.py
Normal file
@@ -0,0 +1,292 @@
|
||||
"""
|
||||
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[self.response_model](
|
||||
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
|
||||
else:
|
||||
# Create BytesIO buffer - caller is responsible for cleanup
|
||||
buffer = io.BytesIO()
|
||||
batch_requests = []
|
||||
for i, messages in enumerate(messages_list):
|
||||
batch_request = BatchRequest[self.response_model](
|
||||
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)
|
||||
|
||||
elif 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
|
||||
31
참고/instructor-main/instructor/batch/providers/__init__.py
Normal file
31
참고/instructor-main/instructor/batch/providers/__init__.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
Provider-specific batch processing implementations.
|
||||
|
||||
This module contains provider-specific implementations for OpenAI and Anthropic
|
||||
batch processing APIs.
|
||||
"""
|
||||
|
||||
from .base import BatchProvider
|
||||
import importlib.util
|
||||
|
||||
if importlib.util.find_spec("openai") is not None:
|
||||
from .openai import OpenAIProvider
|
||||
if importlib.util.find_spec("anthropic") is not None:
|
||||
from .anthropic import AnthropicProvider
|
||||
|
||||
|
||||
def get_provider(provider_name: str) -> BatchProvider:
|
||||
"""Factory function to get the appropriate provider instance"""
|
||||
if provider_name == "openai":
|
||||
if OpenAIProvider is None:
|
||||
raise ValueError("OpenAI is not installed")
|
||||
return OpenAIProvider()
|
||||
elif provider_name == "anthropic":
|
||||
if AnthropicProvider is None:
|
||||
raise ValueError("Anthropic is not installed")
|
||||
return AnthropicProvider()
|
||||
else:
|
||||
raise ValueError(f"Unsupported provider: {provider_name}")
|
||||
|
||||
|
||||
__all__ = ["BatchProvider", "OpenAIProvider", "AnthropicProvider", "get_provider"]
|
||||
243
참고/instructor-main/instructor/batch/providers/anthropic.py
Normal file
243
참고/instructor-main/instructor/batch/providers/anthropic.py
Normal file
@@ -0,0 +1,243 @@
|
||||
"""
|
||||
Anthropic-specific batch processing implementation.
|
||||
|
||||
This module contains the Anthropic batch processing provider class.
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Any, Optional, Union
|
||||
import io
|
||||
import logging
|
||||
from .base import BatchProvider
|
||||
from ..models import BatchJobInfo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AnthropicProvider(BatchProvider):
|
||||
"""Anthropic batch processing provider"""
|
||||
|
||||
def submit_batch(
|
||||
self,
|
||||
file_path_or_buffer: Union[str, io.BytesIO],
|
||||
metadata: Optional[dict[str, Any]] = None,
|
||||
**kwargs,
|
||||
) -> str:
|
||||
"""Submit Anthropic batch job"""
|
||||
_ = kwargs # Unused but accepted for API consistency
|
||||
try:
|
||||
import anthropic
|
||||
|
||||
client = anthropic.Anthropic()
|
||||
|
||||
# Note: Anthropic doesn't support metadata in batch creation
|
||||
# but we accept it for API consistency
|
||||
if metadata:
|
||||
print(
|
||||
f"Note: Anthropic batches don't support metadata. Ignoring: {metadata}"
|
||||
)
|
||||
|
||||
# TODO(#batch-api-stable): Remove beta fallback when stable API is available
|
||||
try:
|
||||
batches_client = client.messages.batches
|
||||
except AttributeError:
|
||||
batches_client = client.beta.messages.batches
|
||||
|
||||
if isinstance(file_path_or_buffer, str):
|
||||
with open(file_path_or_buffer) as f:
|
||||
requests = [json.loads(line) for line in f if line.strip()]
|
||||
elif isinstance(file_path_or_buffer, io.BytesIO):
|
||||
file_path_or_buffer.seek(0)
|
||||
content = file_path_or_buffer.read().decode("utf-8")
|
||||
requests = [
|
||||
json.loads(line) for line in content.split("\n") if line.strip()
|
||||
]
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported file_path_or_buffer type: {type(file_path_or_buffer)}"
|
||||
)
|
||||
|
||||
batch = batches_client.create(requests=requests)
|
||||
return batch.id
|
||||
except (ValueError, TypeError) as e:
|
||||
# Re-raise validation errors as-is
|
||||
logger.error(f"Validation error in Anthropic batch submission: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to submit Anthropic batch: {e}") from e
|
||||
|
||||
def get_status(self, batch_id: str) -> dict[str, Any]:
|
||||
"""Get Anthropic batch status"""
|
||||
try:
|
||||
import anthropic
|
||||
|
||||
client = anthropic.Anthropic()
|
||||
|
||||
# TODO(#batch-api-stable): Remove beta fallback when stable API is available
|
||||
try:
|
||||
batches_client = client.messages.batches
|
||||
except AttributeError:
|
||||
batches_client = client.beta.messages.batches
|
||||
|
||||
batch = batches_client.retrieve(batch_id)
|
||||
return {
|
||||
"id": batch.id,
|
||||
"status": batch.processing_status,
|
||||
"created_at": batch.created_at,
|
||||
"request_counts": getattr(batch, "request_counts", {}),
|
||||
}
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to get Anthropic batch status: {e}") from e
|
||||
|
||||
def retrieve_results(self, batch_id: str) -> str:
|
||||
"""Retrieve Anthropic batch results"""
|
||||
try:
|
||||
import anthropic
|
||||
|
||||
client = anthropic.Anthropic()
|
||||
|
||||
# TODO(#batch-api-stable): Remove beta fallback when stable API is available
|
||||
try:
|
||||
batches_client = client.messages.batches
|
||||
except AttributeError:
|
||||
batches_client = client.beta.messages.batches
|
||||
|
||||
batch = batches_client.retrieve(batch_id)
|
||||
|
||||
# Check for various terminal states
|
||||
if batch.processing_status in ["failed", "cancelled", "expired"]:
|
||||
raise Exception(
|
||||
f"Batch job failed with status: {batch.processing_status}"
|
||||
)
|
||||
|
||||
if batch.processing_status != "ended":
|
||||
raise Exception(
|
||||
f"Batch not completed, status: {batch.processing_status}"
|
||||
)
|
||||
|
||||
# Check if all requests failed
|
||||
request_counts = getattr(batch, "request_counts", None)
|
||||
if request_counts:
|
||||
succeeded = getattr(request_counts, "succeeded", 0)
|
||||
errored = getattr(request_counts, "errored", 0)
|
||||
total = getattr(request_counts, "total", 0)
|
||||
|
||||
if errored > 0 and succeeded == 0:
|
||||
raise RuntimeError(
|
||||
f"All {total} batch requests failed. No results will be available."
|
||||
)
|
||||
|
||||
results = batches_client.results(batch_id)
|
||||
results_lines = []
|
||||
for result in results:
|
||||
results_lines.append(result.model_dump_json())
|
||||
|
||||
return "\n".join(results_lines)
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to retrieve Anthropic results: {e}") from e
|
||||
|
||||
def download_results(self, batch_id: str, file_path: str) -> None:
|
||||
"""Download Anthropic batch results to a file"""
|
||||
try:
|
||||
import anthropic
|
||||
|
||||
client = anthropic.Anthropic()
|
||||
|
||||
# TODO(#batch-api-stable): Remove beta fallback when stable API is available
|
||||
try:
|
||||
batches_client = client.messages.batches
|
||||
except AttributeError:
|
||||
batches_client = client.beta.messages.batches
|
||||
|
||||
batch = batches_client.retrieve(batch_id)
|
||||
|
||||
# Check for various terminal states
|
||||
if batch.processing_status in ["failed", "cancelled", "expired"]:
|
||||
raise Exception(
|
||||
f"Batch job failed with status: {batch.processing_status}"
|
||||
)
|
||||
|
||||
if batch.processing_status != "ended":
|
||||
raise Exception(
|
||||
f"Batch not completed, status: {batch.processing_status}"
|
||||
)
|
||||
|
||||
# Check if all requests failed
|
||||
request_counts = getattr(batch, "request_counts", None)
|
||||
if request_counts:
|
||||
succeeded = getattr(request_counts, "succeeded", 0)
|
||||
errored = getattr(request_counts, "errored", 0)
|
||||
total = getattr(request_counts, "total", 0)
|
||||
|
||||
if errored > 0 and succeeded == 0:
|
||||
raise RuntimeError(
|
||||
f"All {total} batch requests failed. No results will be available."
|
||||
)
|
||||
|
||||
results = batches_client.results(batch_id)
|
||||
with open(file_path, "w") as f:
|
||||
for result in results:
|
||||
f.write(result.model_dump_json() + "\n")
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to download Anthropic results: {e}") from e
|
||||
|
||||
def cancel_batch(self, batch_id: str) -> dict[str, Any]:
|
||||
"""Cancel Anthropic batch job"""
|
||||
try:
|
||||
import anthropic
|
||||
|
||||
client = anthropic.Anthropic()
|
||||
|
||||
# TODO(#batch-api-stable): Remove beta fallback when stable API is available
|
||||
try:
|
||||
batches_client = client.messages.batches
|
||||
except AttributeError:
|
||||
batches_client = client.beta.messages.batches
|
||||
|
||||
batch = batches_client.cancel(batch_id)
|
||||
return batch.model_dump()
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to cancel Anthropic batch: {e}") from e
|
||||
|
||||
def delete_batch(self, batch_id: str) -> dict[str, Any]:
|
||||
"""Delete Anthropic batch job"""
|
||||
try:
|
||||
import anthropic
|
||||
|
||||
client = anthropic.Anthropic()
|
||||
|
||||
# TODO(#batch-api-stable): Remove beta fallback when stable API is available
|
||||
try:
|
||||
batches_client = client.messages.batches
|
||||
except AttributeError:
|
||||
batches_client = client.beta.messages.batches
|
||||
|
||||
batch = batches_client.retrieve(batch_id)
|
||||
return {
|
||||
"id": batch.id,
|
||||
"status": batch.processing_status,
|
||||
"message": "Anthropic does not support batch deletion",
|
||||
}
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to delete Anthropic batch: {e}") from e
|
||||
|
||||
def list_batches(self, limit: int = 10) -> list[BatchJobInfo]:
|
||||
"""List Anthropic batch jobs"""
|
||||
try:
|
||||
import anthropic
|
||||
|
||||
client = anthropic.Anthropic()
|
||||
|
||||
# TODO(#batch-api-stable): Remove beta fallback when stable API is available
|
||||
try:
|
||||
batches_client = client.messages.batches
|
||||
except AttributeError:
|
||||
batches_client = client.beta.messages.batches
|
||||
|
||||
batches = batches_client.list(limit=limit)
|
||||
return [
|
||||
BatchJobInfo.from_anthropic(batch.model_dump())
|
||||
for batch in batches.data
|
||||
]
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to list Anthropic batches: {e}") from e
|
||||
57
참고/instructor-main/instructor/batch/providers/base.py
Normal file
57
참고/instructor-main/instructor/batch/providers/base.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
Base provider class for batch processing.
|
||||
|
||||
This module defines the abstract base class that all batch providers must implement.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Optional, Union
|
||||
import io
|
||||
import logging
|
||||
from ..models import BatchJobInfo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BatchProvider(ABC):
|
||||
"""Abstract base class for batch processing providers"""
|
||||
|
||||
@abstractmethod
|
||||
def submit_batch(
|
||||
self,
|
||||
file_path_or_buffer: Union[str, io.BytesIO],
|
||||
metadata: Optional[dict[str, Any]] = None,
|
||||
**kwargs,
|
||||
) -> str:
|
||||
"""Submit a batch job and return the job ID"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_status(self, batch_id: str) -> dict[str, Any]:
|
||||
"""Get the status of a batch job"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def retrieve_results(self, batch_id: str) -> str:
|
||||
"""Retrieve batch results as a string"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def download_results(self, batch_id: str, file_path: str) -> None:
|
||||
"""Download batch results to a file"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def cancel_batch(self, batch_id: str) -> dict[str, Any]:
|
||||
"""Cancel a batch job"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete_batch(self, batch_id: str) -> dict[str, Any]:
|
||||
"""Delete a batch job"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def list_batches(self, limit: int = 10) -> list[BatchJobInfo]:
|
||||
"""List batch jobs"""
|
||||
pass
|
||||
242
참고/instructor-main/instructor/batch/providers/openai.py
Normal file
242
참고/instructor-main/instructor/batch/providers/openai.py
Normal file
@@ -0,0 +1,242 @@
|
||||
"""
|
||||
OpenAI-specific batch processing implementation.
|
||||
|
||||
This module contains the OpenAI batch processing provider class.
|
||||
"""
|
||||
|
||||
from typing import Any, Optional, Union
|
||||
import io
|
||||
import logging
|
||||
from .base import BatchProvider
|
||||
from ..models import BatchJobInfo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OpenAIProvider(BatchProvider):
|
||||
"""OpenAI batch processing provider"""
|
||||
|
||||
def submit_batch(
|
||||
self,
|
||||
file_path_or_buffer: Union[str, io.BytesIO],
|
||||
metadata: Optional[dict[str, Any]] = None,
|
||||
**kwargs,
|
||||
) -> str:
|
||||
"""Submit OpenAI batch job"""
|
||||
try:
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI()
|
||||
|
||||
if metadata is None:
|
||||
metadata = {"description": "Instructor batch job"}
|
||||
|
||||
logger.debug(f"Submitting batch job with metadata: {metadata}")
|
||||
|
||||
if isinstance(file_path_or_buffer, str):
|
||||
logger.debug(f"Creating batch file from path: {file_path_or_buffer}")
|
||||
with open(file_path_or_buffer, "rb") as f:
|
||||
batch_file = client.files.create(file=f, purpose="batch")
|
||||
elif isinstance(file_path_or_buffer, io.BytesIO):
|
||||
logger.debug("Creating batch file from BytesIO buffer")
|
||||
file_path_or_buffer.seek(0)
|
||||
batch_file = client.files.create(
|
||||
file=file_path_or_buffer, purpose="batch"
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported file_path_or_buffer type: {type(file_path_or_buffer)}"
|
||||
)
|
||||
|
||||
batch_job = client.batches.create(
|
||||
input_file_id=batch_file.id,
|
||||
endpoint="/v1/chat/completions",
|
||||
completion_window=kwargs.get("completion_window", "24h"),
|
||||
metadata=metadata,
|
||||
)
|
||||
logger.info(f"Successfully submitted batch job: {batch_job.id}")
|
||||
return batch_job.id
|
||||
except (ValueError, TypeError) as e:
|
||||
# Re-raise validation errors as-is
|
||||
logger.error(f"Validation error in OpenAI batch submission: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to submit OpenAI batch: {e}")
|
||||
raise RuntimeError(f"Failed to submit OpenAI batch: {e}") from e
|
||||
|
||||
def get_status(self, batch_id: str) -> dict[str, Any]:
|
||||
"""Get OpenAI batch status"""
|
||||
try:
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI()
|
||||
batch = client.batches.retrieve(batch_id)
|
||||
return {
|
||||
"id": batch.id,
|
||||
"status": batch.status,
|
||||
"created_at": batch.created_at,
|
||||
"request_counts": {
|
||||
"total": getattr(batch.request_counts, "total", 0),
|
||||
"completed": getattr(batch.request_counts, "completed", 0),
|
||||
"failed": getattr(batch.request_counts, "failed", 0),
|
||||
},
|
||||
}
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to get OpenAI batch status: {e}") from e
|
||||
|
||||
def retrieve_results(self, batch_id: str) -> str:
|
||||
"""Retrieve OpenAI batch results"""
|
||||
try:
|
||||
from openai import OpenAI
|
||||
import time
|
||||
|
||||
client = OpenAI()
|
||||
batch = client.batches.retrieve(batch_id)
|
||||
|
||||
if batch.status != "completed":
|
||||
raise Exception(f"Batch not completed, status: {batch.status}")
|
||||
|
||||
# Check if all requests failed
|
||||
request_counts = getattr(batch, "request_counts", None)
|
||||
if request_counts:
|
||||
completed = getattr(request_counts, "completed", 0)
|
||||
failed = getattr(request_counts, "failed", 0)
|
||||
total = getattr(request_counts, "total", 0)
|
||||
|
||||
if failed > 0 and completed == 0:
|
||||
raise RuntimeError(
|
||||
f"All {total} batch requests failed. No output file will be available. "
|
||||
)
|
||||
|
||||
if not batch.output_file_id:
|
||||
# Sometimes output file isn't immediately available, wait longer and retry more
|
||||
max_retries = 10
|
||||
for attempt in range(max_retries):
|
||||
wait_time = min(
|
||||
5 + attempt, 15
|
||||
) # Progressive backoff: 5s, 6s, 7s... up to 15s
|
||||
print(
|
||||
f"Output file not ready, waiting {wait_time}s (attempt {attempt + 1}/{max_retries})..."
|
||||
)
|
||||
time.sleep(wait_time)
|
||||
batch = client.batches.retrieve(batch_id)
|
||||
if batch.output_file_id:
|
||||
print(f"Output file now available: {batch.output_file_id}")
|
||||
break
|
||||
# Check if batch failed during our wait
|
||||
if batch.status != "completed":
|
||||
raise Exception(
|
||||
f"Batch status changed to {batch.status} while waiting for output file"
|
||||
)
|
||||
if attempt == max_retries - 1:
|
||||
# Final attempt - provide detailed error info
|
||||
raise RuntimeError(
|
||||
f"No output file available after {max_retries} retries over {sum(range(5, 5 + max_retries))} seconds. "
|
||||
f"Batch status: {batch.status}, Request counts: {getattr(batch, 'request_counts', 'unknown')}. "
|
||||
)
|
||||
|
||||
if batch.output_file_id is None:
|
||||
raise RuntimeError("Batch has no output file ID available")
|
||||
file_response = client.files.content(batch.output_file_id)
|
||||
return file_response.text
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to retrieve OpenAI results: {e}") from e
|
||||
|
||||
def download_results(self, batch_id: str, file_path: str) -> None:
|
||||
"""Download OpenAI batch results to a file"""
|
||||
try:
|
||||
from openai import OpenAI
|
||||
import time
|
||||
|
||||
client = OpenAI()
|
||||
batch = client.batches.retrieve(batch_id)
|
||||
|
||||
if batch.status != "completed":
|
||||
raise Exception(f"Batch not completed, status: {batch.status}")
|
||||
|
||||
# Check if all requests failed
|
||||
request_counts = getattr(batch, "request_counts", None)
|
||||
if request_counts:
|
||||
completed = getattr(request_counts, "completed", 0)
|
||||
failed = getattr(request_counts, "failed", 0)
|
||||
total = getattr(request_counts, "total", 0)
|
||||
|
||||
if failed > 0 and completed == 0:
|
||||
raise RuntimeError(
|
||||
f"All {total} batch requests failed. No output file will be available."
|
||||
)
|
||||
|
||||
if not batch.output_file_id:
|
||||
# Sometimes output file isn't immediately available, wait longer and retry more
|
||||
max_retries = 10
|
||||
for attempt in range(max_retries):
|
||||
wait_time = min(
|
||||
5 + attempt, 15
|
||||
) # Progressive backoff: 5s, 6s, 7s... up to 15s
|
||||
print(
|
||||
f"Output file not ready, waiting {wait_time}s (attempt {attempt + 1}/{max_retries})..."
|
||||
)
|
||||
time.sleep(wait_time)
|
||||
batch = client.batches.retrieve(batch_id)
|
||||
if batch.output_file_id:
|
||||
print(f"Output file now available: {batch.output_file_id}")
|
||||
break
|
||||
# Check if batch failed during our wait
|
||||
if batch.status != "completed":
|
||||
raise Exception(
|
||||
f"Batch status changed to {batch.status} while waiting for output file"
|
||||
)
|
||||
if attempt == max_retries - 1:
|
||||
# Final attempt - provide detailed error info
|
||||
raise Exception(
|
||||
f"No output file available after {max_retries} retries over {sum(range(5, 5 + max_retries))} seconds. "
|
||||
f"Batch status: {batch.status}, Request counts: {getattr(batch, 'request_counts', 'unknown')}."
|
||||
)
|
||||
|
||||
if batch.output_file_id is None:
|
||||
raise RuntimeError("Batch has no output file ID available")
|
||||
file_response = client.files.content(batch.output_file_id)
|
||||
with open(file_path, "w") as f:
|
||||
f.write(file_response.text)
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to download OpenAI results: {e}") from e
|
||||
|
||||
def cancel_batch(self, batch_id: str) -> dict[str, Any]:
|
||||
"""Cancel OpenAI batch job"""
|
||||
try:
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI()
|
||||
batch = client.batches.cancel(batch_id)
|
||||
return batch.model_dump()
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to cancel OpenAI batch: {e}") from e
|
||||
|
||||
def delete_batch(self, batch_id: str) -> dict[str, Any]:
|
||||
"""Delete OpenAI batch job"""
|
||||
try:
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI()
|
||||
# OpenAI doesn't have a delete endpoint, so we'll return the batch info
|
||||
batch = client.batches.retrieve(batch_id)
|
||||
return {
|
||||
"id": batch.id,
|
||||
"status": batch.status,
|
||||
"message": "OpenAI does not support batch deletion",
|
||||
}
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to delete OpenAI batch: {e}") from e
|
||||
|
||||
def list_batches(self, limit: int = 10) -> list[BatchJobInfo]:
|
||||
"""List OpenAI batch jobs"""
|
||||
try:
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI()
|
||||
batches = client.batches.list(limit=limit)
|
||||
return [
|
||||
BatchJobInfo.from_openai(batch.model_dump()) for batch in batches.data
|
||||
]
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to list OpenAI batches: {e}") from e
|
||||
175
참고/instructor-main/instructor/batch/request.py
Normal file
175
참고/instructor-main/instructor/batch/request.py
Normal file
@@ -0,0 +1,175 @@
|
||||
"""
|
||||
Batch request models and schema utilities.
|
||||
|
||||
This module contains the BatchRequest class and related models for creating
|
||||
provider-specific batch requests with JSON schema generation.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import Any, Generic
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
import json
|
||||
import io
|
||||
from .models import T
|
||||
|
||||
|
||||
class Function(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
parameters: Any
|
||||
|
||||
|
||||
class Tool(BaseModel):
|
||||
type: str
|
||||
function: Function
|
||||
|
||||
|
||||
class RequestBody(BaseModel):
|
||||
model: str
|
||||
messages: list[dict[str, Any]]
|
||||
max_tokens: int | None = Field(default=1000)
|
||||
temperature: float | None = Field(default=1.0)
|
||||
tools: list[Tool] | None
|
||||
tool_choice: dict[str, Any] | None
|
||||
|
||||
|
||||
class BatchModel(BaseModel):
|
||||
custom_id: str
|
||||
body: RequestBody
|
||||
url: str
|
||||
method: str
|
||||
|
||||
|
||||
class BatchRequest(BaseModel, Generic[T]):
|
||||
"""Unified batch request that works across all providers using JSON schema"""
|
||||
|
||||
custom_id: str
|
||||
messages: list[dict[str, Any]]
|
||||
response_model: type[T]
|
||||
model: str
|
||||
max_tokens: int | None = Field(default=1000)
|
||||
temperature: float | None = Field(default=0.1)
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
def get_json_schema(self) -> dict[str, Any]:
|
||||
"""Generate JSON schema from response_model"""
|
||||
return self.response_model.model_json_schema()
|
||||
|
||||
def to_openai_format(self) -> dict[str, Any]:
|
||||
"""Convert to OpenAI batch format with JSON schema"""
|
||||
schema = self.get_json_schema()
|
||||
|
||||
# OpenAI strict mode requires additionalProperties to be false
|
||||
def make_strict_schema(schema_dict):
|
||||
"""Recursively add additionalProperties: false for OpenAI strict mode"""
|
||||
if isinstance(schema_dict, dict):
|
||||
if "type" in schema_dict:
|
||||
if schema_dict["type"] == "object":
|
||||
schema_dict["additionalProperties"] = False
|
||||
elif schema_dict["type"] == "array" and "items" in schema_dict:
|
||||
schema_dict["items"] = make_strict_schema(schema_dict["items"])
|
||||
|
||||
# Recursively process properties
|
||||
if "properties" in schema_dict:
|
||||
for prop_name, prop_schema in schema_dict["properties"].items():
|
||||
schema_dict["properties"][prop_name] = make_strict_schema(
|
||||
prop_schema
|
||||
)
|
||||
|
||||
# Process definitions/defs
|
||||
for key in ["definitions", "$defs"]:
|
||||
if key in schema_dict:
|
||||
for def_name, def_schema in schema_dict[key].items():
|
||||
schema_dict[key][def_name] = make_strict_schema(def_schema)
|
||||
|
||||
return schema_dict
|
||||
|
||||
strict_schema = make_strict_schema(schema.copy())
|
||||
|
||||
return {
|
||||
"custom_id": self.custom_id,
|
||||
"method": "POST",
|
||||
"url": "/v1/chat/completions",
|
||||
"body": {
|
||||
"model": self.model,
|
||||
"messages": self.messages,
|
||||
"max_tokens": self.max_tokens,
|
||||
"temperature": self.temperature,
|
||||
"response_format": {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": self.response_model.__name__,
|
||||
"strict": True,
|
||||
"schema": strict_schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def to_anthropic_format(self) -> dict[str, Any]:
|
||||
"""Convert to Anthropic batch format with JSON schema"""
|
||||
schema = self.get_json_schema()
|
||||
|
||||
# Ensure schema has proper format for Anthropic
|
||||
if "type" not in schema:
|
||||
schema["type"] = "object"
|
||||
if "additionalProperties" not in schema:
|
||||
schema["additionalProperties"] = False
|
||||
|
||||
# Extract system message and convert to system parameter
|
||||
system_message = None
|
||||
filtered_messages = []
|
||||
|
||||
for message in self.messages:
|
||||
if message.get("role") == "system":
|
||||
system_message = message.get("content", "")
|
||||
else:
|
||||
filtered_messages.append(message)
|
||||
|
||||
params = {
|
||||
"model": self.model,
|
||||
"max_tokens": self.max_tokens,
|
||||
"temperature": self.temperature,
|
||||
"messages": filtered_messages,
|
||||
"tools": [
|
||||
{
|
||||
"name": "extract_data",
|
||||
"description": f"Extract data matching the {self.response_model.__name__} schema",
|
||||
"input_schema": schema,
|
||||
}
|
||||
],
|
||||
"tool_choice": {"type": "tool", "name": "extract_data"},
|
||||
}
|
||||
|
||||
# Add system parameter if system message exists
|
||||
if system_message:
|
||||
params["system"] = system_message
|
||||
|
||||
return {
|
||||
"custom_id": self.custom_id,
|
||||
"params": params,
|
||||
}
|
||||
|
||||
def save_to_file(
|
||||
self, file_path_or_buffer: str | io.BytesIO, provider: str
|
||||
) -> None:
|
||||
"""Save batch request to file or BytesIO buffer in provider-specific format"""
|
||||
if provider == "openai":
|
||||
data = self.to_openai_format()
|
||||
elif provider == "anthropic":
|
||||
data = self.to_anthropic_format()
|
||||
else:
|
||||
raise ValueError(f"Unsupported provider: {provider}")
|
||||
|
||||
json_line = json.dumps(data) + "\n"
|
||||
|
||||
if isinstance(file_path_or_buffer, str):
|
||||
with open(file_path_or_buffer, "a") as f:
|
||||
f.write(json_line)
|
||||
elif isinstance(file_path_or_buffer, io.BytesIO):
|
||||
file_path_or_buffer.write(json_line.encode("utf-8"))
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported file_path_or_buffer type: {type(file_path_or_buffer)}"
|
||||
)
|
||||
28
참고/instructor-main/instructor/batch/utils.py
Normal file
28
참고/instructor-main/instructor/batch/utils.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
Utility functions for batch processing.
|
||||
|
||||
This module contains helper functions for filtering, extracting, and manipulating
|
||||
batch results.
|
||||
"""
|
||||
|
||||
from .models import BatchResult, BatchSuccess, BatchError, T
|
||||
|
||||
|
||||
def filter_successful(results: list[BatchResult]) -> list[BatchSuccess[T]]:
|
||||
"""Filter to only successful results"""
|
||||
return [r for r in results if r.success]
|
||||
|
||||
|
||||
def filter_errors(results: list[BatchResult]) -> list[BatchError]:
|
||||
"""Filter to only error results"""
|
||||
return [r for r in results if not r.success]
|
||||
|
||||
|
||||
def extract_results(results: list[BatchResult]) -> list[T]:
|
||||
"""Extract just the result objects from successful results"""
|
||||
return [r.result for r in results if r.success]
|
||||
|
||||
|
||||
def get_results_by_custom_id(results: list[BatchResult]) -> dict[str, BatchResult]:
|
||||
"""Create a dictionary mapping custom_id to results"""
|
||||
return {r.custom_id: r for r in results}
|
||||
Reference in New Issue
Block a user