feat: initial commit of agent task executor framework

- Add core task execution framework
- Add LLM integration with DeepSeek
- Add text analysis task implementation
- Add configuration management
- Add tests and documentation
This commit is contained in:
zhukang 2025-01-14 20:53:09 +08:00
parent 8c39084a31
commit dd797ab5e4
20 changed files with 1628 additions and 0 deletions

12
.env.example Normal file
View File

@ -0,0 +1,12 @@
# LLM Configuration
# You can set these environment variables to override the configuration in llm_config.yaml
# API Key (will override secure storage)
LLM_API_KEY=your-api-key-here
# Base URL (will override provider default)
LLM_API_BASE=https://api.deepseek.com
# Provider settings
LLM_PROVIDER=deepseek
LLM_MODEL=deepseek-chat

26
.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
# Python
__pycache__/
*.py[cod]
*$py.class
.pytest_cache/
# Virtual Environment
.venv/
venv/
ENV/
# Environment Variables
.env
# IDE
.vscode/
.idea/
# Secure Configuration
config/secure.json
config/secure.key
# Distribution
dist/
build/
*.egg-info/

1
config/__init__.py Normal file
View File

@ -0,0 +1 @@
"""Configuration management package."""

67
config/config_loader.py Normal file
View File

@ -0,0 +1,67 @@
import os
from typing import Dict, Any
import yaml
from pathlib import Path
from dotenv import load_dotenv
from .secure_config import SecureConfig
def get_api_settings(config: Dict[str, Any], secure_config: Dict[str, Any]) -> Dict[str, str]:
"""Get API settings from config and environment variables."""
llm_config = config["llm"]
provider = llm_config["provider"]
# Get API key with priority:
# 1. Environment variables
# 2. Secure config
# 3. Regular config
api_key = (
os.getenv("LLM_API_KEY") or
os.getenv("OPENAI_API_KEY") or
secure_config.get("api_key") or
llm_config["api"].get("api_key")
)
# Get API base URL with fallback chain:
# 1. Environment variable
# 2. Config api.api_base
# 3. Provider-specific default
api_base = (
os.getenv("LLM_API_BASE") or
llm_config["api"].get("api_base") or
llm_config.get(provider, {}).get("api_base")
)
return {
"api_key": api_key,
"api_base": api_base
}
def save_api_key(api_key: str):
"""Securely save API key."""
secure = SecureConfig()
secure_path = Path(os.getcwd()) / "config" / "secure.json"
secure.save_secure_config({"api_key": api_key}, secure_path)
def load_config() -> Dict[str, Any]:
"""Load configuration from YAML file and secure storage."""
load_dotenv()
config_dir = Path(os.getcwd()) / "config"
# Load main config
with open(config_dir / "llm_config.yaml", "r") as f:
config = yaml.safe_load(f)
# Load secure config
secure = SecureConfig()
secure_config = secure.load_secure_config(config_dir / "secure.json")
# Get API settings
api_settings = get_api_settings(config, secure_config)
# Update config with API settings
llm_config = config["llm"]
llm_config["api_key"] = api_settings["api_key"]
llm_config["api_base"] = api_settings["api_base"]
return config

22
config/llm_config.yaml Normal file
View File

@ -0,0 +1,22 @@
llm:
provider: deepseek # can be: openai, azure, anthropic, deepseek, custom
model: deepseek-chat
max_tokens: 2000
temperature: 0.7
timeout: 30
# API configuration
# These values can be overridden by environment variables
# or secure storage
api:
api_base: null # will use provider default if not set
# Provider-specific settings
openai:
api_base: https://api.openai.com/v1
azure:
api_base: null # Set your Azure endpoint here
anthropic:
api_base: https://api.anthropic.com
deepseek:
api_base: https://api.deepseek.com

131
config/secure_config.py Normal file
View File

@ -0,0 +1,131 @@
import os
import json
from pathlib import Path
from base64 import b64encode, b64decode
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from typing import Dict, Any, Optional
class SecureConfig:
def __init__(self, config_path: str = None):
self.config_path = Path(config_path or Path(__file__).parent / "secure.key")
self._ensure_key_exists()
self.fernet = self._get_fernet()
def _ensure_key_exists(self):
"""Ensure encryption key exists, generate if not."""
self.config_path.parent.mkdir(parents=True, exist_ok=True)
if not self.config_path.exists():
# Generate a new encryption key
key = Fernet.generate_key()
with open(self.config_path, "wb") as f:
f.write(key)
# Set restrictive permissions on Windows or Unix
if os.name == 'nt': # Windows
import win32security
import win32api
import ntsecuritycon as con
# Get current user's SID
username = win32api.GetUserName()
sid = win32security.LookupAccountName(None, username)[0]
# Create a new DACL with full access for the current user
dacl = win32security.ACL()
dacl.AddAccessAllowedAce(
win32security.ACL_REVISION,
con.FILE_ALL_ACCESS,
sid
)
# Create and set the security descriptor
security_desc = win32security.SECURITY_DESCRIPTOR()
security_desc.SetSecurityDescriptorDacl(1, dacl, 0)
win32security.SetFileSecurity(
str(self.config_path),
win32security.DACL_SECURITY_INFORMATION,
security_desc
)
else: # Unix
os.chmod(self.config_path, 0o600)
def _get_fernet(self) -> Fernet:
"""Get Fernet instance for encryption/decryption."""
with open(self.config_path, "rb") as f:
key = f.read()
return Fernet(key)
def encrypt_value(self, value: str) -> str:
"""Encrypt a string value."""
if not value:
return None
return self.fernet.encrypt(value.encode()).decode()
def decrypt_value(self, encrypted_value: str) -> Optional[str]:
"""Decrypt an encrypted string value."""
if not encrypted_value:
return None
try:
return self.fernet.decrypt(encrypted_value.encode()).decode()
except Exception:
return None
def save_secure_config(self, config: Dict[str, Any], file_path: str):
"""Save configuration with encrypted sensitive values."""
file_path = Path(file_path)
file_path.parent.mkdir(parents=True, exist_ok=True)
secure_config = {}
# Encrypt sensitive values
if "api_key" in config:
secure_config["api_key"] = self.encrypt_value(config["api_key"])
# Save to file
with open(file_path, "w") as f:
json.dump(secure_config, f, indent=2)
# Set restrictive permissions
if os.name == 'nt': # Windows
import win32security
import win32api
import ntsecuritycon as con
# Get current user's SID
username = win32api.GetUserName()
sid = win32security.LookupAccountName(None, username)[0]
# Create a new DACL with full access for the current user
dacl = win32security.ACL()
dacl.AddAccessAllowedAce(
win32security.ACL_REVISION,
con.FILE_ALL_ACCESS,
sid
)
# Create and set the security descriptor
security_desc = win32security.SECURITY_DESCRIPTOR()
security_desc.SetSecurityDescriptorDacl(1, dacl, 0)
win32security.SetFileSecurity(
str(file_path),
win32security.DACL_SECURITY_INFORMATION,
security_desc
)
else: # Unix
os.chmod(file_path, 0o600)
def load_secure_config(self, file_path: str) -> Dict[str, Any]:
"""Load and decrypt configuration."""
if not Path(file_path).exists():
return {}
with open(file_path, "r") as f:
secure_config = json.load(f)
# Decrypt sensitive values
config = {}
if "api_key" in secure_config:
config["api_key"] = self.decrypt_value(secure_config["api_key"])
return config

