From dd797ab5e4d066bff1e304d85e2245b132663205 Mon Sep 17 00:00:00 2001 From: zhukang Date: Tue, 14 Jan 2025 20:53:09 +0800 Subject: [PATCH] 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 --- .env.example | 12 ++ .gitignore | 26 +++ config/__init__.py | 1 + config/config_loader.py | 67 +++++++ config/llm_config.yaml | 22 ++ config/secure_config.py | 131 ++++++++++++ llm_client.py | 110 ++++++++++ llm_executor.py | 199 ++++++++++++++++++ pyproject.toml | 27 +++ requirements.txt | 12 ++ sample_task.py | 110 ++++++++++ scripts/__init__.py | 1 + scripts/setup_config.py | 10 + task_executor.py | 190 ++++++++++++++++++ tasksamples/__init__.py | 1 + tasksamples/text_analysis_task.py | 200 ++++++++++++++++++ tests/__init__.py | 1 + tests/test_config.py | 136 +++++++++++++ tests/test_llm.py | 49 +++++ uv.lock | 323 ++++++++++++++++++++++++++++++ 20 files changed, 1628 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 config/__init__.py create mode 100644 config/config_loader.py create mode 100644 config/llm_config.yaml create mode 100644 config/secure_config.py create mode 100644 llm_client.py create mode 100644 llm_executor.py create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 sample_task.py create mode 100644 scripts/__init__.py create mode 100644 scripts/setup_config.py create mode 100644 task_executor.py create mode 100644 tasksamples/__init__.py create mode 100644 tasksamples/text_analysis_task.py create mode 100644 tests/__init__.py create mode 100644 tests/test_config.py create mode 100644 tests/test_llm.py create mode 100644 uv.lock diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e1420e5 --- /dev/null +++ b/.env.example @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e8642b7 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..6cdb9ef --- /dev/null +++ b/config/__init__.py @@ -0,0 +1 @@ +"""Configuration management package.""" diff --git a/config/config_loader.py b/config/config_loader.py new file mode 100644 index 0000000..d87f7d5 --- /dev/null +++ b/config/config_loader.py @@ -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 diff --git a/config/llm_config.yaml b/config/llm_config.yaml new file mode 100644 index 0000000..d88c6ce --- /dev/null +++ b/config/llm_config.yaml @@ -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 diff --git a/config/secure_config.py b/config/secure_config.py new file mode 100644 index 0000000..b59def0 --- /dev/null +++ b/config/secure_config.py @@ -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 diff --git a/llm_client.py b/llm_client.py new file mode 100644 index 0000000..193ca7a --- /dev/null +++ b/llm_client.py @@ -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}") diff --git a/llm_executor.py b/llm_executor.py new file mode 100644 index 0000000..b2990a5 --- /dev/null +++ b/llm_executor.py @@ -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() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..994cb62 --- /dev/null +++ b/pyproject.toml @@ -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", +] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..94e3df4 --- /dev/null +++ b/requirements.txt @@ -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 diff --git a/sample_task.py b/sample_task.py new file mode 100644 index 0000000..659ccb7 --- /dev/null +++ b/sample_task.py @@ -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() diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000..97319b5 --- /dev/null +++ b/scripts/__init__.py @@ -0,0 +1 @@ +"""Scripts package for utility scripts.""" diff --git a/scripts/setup_config.py b/scripts/setup_config.py new file mode 100644 index 0000000..534a088 --- /dev/null +++ b/scripts/setup_config.py @@ -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() diff --git a/task_executor.py b/task_executor.py new file mode 100644 index 0000000..c7deb83 --- /dev/null +++ b/task_executor.py @@ -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() diff --git a/tasksamples/__init__.py b/tasksamples/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tasksamples/__init__.py @@ -0,0 +1 @@ + diff --git a/tasksamples/text_analysis_task.py b/tasksamples/text_analysis_task.py new file mode 100644 index 0000000..93a7b17 --- /dev/null +++ b/tasksamples/text_analysis_task.py @@ -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 到 Devin:AI 编程的四个发展阶段与范式转变 Koji:我们再聊一聊 AI 编程。编程领域今年取得了非常令人兴奋的进展。雨森一直有很强的框架归纳和总结能力。前不久你跟我分享过你提炼出来的 AI 编程发展四段论,要不要在播客里和大家分享一下? 雨森:这其实是和很多朋友一起探讨得出的结果,是大家智慧的结晶。AI 编程从 ChatGPT 出现到现在也就两年出头的时间,但已经经历了四个阶段。 第一个阶段是让 AI 直接写代码,典型代表是早期的 ChatGPT、Claude。我们给它一个需求,比如「帮我写个贪吃蛇」,它就给出一段代码。在这个过程中,它既不知道我为什么要写贪吃蛇,也不知道代码运行情况如何。可能要我去本地编译运行后发现报错,再把错误告诉它,它才能给出调试后的结果。这时的 AI 完全就像一个只能通过邮件交流的笔友,是简单的问答模式。 第二阶段是以 GitHub Copilot 为代表,AI 开始拥有上下文,它可以把整个组织的代码库作为 context。这样 AI 就获得了大量新的背景信息。但这时用户还是需要手动把代码贴到 IDE 里面进行调试。我觉得这是 2.0 阶段,就是我们让 AI 拥有了 codebase 作为上下文。 2024 年一个非常大的进步是以 Cursor 为代表的编程 Copilot 的出现。它的核心理念是预测用户未来要写什么代码。根据你的代码库以及刚才写的代码,它预测你接下来要写什么代码、创建什么文件、做什么操作。这里面对于生成代码的质量和数量,以及文件的创建和修改都有很大提升。后来 Windsurf 还加入了对命令行操作的自动化,这样 AI 就能很好地使用我的电脑。原来的 AI 是在一张纸上写代码,我把代码抄走运行;现在 AI 可以在我的电脑上创建文件、执行命令行操作,进入到「我为你写」的阶段。 当我们觉得这已经很令人兴奋时,Devin 的出现带来了几个重要突破:首先,它可以异步工作。Cursor、Windsurf 这些工具虽然一步操作做的事情比较多,但仍然需要持续的注意力,即「我说一步它做一步」。而 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() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..eb3b3cd --- /dev/null +++ b/tests/test_config.py @@ -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 diff --git a/tests/test_llm.py b/tests/test_llm.py new file mode 100644 index 0000000..b24cefb --- /dev/null +++ b/tests/test_llm.py @@ -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()) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..55730c8 --- /dev/null +++ b/uv.lock @@ -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 }, +]