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:
parent
8c39084a31
commit
dd797ab5e4
12
.env.example
Normal file
12
.env.example
Normal 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
26
.gitignore
vendored
Normal 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
1
config/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Configuration management package."""
|
||||||
67
config/config_loader.py
Normal file
67
config/config_loader.py
Normal 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
22
config/llm_config.yaml
Normal 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
131
config/secure_config.py
Normal 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
110
llm_client.py
Normal 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
199
llm_executor.py
Normal 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
27
pyproject.toml
Normal 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
12
requirements.txt
Normal 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
110
sample_task.py
Normal 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
1
scripts/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Scripts package for utility scripts."""
|
||||||
10
scripts/setup_config.py
Normal file
10
scripts/setup_config.py
Normal 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
190
task_executor.py
Normal 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
1
tasksamples/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
200
tasksamples/text_analysis_task.py
Normal file
200
tasksamples/text_analysis_task.py
Normal 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 到 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()
|
||||||
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
136
tests/test_config.py
Normal file
136
tests/test_config.py
Normal 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
49
tests/test_llm.py
Normal 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
323
uv.lock
generated
Normal 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 },
|
||||||
|
]
|
||||||
Loading…
Reference in New Issue
Block a user