110
llm_client.py Normal file
View File

@ -0,0 +1,110 @@
from abc import ABC, abstractmethod
from typing import Dict, Any, List, Optional
import os
import json
import httpx
from dataclasses import dataclass
from enum import Enum
class LLMProvider(Enum):
OPENAI = "openai"
AZURE = "azure"
ANTHROPIC = "anthropic"
DEEPSEEK = "deepseek"
CUSTOM = "custom"
@dataclass
class LLMConfig:
provider: LLMProvider
api_key: str
api_base: Optional[str] = None
model: str = "gpt-4"
max_tokens: int = 2000
temperature: float = 0.7
timeout: int = 30
class BaseLLMClient(ABC):
@abstractmethod
async def generate(self,
messages: List[Dict[str, str]],
**kwargs) -> Dict[str, Any]:
pass
class OpenAICompatibleClient(BaseLLMClient):
def __init__(self, config: LLMConfig):
self.config = config
async def generate(self,
messages: List[Dict[str, str]],
**kwargs) -> Dict[str, Any]:
"""
Generate a response using OpenAI-compatible API.
Args:
messages: List of message dictionaries with 'role' and 'content'
**kwargs: Additional parameters to pass to the API
Returns:
Dict containing the API response
"""
try:
# Prepare request data
request_data = {
"model": self.config.model,
"messages": messages,
"max_tokens": self.config.max_tokens,
"temperature": self.config.temperature,
**kwargs
}
# Make API request
async with httpx.AsyncClient(
base_url=self.config.api_base or "https://api.openai.com/v1",
timeout=self.config.timeout,
headers={
"Authorization": f"Bearer {self.config.api_key}",
"Content-Type": "application/json"
}
) as client:
response = await client.post(
"/v1/chat/completions",
json=request_data
)
# Check for HTTP errors
response.raise_for_status()
# Parse response
response_data = response.json()
# Add usage information if available
if "usage" in response_data:
response_data["usage"] = {
"prompt_tokens": response_data["usage"].get("prompt_tokens", 0),
"completion_tokens": response_data["usage"].get("completion_tokens", 0),
"total_tokens": response_data["usage"].get("total_tokens", 0)
}
return response_data
except httpx.HTTPError as http_err:
# Handle HTTP errors
error_msg = f"HTTP error occurred: {str(http_err)}"
try:
error_data = http_err.response.json()
if "error" in error_data:
error_msg = f"API error: {error_data['error'].get('message', str(error_data['error']))}"
except:
pass
return {"error": error_msg}
except Exception as e:
# Handle other errors
return {"error": f"Error generating response: {str(e)}"}
def create_llm_client(config: LLMConfig) -> BaseLLMClient:
"""Factory function to create appropriate LLM client."""
if config.provider in [LLMProvider.OPENAI, LLMProvider.AZURE, LLMProvider.DEEPSEEK, LLMProvider.CUSTOM]:
return OpenAICompatibleClient(config)
else:
raise ValueError(f"Unsupported LLM provider: {config.provider}")

199
llm_executor.py Normal file
View File

@ -0,0 +1,199 @@
import os
from typing import Dict, Any, List, Optional
from tenacity import retry, stop_after_attempt, wait_exponential
from dotenv import load_dotenv
import json
from llm_client import LLMConfig, LLMProvider, create_llm_client
from config.config_loader import load_config
load_dotenv()
class LLMExecutor:
def __init__(self, model: str = None):
# Load configuration
config_dict = load_config()
llm_config = config_dict["llm"]
# Get provider-specific settings
provider = LLMProvider(llm_config["provider"])
api_base = (
llm_config["api"].get("api_base") or
llm_config.get(provider.value, {}).get("api_base")
)
# Initialize LLM client with configuration
config = LLMConfig(
provider=provider,
api_key=llm_config["api_key"],
api_base=api_base,
model=model or llm_config["model"],
max_tokens=llm_config.get("max_tokens", 2000),
temperature=llm_config.get("temperature", 0.7),
timeout=llm_config.get("timeout", 30),
)
self.llm_client = create_llm_client(config)
self.conversation_history = []
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
async def execute_step(self,
step_instruction: str,
step_input: Dict[str, Any],
context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""
Execute a single step using the LLM.
Args:
step_instruction: The instruction for the current step
step_input: Input data for the step
context: Optional context information
Returns:
Dict containing the LLM's response and any generated artifacts
"""
try:
# Format the prompt
messages = [
{"role": "system", "content": """You are an AI assistant helping with task execution.
Please process the input according to the instruction and return a JSON response.
The input text may contain Chinese characters. Please handle them correctly.
For example, if asked to validate text, return: {"valid": true/false, "reason": "explanation"}
For text preprocessing, return: {"preprocessed_text": "cleaned text"}
For summary generation, return: {"summary": "generated summary"}
For keyword extraction, return: {"keywords": ["keyword1", "keyword2", ...]}
For final analysis, return: {"analysis": "final analysis text"}
Please ensure all Chinese characters are preserved and handled correctly."""},
{"role": "user", "content": f"""Instruction: {step_instruction}
Input: {json.dumps(step_input, indent=2, ensure_ascii=False)}"""}
]
# Call LLM
try:
response = await self.llm_client.generate(messages)
if "error" in response:
raise Exception(f"LLM API error: {response['error']}")
if "choices" not in response or not response["choices"]:
raise Exception("No response from LLM API")
# Extract and parse response
content = response["choices"][0]["message"]["content"]
try:
# Try to extract JSON from markdown code block if present
if "```json" in content:
json_str = content.split("```json")[1].split("```")[0].strip()
output = json.loads(json_str)
else:
output = json.loads(content)
except json.JSONDecodeError:
# If response is not JSON, wrap it in a result field
output = {"result": content}
return {
"success": True,
"output": output
}
except Exception as api_error:
raise Exception(f"LLM API call failed: {str(api_error)}")
except Exception as e:
return {
"success": False,
"error": str(e)
}
def _build_messages(self,
instruction: str,
input_data: Dict[str, Any],
context: Optional[Dict[str, Any]] = None) -> List[Dict[str, str]]:
"""Build the message list for the LLM conversation."""
messages = [
{
"role": "system",
"content": """You are an AI agent tasked with executing specific steps in a larger task.
Follow these guidelines:
1. Focus only on the current step's instruction
2. Use the provided input data and context
3. Return results in a clear, structured format
4. If you need additional information, specify it in your response
5. If you encounter errors, provide detailed error messages"""
}
]
# Add conversation history
messages.extend(self.conversation_history)
# Add context if provided
if context:
messages.append({
"role": "system",
"content": f"Context for current step: {context}"
})
# Add current instruction and input
messages.append({
"role": "user",
"content": f"""
Instruction: {instruction}
Input Data: {input_data}
Please execute this step and provide your response in the following format:
- Result: (main output)
- Additional Info: (any additional information or explanations)
- Required Next: (any information required for next steps)
"""
})
return messages
def _parse_response(self, response: Dict[str, Any]) -> Dict[str, Any]:
"""Parse the LLM response into a structured format."""
content = response["content"]
# Parse the response into sections
sections = {
"result": "",
"additional_info": "",
"required_next": ""
}
current_section = None
for line in content.split('\n'):
line = line.strip()
if line.startswith('- Result:'):
current_section = "result"
sections[current_section] = line.replace('- Result:', '').strip()
elif line.startswith('- Additional Info:'):
current_section = "additional_info"
sections[current_section] = line.replace('- Additional Info:', '').strip()
elif line.startswith('- Required Next:'):
current_section = "required_next"
sections[current_section] = line.replace('- Required Next:', '').strip()
elif current_section and line:
sections[current_section] += f"\n{line}"
return sections
def _update_conversation_history(self, user_message: Dict[str, str],
assistant_response: Dict[str, Any]):
"""Update the conversation history with the latest interaction."""
self.conversation_history.append(user_message)
self.conversation_history.append({
"role": "assistant",
"content": f"""
Result: {assistant_response['result']}
Additional Info: {assistant_response['additional_info']}
Required Next: {assistant_response['required_next']}
"""
})
# Keep only the last 10 messages to prevent context window from growing too large
if len(self.conversation_history) > 10:
self.conversation_history = self.conversation_history[-10:]
def clear_history(self):
"""Clear the conversation history."""
self.conversation_history = []
async def close(self):
"""Clean up resources."""
await self.llm_client.close()

27
pyproject.toml Normal file
View File

@ -0,0 +1,27 @@
[project]
name = "agent-task-executor"
version = "0.1.0"
description = "An LLM-based agent task execution framework"
authors = [
{name = "Your Name", email = "your.email@example.com"},
]
dependencies = [
"python-dateutil>=2.8.2",
"typing-extensions>=4.4.0",
"openai>=1.0.0",
"python-dotenv>=1.0.0",
"tenacity>=8.2.0",
"httpx>=0.24.0",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.rye]
managed = true
dev-dependencies = [
"pytest>=7.3.1",
"black>=23.7.0",
"isort>=5.12.0",
]

12
requirements.txt Normal file
View File

@ -0,0 +1,12 @@
python-dateutil>=2.8.2
typing-extensions>=4.4.0
openai>=1.0.0
python-dotenv>=1.0.0
tenacity>=8.2.0
PyYAML>=6.0.2
cryptography>=44.0.0
pywin32>=308; platform_system == "Windows"
# Test dependencies
pytest>=8.3.4
pytest-asyncio>=0.25.2

110
sample_task.py Normal file
View File

@ -0,0 +1,110 @@
from task_executor import TaskExecutor, TaskStatus
from typing import Dict, Any
import time
class SampleTaskExecutor(TaskExecutor):
def __init__(self):
super().__init__()
self.task_steps = [
{
"id": "input_validation",
"name": "Validate Input",
"required_info": ["input_data"]
},
{
"id": "resource_check",
"name": "Check Resources",
"required_info": ["system_resources"]
},
{
"id": "task_processing",
"name": "Process Task",
"required_info": ["processed_data"]
},
{
"id": "result_validation",
"name": "Validate Results",
"required_info": ["validation_criteria"]
}
]
def validate_input(self, input_data: Dict[str, Any]) -> bool:
"""Validate specific input requirements for the sample task."""
required_fields = ["input_data", "validation_criteria"]
return all(field in input_data for field in required_fields)
def execute_step(self, step_id: str, step_data: Dict[str, Any]) -> bool:
"""Execute a specific step of the sample task."""
try:
# Simulate step execution
time.sleep(1) # Simulate work being done
step_state = {
"step_id": step_id,
"status": TaskStatus.IN_PROGRESS,
"required_info": step_data.get("required_info", []),
"available_info": [],
"missing_info": [],
"resources_used": {
"time": time.time() - self.start_time,
"memory": "100MB" # Simulated memory usage
}
}
# Update execution state
self.current_step = {
"id": step_id,
"name": step_data["name"],
"status": TaskStatus.COMPLETED.value,
"progress": 100
}
return True
except Exception as e:
self.logger.error(f"Step {step_id} failed: {str(e)}")
return False
def execute(self, task_input: Dict[str, Any]) -> Dict[str, Any]:
"""Execute the sample task with all its steps."""
try:
if not self.validate_input(task_input):
raise ValueError("Invalid task input")
self.logger.info(f"Starting sample task execution: {self.task_id}")
# Execute each step in sequence
for step in self.task_steps:
if not self.execute_step(step["id"], step):
raise Exception(f"Step {step['id']} failed")
if len(self.execution_path) % self.CHECKPOINT_INTERVAL == 0:
self.create_checkpoint()
return self.get_status_update()
except Exception as e:
self.logger.error(f"Sample task failed: {str(e)}")
return self.get_status_update()
def main():
# Example usage
executor = SampleTaskExecutor()
# Sample task input
task_input = {
"input_data": "sample data",
"validation_criteria": ["criteria1", "criteria2"],
"system_resources": {
"memory": "1GB",
"cpu": "2 cores"
}
}
# Execute the task
result = executor.execute(task_input)
print(f"Task execution completed with status: {result['current_step']['status']}")
print(f"Execution path: {result['execution_path']}")
if __name__ == "__main__":
main()

1
scripts/__init__.py Normal file
View File

@ -0,0 +1 @@
"""Scripts package for utility scripts."""

10
scripts/setup_config.py Normal file
View File

@ -0,0 +1,10 @@
from config.config_loader import save_api_key
def main():
"""Set up the configuration with API key."""
api_key = "sk-652f44592f6d4d19bad104c54f5fbf4a"
save_api_key(api_key)
print("API key has been securely saved.")
if __name__ == "__main__":
main()

190
task_executor.py Normal file
View File

@ -0,0 +1,190 @@
import time
import uuid
from typing import List, Dict, Any
from dataclasses import dataclass
from enum import Enum
import json
import logging
import asyncio
from llm_executor import LLMExecutor
class TaskStatus(Enum):
PENDING = "pending"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"
FAILED = "failed"
@dataclass
class StepState:
step_id: str
status: TaskStatus
required_info: List[str]
available_info: List[str]
missing_info: List[str]
resources_used: Dict[str, Any]
class TaskExecutor:
MAX_RETRIES = 3
TIMEOUT = 300 # seconds
CHECKPOINT_INTERVAL = 5 # steps
def __init__(self, llm_model: str = None):
"""Initialize TaskExecutor."""
self.task_id = str(uuid.uuid4())
self.start_time = time.time()
self.checkpoints = []
self.execution_path = []
self.current_step = None
self.retry_count = 0
self.llm_executor = LLMExecutor(model=llm_model)
self.task_input = None
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
self.logger = logging.getLogger(f"TaskExecutor-{self.task_id}")
def get_status_update(self) -> dict:
"""Generate a status update for the current execution state."""
return {
"task_id": self.task_id,
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"),
"current_step": self.current_step,
"checkpoints": self.checkpoints,
"resources": {
"used": {
"time": time.time() - self.start_time,
"memory": "N/A" # To be implemented
},
"available": {
"time": self.TIMEOUT - (time.time() - self.start_time),
"memory": "N/A" # To be implemented
}
},
"execution_path": self.execution_path,
"status": TaskStatus.COMPLETED.value if self.current_step and self.current_step.get("status") == TaskStatus.COMPLETED.value else TaskStatus.IN_PROGRESS.value
}
async def execute_step(self, step_id: str, step_data: Dict[str, Any]) -> bool:
"""Execute a single step of the task using LLM."""
try:
self.current_step = {
"id": step_id,
"name": step_data.get("name", "Unknown"),
"status": TaskStatus.IN_PROGRESS.value,
"progress": 0
}
# Check if execution should continue
if time.time() - self.start_time > self.TIMEOUT:
raise TimeoutError("Task execution timeout")
# Execute step using LLM
step_result = await self.llm_executor.execute_step(
step_instruction=step_data.get("instruction", ""),
step_input=step_data.get("input", {}),
context=step_data.get("context", {})
)
if not step_result["success"]:
raise Exception(f"Step failed: {step_result['error']}")
# Update step status
self.execution_path.append({
"step_id": step_id,
"result": step_result["output"]
})
self.current_step["status"] = TaskStatus.COMPLETED.value
self.current_step["progress"] = 100
# Create checkpoint if needed
if len(self.execution_path) % self.CHECKPOINT_INTERVAL == 0:
self.create_checkpoint()
return True
except Exception as e:
self.current_step["status"] = TaskStatus.FAILED.value
return self.handle_error(e)
def validate_input(self, input_data: Dict[str, Any]) -> bool:
"""Validate the input data for the task."""
return True
def create_checkpoint(self):
"""Create a checkpoint of the current execution state."""
checkpoint = {
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"),
"task_id": self.task_id,
"current_step": self.current_step,
"execution_path": self.execution_path.copy(),
"resources": {
"used": {
"time": time.time() - self.start_time,
"memory": "N/A" # To be implemented
}
}
}
self.checkpoints.append(checkpoint)
self.logger.info(f"Created checkpoint: {json.dumps(checkpoint, indent=2)}")
def rollback_to_checkpoint(self, checkpoint_index: int):
"""Rollback the execution to a specific checkpoint."""
if 0 <= checkpoint_index < len(self.checkpoints):
checkpoint = self.checkpoints[checkpoint_index]
# Implement state restoration logic
self.logger.info(f"Rolling back to checkpoint: {checkpoint['timestamp']}")
return True
return False
def get_next_actions(self) -> List[str]:
"""Determine the next possible actions based on current state."""
actions = []
if self.current_step and self.current_step["status"] == TaskStatus.FAILED.value:
actions.extend(["retry", "rollback", "abort"])
elif self.current_step and self.current_step["status"] == TaskStatus.COMPLETED.value:
actions.extend(["continue", "checkpoint"])
return actions
def handle_error(self, error: Exception):
"""Handle execution errors."""
self.logger.error(f"Error occurred: {str(error)}")
self.retry_count += 1
if self.retry_count >= self.MAX_RETRIES:
self.logger.error("Max retries reached. Terminating execution.")
return False
# Implement error recovery logic
return True
async def execute(self, task_input: Dict[str, Any]) -> Dict[str, Any]:
"""Execute the complete task."""
try:
# Store task input
self.task_input = task_input
if not self.validate_input(task_input):
raise ValueError("Invalid task input")
self.logger.info(f"Starting task execution: {self.task_id}")
# Execute each step in sequence
if hasattr(self, 'task_steps'):
for step in self.task_steps:
if not await self.execute_step(step["id"], step):
raise Exception(f"Step {step['id']} failed")
if len(self.execution_path) % self.CHECKPOINT_INTERVAL == 0:
self.create_checkpoint()
else:
raise ValueError("No task steps defined")
return self.get_status_update()
except Exception as e:
self.logger.error(f"Task failed: {str(e)}")
return self.get_status_update()

1
tasksamples/__init__.py Normal file
View File

@ -0,0 +1 @@

View File

@ -0,0 +1,200 @@
from task_executor import TaskExecutor, TaskStatus
from typing import Dict, Any
import time
import json
import re
class TextAnalysisExecutor(TaskExecutor):
def __init__(self):
super().__init__(llm_model="deepseek-chat")
self.task_steps = [
{
"id": "input_validation",
"name": "Validate Input",
"required_info": ["text"],
"instruction": "Validate if the input text is not empty and contains valid Chinese characters. Check for proper UTF-8 encoding."
},
{
"id": "text_preprocessing",
"name": "Preprocess Text",
"required_info": ["text"],
"instruction": "Clean and preprocess the text by: 1) Normalizing Chinese punctuation, 2) Removing unnecessary whitespace while preserving sentence structure, 3) Standardizing traditional/simplified characters if needed."
},
{
"id": "generate_summary",
"name": "Generate Summary",
"required_info": ["preprocessed_text"],
"instruction": "Generate a concise summary in Chinese that captures the main points. Maintain the original language style and terminology."
},
{
"id": "extract_keywords",
"name": "Extract Keywords",
"required_info": ["preprocessed_text"],
"instruction": "Extract the most important Chinese keywords and key phrases from the text. Include both technical terms and contextual phrases."
},
{
"id": "final_analysis",
"name": "Final Analysis",
"required_info": ["summary", "keywords"],
"instruction": "Combine the summary and keywords into a comprehensive analysis report in Chinese. Structure the report with clear sections for summary, key points, and insights."
}
]
def validate_input(self, input_data: Dict[str, Any]) -> bool:
"""Validate specific input requirements for the text analysis task."""
if "text" not in input_data:
return False
text = input_data.get("text", "")
return isinstance(text, str) and len(text.strip()) > 0
async def execute_step(self, step_id: str, step_data: Dict[str, Any]) -> bool:
"""Execute a specific step of the text analysis task."""
try:
self.current_step = {
"id": step_id,
"name": step_data["name"],
"status": TaskStatus.IN_PROGRESS.value,
"progress": 0
}
# Get step instruction and input
instruction = step_data.get("instruction", "")
step_input = {}
# Prepare step-specific input
if step_id == "input_validation":
text = self.task_input.get("text", "")
# Validate Chinese text encoding
try:
text.encode('utf-8').decode('utf-8')
except UnicodeError:
return {"error": "Invalid text encoding. Please ensure the text is properly encoded in UTF-8."}
step_input = {"text": text}
elif step_id == "text_preprocessing":
text = self.task_input.get("text", "")
# Basic Chinese text preprocessing
# Normalize whitespace while preserving Chinese text structure
text = re.sub(r'\s+', ' ', text).strip()
# Normalize Chinese punctuation (simple example)
text = text.replace('', ',').replace('', '.').replace('', ':')
step_input = {"text": text}
elif step_id == "generate_summary":
# Get the preprocessed text from previous step
prev_result = next(
(step["result"] for step in self.execution_path if step["step_id"] == "text_preprocessing"),
{}
)
step_input = {"text": prev_result.get("preprocessed_text", self.task_input.get("text", ""))}
elif step_id == "extract_keywords":
# Get the preprocessed text from previous step
prev_result = next(
(step["result"] for step in self.execution_path if step["step_id"] == "text_preprocessing"),
{}
)
step_input = {"text": prev_result.get("preprocessed_text", self.task_input.get("text", ""))}
elif step_id == "final_analysis":
# Get summary and keywords from previous steps
summary_result = next(
(step["result"] for step in self.execution_path if step["step_id"] == "generate_summary"),
{}
)
keywords_result = next(
(step["result"] for step in self.execution_path if step["step_id"] == "extract_keywords"),
{}
)
step_input = {
"summary": summary_result.get("summary", ""),
"keywords": keywords_result.get("keywords", [])
}
# Execute step using LLM
step_result = await self.llm_executor.execute_step(
step_instruction=instruction,
step_input=step_input
)
if not step_result.get("success", False):
raise Exception(f"Step failed: {step_result.get('error', 'Unknown error')}")
# Update execution path
self.execution_path.append({
"step_id": step_id,
"result": step_result.get("output", {})
})
# Update step status
self.current_step["status"] = TaskStatus.COMPLETED.value
self.current_step["progress"] = 100
# Create checkpoint if needed
if len(self.execution_path) % self.CHECKPOINT_INTERVAL == 0:
self.create_checkpoint()
return True
except Exception as e:
self.logger.error(f"Step {step_id} failed: {str(e)}")
self.current_step["status"] = TaskStatus.FAILED.value
return False
def main():
# Set console encoding to UTF-8
import sys, io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
# Create the executor
executor = TextAnalysisExecutor()
# Sample Chinese text for analysis
sample_text = """
ChatGPT DevinAI 编程的四个发展阶段与范式转变 Koji我们再聊一聊 AI 编程编程领域今年取得了非常令人兴奋的进展雨森一直有很强的框架归纳和总结能力前不久你跟我分享过你提炼出来的 AI 编程发展四段论要不要在播客里和大家分享一下 雨森这其实是和很多朋友一起探讨得出的结果是大家智慧的结晶AI 编程从 ChatGPT 出现到现在也就两年出头的时间但已经经历了四个阶段 第一个阶段是让 AI 直接写代码典型代表是早期的 ChatGPTClaude我们给它一个需求比如帮我写个贪吃蛇它就给出一段代码在这个过程中它既不知道我为什么要写贪吃蛇也不知道代码运行情况如何可能要我去本地编译运行后发现报错再把错误告诉它它才能给出调试后的结果这时的 AI 完全就像一个只能通过邮件交流的笔友是简单的问答模式 第二阶段是以 GitHub Copilot 为代表AI 开始拥有上下文它可以把整个组织的代码库作为 context这样 AI 就获得了大量新的背景信息但这时用户还是需要手动把代码贴到 IDE 里面进行调试我觉得这是 2.0 阶段就是我们让 AI 拥有了 codebase 作为上下文 2024 年一个非常大的进步是以 Cursor 为代表的编程 Copilot 的出现它的核心理念是预测用户未来要写什么代码根据你的代码库以及刚才写的代码它预测你接下来要写什么代码创建什么文件做什么操作这里面对于生成代码的质量和数量以及文件的创建和修改都有很大提升后来 Windsurf 还加入了对命令行操作的自动化这样 AI 就能很好地使用我的电脑原来的 AI 是在一张纸上写代码我把代码抄走运行现在 AI 可以在我的电脑上创建文件执行命令行操作进入到我为你写的阶段 当我们觉得这已经很令人兴奋时Devin 的出现带来了几个重要突破首先它可以异步工作CursorWindsurf 这些工具虽然一步操作做的事情比较多但仍然需要持续的注意力我说一步它做一步 Devin 可以持续工作把用户的注意力释放出来这是因为它多了一个 Planner可以规划任务 其次它可以通过虚拟机执行更多操作做更多调试工作比如你写个网站它可以自己用虚拟机去访问这个网站检查前端后端的业务逻辑是否正确并且可以随时打断和调整大家用 Cursor 或者 ChatGPT 都知道你无法在它输出的中间做调整必须等它输出完后才能修改 Devin 就像真人一样你可以在它完成任务时给出新指令它会把这个结合到已有的 Planner 里调整计划这就从为你写进化到了为你做 总结一下这四个阶段第一阶段是让 AI 写代码代表是 ChatGPT第二阶段是 AI 开放代码库代表是 GitHub Copilot第三阶段是 AI 可以自动写代码并执行代表是 Cursor Windsurf第四阶段是 AI 虚拟员工Devin 开创了一个很好的先例
"""
# Prepare input
task_input = {
"text": sample_text
}
# Execute the task
import asyncio
result = asyncio.run(executor.execute(task_input))
# Print results with proper formatting
print("\n 文本分析结果:")
print("=" * 50)
# Print each step's result with proper formatting
for step in result.get("execution_path", []):
step_id = step["step_id"]
result_data = step["result"]
print(f"\n {step_id.upper()}:")
print("-" * 30)
if step_id == "input_validation":
print(" 输入验证完成")
elif step_id == "text_preprocessing":
print(" 文本预处理完成")
if "preprocessed_text" in result_data:
print("\n处理后的文本:")
print(result_data["preprocessed_text"])
elif step_id == "generate_summary":
print("\n 文本摘要:")
if "summary" in result_data:
print(result_data["summary"])
elif step_id == "extract_keywords":
print("\n 关键词:")
if "keywords" in result_data:
keywords = result_data["keywords"]
if isinstance(keywords, list):
print("".join(keywords))
else:
print(keywords)
elif step_id == "final_analysis":
print("\n 最终分析:")
if "analysis" in result_data:
print(result_data["analysis"])
if __name__ == "__main__":
main()

1
tests/__init__.py Normal file
View File

@ -0,0 +1 @@

136
tests/test_config.py Normal file
View File

@ -0,0 +1,136 @@
import os
import json
import pytest
from pathlib import Path
from config.secure_config import SecureConfig
from config.config_loader import load_config, save_api_key
@pytest.fixture
def temp_dir(tmp_path):
"""Create a temporary directory for test files."""
return tmp_path
@pytest.fixture
def secure_config(temp_dir):
"""Create a SecureConfig instance with a temporary key file."""
return SecureConfig(str(temp_dir / "test_secure.key"))
@pytest.fixture
def test_config_dir(temp_dir, monkeypatch):
"""Set up a test configuration directory."""
# Create config directory
config_dir = temp_dir / "config"
config_dir.mkdir(parents=True, exist_ok=True)
# Create test YAML config
yaml_content = """
llm:
provider: openai
model: gpt-4
max_tokens: 2000
temperature: 0.7
timeout: 30
api:
api_base: null
openai:
api_base: https://api.openai.com/v1
"""
config_path = config_dir / "llm_config.yaml"
config_path.write_text(yaml_content)
# Set up environment for config loader
monkeypatch.setenv("PYTHONPATH", str(temp_dir))
monkeypatch.chdir(temp_dir)
return temp_dir
class TestSecureConfig:
def test_key_generation(self, secure_config, temp_dir):
"""Test that encryption key is generated correctly."""
key_path = temp_dir / "test_secure.key"
assert key_path.exists()
if os.name == 'nt': # Windows
import win32security
security = win32security.GetFileSecurity(
str(key_path),
win32security.DACL_SECURITY_INFORMATION
)
dacl = security.GetSecurityDescriptorDacl()
assert dacl is not None
# Verify only one ACE exists
assert dacl.GetAceCount() == 1
else: # Unix
assert key_path.stat().st_mode & 0o777 == 0o600 # Check permissions
def test_encryption_decryption(self, secure_config):
"""Test encryption and decryption of values."""
test_value = "test-api-key-123"
encrypted = secure_config.encrypt_value(test_value)
decrypted = secure_config.decrypt_value(encrypted)
assert decrypted == test_value
def test_null_handling(self, secure_config):
"""Test handling of null/None values."""
assert secure_config.encrypt_value(None) is None
assert secure_config.decrypt_value(None) is None
def test_invalid_decrypt(self, secure_config):
"""Test decryption of invalid data."""
assert secure_config.decrypt_value("invalid-data") is None
class TestConfigLoader:
def test_save_load_api_key(self, test_config_dir, monkeypatch):
"""Test saving and loading API key."""
test_api_key = "test-api-key-456"
save_api_key(test_api_key)
# Verify secure.json was created with proper permissions
secure_json = test_config_dir / "config" / "secure.json"
assert secure_json.exists()
if os.name == 'nt': # Windows
import win32security
security = win32security.GetFileSecurity(
str(secure_json),
win32security.DACL_SECURITY_INFORMATION
)
dacl = security.GetSecurityDescriptorDacl()
assert dacl is not None
# Verify only one ACE exists
assert dacl.GetAceCount() == 1
else: # Unix
assert secure_json.stat().st_mode & 0o777 == 0o600
# Load config and verify API key
config = load_config()
assert config["llm"]["api_key"] == test_api_key
def test_config_priority(self, test_config_dir, monkeypatch):
"""Test configuration priority (env vars > secure storage > yaml)."""
# Set up different values for each level
env_api_key = "env-api-key"
secure_api_key = "secure-api-key"
# Save to secure storage
save_api_key(secure_api_key)
# Test without env var
config = load_config()
assert config["llm"]["api_key"] == secure_api_key
# Test with env var
monkeypatch.setenv("LLM_API_KEY", env_api_key)
config = load_config()
assert config["llm"]["api_key"] == env_api_key
def test_api_base_config(self, test_config_dir, monkeypatch):
"""Test API base URL configuration."""
# Test default OpenAI base URL
config = load_config()
assert config["llm"]["api_base"] == "https://api.openai.com/v1"
# Test override with env var
custom_base = "https://custom-api.example.com"
monkeypatch.setenv("LLM_API_BASE", custom_base)
config = load_config()
assert config["llm"]["api_base"] == custom_base

49
tests/test_llm.py Normal file
View File

@ -0,0 +1,49 @@
"""Test LLM API call."""
import asyncio
from config.config_loader import load_config
import httpx
async def test_llm_call():
"""Test LLM API call with a simple prompt."""
config = load_config()
llm_config = config["llm"]
headers = {
"Authorization": f"Bearer {llm_config['api_key']}",
"Content-Type": "application/json"
}
# Deepseek chat completion payload
payload = {
"model": llm_config["model"],
"messages": [
{"role": "user", "content": "Say hello and introduce yourself in one sentence."}
],
"temperature": llm_config["temperature"],
"max_tokens": llm_config["max_tokens"]
}
print("Making API call to Deepseek...")
print(f"Base URL: {llm_config['api_base']}")
print(f"Model: {llm_config['model']}")
async with httpx.AsyncClient() as client:
try:
response = await client.post(
f"{llm_config['api_base']}/v1/chat/completions",
headers=headers,
json=payload,
timeout=llm_config["timeout"]
)
response.raise_for_status()
result = response.json()
print("\nResponse:")
print(result["choices"][0]["message"]["content"])
except httpx.HTTPStatusError as e:
print(f"HTTP error occurred: {e.response.text}")
except Exception as e:
print(f"An error occurred: {str(e)}")
if __name__ == "__main__":
asyncio.run(test_llm_call())

323
uv.lock generated Normal file
View File

@ -0,0 +1,323 @@
version = 1
requires-python = ">=3.11"
[[package]]
name = "agent-task-executor"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "httpx" },
{ name = "openai" },
{ name = "python-dateutil" },
{ name = "python-dotenv" },
{ name = "tenacity" },
{ name = "typing-extensions" },
]
[package.metadata]
requires-dist = [
{ name = "httpx", specifier = ">=0.24.0" },
{ name = "openai", specifier = ">=1.0.0" },
{ name = "python-dateutil", specifier = ">=2.8.2" },
{ name = "python-dotenv", specifier = ">=1.0.0" },
{ name = "tenacity", specifier = ">=8.2.0" },
{ name = "typing-extensions", specifier = ">=4.4.0" },
]
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
]
[[package]]
name = "anyio"
version = "4.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 },
]
[[package]]
name = "certifi"
version = "2024.12.14"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0f/bd/1d41ee578ce09523c81a15426705dd20969f5abf006d1afe8aeff0dd776a/certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db", size = 166010 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a5/32/8f6669fc4798494966bf446c8c4a162e0b5d893dff088afddf76414f70e1/certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", size = 164927 },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
]
[[package]]
name = "distro"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 },
]
[[package]]
name = "h11"
version = "0.14.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
]
[[package]]
name = "httpcore"
version = "1.0.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
]
[[package]]
name = "jiter"
version = "0.8.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f8/70/90bc7bd3932e651486861df5c8ffea4ca7c77d28e8532ddefe2abc561a53/jiter-0.8.2.tar.gz", hash = "sha256:cd73d3e740666d0e639f678adb176fad25c1bcbdae88d8d7b857e1783bb4212d", size = 163007 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b0/c1a7caa7f9dc5f1f6cfa08722867790fe2d3645d6e7170ca280e6e52d163/jiter-0.8.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2dd61c5afc88a4fda7d8b2cf03ae5947c6ac7516d32b7a15bf4b49569a5c076b", size = 303666 },
{ url = "https://files.pythonhosted.org/packages/f5/97/0468bc9eeae43079aaa5feb9267964e496bf13133d469cfdc135498f8dd0/jiter-0.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a6c710d657c8d1d2adbbb5c0b0c6bfcec28fd35bd6b5f016395f9ac43e878a15", size = 311934 },
{ url = "https://files.pythonhosted.org/packages/e5/69/64058e18263d9a5f1e10f90c436853616d5f047d997c37c7b2df11b085ec/jiter-0.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9584de0cd306072635fe4b89742bf26feae858a0683b399ad0c2509011b9dc0", size = 335506 },
{ url = "https://files.pythonhosted.org/packages/9d/14/b747f9a77b8c0542141d77ca1e2a7523e854754af2c339ac89a8b66527d6/jiter-0.8.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5a90a923338531b7970abb063cfc087eebae6ef8ec8139762007188f6bc69a9f", size = 355849 },
{ url = "https://files.pythonhosted.org/packages/53/e2/98a08161db7cc9d0e39bc385415890928ff09709034982f48eccfca40733/jiter-0.8.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21974d246ed0181558087cd9f76e84e8321091ebfb3a93d4c341479a736f099", size = 381700 },
{ url = "https://files.pythonhosted.org/packages/7a/38/1674672954d35bce3b1c9af99d5849f9256ac8f5b672e020ac7821581206/jiter-0.8.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32475a42b2ea7b344069dc1e81445cfc00b9d0e3ca837f0523072432332e9f74", size = 389710 },
{ url = "https://files.pythonhosted.org/packages/f8/9b/92f9da9a9e107d019bcf883cd9125fa1690079f323f5a9d5c6986eeec3c0/jiter-0.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b9931fd36ee513c26b5bf08c940b0ac875de175341cbdd4fa3be109f0492586", size = 345553 },
{ url = "https://files.pythonhosted.org/packages/44/a6/6d030003394e9659cd0d7136bbeabd82e869849ceccddc34d40abbbbb269/jiter-0.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce0820f4a3a59ddced7fce696d86a096d5cc48d32a4183483a17671a61edfddc", size = 376388 },
{ url = "https://files.pythonhosted.org/packages/ad/8d/87b09e648e4aca5f9af89e3ab3cfb93db2d1e633b2f2931ede8dabd9b19a/jiter-0.8.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8ffc86ae5e3e6a93765d49d1ab47b6075a9c978a2b3b80f0f32628f39caa0c88", size = 511226 },
{ url = "https://files.pythonhosted.org/packages/77/95/8008ebe4cdc82eac1c97864a8042ca7e383ed67e0ec17bfd03797045c727/jiter-0.8.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5127dc1abd809431172bc3fbe8168d6b90556a30bb10acd5ded41c3cfd6f43b6", size = 504134 },
{ url = "https://files.pythonhosted.org/packages/26/0d/3056a74de13e8b2562e4d526de6dac2f65d91ace63a8234deb9284a1d24d/jiter-0.8.2-cp311-cp311-win32.whl", hash = "sha256:66227a2c7b575720c1871c8800d3a0122bb8ee94edb43a5685aa9aceb2782d44", size = 203103 },
{ url = "https://files.pythonhosted.org/packages/4e/1e/7f96b798f356e531ffc0f53dd2f37185fac60fae4d6c612bbbd4639b90aa/jiter-0.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:cde031d8413842a1e7501e9129b8e676e62a657f8ec8166e18a70d94d4682855", size = 206717 },
{ url = "https://files.pythonhosted.org/packages/a1/17/c8747af8ea4e045f57d6cfd6fc180752cab9bc3de0e8a0c9ca4e8af333b1/jiter-0.8.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:e6ec2be506e7d6f9527dae9ff4b7f54e68ea44a0ef6b098256ddf895218a2f8f", size = 302027 },
{ url = "https://files.pythonhosted.org/packages/3c/c1/6da849640cd35a41e91085723b76acc818d4b7d92b0b6e5111736ce1dd10/jiter-0.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76e324da7b5da060287c54f2fabd3db5f76468006c811831f051942bf68c9d44", size = 310326 },
{ url = "https://files.pythonhosted.org/packages/06/99/a2bf660d8ccffee9ad7ed46b4f860d2108a148d0ea36043fd16f4dc37e94/jiter-0.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:180a8aea058f7535d1c84183c0362c710f4750bef66630c05f40c93c2b152a0f", size = 334242 },
{ url = "https://files.pythonhosted.org/packages/a7/5f/cea1c17864828731f11427b9d1ab7f24764dbd9aaf4648a7f851164d2718/jiter-0.8.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025337859077b41548bdcbabe38698bcd93cfe10b06ff66617a48ff92c9aec60", size = 356654 },
{ url = "https://files.pythonhosted.org/packages/e9/13/62774b7e5e7f5d5043efe1d0f94ead66e6d0f894ae010adb56b3f788de71/jiter-0.8.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecff0dc14f409599bbcafa7e470c00b80f17abc14d1405d38ab02e4b42e55b57", size = 379967 },
{ url = "https://files.pythonhosted.org/packages/ec/fb/096b34c553bb0bd3f2289d5013dcad6074948b8d55212aa13a10d44c5326/jiter-0.8.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffd9fee7d0775ebaba131f7ca2e2d83839a62ad65e8e02fe2bd8fc975cedeb9e", size = 389252 },
{ url = "https://files.pythonhosted.org/packages/17/61/beea645c0bf398ced8b199e377b61eb999d8e46e053bb285c91c3d3eaab0/jiter-0.8.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14601dcac4889e0a1c75ccf6a0e4baf70dbc75041e51bcf8d0e9274519df6887", size = 345490 },
{ url = "https://files.pythonhosted.org/packages/d5/df/834aa17ad5dcc3cf0118821da0a0cf1589ea7db9832589278553640366bc/jiter-0.8.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92249669925bc1c54fcd2ec73f70f2c1d6a817928480ee1c65af5f6b81cdf12d", size = 376991 },
{ url = "https://files.pythonhosted.org/packages/67/80/87d140399d382fb4ea5b3d56e7ecaa4efdca17cd7411ff904c1517855314/jiter-0.8.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e725edd0929fa79f8349ab4ec7f81c714df51dc4e991539a578e5018fa4a7152", size = 510822 },
{ url = "https://files.pythonhosted.org/packages/5c/37/3394bb47bac1ad2cb0465601f86828a0518d07828a650722e55268cdb7e6/jiter-0.8.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bf55846c7b7a680eebaf9c3c48d630e1bf51bdf76c68a5f654b8524335b0ad29", size = 503730 },
{ url = "https://files.pythonhosted.org/packages/f9/e2/253fc1fa59103bb4e3aa0665d6ceb1818df1cd7bf3eb492c4dad229b1cd4/jiter-0.8.2-cp312-cp312-win32.whl", hash = "sha256:7efe4853ecd3d6110301665a5178b9856be7e2a9485f49d91aa4d737ad2ae49e", size = 203375 },
{ url = "https://files.pythonhosted.org/packages/41/69/6d4bbe66b3b3b4507e47aa1dd5d075919ad242b4b1115b3f80eecd443687/jiter-0.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:83c0efd80b29695058d0fd2fa8a556490dbce9804eac3e281f373bbc99045f6c", size = 204740 },
{ url = "https://files.pythonhosted.org/packages/6c/b0/bfa1f6f2c956b948802ef5a021281978bf53b7a6ca54bb126fd88a5d014e/jiter-0.8.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ca1f08b8e43dc3bd0594c992fb1fd2f7ce87f7bf0d44358198d6da8034afdf84", size = 301190 },
{ url = "https://files.pythonhosted.org/packages/a4/8f/396ddb4e292b5ea57e45ade5dc48229556b9044bad29a3b4b2dddeaedd52/jiter-0.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5672a86d55416ccd214c778efccf3266b84f87b89063b582167d803246354be4", size = 309334 },
{ url = "https://files.pythonhosted.org/packages/7f/68/805978f2f446fa6362ba0cc2e4489b945695940656edd844e110a61c98f8/jiter-0.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58dc9bc9767a1101f4e5e22db1b652161a225874d66f0e5cb8e2c7d1c438b587", size = 333918 },
{ url = "https://files.pythonhosted.org/packages/b3/99/0f71f7be667c33403fa9706e5b50583ae5106d96fab997fa7e2f38ee8347/jiter-0.8.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:37b2998606d6dadbb5ccda959a33d6a5e853252d921fec1792fc902351bb4e2c", size = 356057 },
{ url = "https://files.pythonhosted.org/packages/8d/50/a82796e421a22b699ee4d2ce527e5bcb29471a2351cbdc931819d941a167/jiter-0.8.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ab9a87f3784eb0e098f84a32670cfe4a79cb6512fd8f42ae3d0709f06405d18", size = 379790 },
{ url = "https://files.pythonhosted.org/packages/3c/31/10fb012b00f6d83342ca9e2c9618869ab449f1aa78c8f1b2193a6b49647c/jiter-0.8.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:79aec8172b9e3c6d05fd4b219d5de1ac616bd8da934107325a6c0d0e866a21b6", size = 388285 },
{ url = "https://files.pythonhosted.org/packages/c8/81/f15ebf7de57be488aa22944bf4274962aca8092e4f7817f92ffa50d3ee46/jiter-0.8.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:711e408732d4e9a0208008e5892c2966b485c783cd2d9a681f3eb147cf36c7ef", size = 344764 },
{ url = "https://files.pythonhosted.org/packages/b3/e8/0cae550d72b48829ba653eb348cdc25f3f06f8a62363723702ec18e7be9c/jiter-0.8.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:653cf462db4e8c41995e33d865965e79641ef45369d8a11f54cd30888b7e6ff1", size = 376620 },
{ url = "https://files.pythonhosted.org/packages/b8/50/e5478ff9d82534a944c03b63bc217c5f37019d4a34d288db0f079b13c10b/jiter-0.8.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:9c63eaef32b7bebac8ebebf4dabebdbc6769a09c127294db6babee38e9f405b9", size = 510402 },
{ url = "https://files.pythonhosted.org/packages/8e/1e/3de48bbebbc8f7025bd454cedc8c62378c0e32dd483dece5f4a814a5cb55/jiter-0.8.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:eb21aaa9a200d0a80dacc7a81038d2e476ffe473ffdd9c91eb745d623561de05", size = 503018 },
{ url = "https://files.pythonhosted.org/packages/d5/cd/d5a5501d72a11fe3e5fd65c78c884e5164eefe80077680533919be22d3a3/jiter-0.8.2-cp313-cp313-win32.whl", hash = "sha256:789361ed945d8d42850f919342a8665d2dc79e7e44ca1c97cc786966a21f627a", size = 203190 },
{ url = "https://files.pythonhosted.org/packages/51/bf/e5ca301245ba951447e3ad677a02a64a8845b185de2603dabd83e1e4b9c6/jiter-0.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:ab7f43235d71e03b941c1630f4b6e3055d46b6cb8728a17663eaac9d8e83a865", size = 203551 },
{ url = "https://files.pythonhosted.org/packages/2f/3c/71a491952c37b87d127790dd7a0b1ebea0514c6b6ad30085b16bbe00aee6/jiter-0.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b426f72cd77da3fec300ed3bc990895e2dd6b49e3bfe6c438592a3ba660e41ca", size = 308347 },
{ url = "https://files.pythonhosted.org/packages/a0/4c/c02408042e6a7605ec063daed138e07b982fdb98467deaaf1c90950cf2c6/jiter-0.8.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2dd880785088ff2ad21ffee205e58a8c1ddabc63612444ae41e5e4b321b39c0", size = 342875 },
{ url = "https://files.pythonhosted.org/packages/91/61/c80ef80ed8a0a21158e289ef70dac01e351d929a1c30cb0f49be60772547/jiter-0.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:3ac9f578c46f22405ff7f8b1f5848fb753cc4b8377fbec8470a7dc3997ca7566", size = 202374 },
]
[[package]]
name = "openai"
version = "1.59.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "distro" },
{ name = "httpx" },
{ name = "jiter" },
{ name = "pydantic" },
{ name = "sniffio" },
{ name = "tqdm" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f9/d5/25cf04789c7929b476c4d9ef711f8979091db63d30bfc093828fe4bf5c72/openai-1.59.7.tar.gz", hash = "sha256:043603def78c00befb857df9f0a16ee76a3af5984ba40cb7ee5e2f40db4646bf", size = 345007 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6d/47/7b92f1731c227f4139ef0025b5996062e44f9a749c54315c8bdb34bad5ec/openai-1.59.7-py3-none-any.whl", hash = "sha256:cfa806556226fa96df7380ab2e29814181d56fea44738c2b0e581b462c268692", size = 454844 },
]
[[package]]
name = "pydantic"
version = "2.10.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6a/c7/ca334c2ef6f2e046b1144fe4bb2a5da8a4c574e7f2ebf7e16b34a6a2fa92/pydantic-2.10.5.tar.gz", hash = "sha256:278b38dbbaec562011d659ee05f63346951b3a248a6f3642e1bc68894ea2b4ff", size = 761287 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/58/26/82663c79010b28eddf29dcdd0ea723439535fa917fce5905885c0e9ba562/pydantic-2.10.5-py3-none-any.whl", hash = "sha256:4dd4e322dbe55472cb7ca7e73f4b63574eecccf2835ffa2af9021ce113c83c53", size = 431426 },
]
[[package]]
name = "pydantic-core"
version = "2.27.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 },
{ url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 },
{ url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 },
{ url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 },
{ url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 },
{ url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 },
{ url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 },
{ url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 },
{ url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 },
{ url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 },
{ url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 },
{ url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 },
{ url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 },
{ url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 },
{ url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 },
{ url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 },
{ url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 },
{ url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 },
{ url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 },
{ url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 },
{ url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 },
{ url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 },
{ url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 },
{ url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 },
{ url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 },
{ url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 },
{ url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 },
{ url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 },
{ url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 },
{ url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 },
{ url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 },
{ url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 },
{ url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 },
{ url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 },
{ url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 },
{ url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 },
{ url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 },
{ url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 },
{ url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 },
{ url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 },
{ url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 },
{ url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
]
[[package]]
name = "python-dotenv"
version = "1.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 },
]
[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
]
[[package]]
name = "tenacity"
version = "9.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cd/94/91fccdb4b8110642462e653d5dcb27e7b674742ad68efd146367da7bdb10/tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b", size = 47421 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b6/cb/b86984bed139586d01532a587464b5805f12e397594f19f931c4c2fbfa61/tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539", size = 28169 },
]
[[package]]
name = "tqdm"
version = "4.67.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 },
]
[[package]]
name = "typing-extensions"
version = "4.12.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
]