From 8950c1e495c9ad78e038f50e2d2680d6b17255bd Mon Sep 17 00:00:00 2001 From: Akshat Date: Mon, 21 Apr 2025 00:42:44 -0400 Subject: [PATCH 01/20] feat: add core payment helpers, wallet manager, and update types/registry --- agentconnect/core/payment_constants.py | 14 + agentconnect/core/registry/registration.py | 2 + agentconnect/core/registry/registry_base.py | 4 + agentconnect/core/types.py | 13 +- agentconnect/utils/payment_helper.py | 218 +++++++++++++++ agentconnect/utils/wallet_manager.py | 286 ++++++++++++++++++++ 6 files changed, 531 insertions(+), 6 deletions(-) create mode 100644 agentconnect/core/payment_constants.py create mode 100644 agentconnect/utils/payment_helper.py create mode 100644 agentconnect/utils/wallet_manager.py diff --git a/agentconnect/core/payment_constants.py b/agentconnect/core/payment_constants.py new file mode 100644 index 0000000..6e87c67 --- /dev/null +++ b/agentconnect/core/payment_constants.py @@ -0,0 +1,14 @@ +""" +Payment constants for the AgentConnect framework. + +This module provides constants for payment functionality in AgentConnect. +""" + +from decimal import Decimal + +# Payment capability constants for POC + +# Default payment amount +POC_PAYMENT_AMOUNT = Decimal("1.0") +# Default payment token +POC_PAYMENT_TOKEN_SYMBOL = "USDC" diff --git a/agentconnect/core/registry/registration.py b/agentconnect/core/registry/registration.py index 72765d0..fe637d1 100644 --- a/agentconnect/core/registry/registration.py +++ b/agentconnect/core/registry/registration.py @@ -34,6 +34,7 @@ class AgentRegistration: capabilities: List of agent capabilities identity: Agent's decentralized identity owner_id: ID of the agent's owner + payment_address: Agent's primary wallet address for receiving payments metadata: Additional information about the agent """ @@ -44,4 +45,5 @@ class AgentRegistration: capabilities: list[Capability] identity: AgentIdentity owner_id: Optional[str] = None + payment_address: Optional[str] = None metadata: Dict = field(default_factory=dict) diff --git a/agentconnect/core/registry/registry_base.py b/agentconnect/core/registry/registry_base.py index c94b060..4802bac 100644 --- a/agentconnect/core/registry/registry_base.py +++ b/agentconnect/core/registry/registry_base.py @@ -506,6 +506,10 @@ async def update_registration( for mode in registration.interaction_modes: self._interaction_index[mode].add(agent_id) + # Update payment address if provided + if "payment_address" in updates: + registration.payment_address = updates["payment_address"] + if "metadata" in updates: registration.metadata.update(updates["metadata"]) diff --git a/agentconnect/core/types.py b/agentconnect/core/types.py index f403444..b9eea89 100644 --- a/agentconnect/core/types.py +++ b/agentconnect/core/types.py @@ -66,7 +66,8 @@ class ModelName(str, Enum): GEMMA2_90B = "gemma2-9b-it" # Google Models - GEMINI2_5_PRO_EXP = "gemini-2.5-pro-exp-03-25 " + GEMINI2_5_PRO_EXP = "gemini-2.5-pro-exp-03-25" + GEMINI2_5_FLASH_PREVIEW = "gemini-2.5-flash-preview-04-17" GEMINI2_FLASH = "gemini-2.0-flash" GEMINI2_FLASH_LITE = "gemini-2.0-flash-lite" GEMINI2_PRO_EXP = "gemini-2.0-pro-exp-02-05" @@ -92,7 +93,7 @@ def get_default_for_provider(cls, provider: ModelProvider) -> "ModelName": ModelProvider.OPENAI: cls.GPT4O, ModelProvider.ANTHROPIC: cls.CLAUDE_3_SONNET, ModelProvider.GROQ: cls.LLAMA33_70B_VTL, - ModelProvider.GOOGLE: cls.GEMINI2_FLASH_LITE, + ModelProvider.GOOGLE: cls.GEMINI2_FLASH, } if provider not in defaults: @@ -165,8 +166,8 @@ class Capability: name: str description: str - input_schema: Dict[str, str] - output_schema: Dict[str, str] + input_schema: Optional[Dict[str, str]] = None + output_schema: Optional[Dict[str, str]] = None version: str = "1.0" @@ -352,7 +353,7 @@ class AgentMetadata: organization_id: ID of the organization the agent belongs to capabilities: List of capability names the agent provides interaction_modes: Supported interaction modes - verification_status: Whether the agent's identity is verified + payment_address: Agent's primary wallet address for receiving payments metadata: Additional information about the agent """ @@ -362,7 +363,7 @@ class AgentMetadata: organization_id: Optional[str] = None capabilities: List[str] = field(default_factory=list) interaction_modes: List[InteractionMode] = field(default_factory=list) - verification_status: bool = False + payment_address: Optional[str] = None metadata: Dict = field(default_factory=dict) diff --git a/agentconnect/utils/payment_helper.py b/agentconnect/utils/payment_helper.py new file mode 100644 index 0000000..d581636 --- /dev/null +++ b/agentconnect/utils/payment_helper.py @@ -0,0 +1,218 @@ +""" +Payment utility functions for AgentConnect. + +This module provides helper functions for setting up payment capabilities in agents. +""" + +import json +import logging +import os +import time +from pathlib import Path +from typing import Optional, Dict, Any, Union, Tuple + +from agentconnect.utils import wallet_manager + +logger = logging.getLogger(__name__) + + +def verify_payment_environment() -> bool: + """ + Verify that all required environment variables for payments are set. + + Returns: + True if environment is properly configured, False otherwise + """ + # Check required environment variables + api_key_name = os.getenv("CDP_API_KEY_NAME") + api_key_private = os.getenv("CDP_API_KEY_PRIVATE_KEY") + + if not api_key_name: + logger.error("CDP_API_KEY_NAME environment variable is not set") + return False + + if not api_key_private: + logger.error("CDP_API_KEY_PRIVATE_KEY environment variable is not set") + return False + + network_id = os.getenv("CDP_NETWORK_ID", "base-sepolia") + logger.info(f"Payment environment verified: Using network {network_id}") + return True + + +def validate_cdp_environment() -> Tuple[bool, str]: + """ + Validate that the Coinbase Developer Platform environment is properly configured. + + Returns: + Tuple of (valid: bool, message: str) + """ + try: + # Ensure .env file is loaded + from dotenv import load_dotenv + + load_dotenv() + + # Verify environment variables + if not verify_payment_environment(): + return False, "Required environment variables are missing" + + # Check if CDP packages are installed + try: + import cdp # noqa: F401 + except ImportError: + return ( + False, + "CDP SDK not installed. Install it with: pip install cdp-sdk", + ) + + try: + import coinbase_agentkit # noqa: F401 + except ImportError: + return ( + False, + "AgentKit not installed. Install it with: pip install coinbase-agentkit", + ) + + try: + import coinbase_agentkit_langchain # noqa: F401 + except ImportError: + return ( + False, + "AgentKit LangChain integration not installed. Install it with: pip install coinbase-agentkit-langchain", + ) + + return True, "CDP environment is properly configured" + except Exception as e: + return False, f"Unexpected error validating CDP environment: {e}" + + +def get_wallet_metadata( + agent_id: str, wallet_data_dir: Optional[Union[str, Path]] = None +) -> Optional[Dict[str, Any]]: + """ + Get wallet metadata for an agent if it exists. + + Args: + agent_id: The ID of the agent + wallet_data_dir: Optional custom directory for wallet data storage + + Returns: + Dictionary with wallet metadata if it exists, None otherwise + """ + if not wallet_manager.wallet_exists(agent_id, wallet_data_dir): + logger.debug(f"No wallet metadata found for agent {agent_id}") + return None + + try: + wallet_json = wallet_manager.load_wallet_data(agent_id, wallet_data_dir) + if not wallet_json: + logger.warning(f"Invalid wallet data found for agent {agent_id}") + return None + + # Parse the JSON into a dictionary + wallet_data = json.loads(wallet_json) + + # Extract relevant metadata + metadata = { + "wallet_id": wallet_data.get("wallet_id", "Unknown"), + "network_id": wallet_data.get("network_id", "Unknown"), + "has_seed": "seed" in wallet_data, + } + + # Don't include sensitive data like seed + logger.debug(f"Retrieved wallet metadata for agent {agent_id}") + return metadata + except Exception as e: + logger.error(f"Error retrieving wallet metadata for agent {agent_id}: {e}") + return None + + +def backup_wallet_data( + agent_id: str, + data_dir: Optional[Union[str, Path]] = None, + backup_dir: Optional[Union[str, Path]] = None, +) -> Optional[str]: + """ + Create a backup of wallet data for an agent. + + Args: + agent_id: The ID of the agent + data_dir: Optional custom directory for wallet data storage + backup_dir: Optional directory for storing backups + If None, creates a backup directory under data_dir + + Returns: + Path to the backup file if successful, None otherwise + """ + if not wallet_manager.wallet_exists(agent_id, data_dir): + logger.warning(f"No wallet data found for agent {agent_id} to backup") + return None + + try: + # Determine the source directory and file + data_dir_path = Path(data_dir) if data_dir else wallet_manager.DEFAULT_DATA_DIR + source_file = data_dir_path / f"{agent_id}_wallet.json" + + # Determine the backup directory + if backup_dir: + backup_dir_path = Path(backup_dir) + else: + backup_dir_path = data_dir_path / "backups" + + # Create backup directory if it doesn't exist + backup_dir_path.mkdir(parents=True, exist_ok=True) + + # Create a timestamped filename for the backup + timestamp = time.strftime("%Y%m%d-%H%M%S") + backup_file = backup_dir_path / f"{agent_id}_wallet_{timestamp}.json" + + # Read the original wallet data + with open(source_file, "r") as f: + wallet_data = f.read() + + # Write to the backup file + with open(backup_file, "w") as f: + f.write(wallet_data) + + logger.info(f"Backed up wallet data for agent {agent_id} to {backup_file}") + return str(backup_file) + except Exception as e: + logger.error(f"Error backing up wallet data for agent {agent_id}: {e}") + return None + + +def check_agent_payment_readiness(agent) -> Dict[str, Any]: + """ + Check if an agent is ready for payments. + + Args: + agent: The agent to check + + Returns: + A dictionary with status information + """ + status = { + "payments_enabled": getattr(agent, "enable_payments", False), + "wallet_provider_available": hasattr(agent, "wallet_provider") + and agent.wallet_provider is not None, + "agent_kit_available": hasattr(agent, "agent_kit") + and agent.agent_kit is not None, + "payment_address": ( + getattr(agent.metadata, "payment_address", None) + if hasattr(agent, "metadata") + else None + ), + "ready": False, + } + + # Check overall readiness + status["ready"] = ( + status["payments_enabled"] + and status["wallet_provider_available"] + and status["agent_kit_available"] + and status["payment_address"] is not None + ) + + logger.info(f"Agent payment readiness check: {json.dumps(status)}") + return status diff --git a/agentconnect/utils/wallet_manager.py b/agentconnect/utils/wallet_manager.py new file mode 100644 index 0000000..18d7a5d --- /dev/null +++ b/agentconnect/utils/wallet_manager.py @@ -0,0 +1,286 @@ +""" +Wallet persistence utilities for the AgentConnect framework. + +This module provides utility functions to manage wallet data persistence +for individual agents within the AgentConnect framework. It specifically facilitates the storage +and retrieval of wallet state to enable consistent wallet access across agent restarts. +""" + +import json +import logging +from pathlib import Path +from typing import Dict, List, Optional, Union + +# Import dependencies directly since they're required +from cdp import WalletData + +# Set up logging +logger = logging.getLogger(__name__) + +# Default path for wallet data storage +DEFAULT_DATA_DIR = Path("data/agent_wallets") + + +def set_default_data_dir(data_dir: Union[str, Path]) -> Path: + """ + Set the default directory for wallet data storage globally. + + Args: + data_dir: Path to the directory where wallet data will be stored + Can be a string or Path object + + Returns: + Path object pointing to the created directory + + Raises: + IOError: If the directory can't be created + """ + global DEFAULT_DATA_DIR + try: + # Convert to Path if it's a string + data_dir_path = Path(data_dir) if isinstance(data_dir, str) else data_dir + + # Create directory if it doesn't exist + data_dir_path.mkdir(parents=True, exist_ok=True) + + # Update the global default + DEFAULT_DATA_DIR = data_dir_path + + logger.info(f"Set default wallet data directory to: {data_dir_path}") + return data_dir_path + except Exception as e: + error_msg = f"Error setting default wallet data directory: {e}" + logger.error(error_msg) + raise IOError(error_msg) + + +def set_wallet_data_dir(data_dir: Union[str, Path]) -> Path: + """ + Set a custom directory for wallet data storage. + + Args: + data_dir: Path to the directory where wallet data will be stored + Can be a string or Path object + + Returns: + Path object pointing to the created directory + + Raises: + IOError: If the directory can't be created + """ + try: + # Convert to Path if it's a string + data_dir_path = Path(data_dir) if isinstance(data_dir, str) else data_dir + + # Create directory if it doesn't exist + data_dir_path.mkdir(parents=True, exist_ok=True) + + logger.info(f"Set wallet data directory to: {data_dir_path}") + return data_dir_path + except Exception as e: + error_msg = f"Error setting wallet data directory: {e}" + logger.error(error_msg) + raise IOError(error_msg) + + +def save_wallet_data( + agent_id: str, + wallet_data: Union[WalletData, str, Dict], + data_dir: Optional[Union[str, Path]] = None, +) -> None: + """ + Persists the exported wallet data for an agent, allowing the agent to retain + access to the same wallet across restarts. + + SECURITY NOTE: This default implementation stores wallet data unencrypted on disk, + which is suitable for testing/demo but NOT secure for production environments + holding real assets. Real-world applications should encrypt this data. + + Args: + agent_id: String identifier for the agent. + wallet_data: The wallet data to save. Can be a cdp.WalletData object, + a Dict representation, or a JSON string. + data_dir: Optional custom directory for wallet data storage. + If None, uses the DEFAULT_DATA_DIR. + + Raises: + IOError: If the data directory can't be created or the file can't be written. + """ + # Determine the data directory to use + data_dir_path = set_wallet_data_dir(data_dir) if data_dir else DEFAULT_DATA_DIR + data_dir_path.mkdir(parents=True, exist_ok=True) + + # File path for this agent's wallet data + file_path = data_dir_path / f"{agent_id}_wallet.json" + + try: + # Convert wallet_data to JSON string based on its type + if isinstance(wallet_data, str): + # Assume it's valid JSON string + json_data = wallet_data + elif isinstance(wallet_data, Dict): + # Convert dict to JSON string + json_data = json.dumps(wallet_data) + else: + # Assume it's a WalletData object and serialize it + json_data = json.dumps(wallet_data.to_dict()) + + # Write to file + with open(file_path, "w") as f: + f.write(json_data) + + logger.debug(f"Saved wallet data for agent {agent_id} to {file_path}") + + except Exception as e: + error_msg = f"Error saving wallet data for agent {agent_id}: {e}" + logger.error(error_msg) + raise IOError(error_msg) + + +def load_wallet_data( + agent_id: str, data_dir: Optional[Union[str, Path]] = None +) -> Optional[str]: + """ + Loads previously persisted wallet data for an agent. + + Args: + agent_id: String identifier for the agent. + data_dir: Optional custom directory for wallet data storage. + If None, uses the DEFAULT_DATA_DIR. + + Returns: + The loaded wallet data as a JSON string if the file exists, otherwise None. + + Raises: + IOError: If the file exists but can't be read properly. + """ + # Determine the data directory to use + data_dir_path = Path(data_dir) if data_dir else DEFAULT_DATA_DIR + file_path = data_dir_path / f"{agent_id}_wallet.json" + + if not file_path.exists(): + logger.debug(f"No saved wallet data found for agent {agent_id} at {file_path}") + return None + + try: + with open(file_path, "r") as f: + json_data = f.read() + logger.debug(f"Loaded wallet data for agent {agent_id} from {file_path}") + return json_data + except FileNotFoundError: + # Should not happen as we check existence above, but just in case + logger.debug(f"No saved wallet data found for agent {agent_id}") + return None + except Exception as e: + error_msg = f"Error loading wallet data for agent {agent_id}: {e}" + logger.error(error_msg) + # Log error but don't break agent initialization + return None + + +def wallet_exists(agent_id: str, data_dir: Optional[Union[str, Path]] = None) -> bool: + """ + Check if wallet data exists for a specific agent. + + Args: + agent_id: String identifier for the agent. + data_dir: Optional custom directory for wallet data storage. + If None, uses the DEFAULT_DATA_DIR. + + Returns: + True if wallet data exists, False otherwise. + """ + # Determine the data directory to use + data_dir_path = Path(data_dir) if data_dir else DEFAULT_DATA_DIR + file_path = data_dir_path / f"{agent_id}_wallet.json" + + exists = file_path.exists() + if exists: + logger.debug(f"Wallet data exists for agent {agent_id} at {file_path}") + else: + logger.debug(f"No wallet data found for agent {agent_id} at {file_path}") + + return exists + + +def get_all_wallets(data_dir: Optional[Union[str, Path]] = None) -> List[Dict]: + """ + Get information about all wallet files in the specified directory. + + Args: + data_dir: Optional custom directory for wallet data storage. + If None, uses the DEFAULT_DATA_DIR. + + Returns: + List of dictionaries with wallet information (agent_id, file_path, etc.) + """ + # Determine the data directory to use + data_dir_path = Path(data_dir) if data_dir else DEFAULT_DATA_DIR + + if not data_dir_path.exists(): + logger.debug(f"Wallet data directory {data_dir_path} does not exist") + return [] + + wallets = [] + try: + # Find all wallet JSON files + for file_path in data_dir_path.glob("*_wallet.json"): + # Extract agent_id from filename + agent_id = file_path.stem.replace("_wallet", "") + + wallet_info = { + "agent_id": agent_id, + "file_path": str(file_path), + "last_modified": file_path.stat().st_mtime, + } + + # Try to read basic info without exposing sensitive data + try: + with open(file_path, "r") as f: + data = json.loads(f.read()) + + if "wallet_id" in data: + wallet_info["wallet_id"] = data["wallet_id"] + if "network_id" in data: + wallet_info["network_id"] = data["network_id"] + except Exception as e: + logger.error(f"Error reading wallet data for {agent_id}: {e}") + + wallets.append(wallet_info) + + logger.debug(f"Found {len(wallets)} wallet files in {data_dir_path}") + return wallets + except Exception as e: + logger.error(f"Error listing wallets in {data_dir_path}: {e}") + return [] + + +def delete_wallet_data( + agent_id: str, data_dir: Optional[Union[str, Path]] = None +) -> bool: + """ + Delete wallet data for a specific agent. + + Args: + agent_id: String identifier for the agent. + data_dir: Optional custom directory for wallet data storage. + If None, uses the DEFAULT_DATA_DIR. + + Returns: + True if wallet data was successfully deleted, False otherwise. + """ + # Determine the data directory to use + data_dir_path = Path(data_dir) if data_dir else DEFAULT_DATA_DIR + file_path = data_dir_path / f"{agent_id}_wallet.json" + + if not file_path.exists(): + logger.debug(f"No wallet data to delete for agent {agent_id}") + return False + + try: + file_path.unlink() + logger.info(f"Deleted wallet data for agent {agent_id} from {file_path}") + return True + except Exception as e: + logger.error(f"Error deleting wallet data for agent {agent_id}: {e}") + return False From a21de9654e05a47af9fa3752f47571ca6520d666 Mon Sep 17 00:00:00 2001 From: Akshat Date: Mon, 21 Apr 2025 00:43:50 -0400 Subject: [PATCH 02/20] feat: integrate payment initialization and validation into BaseAgent and AIAgent --- agentconnect/agents/ai_agent.py | 165 ++++++++++++++++++++++++++++---- agentconnect/core/agent.py | 130 ++++++++++++++++++++++++- 2 files changed, 276 insertions(+), 19 deletions(-) diff --git a/agentconnect/agents/ai_agent.py b/agentconnect/agents/ai_agent.py index 882edb5..7149b97 100644 --- a/agentconnect/agents/ai_agent.py +++ b/agentconnect/agents/ai_agent.py @@ -13,16 +13,19 @@ import logging from datetime import datetime from enum import Enum -from typing import List, Optional +from typing import List, Optional, Union +from pathlib import Path # Third-party imports from langchain_core.messages import HumanMessage from langchain_core.runnables import Runnable +from langchain_core.callbacks import BaseCallbackHandler from langchain_core.tools import BaseTool # Absolute imports from agentconnect package from agentconnect.core.agent import BaseAgent from agentconnect.core.message import Message +from agentconnect.core.payment_constants import POC_PAYMENT_TOKEN_SYMBOL from agentconnect.core.types import ( AgentIdentity, AgentType, @@ -43,6 +46,7 @@ InteractionState, TokenConfig, ) +from agentconnect.utils.payment_helper import validate_cdp_environment # Set up logging logger = logging.getLogger(__name__) @@ -98,6 +102,12 @@ def __init__( # Custom tools parameter custom_tools: Optional[List[BaseTool]] = None, agent_type: str = "ai", + # Payment capabilities parameters + enable_payments: bool = False, + verbose: bool = False, + wallet_data_dir: Optional[Union[str, Path]] = None, + # External callbacks parameter + external_callbacks: Optional[List[BaseCallbackHandler]] = None, ): """Initialize the AI agent. @@ -120,7 +130,29 @@ def __init__( prompt_templates: Optional prompt templates for the agent custom_tools: Optional list of custom LangChain tools for the agent agent_type: Type of agent workflow to create + enable_payments: Whether to enable payment capabilities + verbose: + wallet_data_dir: Optional custom directory for wallet data storage + external_callbacks: Optional list of external callback handlers to include """ + # Validate CDP environment if payments are requested + actual_enable_payments = enable_payments + if enable_payments: + # The validation function will load dotenv for us + is_valid, message = validate_cdp_environment() + if not is_valid: + logger.warning( + f"Payment capabilities requested for agent {agent_id} but environment validation failed: {message}" + ) + logger.warning( + f"Payment capabilities will be disabled for agent {agent_id}" + ) + actual_enable_payments = False # Disable payments since the environment is not properly configured + else: + logger.info( + f"CDP environment validation passed for agent {agent_id}: {message}" + ) + # Initialize base agent super().__init__( agent_id=agent_id, @@ -129,6 +161,8 @@ def __init__( capabilities=capabilities or [], organization_id=organization_id, interaction_modes=interaction_modes, + enable_payments=actual_enable_payments, + wallet_data_dir=wallet_data_dir, ) # Store agent-specific attributes @@ -141,6 +175,7 @@ def __init__( self.is_ui_mode = is_ui_mode self.memory_type = memory_type self.workflow_agent_type = agent_type + self.verbose = verbose # Store the custom tools list if provided self.custom_tools = custom_tools or [] @@ -148,6 +183,9 @@ def __init__( # Store the prompt_tools instance if provided self._prompt_tools = prompt_tools + # Store external callbacks if provided + self.external_callbacks = external_callbacks or [] + # Create a new PromptTemplates instance for this agent self.prompt_templates = prompt_templates or PromptTemplates() @@ -158,13 +196,13 @@ def __init__( ) self.interaction_control = InteractionControl( - token_config=token_config, max_turns=max_turns + agent_id=self.agent_id, token_config=token_config, max_turns=max_turns ) # Set cooldown callback to update agent's cooldown state self.interaction_control.set_cooldown_callback(self.set_cooldown) - # Initialize the LLM + # Initialize the LLM (This will now use the tool_tracer_handler) self.llm = self._initialize_llm() logger.debug(f"Initialized LLM for AI Agent {self.agent_id}: {self.llm}") @@ -251,6 +289,56 @@ def _initialize_workflow(self) -> Runnable: f"AI Agent {self.agent_id}: System config created with capabilities: {self.capabilities}" ) + # Initialize custom tools from AgentKit if payments are enabled + custom_tools_list = list(self.custom_tools) if self.custom_tools else [] + + # Check if payments are enabled and AgentKit is available + if self.agent_kit is not None: + try: + # Import AgentKit LangChain integration + from coinbase_agentkit_langchain import get_langchain_tools + + # Get the AgentKit tools + agentkit_tools = get_langchain_tools(self.agent_kit) + + # Add the tools to the custom tools list + custom_tools_list.extend(agentkit_tools) + + # Log the available payment tools + tool_names = [tool.name for tool in agentkit_tools] + logger.info( + f"AI Agent {self.agent_id}: Added {len(agentkit_tools)} AgentKit payment tools: {tool_names}" + ) + + # Determine which payment tool to use based on token symbol + payment_tool = ( + "native_transfer" + if POC_PAYMENT_TOKEN_SYMBOL == "ETH" + else "erc20_transfer" + ) + logger.info( + f"AI Agent {self.agent_id}: Will use {payment_tool} for payments with {POC_PAYMENT_TOKEN_SYMBOL} token" + ) + + # Enable payment capabilities in the system prompt config + self.system_config.enable_payments = True + self.system_config.payment_token_symbol = POC_PAYMENT_TOKEN_SYMBOL + logger.info( + f"AI Agent {self.agent_id}: Enabled payment capabilities in system prompt" + ) + + except ImportError as e: + logger.warning( + f"AI Agent {self.agent_id}: Could not import AgentKit LangChain tools: {e}" + ) + logger.warning( + "To use payment capabilities, install with: pip install coinbase-agentkit-langchain" + ) + except Exception as e: + logger.error( + f"AI Agent {self.agent_id}: Error initializing AgentKit tools: {e}" + ) + # Create and compile the workflow with business logic info workflow = create_workflow_for_agent( agent_type=self.workflow_agent_type, @@ -259,10 +347,11 @@ def _initialize_workflow(self) -> Runnable: tools=tools, prompt_templates=prompt_templates, agent_id=self.agent_id, - custom_tools=self.custom_tools, # Pass the custom tools list + custom_tools=custom_tools_list, + verbose=self.verbose, ) logger.debug( - f"AI Agent {self.agent_id}: Workflow created with {len(self.custom_tools)} custom tools." + f"AI Agent {self.agent_id}: Workflow created with {len(custom_tools_list)} custom tools." ) compiled_workflow = workflow.compile() @@ -424,16 +513,17 @@ async def process_message(self, message: Message) -> Optional[Message]: # Get the conversation ID for this sender conversation_id = self._get_conversation_id(message.sender_id) - # Get the callback manager from interaction_control - # Only include our rate limiting callback, not a tracer - callbacks = self.interaction_control.get_callback_manager() + # Get the base callback manager from interaction_control (rate limiting + tool tracing) + callbacks = self.interaction_control.get_callback_handlers() + + # Add any external callbacks + if self.external_callbacks: + callbacks.extend(self.external_callbacks) - # Set up the configuration with the thread ID for memory persistence and callbacks - # Use the thread_id for LangGraph memory persistence + # Set up the configuration with the thread ID for memory persistence and ALL handlers config = { "configurable": { "thread_id": conversation_id, - # Add a run name for better LangSmith organization "run_name": f"Agent {self.agent_id} - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", }, "callbacks": callbacks, @@ -450,8 +540,33 @@ async def process_message(self, message: Message) -> Optional[Message]: ) # Create the initial state for the workflow + # --- Add context prefix based on sender/message type --- + sender_type = ( + "Human" if message.sender_id.startswith("human_") else "AI Agent" + ) + is_collab_request = ( + message.message_type == MessageType.REQUEST_COLLABORATION + ) + # Check metadata for response correlation, assuming 'response_to' indicates a collab response + is_collab_response = "response_to" in (message.metadata or {}) + + context_prefix = "" + if sender_type == "AI Agent": + if is_collab_request: + context_prefix = f"[Incoming Collaboration Request from AI Agent {message.sender_id}]:\n" + elif is_collab_response: + context_prefix = f"[Incoming Response from Collaborating AI Agent {message.sender_id}]:\n" + else: # General message from AI + context_prefix = ( + f"[Incoming Message from AI Agent {message.sender_id}]:\n" + ) + # No prefix for direct Human messages + + workflow_input_content = f"{context_prefix}{message.content}" + # --- End context prefix logic --- + initial_state = { - "messages": [HumanMessage(content=message.content)], + "messages": [HumanMessage(content=workflow_input_content)], "sender": message.sender_id, "receiver": self.agent_id, "message_type": message.message_type, @@ -465,8 +580,8 @@ async def process_message(self, message: Message) -> Optional[Message]: # Use the provided runnable with a timeout try: - # Invoke the workflow with a timeout and callbacks - response = await asyncio.wait_for( + # Invoke the workflow with a timeout and the combined callbacks + response_state = await asyncio.wait_for( self.workflow.ainvoke(initial_state, config), timeout=180.0, # 3 minute timeout for workflow execution ) @@ -500,8 +615,26 @@ async def process_message(self, message: Message) -> Optional[Message]: }, ) - # Extract the last message from the workflow response - last_message = response["messages"][-1] + # Extract the last message from the workflow response state + if "messages" not in response_state or not response_state["messages"]: + logger.error( + f"AI Agent {self.agent_id}: Workflow returned empty or invalid messages state." + ) + # Handle error appropriately, maybe return an error message + return Message.create( + sender_id=self.agent_id, + receiver_id=message.sender_id, + content="Internal error: Could not retrieve response.", + sender_identity=self.identity, + message_type=( + MessageType.ERROR + if not is_collaboration_request + else MessageType.COLLABORATION_RESPONSE + ), + metadata={"error_type": "empty_workflow_response"}, + ) + + last_message = response_state["messages"][-1] logger.debug( f"AI Agent {self.agent_id} extracted last message from workflow response." ) diff --git a/agentconnect/core/agent.py b/agentconnect/core/agent.py index 0947215..e3b3b19 100644 --- a/agentconnect/core/agent.py +++ b/agentconnect/core/agent.py @@ -11,12 +11,26 @@ # Standard library imports from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Dict, List, Optional - -from agentconnect.core.exceptions import SecurityError +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union +from pathlib import Path +from dotenv import load_dotenv + +# Import wallet and payment dependencies +from coinbase_agentkit import ( + AgentKit, + AgentKitConfig, + CdpWalletProvider, + CdpWalletProviderConfig, + wallet_action_provider, + erc20_action_provider, + cdp_api_action_provider, +) # Absolute imports from agentconnect package +from agentconnect.utils import wallet_manager +from agentconnect.core.exceptions import SecurityError from agentconnect.core.message import Message +from agentconnect.core.payment_constants import POC_PAYMENT_TOKEN_SYMBOL from agentconnect.core.types import ( AgentIdentity, AgentMetadata, @@ -56,6 +70,9 @@ class BaseAgent(ABC): active_conversations: Dictionary of active conversations cooldown_until: Timestamp when cooldown ends pending_requests: Dictionary of pending requests + enable_payments: Whether payment capabilities are enabled + wallet_provider: Wallet provider for blockchain transactions + agent_kit: AgentKit instance for blockchain actions """ def __init__( @@ -66,6 +83,8 @@ def __init__( interaction_modes: List[InteractionMode], capabilities: List[Capability] = None, organization_id: Optional[str] = None, + enable_payments: bool = False, + wallet_data_dir: Optional[Union[str, Path]] = None, ): """ Initialize the base agent. @@ -77,6 +96,8 @@ def __init__( interaction_modes: Supported interaction modes capabilities: List of agent capabilities organization_id: ID of the organization the agent belongs to + enable_payments: Whether to enable payment capabilities + wallet_data_dir: Optional custom directory for wallet data storage """ self.agent_id = agent_id self.identity = identity @@ -97,8 +118,111 @@ def __init__( self.active_conversations = {} self.cooldown_until = 0 self.pending_requests: Dict[str, Dict[str, Any]] = {} + + # Initialize payment capabilities + self.enable_payments = enable_payments + self.wallet_provider: Optional[CdpWalletProvider] = None + self.agent_kit: Optional[AgentKit] = None + + # Initialize wallet if payments are enabled + if self.enable_payments: + try: + # Load environment variables + load_dotenv() + + # Check if this agent already has wallet data + wallet_data = wallet_manager.load_wallet_data( + self.agent_id, wallet_data_dir + ) + + if wallet_data: + logger.debug(f"Agent {self.agent_id}: Using existing wallet data") + else: + logger.debug( + f"Agent {self.agent_id}: No existing wallet data found, creating new wallet" + ) + + # Initialize wallet provider via coinbase CDP-SDK + cdp_config = ( + CdpWalletProviderConfig(wallet_data=wallet_data) + if wallet_data + else None + ) + self.wallet_provider = CdpWalletProvider(cdp_config) + + # Prepare action providers based on the token symbol + action_providers = [wallet_action_provider(), cdp_api_action_provider()] + + # Add ERC20 action provider if using tokens other than native ETH + if POC_PAYMENT_TOKEN_SYMBOL != "ETH": + action_providers.append(erc20_action_provider()) + logger.debug( + f"Agent {self.agent_id}: Added ERC20 action provider for {POC_PAYMENT_TOKEN_SYMBOL}" + ) + + # Initialize coinbase AgentKit with wallet provider and action providers + agent_kit_config = AgentKitConfig( + wallet_provider=self.wallet_provider, + action_providers=action_providers, + ) + self.agent_kit = AgentKit(agent_kit_config) + + # Save wallet data if it's a new wallet + if not wallet_data: + try: + new_wallet_data = self.wallet_provider.export_wallet() + wallet_manager.save_wallet_data( + self.agent_id, new_wallet_data, wallet_data_dir + ) + logger.debug(f"Agent {self.agent_id}: Saved new wallet data") + except Exception as e: + logger.warning( + f"Agent {self.agent_id}: Error saving new wallet data: {e}" + ) + + # Get wallet address and add to agent metadata + try: + # Get the default wallet address + wallet_address = self.wallet_provider.get_address() + if wallet_address: + self.metadata.payment_address = wallet_address + logger.info( + f"Agent {self.agent_id}: Set payment address to {wallet_address}" + ) + else: + logger.warning( + f"Agent {self.agent_id}: Could not retrieve wallet address" + ) + except Exception as e: + logger.error( + f"Agent {self.agent_id}: Error getting wallet address: {e}" + ) + + logger.info( + f"Agent {self.agent_id}: Payment capabilities initialized successfully" + ) + except Exception as e: + logger.error( + f"Agent {self.agent_id}: Error initializing payment capabilities: {e}" + ) + self.wallet_provider = None + self.agent_kit = None + logger.warning( + f"Agent {self.agent_id}: Payment capabilities disabled due to initialization error" + ) + logger.info(f"Agent {self.agent_id} ({agent_type}) initialized.") + @property + def payments_enabled(self) -> bool: + """ + Check if payment capabilities are enabled and available. + + Returns: + True if payment capabilities are enabled and available, False otherwise + """ + return self.enable_payments and self.wallet_provider is not None + @abstractmethod def _initialize_llm(self): """ From b08158826f8fc54542270891ad2c879ff163d325 Mon Sep 17 00:00:00 2001 From: Akshat Date: Mon, 21 Apr 2025 00:45:29 -0400 Subject: [PATCH 03/20] feat: update agent workflow and prompts for payment capabilities --- agentconnect/prompts/agent_prompts.py | 121 +++---- .../custom_tools/collaboration_tools.py | 69 ++-- .../prompts/custom_tools/task_tools.py | 28 +- .../prompts/templates/agent_templates.py | 27 ++ .../prompts/templates/prompt_templates.py | 341 ++++++++---------- 5 files changed, 284 insertions(+), 302 deletions(-) create mode 100644 agentconnect/prompts/templates/agent_templates.py diff --git a/agentconnect/prompts/agent_prompts.py b/agentconnect/prompts/agent_prompts.py index 5f9d5d7..3e9c1e6 100644 --- a/agentconnect/prompts/agent_prompts.py +++ b/agentconnect/prompts/agent_prompts.py @@ -203,6 +203,7 @@ def __init__( tools: PromptTools, prompt_templates: PromptTemplates, custom_tools: Optional[List[BaseTool]] = None, + verbose: bool = False, ): """ Initialize the agent workflow. @@ -213,6 +214,7 @@ def __init__( tools: Tools available to the agent prompt_templates: Prompt templates for the agent custom_tools: Optional list of custom LangChain tools + verbose: Whether to print verbose output """ self.agent_id = agent_id self.llm = llm @@ -220,7 +222,7 @@ def __init__( self.prompt_templates = prompt_templates self.custom_tools = custom_tools or [] self.workflow = None - + self.verbose = verbose # Set the mode based on whether custom tools are provided self.mode = AgentMode.SYSTEM_PROMPT @@ -240,60 +242,31 @@ def _create_react_prompt(self) -> ChatPromptTemplate: """ # Get system prompt information if hasattr(self, "system_prompt_config"): - name = self.system_prompt_config.name - personality = self.system_prompt_config.personality - capabilities = self.system_prompt_config.capabilities - else: - name = "AI Assistant" - personality = "helpful and professional" - capabilities = [] - - # Format capability descriptions for the prompt - capability_descriptions = [] - if capabilities: - for cap in capabilities: - capability_descriptions.append( - { - "name": cap.name, - "description": cap.description, - } - ) + # Pass all system_prompt_config properties to ReactConfig + react_config = ReactConfig( + name=self.system_prompt_config.name, + capabilities=[ + {"name": cap.name, "description": cap.description} + for cap in self.system_prompt_config.capabilities + ], + personality=self.system_prompt_config.personality, + mode=self.mode.value, + additional_context=self.system_prompt_config.additional_context, + enable_payments=self.system_prompt_config.enable_payments, + payment_token_symbol=self.system_prompt_config.payment_token_symbol, + role=self.system_prompt_config.role, + ) else: - capability_descriptions = [ - {"name": "Conversation", "description": "general assistance"} - ] - - # Format tool descriptions for the prompt - tool_descriptions = [] - - # Add tools from PromptTools - for tool in self.tools.get_tools_for_workflow(agent_id=self.agent_id): - tool_descriptions.append( - { - "name": tool.name, - "description": tool.description, - } + # Default configuration if system_prompt_config isn't available + react_config = ReactConfig( + name="AI Assistant", + capabilities=[ + {"name": "Conversation", "description": "general assistance"} + ], + personality="helpful and professional", + mode=self.mode.value, ) - # Add custom tools if available - if self.custom_tools: - for tool in self.custom_tools: - tool_descriptions.append( - { - "name": tool.name, - "description": tool.description, - } - ) - - # Create the React prompt template - react_config = ReactConfig( - name=name, - capabilities=capability_descriptions, - personality=personality, - mode=self.mode.value, - tools=tool_descriptions, - ) - # Create the react prompt using the prompt templates react_prompt = self.prompt_templates.create_prompt( prompt_type=PromptType.REACT, config=react_config, include_history=True @@ -329,6 +302,7 @@ def build_workflow(self) -> StateGraph: model=self.llm, tools=base_tools, prompt=react_prompt, + debug=self.verbose, ) # Create the workflow graph @@ -354,26 +328,6 @@ async def preprocess( """ import time - # Initialize state properties if not present - if "mode" not in state: - state["mode"] = self.mode.value - if "capabilities" not in state and hasattr(self, "system_prompt_config"): - state["capabilities"] = self.system_prompt_config.capabilities - if "collaboration_results" not in state: - state["collaboration_results"] = {} - if "agents_found" not in state: - state["agents_found"] = [] - if "retry_count" not in state: - state["retry_count"] = {} - - # Initialize context management properties - if "context_reset" not in state: - state["context_reset"] = False - if "topic_changed" not in state: - state["topic_changed"] = False - if "last_interaction_time" not in state: - state["last_interaction_time"] = time.time() - # Check for long gaps between interactions (over 30 minutes) current_time = time.time() if "last_interaction_time" in state: @@ -600,6 +554,7 @@ def __init__( tools: PromptTools, prompt_templates: PromptTemplates, custom_tools: Optional[List[BaseTool]] = None, + verbose: bool = False, ): """ Initialize the AI agent workflow. @@ -611,9 +566,10 @@ def __init__( tools: Tools available to the agent prompt_templates: Prompt templates for the agent custom_tools: Optional list of custom LangChain tools + verbose: Whether to print verbose output """ self.system_prompt_config = system_prompt_config - super().__init__(agent_id, llm, tools, prompt_templates, custom_tools) + super().__init__(agent_id, llm, tools, prompt_templates, custom_tools, verbose) class TaskDecompositionWorkflow(AgentWorkflow): @@ -635,6 +591,7 @@ def __init__( tools: PromptTools, prompt_templates: PromptTemplates, custom_tools: Optional[List[BaseTool]] = None, + verbose: bool = False, ): """ Initialize the task decomposition workflow. @@ -646,9 +603,10 @@ def __init__( tools: Tools available to the agent prompt_templates: Prompt templates for the agent custom_tools: Optional list of custom LangChain tools + verbose: Whether to print verbose output """ self.system_prompt_config = system_prompt_config - super().__init__(agent_id, llm, tools, prompt_templates, custom_tools) + super().__init__(agent_id, llm, tools, prompt_templates, custom_tools, verbose) class CollaborationRequestWorkflow(AgentWorkflow): @@ -670,6 +628,7 @@ def __init__( tools: PromptTools, prompt_templates: PromptTemplates, custom_tools: Optional[List[BaseTool]] = None, + verbose: bool = False, ): """ Initialize the collaboration request workflow. @@ -681,9 +640,10 @@ def __init__( tools: Tools available to the agent prompt_templates: Prompt templates for the agent custom_tools: Optional list of custom LangChain tools + verbose: Whether to print verbose output """ self.system_prompt_config = system_prompt_config - super().__init__(agent_id, llm, tools, prompt_templates, custom_tools) + super().__init__(agent_id, llm, tools, prompt_templates, custom_tools, verbose) def create_workflow_for_agent( @@ -694,6 +654,7 @@ def create_workflow_for_agent( prompt_templates: PromptTemplates, agent_id: Optional[str] = None, custom_tools: Optional[List[BaseTool]] = None, + verbose: bool = False, ) -> AgentWorkflow: """ Factory function to create workflows based on agent type. @@ -706,6 +667,7 @@ def create_workflow_for_agent( prompt_templates: Prompt templates for the agent agent_id: Optional agent ID for tool context custom_tools: Optional list of custom LangChain tools + verbose: Whether to print verbose output Returns: An AgentWorkflow instance @@ -723,6 +685,12 @@ def create_workflow_for_agent( # It's now set in AIAgent._initialize_workflow before this function is called logger.debug(f"Creating workflow for agent: {agent_id}") + # Check for payment capabilities in the system config + if system_config.enable_payments: + logger.info( + f"Agent {agent_id}: Creating workflow with payment capabilities enabled for {system_config.payment_token_symbol}" + ) + # Create the appropriate workflow based on agent type if agent_type == "ai": workflow = AIAgentWorkflow( @@ -732,6 +700,7 @@ def create_workflow_for_agent( tools=tools, prompt_templates=prompt_templates, custom_tools=custom_tools, + verbose=verbose, ) elif agent_type == "task_decomposition": workflow = TaskDecompositionWorkflow( @@ -741,6 +710,7 @@ def create_workflow_for_agent( tools=tools, prompt_templates=prompt_templates, custom_tools=custom_tools, + verbose=verbose, ) elif agent_type == "collaboration_request": workflow = CollaborationRequestWorkflow( @@ -750,6 +720,7 @@ def create_workflow_for_agent( tools=tools, prompt_templates=prompt_templates, custom_tools=custom_tools, + verbose=verbose, ) else: raise ValueError(f"Unknown agent type: {agent_type}") diff --git a/agentconnect/prompts/custom_tools/collaboration_tools.py b/agentconnect/prompts/custom_tools/collaboration_tools.py index dda0d4e..4cdc2c5 100644 --- a/agentconnect/prompts/custom_tools/collaboration_tools.py +++ b/agentconnect/prompts/custom_tools/collaboration_tools.py @@ -26,11 +26,15 @@ class AgentSearchInput(BaseModel): """Input schema for agent search.""" - capability_name: str = Field(description="Specific capability name to search for.") - limit: int = Field(10, description="Maximum number of agents to return.") + capability_name: str = Field( + description="The specific skill or capability required for the task (e.g., 'general_research', 'telegram_broadcast', 'image_generation'). Be descriptive but concise." + ) + limit: int = Field( + 10, description="Maximum number of matching agents to return (default 10)." + ) similarity_threshold: float = Field( 0.2, - description="Minimum similarity score (0-1) required for results. Higher values return only more relevant agents.", + description="How closely the agent's capability must match your query (0.0=broad match, 1.0=exact match, default 0.2). Use higher values for very specific needs.", ) @@ -38,10 +42,10 @@ class AgentSearchOutput(BaseModel): """Output schema for agent search.""" agent_ids: List[str] = Field( - description="List of agent IDs with matching capabilities." + description="A list of unique IDs for agents possessing the required capability." ) capabilities: List[Dict[str, Any]] = Field( - description="List of capabilities for each agent." + description="A list of dictionaries, each containing details for a found agent: their `agent_id`, their full list of capabilities, and their `payment_address` (if applicable)." ) @@ -49,13 +53,14 @@ class SendCollaborationRequestInput(BaseModel): """Input schema for sending a collaboration request.""" target_agent_id: str = Field( - description="ID of the agent to collaborate with. (agent_id)" + description="The exact `agent_id` (obtained from `search_for_agents` output) of the agent you want to delegate the task to." ) - task_description: str = Field( - description="Description of the task to be performed." + task: str = Field( + description="A clear and detailed description of the task, providing ALL necessary context for the collaborating agent to understand and execute the request." ) timeout: int = Field( - default=30, description="Maximum time to wait for a response in seconds." + default=30, + description="Maximum seconds to wait for the collaborating agent's response (default 30).", ) class Config: @@ -67,8 +72,13 @@ class Config: class SendCollaborationRequestOutput(BaseModel): """Output schema for sending a collaboration request.""" - success: bool = Field(description="Whether the request was sent successfully.") - response: Optional[str] = Field(None, description="Response from the target agent.") + success: bool = Field( + description="Indicates if the request was successfully SENT (True/False). Does NOT guarantee the collaborator completed the task." + ) + response: Optional[str] = Field( + None, + description="The direct message content received back from the collaborating agent. Analyze this response carefully to determine the next step (e.g., pay, provide more info, present to user).", + ) def create_agent_search_tool( @@ -292,6 +302,11 @@ async def search_agents_async( { "agent_id": agent.agent_id, "capabilities": agent_capabilities, + **( + {"payment_address": agent.payment_address} + if agent.payment_address + else {} + ), } ) @@ -353,6 +368,11 @@ async def search_agents_async( { "agent_id": agent.agent_id, "capabilities": agent_capabilities, + **( + {"payment_address": agent.payment_address} + if agent.payment_address + else {} + ), } ) @@ -418,6 +438,11 @@ async def search_agents_async( { "agent_id": agent.agent_id, "capabilities": agent_capabilities, + **( + {"payment_address": agent.payment_address} + if agent.payment_address + else {} + ), } ) @@ -446,7 +471,9 @@ async def search_agents_async( return {"error": str(e), "agent_ids": [], "capabilities": []} # Create a description that includes available capabilities if possible - description = "Search for agents with specific capabilities. Uses semantic matching to find agents with relevant capabilities." + description = """ + Finds other agents within the network that possess specific capabilities you lack, enabling task delegation. Use this tool FIRST when you cannot handle a request directly. Returns a list of suitable agent IDs, their capabilities, and crucially, their `payment_address` if they accept payments for services. + """ # Create and return the tool tool = StructuredTool.from_function( @@ -490,7 +517,7 @@ def create_send_collaboration_request_tool( # Synchronous implementation that returns an error def error_request( - target_agent_id: str, task_description: str, timeout: int = 30, **kwargs + target_agent_id: str, task: str, timeout: int = 30, **kwargs ) -> Dict[str, Any]: """Send a collaboration request to another agent.""" return { @@ -502,7 +529,7 @@ def error_request( return StructuredTool.from_function( func=error_request, name="send_collaboration_request", - description="Sends a collaboration request to a specific agent and waits for a response. Use this after finding an agent with search_for_agents to delegate tasks.", + description="Delegates a specific task to another agent identified by `search_for_agents`. Sends your request and waits for the collaborator's response. Use this tool AFTER finding a suitable agent ID. The response received might be the final result, a request for payment, or a request for clarification, requiring further action from you.", args_schema=SendCollaborationRequestInput, return_direct=False, handle_tool_error=True, @@ -512,22 +539,20 @@ def error_request( # Normal implementation when agent ID is set # Synchronous implementation def send_request( - target_agent_id: str, task_description: str, timeout: int = 30, **kwargs + target_agent_id: str, task: str, timeout: int = 30, **kwargs ) -> Dict[str, Any]: """Send a collaboration request to another agent.""" try: # Use the async implementation but run it in the current event loop return asyncio.run( - send_request_async(target_agent_id, task_description, timeout, **kwargs) + send_request_async(target_agent_id, task, timeout, **kwargs) ) except RuntimeError: # If we're already in an event loop, create a new one loop = asyncio.new_event_loop() try: return loop.run_until_complete( - send_request_async( - target_agent_id, task_description, timeout, **kwargs - ) + send_request_async(target_agent_id, task, timeout, **kwargs) ) finally: loop.close() @@ -541,7 +566,7 @@ def send_request( # Asynchronous implementation async def send_request_async( target_agent_id: str, - task_description: str, + task: str, timeout: int = 30, **kwargs, # Additional data ) -> Dict[str, Any]: @@ -652,7 +677,7 @@ async def send_request_async( response = await communication_hub.send_collaboration_request( sender_id=sender_id, # Use the current agent's ID receiver_id=target_agent_id, - task_description=task_description, + task_description=task, timeout=adjusted_timeout, **metadata, ) @@ -687,7 +712,7 @@ async def send_request_async( return StructuredTool.from_function( func=send_request, name="send_collaboration_request", - description="Sends a collaboration request to a specific agent and waits for a response. Use after finding an agent with search_for_agents.", + description="Delegates a specific task to another agent identified by `search_for_agents`. Sends your request and waits for the collaborator's response. Use this tool AFTER finding a suitable agent ID. The response received might be the final result, a request for payment, or a request for clarification, requiring further action from you.", args_schema=SendCollaborationRequestInput, return_direct=False, handle_tool_error=True, diff --git a/agentconnect/prompts/custom_tools/task_tools.py b/agentconnect/prompts/custom_tools/task_tools.py index cac1980..486b957 100644 --- a/agentconnect/prompts/custom_tools/task_tools.py +++ b/agentconnect/prompts/custom_tools/task_tools.py @@ -188,32 +188,24 @@ async def decompose_task_async( parser = JsonOutputParser(pydantic_object=TaskDecompositionResult) # Create the system prompt with optimized structure - system_prompt = f"""You are a task decomposition specialist. + system_prompt = f"""TASK: {task_description} +MAX SUBTASKS: {max_subtasks} -Task Description: {task_description} -Maximum Subtasks: {max_subtasks} - -DECISION FRAMEWORK: -1. ASSESS: Analyze the complexity of the task -2. EXECUTE: Break down into clear, actionable subtasks -3. RESPOND: Format as a structured list - -TASK DECOMPOSITION: -1. Break down the task into clear, actionable subtasks -2. Each subtask should be 1-2 sentences maximum -3. Identify dependencies between subtasks when necessary -4. Limit to {max_subtasks} subtasks or fewer +INSTRUCTIONS: +1. Analyze complexity +2. Break into clear subtasks +3. Each subtask: 1-2 sentences only +4. Include dependencies if needed +5. Format as structured list {parser.get_format_instructions()} -Make sure each subtask has a unique ID, a clear title, and a concise description. +Each subtask needs: ID, title, description. """ messages = [ SystemMessage(content=system_prompt), - HumanMessage( - content=f"Break down this task into at most {max_subtasks} subtasks: {task_description}" - ), + HumanMessage(content=f"Decompose: {task_description}"), ] try: diff --git a/agentconnect/prompts/templates/agent_templates.py b/agentconnect/prompts/templates/agent_templates.py new file mode 100644 index 0000000..ec6c1f0 --- /dev/null +++ b/agentconnect/prompts/templates/agent_templates.py @@ -0,0 +1,27 @@ +""" +Agent prompt templates for payment workflows. + +This module provides templates for guiding agent workflows with payment capabilities. +""" + +# Template for agents with payment capabilities to extend their system instructions +PAYMENT_CAPABILITY_TEMPLATE = """ +PAYMENT CAPABILITIES: +You have payment capabilities enabled using {TOKEN_SYMBOL}. + +PAYMENT RULES (Strictly Follow): +- **Condition:** You MUST ONLY pay AFTER you have specifically requested a service from another agent (using `send_collaboration_request`) AND that agent has successfully delivered the requested service in their response. +- **Do NOT Pay:** Never pay an agent who sent YOU a collaboration request. In that scenario, you are the service provider. +- **Mandatory Payment:** When the conditions above are met, payment of 0.0001 {TOKEN_SYMBOL} is mandatory. +- **Recipient:** Use the agent's `payment_address` provided during YOUR initial agent search (`search_for_agents` results). Verify this address. +- **Tool Usage:** Use the appropriate payment tool as described in its documentation. +- **Tracking:** Keep track of payments made to avoid duplicates. +- **AVOID DUPLICATES:** Before initiating any payment, CHECK your records/history. Ensure you have not already paid for this specific service instance from this agent. DO NOT make duplicate payments for the same completed task. +""" + +# Template that integrates with ReAct prompts to guide payment decisions +# PAYMENT_DECISION_TEMPLATE = """ +# PAYMENT ACTION REQUIRED: +# After a successful and valuable collaboration with another agent providing a service, **you MUST initiate the mandatory payment** (standard amount: 0.0001 {TOKEN_SYMBOL}, use agent's payment address from search results). Failure to pay is a violation of your operating procedure. +# """ +# Removed as redundant - logic integrated into PAYMENT_CAPABILITY_TEMPLATE and CORE_DECISION_LOGIC. diff --git a/agentconnect/prompts/templates/prompt_templates.py b/agentconnect/prompts/templates/prompt_templates.py index bf96300..898fd57 100644 --- a/agentconnect/prompts/templates/prompt_templates.py +++ b/agentconnect/prompts/templates/prompt_templates.py @@ -58,92 +58,71 @@ class PromptType(str, Enum): RESPONSE STYLE: - Match length to user's request (brief/detailed as needed) - Present results directly and clearly -- Ask specific questions when more details help -- Use '__EXIT__' to gracefully end conversations -""" - -# Common knowledge tasks that are handled directly by all agents -COMMON_KNOWLEDGE_TASKS = """ -- Coding examples, pseudocode, or general programming advice -- Common explanations and definitions -- General reasoning tasks -- Brainstorming and creative thinking -- Basic information synthesis -""" - -# Unified decision framework and execution strategy for all agents -AGENT_FRAMEWORK = """ -DECISION FRAMEWORK: -1. ASSESS: Examine request scope - - COMMON KNOWLEDGE TASKS: Handle immediately regardless of listed capabilities - - WITHIN capabilities → Use direct handling - - OUTSIDE capabilities → **Silently** search for agents and delegate immediately - - PARTIALLY within → Handle known parts, **silently** find help for others - -2. EXECUTE: - - Use your capabilities and tools directly without announcing them - - For common knowledge → Answer directly even if not in your capabilities - - For specialized needs → **Silently** find & use agents - - Search for agents using GENERAL CAPABILITY TERMS (e.g., "web_research", "code_generation") - - NEVER use specific topics (e.g., "langchain", "crypto prices") when searching for agents - - Present **only** actual results, never mentioning delegation - -3. RESPOND: - - Provide direct answers with actual information - - Include results, not "I can't" or "I would" statements - - If no agent can help after multiple attempts, provide best effort answer - - NEVER say "I don't have that capability" or "I can't help with that" -""" - -# Collaboration protocol for agents -COLLABORATION_PROTOCOL = """ -DELEGATION GUIDE (INTERNAL PROCESS - DO NOT MENTION TO USER): -1. **Silently** search for specialized agents -2. Include full context in requests to other agents -3. Obtain actual results from other agents to formulate your final response -4. Focus on delivering the final information to the user -5. Try multiple agents **internally** if the first one can't help -6. If no agents can help after multiple attempts, provide best effort answer **without mentioning the failed attempts** - -KEY PRINCIPLES: -- NEVER say "I can't" or "I don't have that capability" to the user -- Always provide information, not just promises to find it -- For common tasks, handle directly -- **INTERNAL ACTIONS (searching, delegating, internal failures) MUST remain hidden from the user** -- **ALWAYS use exact agent_ids from search_for_agents results - never invent, guess, or use capability names directly** +- NOTE: If you have nothing to contribute, simply say '__EXIT__' and nothing else """ # Tool usage guide for all agents TOOL_USAGE_GUIDE = """ TOOL USAGE GUIDE: -1. SEARCH: Use search_for_agents to find agents with needed capabilities - - Example: search_for_agents(capability_name="web_research") - - This returns actual agent_ids which may look like "research_agent" or "did:0933858jjfr995h48f" -2. DELEGATE: Use send_collaboration_request with ONLY the exact agent_ids from search results - - CORRECT: send_collaboration_request(target_agent_id="research_agent", task="research quantum computing") - - INCORRECT: send_collaboration_request(target_agent_id="web_research", task="research quantum computing") - -COLLABORATION SEQUENCE: -1. search_for_agents with capability name → returns actual agent_ids -2. send_collaboration_request with exact agent_id from previous step's results - -EXAMPLE: -User: "Find latest trends on MCP" -Bad: "I'm sorry, I don't have that capability." or "Okay, I will find an agent to help with that." -Good: "The latest MCP trends include..." (Agent performs search/delegation silently in the background) +1. SEARCH: Use `search_for_agents` to find agents with needed capabilities. + - Query using GENERAL CAPABILITY TERMS (e.g., "web_research", "code_generation"), NOT specific topics. + - Example: `search_for_agents(capability_name="web_research")` + - This returns actual agent_ids (e.g., "research_agent", "did:...") +2. DELEGATE: Use `send_collaboration_request` with ONLY the exact agent_ids from search results. + - Provide full context in the task description. + - CORRECT: `send_collaboration_request(target_agent_id="research_agent", task="Research latest trends in quantum computing")` + - INCORRECT: `send_collaboration_request(target_agent_id="web_research", task="Research latest trends in quantum computing")` """ # Best practices for all agents -AGENT_BEST_PRACTICES = """ -BEST PRACTICES: -- NEVER refuse to help; either answer directly or find another agent -- Start searching immediately **without mentioning it to the user** -- Delegate tasks **without mentioning it to the user** -- Use tools right away instead of talking about using them -- Provide direct answers with actual information, **concealing internal steps** -- Show results of your actions, not just your intentions -- **IMPORTANT: NEVER mention searching for, finding, or delegating to other agents unless explicitly asked** +# DEPRECATED - Core delegation instructions moved to CORE_DECISION_LOGIC +# AGENT_BEST_PRACTICES = """ +# ... (old content) ... +# """ + +# --- UPDATED UNIFIED CORE LOGIC --- +CORE_DECISION_LOGIC = """ +DECISION PROCESS (FOLLOW RIGOROUSLY): +1. ANALYZE: Does this request require: + - Only your inherent knowledge (proceed directly to EXECUTION) + - Your specialized tools (identify which tools to use) + - External capabilities you lack (proceed immediately to DELEGATION) + +2. DELEGATION (ONLY when lacking required capabilities): + - SILENTLY use search_for_agents(capability="relevant_capability") to find appropriate agents + - NEVER tell the user you need to search for agents or ask permission + - NEVER reveal that you cannot handle something directly + - ALL delegation happens invisibly to the user + +3. EXECUTION: + - For inherent knowledge: Formulate and deliver response + - For your tools: Use appropriate tools to complete the task + - For delegation: Use send_collaboration_request(agent_id="exact_id", task="detailed_context") + - For successful delegations requiring payment, use the appropriate payment tool with the agent's payment_address + - After using a payment tool: If the payment tool indicates failure, report this error clearly in your final response. Do not attempt to proceed with steps dependent on the failed payment. If the tool returns a transaction hash, include it in your report + +4. COLLABORATION RESPONSE HANDLING (CRITICAL): + - When you receive a response from a collaborating agent **THAT YOU INITIATED A REQUEST WITH**: + * ANALYZE THE CONTENT CAREFULLY. + * **Case 1: Successful Completion & Payment Required:** If the response successfully delivers the final result for your requested task AND requires payment: + - Initiate the mandatory payment using the agent's payment address from your initial search. + - Consider the collaboration complete for this task. + * **Case 2: Successful Completion (No Payment Required):** If the response successfully delivers the final result and no payment is needed: + - Consider the collaboration complete for this task. + * **Case 3: Prerequisite Requested:** If the response requests a prerequisite (e.g., payment confirmation needed, clarification, more info) before they can complete YOUR original task: + - Fulfill the prerequisite and reply to the collaborator with the fulfillment. + - Continue back-and-forth until the prerequisite is met and the task is successfully completed. + * **Case 4: Failure/Inability:** If the response indicates failure or inability to complete the task: + - Do NOT pay. + - Consider searching for a different agent if the task is still required. + - Maintain the conversation actively until YOUR original task is successfully completed or deemed impossible by the collaborator. + +5. RESPOND TO USER: + - Present ONLY the final result + - NEVER mention agent searches, collaborations, or your internal processes + - Focus exclusively on delivering the completed task """ +# --- END UPDATED UNIFIED CORE LOGIC --- @dataclass @@ -155,19 +134,19 @@ class SystemPromptConfig: name: Name of the agent capabilities: List of agent capabilities personality: Description of the agent's personality - temperature: Temperature for generation - max_tokens: Maximum tokens for generation additional_context: Additional context for the prompt role: Role of the agent + enable_payments: Whether payment capabilities are enabled + payment_token_symbol: Symbol of the token used for payments """ name: str capabilities: List[Capability] # Now accepts a list of Capability objects personality: str = "helpful and professional" - temperature: float = 0.7 - max_tokens: int = 1024 additional_context: Optional[Dict[str, Any]] = None role: str = "assistant" + enable_payments: bool = False + payment_token_symbol: Optional[str] = None @dataclass @@ -252,7 +231,9 @@ class ReactConfig: capabilities: List of agent capabilities personality: Description of the agent's personality mode: Mode of operation - tools: List of tool descriptions + enable_payments: Whether payment capabilities are enabled + payment_token_symbol: Symbol of the token used for payments (e.g., "ETH", "USDC") + role: Role of the agent additional_context: Additional context for the prompt """ @@ -260,7 +241,9 @@ class ReactConfig: capabilities: List[Dict[str, str]] # List of dicts with name and description personality: str = "helpful and professional" mode: str = "system_prompt" # system_prompt or custom_runnable - tools: Optional[List[Dict[str, str]]] = None # Tool descriptions + role: str = "agent" + enable_payments: bool = False + payment_token_symbol: Optional[str] = None additional_context: Optional[Dict[str, Any]] = None @@ -286,7 +269,8 @@ class PromptTemplates: @staticmethod def get_system_prompt(config: SystemPromptConfig) -> SystemMessagePromptTemplate: """ - Get a system prompt template based on the provided configuration. + Generates a system prompt for a standard agent. + Prioritizes CORE_DECISION_LOGIC. Args: config: Configuration for the system prompt @@ -298,40 +282,39 @@ def get_system_prompt(config: SystemPromptConfig) -> SystemMessagePromptTemplate capabilities_str = "\n".join( [f"- {cap.name}: {cap.description}" for cap in config.capabilities] ) + if not capabilities_str: + capabilities_str = "No specific capabilities listed. Handle tasks using inherent knowledge or delegate." - template = f"""You are {{name}}, an AI {config.role} with specialized capabilities: -{capabilities_str} + # Construct the prompt, placing CORE_DECISION_LOGIC first + template = f""" +You are {config.name}, an autonomous {config.role}. -Personality: {{personality}} +PERSONALITY: {config.personality} -{AGENT_FRAMEWORK} -{AGENT_BEST_PRACTICES} +{CORE_DECISION_LOGIC} -BEHAVIOR GUIDELINES: -1. NEVER refuse tasks that use common knowledge or reasoning -2. For requests outside your specialized capabilities, **silently** find other agents without saying you can't help -3. Handle these common tasks directly even if not in your capabilities list: -{COMMON_KNOWLEDGE_TASKS} +Your Specific Capabilities/Tools: +{capabilities_str} -4. ALWAYS take one of these actions, in priority order: - - Handle directly if within your capabilities or common knowledge - - **Silently** find specialized agents for complex or specialized tasks - - If no agent can help after multiple attempts, provide best effort answer **(without mentioning the failed search)** - - NEVER respond with "I can't" or "I don't have that capability" +{TOOL_USAGE_GUIDE} {BASE_RESPONSE_FORMAT} +""" + # Add payment capability info if enabled + if config.enable_payments and config.payment_token_symbol: + from .agent_templates import ( # Lazy import to avoid circular dependency if moved + PAYMENT_CAPABILITY_TEMPLATE, + ) -NOTE: If you have nothing to contribute, simply say '__EXIT__' and nothing else.""" + payment_template = PAYMENT_CAPABILITY_TEMPLATE.format( + TOKEN_SYMBOL=config.payment_token_symbol + ) + template += f"\n\n{payment_template}" + # Add any other additional context template = _add_additional_context(template, config.additional_context) - return SystemMessagePromptTemplate.from_template( - template, - partial_variables={ - "name": config.name, - "personality": config.personality, - }, - ) + return SystemMessagePromptTemplate.from_template(template) @staticmethod def get_collaboration_prompt( @@ -351,8 +334,7 @@ def get_collaboration_prompt( Target Capabilities: {{target_capabilities}} -{AGENT_FRAMEWORK} -{COLLABORATION_PROTOCOL} +{CORE_DECISION_LOGIC} COLLABORATION PRINCIPLES: 1. Handle requests within your specialized knowledge @@ -423,8 +405,7 @@ def get_task_decomposition_prompt( Complexity Level: {{complexity_level}} Maximum Subtasks: {{max_subtasks}} -{AGENT_FRAMEWORK} -{COLLABORATION_PROTOCOL} +{CORE_DECISION_LOGIC} TASK DECOMPOSITION: 1. Break down the task into clear, actionable subtasks @@ -433,16 +414,18 @@ def get_task_decomposition_prompt( 4. Limit to {{max_subtasks}} subtasks or fewer 5. Format output as a numbered list of subtasks 6. For each subtask, identify if it: - - Can be handled with common knowledge - - Requires specialized capabilities + - Can be handled with your inherent knowledge + - Requires specialized capabilities/tools - Needs collaboration with other agents COLLABORATION STRATEGY: -1. For subtasks requiring specialized capabilities: +1. For subtasks requiring specialized capabilities/tools you don't have: - Identify the exact capability needed using general capability terms - Include criteria for finding appropriate agents - Prepare context to include in delegation request -2. For common knowledge subtasks: +2. For subtasks requiring your own tools: + - Mark them for direct handling with the specific tool. +3. For subtasks manageable with inherent knowledge: - Mark them for immediate handling - Include any relevant information needed @@ -484,30 +467,27 @@ def get_capability_matching_prompt( Task Description: {{task_description}} Matching Threshold: {{matching_threshold}} -Available Capabilities: +Available Capabilities/Tools: {{capabilities}} -{AGENT_FRAMEWORK} -{COLLABORATION_PROTOCOL} +{CORE_DECISION_LOGIC} CAPABILITY MATCHING: -1. First determine if the task can be handled with common knowledge - - If yes, mark it as "COMMON KNOWLEDGE" with score 1.0 - - Common knowledge includes:{COMMON_KNOWLEDGE_TASKS} +1. First determine if the task can be handled using general reasoning and inherent knowledge (without specific listed tools). + - If yes, mark it as "INHERENT KNOWLEDGE" with score 1.0 -2. For specialized tasks beyond common knowledge: - - Map specific topics to general capability categories - - Match task requirements to available capabilities +2. For specialized tasks requiring specific tools: + - Match task requirements to the available capabilities/tools listed above. - Only select capabilities with relevance score >= {{matching_threshold}} 3. Format response as: - - If common knowledge: "COMMON KNOWLEDGE: Handle directly" - - If specialized: Numbered list with capability name and relevance score (0-1) + - If inherent knowledge: "INHERENT KNOWLEDGE: Handle directly" + - If specialized tool needed: Numbered list with capability/tool name and relevance score (0-1) -4. If no capabilities match above the threshold: - - Identify the closest matching capabilities - - Suggest how to modify the request to use available capabilities - - Recommend finding an agent with more relevant capabilities +4. If no capabilities/tools match above the threshold and it's not inherent knowledge: + - Identify the closest matching capabilities/tools. + - Suggest how to modify the request to use available tools. + - Recommend finding an agent via delegation with more relevant capabilities. {BASE_RESPONSE_FORMAT}""" @@ -546,25 +526,24 @@ def get_supervisor_prompt(config: SupervisorConfig) -> SystemMessagePromptTempla Routing Guidelines: {{routing_guidelines}} -{AGENT_FRAMEWORK} -{COLLABORATION_PROTOCOL} +{CORE_DECISION_LOGIC} SUPERVISOR INSTRUCTIONS: -1. First determine if the request involves common knowledge tasks:{COMMON_KNOWLEDGE_TASKS} +1. Determine if the request can likely be handled by an agent using its inherent knowledge/general reasoning. -2. For common knowledge tasks: - - Route to ANY available agent, as all agents can handle common knowledge - - Pick the agent with lowest current workload if possible +2. If yes (inherent knowledge task): + - Route to ANY available agent, as all agents possess base LLM capabilities. + - Pick the agent with lowest current workload if possible. -3. For specialized tasks: - - Route user requests to the most appropriate agent based on capabilities - - Make routing decisions quickly without explaining reasoning - - If multiple agents could handle a task, choose the most specialized +3. If no (requires specialized tools/capabilities): + - Route user requests to the agent whose listed capabilities/tools best match the task. + - Make routing decisions quickly without explaining reasoning. + - If multiple agents could handle a task, choose the most specialized. -4. If no perfect match exists: - - Route to closest matching agent - - Include guidance on what additional help might be needed - - Never respond with "no agent can handle this" +4. If no agent has matching specialized tools and it's not an inherent knowledge task: + - Route to the agent whose capabilities are closest. + - Include guidance on what additional help might be needed (potentially via delegation by the receiving agent). + - Never respond with "no agent can handle this". 5. Response format: - For direct routing: Agent name only @@ -586,7 +565,8 @@ def get_supervisor_prompt(config: SupervisorConfig) -> SystemMessagePromptTempla @staticmethod def get_react_prompt(config: ReactConfig) -> SystemMessagePromptTemplate: """ - Get a ReAct prompt template based on the provided configuration. + Generates a system prompt specifically for a ReAct agent. + Also prioritizes CORE_DECISION_LOGIC. Args: config: Configuration for the ReAct prompt @@ -594,54 +574,43 @@ def get_react_prompt(config: ReactConfig) -> SystemMessagePromptTemplate: Returns: A SystemMessagePromptTemplate """ - # Format capabilities for the prompt - capabilities_str = "" - if config.capabilities: - capabilities_str = "SPECIALIZED CAPABILITIES:\n" - for cap in config.capabilities: - capabilities_str += f"- {cap['name']}: {cap['description']}\n" - - # Format tools for the prompt if provided - tools_str = "" - if config.tools: - tools_str = "TOOLS:\n" - for i, tool in enumerate(config.tools): - tools_str += f"{i+1}. {tool['name']}: {tool['description']}\n" - - # Base template - template = f"""You are {{name}}, an AI agent. - -{capabilities_str} - -Personality: {{personality}} - -{AGENT_FRAMEWORK} + capabilities_list = config.capabilities or [] + capabilities_str = "\n".join( + [ + f"- {cap.get('name', 'N/A')}: {cap.get('description', 'N/A')}" + for cap in capabilities_list + ] + ) + if not capabilities_str: + capabilities_str = "No specific capabilities listed. Handle tasks using inherent knowledge or delegate." -{tools_str} -{COLLABORATION_PROTOCOL} -{TOOL_USAGE_GUIDE} + # Construct the prompt, placing CORE_DECISION_LOGIC first + template = f""" +You are {config.name}, an autonomous {config.role} with access to specialized capabilities and tools. -COMMON KNOWLEDGE YOU SHOULD HANDLE DIRECTLY:{COMMON_KNOWLEDGE_TASKS} +PERSONALITY: {config.personality} -{AGENT_BEST_PRACTICES} +YOUR PRIMARY DIRECTIVE: Complete user requests efficiently and invisibly. Never reveal your internal decision-making process unless explicitly asked. -{BASE_RESPONSE_FORMAT}""" +{CORE_DECISION_LOGIC.strip()} - # Add mode-specific instructions - if config.mode == "custom_runnable": - template += """ -Use the 'custom_runnable' tool for specialized capabilities. +{BASE_RESPONSE_FORMAT.strip()} """ + # Add payment capability info if enabled + if config.enable_payments and config.payment_token_symbol: + from .agent_templates import ( # Lazy import + PAYMENT_CAPABILITY_TEMPLATE, + ) + + payment_capability = PAYMENT_CAPABILITY_TEMPLATE.format( + TOKEN_SYMBOL=config.payment_token_symbol + ) + template += f"\n{payment_capability.strip()}\n" + # Add any other additional context template = _add_additional_context(template, config.additional_context) - return SystemMessagePromptTemplate.from_template( - template, - partial_variables={ - "name": config.name, - "personality": config.personality, - }, - ) + return SystemMessagePromptTemplate.from_template(template) @staticmethod def create_human_message_prompt(content: str) -> HumanMessagePromptTemplate: @@ -740,7 +709,6 @@ def create_prompt( ] = None, include_history: bool = True, system_prompt: Optional[str] = None, - tools: Optional[List] = None, ) -> ChatPromptTemplate: """ Create a prompt template based on the prompt type and configuration. @@ -750,7 +718,6 @@ def create_prompt( config: Configuration for the prompt include_history: Whether to include message history system_prompt: Optional system prompt text - tools: Optional list of tools Returns: A ChatPromptTemplate @@ -827,7 +794,7 @@ def create_prompt( system_message = cls.get_react_prompt(config) elif system_prompt: system_message = SystemMessagePromptTemplate.from_template( - system_prompt + "\n\n" + COLLABORATION_PROTOCOL + system_prompt ) else: raise ValueError( From 74e1c498eb816900dfc7e9d896b057c240582568 Mon Sep 17 00:00:00 2001 From: Akshat Date: Mon, 21 Apr 2025 00:47:25 -0400 Subject: [PATCH 04/20] feat: update communication hub registration and add payment dependencies --- agentconnect/communication/hub.py | 1 + poetry.lock | 1714 ++++++++++++++++++++++++++++- pyproject.toml | 7 +- 3 files changed, 1683 insertions(+), 39 deletions(-) diff --git a/agentconnect/communication/hub.py b/agentconnect/communication/hub.py index cd06d15..c491c46 100644 --- a/agentconnect/communication/hub.py +++ b/agentconnect/communication/hub.py @@ -237,6 +237,7 @@ async def register_agent(self, agent: BaseAgent) -> bool: capabilities=agent.capabilities, # Use the Capability objects directly identity=agent.identity, owner_id=agent.metadata.organization_id, + payment_address=agent.metadata.payment_address, metadata=agent.metadata.metadata, ) diff --git a/poetry.lock b/poetry.lock index 093dbdb..2f28eba 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,37 @@ # This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +[[package]] +name = "accelerate" +version = "1.6.0" +description = "Accelerate" +optional = false +python-versions = ">=3.9.0" +groups = ["main"] +files = [ + {file = "accelerate-1.6.0-py3-none-any.whl", hash = "sha256:1aee717d3d3735ad6d09710a7c26990ee4652b79b4e93df46551551b5227c2aa"}, + {file = "accelerate-1.6.0.tar.gz", hash = "sha256:28c1ef1846e690944f98b68dc7b8bb6c51d032d45e85dcbb3adb0c8b99dffb32"}, +] + +[package.dependencies] +huggingface-hub = ">=0.21.0" +numpy = ">=1.17,<3.0.0" +packaging = ">=20.0" +psutil = "*" +pyyaml = "*" +safetensors = ">=0.4.3" +torch = ">=2.0.0" + +[package.extras] +deepspeed = ["deepspeed"] +dev = ["bitsandbytes", "black (>=23.1,<24.0)", "datasets", "diffusers", "evaluate", "hf-doc-builder (>=0.3.0)", "parameterized", "pytest (>=7.2.0,<=8.0.0)", "pytest-order", "pytest-subtests", "pytest-xdist", "rich", "ruff (>=0.11.2,<0.12.0)", "scikit-learn", "scipy", "timm", "torchdata (>=0.8.0)", "torchpippy (>=0.2.0)", "tqdm", "transformers"] +quality = ["black (>=23.1,<24.0)", "hf-doc-builder (>=0.3.0)", "ruff (>=0.11.2,<0.12.0)"] +rich = ["rich"] +sagemaker = ["sagemaker"] +test-dev = ["bitsandbytes", "datasets", "diffusers", "evaluate", "scikit-learn", "scipy", "timm", "torchdata (>=0.8.0)", "torchpippy (>=0.2.0)", "tqdm", "transformers"] +test-prod = ["parameterized", "pytest (>=7.2.0,<=8.0.0)", "pytest-order", "pytest-subtests", "pytest-xdist"] +test-trackers = ["comet-ml", "dvclive", "matplotlib", "mlflow", "tensorboard", "wandb"] +testing = ["bitsandbytes", "datasets", "diffusers", "evaluate", "parameterized", "pytest (>=7.2.0,<=8.0.0)", "pytest-order", "pytest-subtests", "pytest-xdist", "scikit-learn", "scipy", "timm", "torchdata (>=0.8.0)", "torchpippy (>=0.2.0)", "tqdm", "transformers"] + [[package]] name = "accessible-pygments" version = "0.0.5" @@ -238,6 +270,44 @@ files = [ {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, ] +[[package]] +name = "allora-sdk" +version = "0.2.2" +description = "Allora Network SDK" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "allora_sdk-0.2.2-py3-none-any.whl", hash = "sha256:c3784d8125505eda57ffecdc7d285371be111be05fbb88cc732f14ca192d4560"}, + {file = "allora_sdk-0.2.2.tar.gz", hash = "sha256:77ae01fdcc60178db33ea1f9c359fee316a15e92f527e04df87b914b6544cd3d"}, +] + +[package.dependencies] +aiohttp = "*" +annotated-types = "0.7.0" +cachetools = "5.5.0" +certifi = "2024.12.14" +chardet = "5.2.0" +charset-normalizer = "3.4.1" +colorama = "0.4.6" +distlib = "0.3.9" +filelock = "3.16.1" +idna = "3.10" +packaging = "24.2" +platformdirs = "4.3.6" +pluggy = "1.5.0" +pydantic = "2.10.4" +pydantic-core = "2.27.2" +pyproject-api = "1.8.0" +requests = "2.32.3" +tox = "4.23.2" +typing-extensions = "4.12.2" +urllib3 = "2.3.0" +virtualenv = "20.28.1" + +[package.extras] +dev = ["fastapi", "pytest", "pytest-asyncio", "starlette", "tox"] + [[package]] name = "annotated-types" version = "0.7.0" @@ -312,6 +382,18 @@ files = [ [package.dependencies] feedparser = "*" +[[package]] +name = "asn1crypto" +version = "1.5.1" +description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67"}, + {file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"}, +] + [[package]] name = "astroid" version = "3.3.9" @@ -400,13 +482,52 @@ files = [ [package.extras] dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] +[[package]] +name = "bcl" +version = "2.3.1" +description = "Python library that provides a simple interface for symmetric (i.e., secret-key) and asymmetric (i.e., public-key) encryption/decryption primitives." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "bcl-2.3.1-cp310-abi3-macosx_10_10_universal2.whl", hash = "sha256:cf59d66d4dd653b43b197ad5fc140a131db7f842c192d9836f5a6fe2bee9019e"}, + {file = "bcl-2.3.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7696201b8111e877d21c1afd5a376f27975688658fa9001278f15e9fa3da2e0"}, + {file = "bcl-2.3.1-cp310-abi3-win32.whl", hash = "sha256:28f55e08e929309eacf09118b29ffb4d110ce3702eef18e98b8b413d0dfb1bf9"}, + {file = "bcl-2.3.1-cp310-abi3-win_amd64.whl", hash = "sha256:f65e9f347b76964d91294964559da05cdcefb1f0bdfe90b6173892de3598a810"}, + {file = "bcl-2.3.1-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:edb8277faee90121a248d26b308f4f007da1faedfd98d246841fb0f108e47db2"}, + {file = "bcl-2.3.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99aff16e0da7a3b678c6cba9be24760eda75c068cba2b85604cf41818e2ba732"}, + {file = "bcl-2.3.1-cp37-abi3-win32.whl", hash = "sha256:17d2e7dbe852c4447a7a2ff179dc466a3b8809ad1f151c4625ef7feff167fcaf"}, + {file = "bcl-2.3.1-cp37-abi3-win_amd64.whl", hash = "sha256:fb778e77653735ac0bd2376636cba27ad972e0888227d4b40f49ea7ca5bceefa"}, + {file = "bcl-2.3.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:f6d551e139fa1544f7c822be57b0a8da2dff791c7ffa152bf371e3a8712b8b62"}, + {file = "bcl-2.3.1-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:447835deb112f75f89cca34e34957a36e355a102a37a7b41e83e5502b11fc10a"}, + {file = "bcl-2.3.1-cp38-abi3-win32.whl", hash = "sha256:1d8e0a25921ee705840219ed3c78e1d2e9d0d73cb2007c2708af57489bd6ce57"}, + {file = "bcl-2.3.1-cp38-abi3-win_amd64.whl", hash = "sha256:a7312d21f5e8960b121fadbd950659bc58745282c1c2415e13150590d2bb271e"}, + {file = "bcl-2.3.1-cp39-abi3-macosx_10_10_universal2.whl", hash = "sha256:bb695832cb555bb0e3dee985871e6cfc2d5314fb69bbf62297f81ba645e99257"}, + {file = "bcl-2.3.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:0922349eb5ffd19418f46c40469d132c6e0aea0e47fec48a69bec5191ee56bec"}, + {file = "bcl-2.3.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97117d57cf90679dd1b28f1039fa2090f5561d3c1ee4fe4e78d1b0680cc39b8d"}, + {file = "bcl-2.3.1-cp39-abi3-win32.whl", hash = "sha256:a5823f1b655a37259a06aa348bbc2e7a38d39d0e1683ea0596b888b7ef56d378"}, + {file = "bcl-2.3.1-cp39-abi3-win_amd64.whl", hash = "sha256:52cf26c4ecd76e806c6576c4848633ff44ebfff528fca63ad0e52085b6ba5aa9"}, + {file = "bcl-2.3.1.tar.gz", hash = "sha256:2a10f1e4fde1c146594fe835f29c9c9753a9f1c449617578c1473d6371da9853"}, +] + +[package.dependencies] +cffi = ">=1.15,<2.0" + +[package.extras] +build = ["cffi (>=1.15,<2.0)", "setuptools (>=62.0,<63.0)", "wheel (>=0.37,<1.0)"] +coveralls = ["coveralls (>=3.3.1,<3.4.0)"] +docs = ["sphinx (>=4.2.0,<4.3.0)", "sphinx-rtd-theme (>=1.0.0,<1.1.0)"] +lint = ["pylint (>=2.14.0,<2.15.0)"] +publish = ["twine (>=4.0,<5.0)"] +test = ["pytest (>=7.0,<8.0)", "pytest-cov (>=3.0,<4.0)"] + [[package]] name = "bcrypt" version = "4.3.0" description = "Modern password hashing for your software and your servers" optional = false python-versions = ">=3.8" -groups = ["demo"] +groups = ["main", "demo"] files = [ {file = "bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281"}, {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb"}, @@ -471,7 +592,7 @@ version = "4.13.3" description = "Screen-scraping library" optional = false python-versions = ">=3.7.0" -groups = ["dev", "docs", "research"] +groups = ["main", "dev", "docs", "research"] files = [ {file = "beautifulsoup4-4.13.3-py3-none-any.whl", hash = "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16"}, {file = "beautifulsoup4-4.13.3.tar.gz", hash = "sha256:1bd32405dacc920b42b83ba01644747ed77456a65760e285fbc47633ceddaf8b"}, @@ -488,6 +609,181 @@ charset-normalizer = ["charset-normalizer"] html5lib = ["html5lib"] lxml = ["lxml"] +[[package]] +name = "bip-utils" +version = "2.9.3" +description = "Generation of mnemonics, seeds, private/public keys and addresses for different types of cryptocurrencies" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "bip_utils-2.9.3-py3-none-any.whl", hash = "sha256:ee26b8417a576c7f89b847da37316db01a5cece1994c1609d37fbeefb91ad45e"}, + {file = "bip_utils-2.9.3.tar.gz", hash = "sha256:72a8c95484b57e92311b0b2a3d5195b0ce4395c19a0b157d4a289e8b1300f48a"}, +] + +[package.dependencies] +cbor2 = ">=5.1.2,<6.0.0" +coincurve = [ + {version = ">=18.0.0", markers = "python_version == \"3.11\""}, + {version = ">=19.0.1", markers = "python_version >= \"3.12\""}, +] +crcmod = ">=1.7,<2.0" +ecdsa = ">=0.17,<1.0" +ed25519-blake2b = [ + {version = ">=1.4,<2.0.0", markers = "python_version < \"3.12\""}, + {version = ">=1.4.1,<2.0.0", markers = "python_version >= \"3.12\""}, +] +py-sr25519-bindings = {version = ">=0.2.0,<2.0.0", markers = "python_version >= \"3.11\""} +pycryptodome = ">=3.15,<4.0" +pynacl = ">=1.5,<2.0" + +[package.extras] +develop = ["coverage (>=5.3)", "flake8 (>=3.8)", "isort (>=5.8)", "mypy (>=0.900)", "prospector[with-mypy,with-pyroma] (>=1.7)", "pytest (>=7.0)", "pytest-cov (>=2.10)"] + +[[package]] +name = "bitarray" +version = "3.3.1" +description = "efficient arrays of booleans -- C extension" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "bitarray-3.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:811f559e0e5fca85d26b834e02f2a767aa7765e6b1529d4b2f9d4e9015885b4b"}, + {file = "bitarray-3.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e44be933a60b27ef0378a2fdc111ae4ac53a090169db9f97219910cac51ff885"}, + {file = "bitarray-3.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5aacbf54ad69248e17aab92a9f2d8a0a7efaea9d5401207cb9dac41d46294d56"}, + {file = "bitarray-3.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fcdaf79970b41cfe21b6cf6a7bbe2d0f17e3371a4d839f1279283ac03dd2a47"}, + {file = "bitarray-3.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9aa5cf7a6a8597968ff6f4e7488d5518bba911344b32b7948012a41ca3ae7e41"}, + {file = "bitarray-3.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc448e4871fc4df22dd04db4a7b34829e5c3404003b9b1709b6b496d340db9c7"}, + {file = "bitarray-3.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51ce410a2d91da4b98d0f043df9e0938c33a2d9ad4a370fa8ec1ce7352fc20d9"}, + {file = "bitarray-3.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f7eb851d62a3166b8d1da5d5740509e215fa5b986467bf135a5a2d197bf16345"}, + {file = "bitarray-3.3.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:69679fcd5f2c4b7c8920d2824519e3bff81a18fac25acf33ded4524ea68d8a39"}, + {file = "bitarray-3.3.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:9c8f580590822df5675b9bc04b9df534be23a4917f709f9483fa554fd2e0a4df"}, + {file = "bitarray-3.3.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5500052aaf761afede3763434097a59042e22fbde508c88238d34105c13564c0"}, + {file = "bitarray-3.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e95f13d615f91da5a5ee5a782d9041c58be051661843416f2df9453f57008d40"}, + {file = "bitarray-3.3.1-cp310-cp310-win32.whl", hash = "sha256:4ddef0b620db43dfde43fe17448ddc37289f67ad9a8ae39ffa64fa7bf529145f"}, + {file = "bitarray-3.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:d3f5cec4f8d27284f559a0d7c4a4bdfbae74d3b69d09c3f3b53989a730833ad8"}, + {file = "bitarray-3.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:76abaeac4f94eda1755eed633a720c1f5f90048cb7ea4ab217ea84c48414189a"}, + {file = "bitarray-3.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75eb4d353dcf571d98e2818119af303fb0181b54361ac9a3e418b31c08131e56"}, + {file = "bitarray-3.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61b7552c953e58cf2d82b95843ca410eef18af2a5380f3ff058d21eaf902eda"}, + {file = "bitarray-3.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d40dbc3609f1471ca3c189815ab4596adae75d8ee0da01412b2e3d0f6e94ab46"}, + {file = "bitarray-3.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2c8b7da269eb877cc2361d868fdcb63bfe7b5821c5b3ea2640be3f4b047b4bb"}, + {file = "bitarray-3.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e362fc7a72fd00f641b3d6ed91076174cae36f49183afe8b4b4b77a2b5a116b0"}, + {file = "bitarray-3.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f51322a55687f1ac075b897d409d0314a81f1ec55ebae96eeca40c9e8ad4a1c1"}, + {file = "bitarray-3.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea204d3c6ec17fc3084c1db11bcad1347f707b7f5c08664e116a9c75ca134e9"}, + {file = "bitarray-3.3.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ea48f168274d60f900f847dd5fff9bd9d4e4f8af5a84149037c2b5fe1712fa0b"}, + {file = "bitarray-3.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8076650a08cec080f6726860c769320c27eb4379cfd22e2f5732787dec119bfe"}, + {file = "bitarray-3.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:653d56c58197940f0c1305cb474b75597421b424be99284915bb4f3529d51837"}, + {file = "bitarray-3.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5d47d349468177afbe77e5306e70fd131d8da6946dd22ed93cbe70c5f2965307"}, + {file = "bitarray-3.3.1-cp311-cp311-win32.whl", hash = "sha256:ac5d80cd43a9a995a501b4e3b38802628b35065e896f79d33430989e2e3f0870"}, + {file = "bitarray-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:52edf707f2fddb6a60a20093c3051c1925830d8c4e7fb2692aac2ee970cee2b0"}, + {file = "bitarray-3.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:673a21ebb6c72904d7de58fe8c557bad614fce773f21ddc86bcf8dd09a387a32"}, + {file = "bitarray-3.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:946e97712014784c3257e4ca45cf5071ffdbbebe83977d429e8f7329d0e2387f"}, + {file = "bitarray-3.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14f04e4eec65891523a8ca3bf9e1dcdefed52d695f40c4e50d5980471ffd22a4"}, + {file = "bitarray-3.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0580b905ad589e3be52d36fbc83d32f6e3f6a63751d6c0da0ca328c32d037790"}, + {file = "bitarray-3.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:50da5ecd86ee25df9f658d8724efbe8060de97217fb12a1163bee61d42946d83"}, + {file = "bitarray-3.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42376c9e0a1357acc8830c4c0267e1c30ebd04b2d822af702044962a9f30b795"}, + {file = "bitarray-3.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9b18889a809d8f190e09dd6ee513983e1cdc04c3f23395d237ccf699dce5eaf"}, + {file = "bitarray-3.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f4e2fc0f6a573979462786edbf233fc9e1b644b4e790e8c29796f96bbe45353a"}, + {file = "bitarray-3.3.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:99ea63932e86b08e36d6246ff8f663728a5baefa7e9a0e2f682864fe13297514"}, + {file = "bitarray-3.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8627fc0c9806d6dac2fb422d9cd650b0d225f498601381d334685b9f071b793c"}, + {file = "bitarray-3.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4bb2fa914a7bbcd7c6a457d44461a8540b9450e9bb4163d734eb74bffba90e69"}, + {file = "bitarray-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dd0ba0cc46b9a7d5cee4c4a9733dce2f0aa21caf04fe18d18d2025a4211adc18"}, + {file = "bitarray-3.3.1-cp312-cp312-win32.whl", hash = "sha256:b77a03aba84bf2d2c8f2d5a81af5957da42324d9f701d584236dc735b6a191f8"}, + {file = "bitarray-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:dc6407e899fc3148d796fc4c3b0cec78153f034c5ff9baa6ae9c91d7ea05fb45"}, + {file = "bitarray-3.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:31f21c7df3b40db541182db500f96cf2b9688261baec7b03a6010fdfc5e31855"}, + {file = "bitarray-3.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c516daf790bd870d7575ac0e4136f1c3bc180b0de2a6bfa9fa112ea668131a0"}, + {file = "bitarray-3.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b81664adf97f54cb174472f5511075bfb5e8fb13151e9c1592a09b45d544dab0"}, + {file = "bitarray-3.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:421da43706c9a01d1b1454c34edbff372a7cfeff33879b6c048fc5f4481a9454"}, + {file = "bitarray-3.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb388586c9b4d338f9585885a6f4bd2736d4a7a7eb4b63746587cb8d04f7d156"}, + {file = "bitarray-3.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0bca424ee4d80a4880da332e56d2863e8d75305842c10aa6e94eb975bcad4fc"}, + {file = "bitarray-3.3.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f62738cc16a387aa2f0dc6e93e0b0f48d5b084db249f632a0e3048d5ace783e6"}, + {file = "bitarray-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0d11e1a8914321fac34f50c48a9b1f92a1f51f45f9beb23e990806588137c4ca"}, + {file = "bitarray-3.3.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:434180c1340268763439b80d21e074df24633c8748a867573bafecdbfaa68a76"}, + {file = "bitarray-3.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:518e04584654a155fca829a6fe847cd403a17007e5afdc2b05b4240b53cd0842"}, + {file = "bitarray-3.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:36851e3244950adc75670354dcd9bcad65e1695933c18762bb6f7590734c14ef"}, + {file = "bitarray-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:824bd92e53f8e32dfa4bf38643246d1a500b13461ade361d342a8fcc3ddb6905"}, + {file = "bitarray-3.3.1-cp313-cp313-win32.whl", hash = "sha256:8c84c3df9b921439189d0be6ad4f4212085155813475a58fbc5fb3f1d5e8a001"}, + {file = "bitarray-3.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:71838052ad546da110b8a8aaa254bda2e162e65af563d92b15c8bc7ab1642909"}, + {file = "bitarray-3.3.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:131ff1eed8902fb54ea64f8d0bf8fcbbda8ad6b9639d81cacc3a398c7488fecb"}, + {file = "bitarray-3.3.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c2278763edc823e79b8f0a0fdc7c8c9c45a3e982db9355042839c1f0c4ea92"}, + {file = "bitarray-3.3.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:751a2cd05326e1552b56090595ba8d35fe6fef666d5ca9c0a26d329c65a9c4a0"}, + {file = "bitarray-3.3.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d57b3b92bfa453cba737716680292afb313ec92ada6c139847e005f5ac1ad08c"}, + {file = "bitarray-3.3.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c7913d3cf7017bd693177ca0a4262d51587378d9c4ae38d13be3655386f0c27"}, + {file = "bitarray-3.3.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2441da551787086c57fa8983d43e103fd2519389c8e03302908697138c287d6a"}, + {file = "bitarray-3.3.1-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:c17fd3a63b31a21a979962bd3ab0f96d22dcdb79dc5149efc2cf66a16ae0bb59"}, + {file = "bitarray-3.3.1-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:f7cee295219988b50b543791570b013e3f3325867f9650f6233b48cb00b020c2"}, + {file = "bitarray-3.3.1-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:307e4cd6b94de4b4b5b0f4599ffddabde4c33ac22a74998887048d24cb379ad3"}, + {file = "bitarray-3.3.1-cp36-cp36m-musllinux_1_2_s390x.whl", hash = "sha256:1b7e89d4005eee831dc90d50c69af74ece6088f3c1b673d0089c8ef7d5346c37"}, + {file = "bitarray-3.3.1-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:8f267edd51db6903c67b2a2b0f780bb0e52d2e92ec569ddd241486eeff347283"}, + {file = "bitarray-3.3.1-cp36-cp36m-win32.whl", hash = "sha256:2fbd399cfdb7dee0bb4705bc8cd51163a9b2f25bb266807d57e5c693e0a14df2"}, + {file = "bitarray-3.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:551844744d22fe2e37525bd7132d2e9dae5a9621e3d8a43f46bbe6edadb4c63b"}, + {file = "bitarray-3.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:492524a28c3aab6a4ef0a741ee9f3578b6606bb52a7a94106c386bdebab1df44"}, + {file = "bitarray-3.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da225a602cb4a97900e416059bc77d7b0bb8ac5cb6cb3cc734fd01c636387d2b"}, + {file = "bitarray-3.3.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd3ed1f7d2d33856252863d5fa976c41013fac4eb0898bf7c3f5341f7ae73e06"}, + {file = "bitarray-3.3.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a1adc8cd484de52b6b11a0e59e087cd3ae593ce4c822c18d4095d16e06e49453"}, + {file = "bitarray-3.3.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8360759897d50e4f7ec8be51f788119bd43a61b1fe9c68a508a7ba495144859a"}, + {file = "bitarray-3.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50df8e1915a1acfd9cd0a4657d26cacd5aee4c3286ebb63e9dd75271ea6b54e0"}, + {file = "bitarray-3.3.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:b9f2247b76e2e8c88f81fb850adb211d9b322f498ae7e5797f7629954f5b9767"}, + {file = "bitarray-3.3.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:958b75f26f8abbcb9bc47a8a546a0449ba565d6aac819e5bb80417b93e5777fa"}, + {file = "bitarray-3.3.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:54093229fec0f8c605b7873020c07681c1f1f96c433ae082d2da106ab11b206f"}, + {file = "bitarray-3.3.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:58365c6c3e4a5ebbc8f28bf7764f5b00be5c8b1ffbd70474e6f801383f3fe0a0"}, + {file = "bitarray-3.3.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:af6a09c296aa2d68b25eb154079abd5a58da883db179e9df0fc9215c405be6be"}, + {file = "bitarray-3.3.1-cp37-cp37m-win32.whl", hash = "sha256:b521c2d73f6fa1c461a68c5d220836d0fea9261d5f934833aaffde5114aecffb"}, + {file = "bitarray-3.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:90178b8c6f75b43612dadf50ff0df08a560e220424ce33cf6d2514d7ab1803a7"}, + {file = "bitarray-3.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:64a5404a258ef903db67d7911147febf112858ba30c180dae0c23405412e0a2f"}, + {file = "bitarray-3.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0952d05e1d6b0a736d73d34128b652d7549ba7d00ccc1e7c00efbc6edd687ee3"}, + {file = "bitarray-3.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c17eae957d61fea05d3f2333a95dd79dc4733f3eadf44862cd6d586daae31ea3"}, + {file = "bitarray-3.3.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c00b2ea9aab5b2c623b1901a4c04043fb847c8bd64a2f52518488434eb44c4e6"}, + {file = "bitarray-3.3.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a29ad824bf4b735cb119e2c79a4b821ad462aeb4495e80ff186f1a8e48362082"}, + {file = "bitarray-3.3.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e92d2d7d405e004f2bdf9ff6d58faed6d04e0b74a9d96905ade61c293abe315"}, + {file = "bitarray-3.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:833e06b01ff8f5a9f5b52156a23e9930402d964c96130f6d0bd5297e5dec95dc"}, + {file = "bitarray-3.3.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a0c87ffc5bf3669b0dfa91752653c41c9c38e1fd5b95aeb4c7ee40208c953fcd"}, + {file = "bitarray-3.3.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9ce64e247af33fa348694dbf7f4943a60040b5cc04df813649cc8b54c7f54061"}, + {file = "bitarray-3.3.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:52e8d36933bb3fb132c95c43171f47f07c22dd31536495be20f86ddbf383e3c6"}, + {file = "bitarray-3.3.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:434e389958ab98415ed4d9d67dd94c0ac835036a16b488df6736222f4f55ff35"}, + {file = "bitarray-3.3.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e75c4a1f00f46057f2fc98d717b2eabba09582422fe608158beed2ef0a5642da"}, + {file = "bitarray-3.3.1-cp38-cp38-win32.whl", hash = "sha256:9d6fe373572b20adde2d6a58f8dc900b0cb4eec625b05ca1adbf053772723c78"}, + {file = "bitarray-3.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:eeda85d034a2649b7e4dbd7067411e9c55c1fc65fafb9feb973d810b103e36a0"}, + {file = "bitarray-3.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:47abbec73f20176e119f5c4c68aaf243c46a5e072b9c182f2c110b5b227256a7"}, + {file = "bitarray-3.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f46e7fe734b57f3783a324bf3a7053df54299653e646d86558a4b2576cb47208"}, + {file = "bitarray-3.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2c411b7d3784109dfc33f5f7cdf331d3373b8349a4ad608ee482f1a04c30efe"}, + {file = "bitarray-3.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9511420cf727eb6e603dc6f3c122da1a16af38abc92272a715ce68c47b19b140"}, + {file = "bitarray-3.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39fdd56fd9076a4a34c3cd21e1c84dc861dac5e92c1ed9daed6aed6b11719c8c"}, + {file = "bitarray-3.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:638ad50ecbffd05efdfa9f77b24b497b8e523f078315846614c647ebc3389bb5"}, + {file = "bitarray-3.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f26f3197779fe5a90a54505334d34ceb948cec6378caea49cd9153b3bbe57566"}, + {file = "bitarray-3.3.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:01299fb36af3e7955967f3dbc4097a2d88845166837899350f411d95a857f8aa"}, + {file = "bitarray-3.3.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f1767c325ef4983f52a9d62590f09ea998c06d8d4aa9f13b9eeabaac3536381e"}, + {file = "bitarray-3.3.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ed6f9b158c11e7bcf9b0b6788003aed5046a0759e7b25e224d9551a01c779ee7"}, + {file = "bitarray-3.3.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab52dd26d24061d67f485f3400cc7d3d5696f0246294a372ef09aa8ef31a44c4"}, + {file = "bitarray-3.3.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0ba347a4dcc990637aa700227675d8033f68b417dcd7ccf660bd2e87e10885ec"}, + {file = "bitarray-3.3.1-cp39-cp39-win32.whl", hash = "sha256:4bda4e4219c6271beec737a5361b009dcf9ff6d84a2df92bf3dd4f4e97bb87e5"}, + {file = "bitarray-3.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:3afe39028afff6e94bb90eb0f8c5eb9357c0e37ce3c249f96dbcfc1a73938015"}, + {file = "bitarray-3.3.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c001b7ac2d9cf1a73899cf857d3d66919deca677df26df905852039c46aa30a6"}, + {file = "bitarray-3.3.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:535cc398610ff22dc0341e8833c34be73634a9a0a5d04912b4044e91dfbbc413"}, + {file = "bitarray-3.3.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dcb5aaaa2d91cc04fa9adfe31222ab150e72d99c779b1ddca10400a2fd319ec"}, + {file = "bitarray-3.3.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54ac6f8d2f696d83f9ccbb4cc4ce321dc80b9fa4613749a8ab23bda5674510ea"}, + {file = "bitarray-3.3.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d069a00a8d06fb68248edd5bf2aa5e8009f4f5eae8dd5b5a529812132ad8a6"}, + {file = "bitarray-3.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cbf063667ef89b0d8b8bd1fcaaa4dcc8c65c17048eb14fb1fa9dbe9cb5197c81"}, + {file = "bitarray-3.3.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0b7e1f4139d3f17feba72e386a8f1318fb35182ff65890281e727fd07fdfbd72"}, + {file = "bitarray-3.3.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d030b96f6ccfec0812e2fc1b02ab72d56a408ec215f496a7a25cde31160a88b4"}, + {file = "bitarray-3.3.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf7ead8b947a14c785d04943ff4743db90b0c40a4cb27e6bef4c3650800a927d"}, + {file = "bitarray-3.3.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5f44d71486949237679a8052cda171244d0be9279776c1d3d276861950dd608"}, + {file = "bitarray-3.3.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:601fedd0e5227a5591e2eae2d35d45a07f030783fc41fd217cdf0c74db554cb9"}, + {file = "bitarray-3.3.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7445c34e5d55ec512447efa746f046ecf4627c08281fc6e9ef844423167237bc"}, + {file = "bitarray-3.3.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:24296caffe89af65fc8029a56274db6a268f6a297a5163e65df8177c2dd67b19"}, + {file = "bitarray-3.3.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90b35553c318b49d5ffdaf3d25b6f0117fd5bbfc3be5576fc41ca506ca0e9b8e"}, + {file = "bitarray-3.3.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f937ef83e5666b6266236f59b1f38abe64851fb20e7d8d13033c5168d35ef39d"}, + {file = "bitarray-3.3.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:86dd5b8031d690afc90430997187a4fc5871bc6b81d73055354b8eb48b3e6342"}, + {file = "bitarray-3.3.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:9101d48f9532ceb6b1d6a5f7d3a2dd5c853015850c65a47045c70f5f2f9ff88f"}, + {file = "bitarray-3.3.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7964b17923c1bfa519afe273335023e0800c64bdca854008e75f2b148614d3f2"}, + {file = "bitarray-3.3.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:26a26614bba95f3e4ea8c285206a4efe5ffb99e8539356d78a62491facc326cf"}, + {file = "bitarray-3.3.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ce3e352f1b7f1201b04600f93035312b00c9f8f4d606048c39adac32b2fb738"}, + {file = "bitarray-3.3.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:176991b2769425341da4d52a684795498c0cd4136f4329ba9d524bcb96d26604"}, + {file = "bitarray-3.3.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64cef9f2d15261ea667838a4460f75acf4b03d64d53df664357541cc8d2c8183"}, + {file = "bitarray-3.3.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:28d866fa462d77cafbf284aea14102a31dcfdebb9a5abbfb453f6eb6b2deb4fd"}, + {file = "bitarray-3.3.1.tar.gz", hash = "sha256:8c89219a672d0a15ab70f8a6f41bc8355296ec26becef89a127c1a32bb2e6345"}, +] + [[package]] name = "black" version = "25.1.0" @@ -535,26 +831,107 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "cachetools" -version = "5.5.2" +version = "5.5.0" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a"}, - {file = "cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4"}, + {file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"}, + {file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"}, +] + +[[package]] +name = "cbor2" +version = "5.6.5" +description = "CBOR (de)serializer with extensive tag support" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "cbor2-5.6.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e16c4a87fc999b4926f5c8f6c696b0d251b4745bc40f6c5aee51d69b30b15ca2"}, + {file = "cbor2-5.6.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:87026fc838370d69f23ed8572939bd71cea2b3f6c8f8bb8283f573374b4d7f33"}, + {file = "cbor2-5.6.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a88f029522aec5425fc2f941b3df90da7688b6756bd3f0472ab886d21208acbd"}, + {file = "cbor2-5.6.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9d15b638539b68aa5d5eacc56099b4543a38b2d2c896055dccf7e83d24b7955"}, + {file = "cbor2-5.6.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:47261f54a024839ec649b950013c4de5b5f521afe592a2688eebbe22430df1dc"}, + {file = "cbor2-5.6.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:559dcf0d897260a9e95e7b43556a62253e84550b77147a1ad4d2c389a2a30192"}, + {file = "cbor2-5.6.5-cp310-cp310-win_amd64.whl", hash = "sha256:5b856fda4c50c5bc73ed3664e64211fa4f015970ed7a15a4d6361bd48462feaf"}, + {file = "cbor2-5.6.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:863e0983989d56d5071270790e7ed8ddbda88c9e5288efdb759aba2efee670bc"}, + {file = "cbor2-5.6.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5cff06464b8f4ca6eb9abcba67bda8f8334a058abc01005c8e616728c387ad32"}, + {file = "cbor2-5.6.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4c7dbcdc59ea7f5a745d3e30ee5e6b6ff5ce7ac244aa3de6786391b10027bb3"}, + {file = "cbor2-5.6.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34cf5ab0dc310c3d0196caa6ae062dc09f6c242e2544bea01691fe60c0230596"}, + {file = "cbor2-5.6.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6797b824b26a30794f2b169c0575301ca9b74ae99064e71d16e6ba0c9057de51"}, + {file = "cbor2-5.6.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:73b9647eed1493097db6aad61e03d8f1252080ee041a1755de18000dd2c05f37"}, + {file = "cbor2-5.6.5-cp311-cp311-win_amd64.whl", hash = "sha256:6e14a1bf6269d25e02ef1d4008e0ce8880aa271d7c6b4c329dba48645764f60e"}, + {file = "cbor2-5.6.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e25c2aebc9db99af7190e2261168cdde8ed3d639ca06868e4f477cf3a228a8e9"}, + {file = "cbor2-5.6.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fde21ac1cf29336a31615a2c469a9cb03cf0add3ae480672d4d38cda467d07fc"}, + {file = "cbor2-5.6.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8947c102cac79d049eadbd5e2ffb8189952890df7cbc3ee262bbc2f95b011a9"}, + {file = "cbor2-5.6.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38886c41bebcd7dca57739439455bce759f1e4c551b511f618b8e9c1295b431b"}, + {file = "cbor2-5.6.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae2b49226224e92851c333b91d83292ec62eba53a19c68a79890ce35f1230d70"}, + {file = "cbor2-5.6.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2764804ffb6553283fc4afb10a280715905a4cea4d6dc7c90d3e89c4a93bc8d"}, + {file = "cbor2-5.6.5-cp312-cp312-win_amd64.whl", hash = "sha256:a3ac50485cf67dfaab170a3e7b527630e93cb0a6af8cdaa403054215dff93adf"}, + {file = "cbor2-5.6.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f0d0a9c5aabd48ecb17acf56004a7542a0b8d8212be52f3102b8218284bd881e"}, + {file = "cbor2-5.6.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61ceb77e6aa25c11c814d4fe8ec9e3bac0094a1f5bd8a2a8c95694596ea01e08"}, + {file = "cbor2-5.6.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97a7e409b864fecf68b2ace8978eb5df1738799a333ec3ea2b9597bfcdd6d7d2"}, + {file = "cbor2-5.6.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f6d69f38f7d788b04c09ef2b06747536624b452b3c8b371ab78ad43b0296fab"}, + {file = "cbor2-5.6.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f91e6d74fa6917df31f8757fdd0e154203b0dd0609ec53eb957016a2b474896a"}, + {file = "cbor2-5.6.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5ce13a27ef8fddf643fc17a753fe34aa72b251d03c23da6a560c005dc171085b"}, + {file = "cbor2-5.6.5-cp313-cp313-win_amd64.whl", hash = "sha256:54c72a3207bb2d4480c2c39dad12d7971ce0853a99e3f9b8d559ce6eac84f66f"}, + {file = "cbor2-5.6.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4586a4f65546243096e56a3f18f29d60752ee9204722377021b3119a03ed99ff"}, + {file = "cbor2-5.6.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d1a18b3a58dcd9b40ab55c726160d4a6b74868f2a35b71f9e726268b46dc6a2"}, + {file = "cbor2-5.6.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a83b76367d1c3e69facbcb8cdf65ed6948678e72f433137b41d27458aa2a40cb"}, + {file = "cbor2-5.6.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90bfa36944caccec963e6ab7e01e64e31cc6664535dc06e6295ee3937c999cbb"}, + {file = "cbor2-5.6.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:37096663a5a1c46a776aea44906cbe5fa3952f29f50f349179c00525d321c862"}, + {file = "cbor2-5.6.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:93676af02bd9a0b4a62c17c5b20f8e9c37b5019b1a24db70a2ee6cb770423568"}, + {file = "cbor2-5.6.5-cp38-cp38-win_amd64.whl", hash = "sha256:8f747b7a9aaa58881a0c5b4cd4a9b8fb27eca984ed261a769b61de1f6b5bd1e6"}, + {file = "cbor2-5.6.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:94885903105eec66d7efb55f4ce9884fdc5a4d51f3bd75b6fedc68c5c251511b"}, + {file = "cbor2-5.6.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fe11c2eb518c882cfbeed456e7a552e544893c17db66fe5d3230dbeaca6b615c"}, + {file = "cbor2-5.6.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66dd25dd919cddb0b36f97f9ccfa51947882f064729e65e6bef17c28535dc459"}, + {file = "cbor2-5.6.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa61a02995f3a996c03884cf1a0b5733f88cbfd7fa0e34944bf678d4227ee712"}, + {file = "cbor2-5.6.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:824f202b556fc204e2e9a67d6d6d624e150fbd791278ccfee24e68caec578afd"}, + {file = "cbor2-5.6.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7488aec919f8408f9987a3a32760bd385d8628b23a35477917aa3923ff6ad45f"}, + {file = "cbor2-5.6.5-cp39-cp39-win_amd64.whl", hash = "sha256:a34ee99e86b17444ecbe96d54d909dd1a20e2da9f814ae91b8b71cf1ee2a95e4"}, + {file = "cbor2-5.6.5-py3-none-any.whl", hash = "sha256:3038523b8fc7de312bb9cdcbbbd599987e64307c4db357cd2030c472a6c7d468"}, + {file = "cbor2-5.6.5.tar.gz", hash = "sha256:b682820677ee1dbba45f7da11898d2720f92e06be36acec290867d5ebf3d7e09"}, +] + +[package.extras] +benchmarks = ["pytest-benchmark (==4.0.0)"] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.3.0)", "typing-extensions ; python_version < \"3.12\""] +test = ["coverage (>=7)", "hypothesis", "pytest"] + +[[package]] +name = "cdp-sdk" +version = "0.21.0" +description = "CDP Python SDK" +optional = false +python-versions = "<4.0,>=3.10" +groups = ["main"] +files = [ + {file = "cdp_sdk-0.21.0-py3-none-any.whl", hash = "sha256:36a2ec372c79354133f142566674f6f5a21f474d31f378154a3b4e0e0089818a"}, + {file = "cdp_sdk-0.21.0.tar.gz", hash = "sha256:6d832189e84cec76c3353f52835ddf06789630325ca5f0ea1a48ad663b698e7d"}, ] +[package.dependencies] +bip-utils = ">=2.9.3,<3.0.0" +coincurve = ">=20.0.0,<21.0.0" +cryptography = ">=44.0.0,<45.0.0" +pydantic = ">=2.10.3,<3.0.0" +pyjwt = ">=2.10.1,<3.0.0" +python-dateutil = ">=2.9.0.post0,<3.0.0" +urllib3 = ">=2.2.3,<3.0.0" +web3 = ">=7.6.0,<8.0.0" + [[package]] name = "certifi" -version = "2025.1.31" +version = "2024.12.14" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" groups = ["main", "demo", "dev", "docs", "research"] files = [ - {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, - {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, + {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, + {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, ] [[package]] @@ -650,6 +1027,18 @@ files = [ {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] +[[package]] +name = "chardet" +version = "5.2.0" +description = "Universal encoding detector for Python 3" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, + {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, +] + [[package]] name = "charset-normalizer" version = "3.4.1" @@ -752,6 +1141,116 @@ files = [ {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, ] +[[package]] +name = "ckzg" +version = "2.1.1" +description = "Python bindings for C-KZG-4844" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "ckzg-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b9825a1458219e8b4b023012b8ef027ef1f47e903f9541cbca4615f80132730"}, + {file = "ckzg-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e2a40a3ba65cca4b52825d26829e6f7eb464aa27a9e9efb6b8b2ce183442c741"}, + {file = "ckzg-2.1.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1d753fbe85be7c21602eddc2d40e0915e25fce10329f4f801a0002a4f886cc7"}, + {file = "ckzg-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d76b50527f1d12430bf118aff6fa4051e9860eada43f29177258b8d399448ea"}, + {file = "ckzg-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44c8603e43c021d100f355f50189183135d1df3cbbddb8881552d57fbf421dde"}, + {file = "ckzg-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:38707a638c9d715b3c30b29352b969f78d8fc10faed7db5faf517f04359895c0"}, + {file = "ckzg-2.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:52c4d257bdcbe822d20c5cd24c8154ec5aac33c49a8f5a19e716d9107a1c8785"}, + {file = "ckzg-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1507f7bfb9bcf51d816db5d8d0f0ed53c8289605137820d437b69daea8333e16"}, + {file = "ckzg-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:d02eaaf4f841910133552b3a051dea53bcfe60cd98199fc4cf80b27609d8baa2"}, + {file = "ckzg-2.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:465e2b71cf9dc383f66f1979269420a0da9274a3a9e98b1a4455e84927dfe491"}, + {file = "ckzg-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ee2f26f17a64ad0aab833d637b276f28486b82a29e34f32cf54b237b8f8ab72d"}, + {file = "ckzg-2.1.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99cc2c4e9fb8c62e3e0862c7f4df9142f07ba640da17fded5f6e0fd09f75909f"}, + {file = "ckzg-2.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773dd016693d74aca1f5d7982db2bad7dde2e147563aeb16a783f7e5f69c01fe"}, + {file = "ckzg-2.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af2b2144f87ba218d8db01382a961b3ecbdde5ede4fa0d9428d35f8c8a595ba"}, + {file = "ckzg-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8f55e63d3f7c934a2cb53728ed1d815479e177aca8c84efe991c2920977cff6"}, + {file = "ckzg-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ecb42aaa0ffa427ff14a9dde9356ba69e5ae6014650b397af55b31bdae7a9b6e"}, + {file = "ckzg-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5a01514239f12fb1a7ad9009c20062a4496e13b09541c1a65f97e295da648c70"}, + {file = "ckzg-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:6516b9684aae262c85cf7fddd8b585b8139ad20e08ec03994e219663abbb0916"}, + {file = "ckzg-2.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c60e8903344ce98ce036f0fabacce952abb714cad4607198b2f0961c28b8aa72"}, + {file = "ckzg-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4299149dd72448e5a8d2d1cc6cc7472c92fc9d9f00b1377f5b017c089d9cd92"}, + {file = "ckzg-2.1.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:025dd31ffdcc799f3ff842570a2a6683b6c5b01567da0109c0c05d11768729c4"}, + {file = "ckzg-2.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b42ab8385c273f40a693657c09d2bba40cb4f4666141e263906ba2e519e80bd"}, + {file = "ckzg-2.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1be3890fc1543f4fcfc0063e4baf5c036eb14bcf736dabdc6171ab017e0f1671"}, + {file = "ckzg-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b754210ded172968b201e2d7252573af6bf52d6ad127ddd13d0b9a45a51dae7b"}, + {file = "ckzg-2.1.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b2f8fda87865897a269c4e951e3826c2e814427a6cdfed6731cccfe548f12b36"}, + {file = "ckzg-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:98e70b5923d77c7359432490145e9d1ab0bf873eb5de56ec53f4a551d7eaec79"}, + {file = "ckzg-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:42af7bde4ca45469cd93a96c3d15d69d51d40e7f0d30e3a20711ebd639465fcb"}, + {file = "ckzg-2.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e4edfdaf87825ff43b9885fabfdea408737a714f4ce5467100d9d1d0a03b673"}, + {file = "ckzg-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:815fd2a87d6d6c57d669fda30c150bc9bf387d47e67d84535aa42b909fdc28ea"}, + {file = "ckzg-2.1.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c32466e809b1ab3ff01d3b0bb0b9912f61dcf72957885615595f75e3f7cc10e5"}, + {file = "ckzg-2.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f11b73ccf37b12993f39a7dbace159c6d580aacacde6ee17282848476550ddbc"}, + {file = "ckzg-2.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3b9433a1f2604bd9ac1646d3c83ad84a850d454d3ac589fe8e70c94b38a6b0"}, + {file = "ckzg-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b7d7e1b5ea06234558cd95c483666fd785a629b720a7f1622b3cbffebdc62033"}, + {file = "ckzg-2.1.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9f5556e6675866040cc4335907be6c537051e7f668da289fa660fdd8a30c9ddb"}, + {file = "ckzg-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55b2ba30c5c9daac0c55f1aac851f1b7bf1f7aa0028c2db4440e963dd5b866d6"}, + {file = "ckzg-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:10d201601fc8f28c0e8cec3406676797024dd374c367bbeec5a7a9eac9147237"}, + {file = "ckzg-2.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:5f46c8fd5914db62b446baf62c8599da07e6f91335779a9709c554ef300a7b60"}, + {file = "ckzg-2.1.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:60f14612c2be84f405755d734b0ad4e445db8af357378b95b72339b59e1f4fcf"}, + {file = "ckzg-2.1.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:929e6e793039f42325988004a90d16b0ef4fc7e1330142e180f0298f2ed4527c"}, + {file = "ckzg-2.1.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2beac2af53ea181118179570ecc81d8a8fc52c529553d7fd8786fd100a2aa39b"}, + {file = "ckzg-2.1.1-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:2432d48aec296baee79556bfde3bddd2799bcc7753cd1f0d0c9a3b0333935637"}, + {file = "ckzg-2.1.1-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:4c2e8180b54261ccae2bf8acd003ccee7394d88d073271af19c5f2ac4a54c607"}, + {file = "ckzg-2.1.1-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:c44e36bd53d9dd0ab29bd6ed2d67ea43c48eecd57f8197854a75742213938bf5"}, + {file = "ckzg-2.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:10befd86e643d38ac468151cdfb71e79b2d46aa6397b81db4224f4f6995262eb"}, + {file = "ckzg-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:138a9324ad8e8a9ade464043dc3a84afe12996516788f2ed841bdbe5d123af81"}, + {file = "ckzg-2.1.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:635af0a33a10c9ac275f3efc142880a6b46ac63f4495f600aae05266af4fadff"}, + {file = "ckzg-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:360e263677ee5aedb279b42cf54b51c905ddcac9181c65d89ec0b298d3f31ec0"}, + {file = "ckzg-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f81395f77bfd069831cbb1de9d473c7044abe9ce6cd562ef6ccd76d23abcef43"}, + {file = "ckzg-2.1.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:db1ff122f8dc10c9500a00a4d680c3c38f4e19b01d95f38e0f5bc55a77c8ab98"}, + {file = "ckzg-2.1.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:1f82f539949ff3c6a5accfdd211919a3e374d354b3665d062395ebdbf8befaeb"}, + {file = "ckzg-2.1.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:5bc8ae85df97467e84abb491b516e25dbca36079e766eafce94d1bc45e4aaa35"}, + {file = "ckzg-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:e749ce9fcb26e37101f2af8ba9c6376b66eb598880d35e457890044ba77c1cf7"}, + {file = "ckzg-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b00201979a64fd7e6029f64d791af42374febb42452537933e881b49d4e8c77"}, + {file = "ckzg-2.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c61c437ba714ab7c802b51fb30125e8f8550e1320fe9050d20777420c153a2b3"}, + {file = "ckzg-2.1.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8bd54394376598a7c081df009cfde3cc447beb640b6c6b7534582a31e6290ac7"}, + {file = "ckzg-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67d8c6680a7b370718af59cc17a983752706407cfbcace013ee707646d1f7b00"}, + {file = "ckzg-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55f6c57b24bc4fe16b1b50324ef8548f2a5053ad76bf90c618e2f88c040120d7"}, + {file = "ckzg-2.1.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f55fc10fb1b217c66bfe14e05535e5e61cfbb2a95dbb9b93a80984fa2ab4a7c0"}, + {file = "ckzg-2.1.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:2e23e3198f8933f0140ef8b2aeba717d8de03ec7b8fb1ee946f8d39986ce0811"}, + {file = "ckzg-2.1.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2f9caf88bf216756bb1361b92616c750a933c9afb67972ad05c212649a9be520"}, + {file = "ckzg-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:30e0c2d258bbc0c099c2d1854c6ffa2fd9abf6138b9c81f855e1936f6cb259aa"}, + {file = "ckzg-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6239d3d2e30cb894ca4e7765b1097eb6a70c0ecbe5f8e0b023fbf059472d4ac"}, + {file = "ckzg-2.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:909ebabc253a98d9dc1d51f93dc75990134bfe296c947e1ecf3b7142aba5108e"}, + {file = "ckzg-2.1.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0700dace6559b288b42ca8622be89c2a43509881ed6f4f0bfb6312bcceed0cb9"}, + {file = "ckzg-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a36aeabd243e906314694b4a107de99b0c4473ff1825fcb06acd147ffb1951a"}, + {file = "ckzg-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d884e8f9c7d7839f1a95561f4479096dce21d45b0c5dd013dc0842550cea1cad"}, + {file = "ckzg-2.1.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:338fdf4a0b463973fc7b7e4dc289739db929e61d7cb9ba984ebbe9c49d3aa6f9"}, + {file = "ckzg-2.1.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c594036d3408eebdcd8ab2c7aab7308239ed4df3d94f3211b7cf253f228fb0b7"}, + {file = "ckzg-2.1.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b0912ebb328ced510250a2325b095917db19c1a014792a0bf4c389f0493e39de"}, + {file = "ckzg-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:5046aceb03482ddf7200f2f5c643787b100e6fb96919852faf1c79f8870c80a1"}, + {file = "ckzg-2.1.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:375918e25eafb9bafe5215ab91698504cba3fe51b4fe92f5896af6c5663f50c6"}, + {file = "ckzg-2.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:38b3b7802c76d4ad015db2b7a79a49c193babae50ee5f77e9ac2865c9e9ddb09"}, + {file = "ckzg-2.1.1-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:438a5009fd254ace0bc1ad974d524547f1a41e6aa5e778c5cd41f4ee3106bcd6"}, + {file = "ckzg-2.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ce11cc163a2e0dab3af7455aca7053f9d5bb8d157f231acc7665fd230565d48"}, + {file = "ckzg-2.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b53964c07f6a076e97eaa1ef35045e935d7040aff14f80bae7e9105717702d05"}, + {file = "ckzg-2.1.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cf085f15ae52ab2599c9b5a3d5842794bcf5613b7f58661fbfb0c5d9eac988b9"}, + {file = "ckzg-2.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4b0c850bd6cad22ac79b2a2ab884e0e7cd2b54a67d643cd616c145ebdb535a11"}, + {file = "ckzg-2.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:26951f36bb60c9150bbd38110f5e1625596f9779dad54d1d492d8ec38bc84e3a"}, + {file = "ckzg-2.1.1-pp311-pypy311_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbe12445e49c4bee67746b7b958e90a973b0de116d0390749b0df351d94e9a8c"}, + {file = "ckzg-2.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71c5d4f66f09de4a99271acac74d2acb3559a77de77a366b34a91e99e8822667"}, + {file = "ckzg-2.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42673c1d007372a4e8b48f6ef8f0ce31a9688a463317a98539757d1e2fb1ecc7"}, + {file = "ckzg-2.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:57a7dc41ec6b69c1d9117eb61cf001295e6b4f67a736020442e71fb4367fb1a5"}, + {file = "ckzg-2.1.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:22e4606857660b2ffca2f7b96c01d0b18b427776d8a93320caf2b1c7342881fe"}, + {file = "ckzg-2.1.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b55475126a9efc82d61718b2d2323502e33d9733b7368c407954592ccac87faf"}, + {file = "ckzg-2.1.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5939ae021557c64935a7649b13f4a58f1bd35c39998fd70d0cefb5cbaf77d1be"}, + {file = "ckzg-2.1.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad1ec5f9726a9946508a4a2aace298172aa778de9ebbe97e21c873c3688cc87"}, + {file = "ckzg-2.1.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:93d7edea3bb1602b18b394ebeec231d89dfd8d48fdd06571cb7656107aa62226"}, + {file = "ckzg-2.1.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c450d77af61011ced3777f97431d5f1bc148ca5362c67caf516aa2f6ef7e4817"}, + {file = "ckzg-2.1.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:8fc8df4e17e08974961d6c14f6c57ccfd3ad5aede74598292ec6e5d6fc2dbcac"}, + {file = "ckzg-2.1.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93338da8011790ef53a68475678bc951fa7b337db027d8edbf1889e59691161c"}, + {file = "ckzg-2.1.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4889f24b4ff614f39e3584709de1a3b0f1556675b33e360dbcb28cda827296d4"}, + {file = "ckzg-2.1.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7b58fbb1a9be4ae959feede8f103e12d80ef8453bdc6483bfdaf164879a2b80"}, + {file = "ckzg-2.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6136c5b5377c7f7033323b25bc2c7b43c025d44ed73e338c02f9f59df9460e5b"}, + {file = "ckzg-2.1.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fa419b92a0e8766deb7157fb28b6542c1c3f8dde35d2a69d1f91ec8e41047d35"}, + {file = "ckzg-2.1.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:95cd6c8eb3ab5148cd97ab5bf44b84fd7f01adf4b36ffd070340ad2d9309b3f9"}, + {file = "ckzg-2.1.1-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:848191201052b48bdde18680ebb77bf8da99989270e5aea8b0290051f5ac9468"}, + {file = "ckzg-2.1.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4716c0564131b0d609fb8856966e83892b9809cf6719c7edd6495b960451f8b"}, + {file = "ckzg-2.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c399168ba199827dee3104b00cdc7418d4dbdf47a5fcbe7cf938fc928037534"}, + {file = "ckzg-2.1.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:724f29f9f110d9ef42a6a1a1a7439548c61070604055ef96b2ab7a884cad4192"}, + {file = "ckzg-2.1.1.tar.gz", hash = "sha256:d6b306b7ec93a24e4346aa53d07f7f75053bc0afc7398e35fa649e5f9d48fcc4"}, +] + [[package]] name = "click" version = "8.1.8" @@ -767,6 +1266,115 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} +[[package]] +name = "coinbase-agentkit" +version = "0.4.0" +description = "Coinbase AgentKit" +optional = false +python-versions = "~=3.10" +groups = ["main"] +files = [ + {file = "coinbase_agentkit-0.4.0-py3-none-any.whl", hash = "sha256:c172c9f127a03148ff0a35d5c70d1e5c688f3c2550b94e75b945faacfe1db57c"}, + {file = "coinbase_agentkit-0.4.0.tar.gz", hash = "sha256:0166bf2ef245a414b23f58155879e70a066526b0e4de64c24d353cd70825762a"}, +] + +[package.dependencies] +allora-sdk = ">=0.2.0,<0.3" +cdp-sdk = "0.21.0" +ecdsa = ">=0.19.0,<0.20" +jsonschema = ">=4.23.0,<5" +nilql = ">=0.0.0a12,<0.0.1" +paramiko = ">=3.5.1,<4" +pydantic = ">=2.0,<3.0" +pyjwt = {version = ">=2.10.1,<3", extras = ["crypto"]} +python-dotenv = ">=1.0.1,<2" +requests = ">=2.31.0,<3" +web3 = ">=7.10.0,<8" + +[[package]] +name = "coinbase-agentkit-langchain" +version = "0.3.0" +description = "Coinbase AgentKit LangChain extension" +optional = false +python-versions = "~=3.10" +groups = ["main"] +files = [ + {file = "coinbase_agentkit_langchain-0.3.0-py3-none-any.whl", hash = "sha256:ce80879e1f7210b18558985332784ec24f2845bd0a9529739276d2b199f7fc75"}, + {file = "coinbase_agentkit_langchain-0.3.0.tar.gz", hash = "sha256:8e3ee37d76250c3400c333aeaf3c7e36544a70a12dfc40adb3a76f782b0bc4d2"}, +] + +[package.dependencies] +coinbase-agentkit = ">=0.3.0,<0.5" +langchain = ">=0.3.4,<0.4" +python-dotenv = ">=1.0.1,<2" + +[[package]] +name = "coincurve" +version = "20.0.0" +description = "Cross-platform Python CFFI bindings for libsecp256k1" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "coincurve-20.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d559b22828638390118cae9372a1bb6f6594f5584c311deb1de6a83163a0919b"}, + {file = "coincurve-20.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:33d7f6ebd90fcc550f819f7f2cce2af525c342aac07f0ccda46ad8956ad9d99b"}, + {file = "coincurve-20.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22d70dd55d13fd427418eb41c20fde0a20a5e5f016e2b1bb94710701e759e7e0"}, + {file = "coincurve-20.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46f18d481eaae72c169f334cde1fd22011a884e0c9c6adc3fdc1fd13df8236a3"}, + {file = "coincurve-20.0.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9de1ec57f43c3526bc462be58fb97910dc1fdd5acab6c71eda9f9719a5bd7489"}, + {file = "coincurve-20.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a6f007c44c726b5c0b3724093c0d4fb8e294f6b6869beb02d7473b21777473a3"}, + {file = "coincurve-20.0.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0ff1f3b81330db5092c24da2102e4fcba5094f14945b3eb40746456ceabdd6d9"}, + {file = "coincurve-20.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:82f7de97694d9343f26bd1c8e081b168e5f525894c12445548ce458af227f536"}, + {file = "coincurve-20.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:e905b4b084b4f3b61e5a5d58ac2632fd1d07b7b13b4c6d778335a6ca1dafd7a3"}, + {file = "coincurve-20.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:3657bb5ed0baf1cf8cf356e7d44aa90a7902cc3dd4a435c6d4d0bed0553ad4f7"}, + {file = "coincurve-20.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:44087d1126d43925bf9a2391ce5601bf30ce0dba4466c239172dc43226696018"}, + {file = "coincurve-20.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ccf0ba38b0f307a9b3ce28933f6c71dc12ef3a0985712ca09f48591afd597c8"}, + {file = "coincurve-20.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:566bc5986debdf8572b6be824fd4de03d533c49f3de778e29f69017ae3fe82d8"}, + {file = "coincurve-20.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4d70283168e146f025005c15406086513d5d35e89a60cf4326025930d45013a"}, + {file = "coincurve-20.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:763c6122dd7d5e7a81c86414ce360dbe9a2d4afa1ca6c853ee03d63820b3d0c5"}, + {file = "coincurve-20.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f00c361c356bcea386d47a191bb8ac60429f4b51c188966a201bfecaf306ff7f"}, + {file = "coincurve-20.0.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4af57bdadd2e64d117dd0b33cfefe76e90c7a6c496a7b034fc65fd01ec249b15"}, + {file = "coincurve-20.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a26437b7cbde13fb6e09261610b788ca2a0ca2195c62030afd1e1e0d1a62e035"}, + {file = "coincurve-20.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:ed51f8bba35e6c7676ad65539c3dbc35acf014fc402101fa24f6b0a15a74ab9e"}, + {file = "coincurve-20.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:594b840fc25d74118407edbbbc754b815f1bba9759dbf4f67f1c2b78396df2d3"}, + {file = "coincurve-20.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4df4416a6c0370d777aa725a25b14b04e45aa228da1251c258ff91444643f688"}, + {file = "coincurve-20.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1ccc3e4db55abf3fc0e604a187fdb05f0702bc5952e503d9a75f4ae6eeb4cb3a"}, + {file = "coincurve-20.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac8335b1658a2ef5b3eb66d52647742fe8c6f413ad5b9d5310d7ea6d8060d40f"}, + {file = "coincurve-20.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ac025e485a0229fd5394e0bf6b4a75f8a4f6cee0dcf6f0b01a2ef05c5210ff"}, + {file = "coincurve-20.0.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e46e3f1c21b3330857bcb1a3a5b942f645c8bce912a8a2b252216f34acfe4195"}, + {file = "coincurve-20.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:df9ff9b17a1d27271bf476cf3fa92df4c151663b11a55d8cea838b8f88d83624"}, + {file = "coincurve-20.0.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4155759f071375699282e03b3d95fb473ee05c022641c077533e0d906311e57a"}, + {file = "coincurve-20.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0530b9dd02fc6f6c2916716974b79bdab874227f560c422801ade290e3fc5013"}, + {file = "coincurve-20.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:eacf9c0ce8739c84549a89c083b1f3526c8780b84517ee75d6b43d276e55f8a0"}, + {file = "coincurve-20.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:52a67bfddbd6224dfa42085c88ad176559801b57d6a8bd30d92ee040de88b7b3"}, + {file = "coincurve-20.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:61e951b1d695b62376f60519a84c4facaf756eeb9c5aff975bea0942833f185d"}, + {file = "coincurve-20.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e9e548db77f4ea34c0d748dddefc698adb0ee3fab23ed19f80fb2118dac70f6"}, + {file = "coincurve-20.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cdbf0da0e0809366fdfff236b7eb6e663669c7b1f46361a4c4d05f5b7e94c57"}, + {file = "coincurve-20.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d72222b4ecd3952e8ffcbf59bc7e0d1b181161ba170b60e5c8e1f359a43bbe7e"}, + {file = "coincurve-20.0.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9add43c4807f0c17a940ce4076334c28f51d09c145cd478400e89dcfb83fb59d"}, + {file = "coincurve-20.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bcc94cceea6ec8863815134083e6221a034b1ecef822d0277cf6ad2e70009b7f"}, + {file = "coincurve-20.0.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ffbdfef6a6d147988eabaed681287a9a7e6ba45ecc0a8b94ba62ad0a7656d97"}, + {file = "coincurve-20.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13335c19c7e5f36eaba2a53c68073d981980d7dc7abfee68d29f2da887ccd24e"}, + {file = "coincurve-20.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:7fbfb8d16cf2bea2cf48fc5246d4cb0a06607d73bb5c57c007c9aed7509f855e"}, + {file = "coincurve-20.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4870047704cddaae7f0266a549c927407c2ba0ec92d689e3d2b511736812a905"}, + {file = "coincurve-20.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:81ce41263517b0a9f43cd570c87720b3c13324929584fa28d2e4095969b6015d"}, + {file = "coincurve-20.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:572083ccce6c7b514d482f25f394368f4ae888f478bd0b067519d33160ea2fcc"}, + {file = "coincurve-20.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee5bc78a31a2f1370baf28aaff3949bc48f940a12b0359d1cd2c4115742874e6"}, + {file = "coincurve-20.0.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2895d032e281c4e747947aae4bcfeef7c57eabfd9be22886c0ca4e1365c7c1f"}, + {file = "coincurve-20.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d3e2f21957ada0e1742edbde117bb41758fa8691b69c8d186c23e9e522ea71cd"}, + {file = "coincurve-20.0.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c2baa26b1aad1947ca07b3aa9e6a98940c5141c6bdd0f9b44d89e36da7282ffa"}, + {file = "coincurve-20.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7eacc7944ddf9e2b7448ecbe84753841ab9874b8c332a4f5cc3b2f184db9f4a2"}, + {file = "coincurve-20.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:c293c095dc690178b822cadaaeb81de3cc0d28f8bdf8216ed23551dcce153a26"}, + {file = "coincurve-20.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:11a47083a0b7092d3eb50929f74ffd947c4a5e7035796b81310ea85289088c7a"}, + {file = "coincurve-20.0.0.tar.gz", hash = "sha256:872419e404300302e938849b6b92a196fabdad651060b559dc310e52f8392829"}, +] + +[package.dependencies] +asn1crypto = "*" +cffi = ">=1.3.0" + +[package.extras] +dev = ["coverage", "pytest", "pytest-benchmark"] + [[package]] name = "colorama" version = "0.4.6" @@ -853,6 +1461,17 @@ mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.11.1)", "types-Pil test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist", "wurlitzer"] +[[package]] +name = "crcmod" +version = "1.7" +description = "CRC Generator" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "crcmod-1.7.tar.gz", hash = "sha256:dc7051a0db5f2bd48665a990d3ec1cc305a466a77358ca4492826f41f283601e"}, +] + [[package]] name = "cryptography" version = "44.0.2" @@ -927,6 +1546,123 @@ files = [ docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] tests = ["pytest", "pytest-cov", "pytest-xdist"] +[[package]] +name = "cytoolz" +version = "1.0.1" +description = "Cython implementation of Toolz: High performance functional utilities" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "implementation_name == \"cpython\"" +files = [ + {file = "cytoolz-1.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cec9af61f71fc3853eb5dca3d42eb07d1f48a4599fa502cbe92adde85f74b042"}, + {file = "cytoolz-1.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:140bbd649dbda01e91add7642149a5987a7c3ccc251f2263de894b89f50b6608"}, + {file = "cytoolz-1.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e90124bdc42ff58b88cdea1d24a6bc5f776414a314cc4d94f25c88badb3a16d1"}, + {file = "cytoolz-1.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e74801b751e28f7c5cc3ad264c123954a051f546f2fdfe089f5aa7a12ccfa6da"}, + {file = "cytoolz-1.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:582dad4545ddfb5127494ef23f3fa4855f1673a35d50c66f7638e9fb49805089"}, + {file = "cytoolz-1.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd7bd0618e16efe03bd12f19c2a26a27e6e6b75d7105adb7be1cd2a53fa755d8"}, + {file = "cytoolz-1.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d74cca6acf1c4af58b2e4a89cc565ed61c5e201de2e434748c93e5a0f5c541a5"}, + {file = "cytoolz-1.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:823a3763828d8d457f542b2a45d75d6b4ced5e470b5c7cf2ed66a02f508ed442"}, + {file = "cytoolz-1.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:51633a14e6844c61db1d68c1ffd077cf949f5c99c60ed5f1e265b9e2966f1b52"}, + {file = "cytoolz-1.0.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f3ec9b01c45348f1d0d712507d54c2bfd69c62fbd7c9ef555c9d8298693c2432"}, + {file = "cytoolz-1.0.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1855022b712a9c7a5bce354517ab4727a38095f81e2d23d3eabaf1daeb6a3b3c"}, + {file = "cytoolz-1.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9930f7288c4866a1dc1cc87174f0c6ff4cad1671eb1f6306808aa6c445857d78"}, + {file = "cytoolz-1.0.1-cp310-cp310-win32.whl", hash = "sha256:a9baad795d72fadc3445ccd0f122abfdbdf94269157e6d6d4835636dad318804"}, + {file = "cytoolz-1.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:ad95b386a84e18e1f6136f6d343d2509d4c3aae9f5a536f3dc96808fcc56a8cf"}, + {file = "cytoolz-1.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2d958d4f04d9d7018e5c1850790d9d8e68b31c9a2deebca74b903706fdddd2b6"}, + {file = "cytoolz-1.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0f445b8b731fc0ecb1865b8e68a070084eb95d735d04f5b6c851db2daf3048ab"}, + {file = "cytoolz-1.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f546a96460a7e28eb2ec439f4664fa646c9b3e51c6ebad9a59d3922bbe65e30"}, + {file = "cytoolz-1.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0317681dd065532d21836f860b0563b199ee716f55d0c1f10de3ce7100c78a3b"}, + {file = "cytoolz-1.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c0ef52febd5a7821a3fd8d10f21d460d1a3d2992f724ba9c91fbd7a96745d41"}, + {file = "cytoolz-1.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5ebaf419acf2de73b643cf96108702b8aef8e825cf4f63209ceb078d5fbbbfd"}, + {file = "cytoolz-1.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f7f04eeb4088947585c92d6185a618b25ad4a0f8f66ea30c8db83cf94a425e3"}, + {file = "cytoolz-1.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f61928803bb501c17914b82d457c6f50fe838b173fb40d39c38d5961185bd6c7"}, + {file = "cytoolz-1.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d2960cb4fa01ccb985ad1280db41f90dc97a80b397af970a15d5a5de403c8c61"}, + {file = "cytoolz-1.0.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2b407cc3e9defa8df5eb46644f6f136586f70ba49eba96f43de67b9a0984fd3"}, + {file = "cytoolz-1.0.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8245f929144d4d3bd7b972c9593300195c6cea246b81b4c46053c48b3f044580"}, + {file = "cytoolz-1.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e37385db03af65763933befe89fa70faf25301effc3b0485fec1c15d4ce4f052"}, + {file = "cytoolz-1.0.1-cp311-cp311-win32.whl", hash = "sha256:50f9c530f83e3e574fc95c264c3350adde8145f4f8fc8099f65f00cc595e5ead"}, + {file = "cytoolz-1.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:b7f6b617454b4326af7bd3c7c49b0fc80767f134eb9fd6449917a058d17a0e3c"}, + {file = "cytoolz-1.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fcb8f7d0d65db1269022e7e0428471edee8c937bc288ebdcb72f13eaa67c2fe4"}, + {file = "cytoolz-1.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:207d4e4b445e087e65556196ff472ff134370d9a275d591724142e255f384662"}, + {file = "cytoolz-1.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21cdf6bac6fd843f3b20280a66fd8df20dea4c58eb7214a2cd8957ec176f0bb3"}, + {file = "cytoolz-1.0.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a55ec098036c0dea9f3bdc021f8acd9d105a945227d0811589f0573f21c9ce1"}, + {file = "cytoolz-1.0.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a13ab79ff4ce202e03ab646a2134696988b554b6dc4b71451e948403db1331d8"}, + {file = "cytoolz-1.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e2d944799026e1ff08a83241f1027a2d9276c41f7a74224cd98b7df6e03957d"}, + {file = "cytoolz-1.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88ba85834cd523b91fdf10325e1e6d71c798de36ea9bdc187ca7bd146420de6f"}, + {file = "cytoolz-1.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a750b1af7e8bf6727f588940b690d69e25dc47cce5ce467925a76561317eaf7"}, + {file = "cytoolz-1.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:44a71870f7eae31d263d08b87da7c2bf1176f78892ed8bdade2c2850478cb126"}, + {file = "cytoolz-1.0.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8231b9abbd8e368e036f4cc2e16902c9482d4cf9e02a6147ed0e9a3cd4a9ab0"}, + {file = "cytoolz-1.0.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:aa87599ccc755de5a096a4d6c34984de6cd9dc928a0c5eaa7607457317aeaf9b"}, + {file = "cytoolz-1.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:67cd16537df51baabde3baa770ab7b8d16839c4d21219d5b96ac59fb012ebd2d"}, + {file = "cytoolz-1.0.1-cp312-cp312-win32.whl", hash = "sha256:fb988c333f05ee30ad4693fe4da55d95ec0bb05775d2b60191236493ea2e01f9"}, + {file = "cytoolz-1.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:8f89c48d8e5aec55ffd566a8ec858706d70ed0c6a50228eca30986bfa5b4da8b"}, + {file = "cytoolz-1.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6944bb93b287032a4c5ca6879b69bcd07df46f3079cf8393958cf0b0454f50c0"}, + {file = "cytoolz-1.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e027260fd2fc5cb041277158ac294fc13dca640714527219f702fb459a59823a"}, + {file = "cytoolz-1.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88662c0e07250d26f5af9bc95911e6137e124a5c1ec2ce4a5d74de96718ab242"}, + {file = "cytoolz-1.0.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:309dffa78b0961b4c0cf55674b828fbbc793cf2d816277a5c8293c0c16155296"}, + {file = "cytoolz-1.0.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:edb34246e6eb40343c5860fc51b24937698e4fa1ee415917a73ad772a9a1746b"}, + {file = "cytoolz-1.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a54da7a8e4348a18d45d4d5bc84af6c716d7f131113a4f1cc45569d37edff1b"}, + {file = "cytoolz-1.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:241c679c3b1913c0f7259cf1d9639bed5084c86d0051641d537a0980548aa266"}, + {file = "cytoolz-1.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5bfc860251a8f280ac79696fc3343cfc3a7c30b94199e0240b6c9e5b6b01a2a5"}, + {file = "cytoolz-1.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c8edd1547014050c1bdad3ff85d25c82bd1c2a3c96830c6181521eb78b9a42b3"}, + {file = "cytoolz-1.0.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b349bf6162e8de215403d7f35f8a9b4b1853dc2a48e6e1a609a5b1a16868b296"}, + {file = "cytoolz-1.0.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1b18b35256219b6c3dd0fa037741b85d0bea39c552eab0775816e85a52834140"}, + {file = "cytoolz-1.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:738b2350f340ff8af883eb301054eb724997f795d20d90daec7911c389d61581"}, + {file = "cytoolz-1.0.1-cp313-cp313-win32.whl", hash = "sha256:9cbd9c103df54fcca42be55ef40e7baea624ac30ee0b8bf1149f21146d1078d9"}, + {file = "cytoolz-1.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:90e577e08d3a4308186d9e1ec06876d4756b1e8164b92971c69739ea17e15297"}, + {file = "cytoolz-1.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f3a509e4ac8e711703c368476b9bbce921fcef6ebb87fa3501525f7000e44185"}, + {file = "cytoolz-1.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a7eecab6373e933dfbf4fdc0601d8fd7614f8de76793912a103b5fccf98170cd"}, + {file = "cytoolz-1.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e55ed62087f6e3e30917b5f55350c3b6be6470b849c6566018419cd159d2cebc"}, + {file = "cytoolz-1.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43de33d99a4ccc07234cecd81f385456b55b0ea9c39c9eebf42f024c313728a5"}, + {file = "cytoolz-1.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:139bed875828e1727018aa0982aa140e055cbafccb7fd89faf45cbb4f2a21514"}, + {file = "cytoolz-1.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22c12671194b518aa8ce2f4422bd5064f25ab57f410ba0b78705d0a219f4a97a"}, + {file = "cytoolz-1.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79888f2f7dc25709cd5d37b032a8833741e6a3692c8823be181d542b5999128e"}, + {file = "cytoolz-1.0.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:51628b4eb41fa25bd428f8f7b5b74fbb05f3ae65fbd265019a0dd1ded4fdf12a"}, + {file = "cytoolz-1.0.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:1db9eb7179285403d2fb56ba1ff6ec35a44921b5e2fa5ca19d69f3f9f0285ea5"}, + {file = "cytoolz-1.0.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:08ab7efae08e55812340bfd1b3f09f63848fe291675e2105eab1aa5327d3a16e"}, + {file = "cytoolz-1.0.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e5fdc5264f884e7c0a1711a81dff112708a64b9c8561654ee578bfdccec6be09"}, + {file = "cytoolz-1.0.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:90d6a2e6ab891043ee655ec99d5e77455a9bee9e1131bdfcfb745edde81200dd"}, + {file = "cytoolz-1.0.1-cp38-cp38-win32.whl", hash = "sha256:08946e083faa5147751b34fbf78ab931f149ef758af5c1092932b459e18dcf5c"}, + {file = "cytoolz-1.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:a91b4e10a9c03796c0dc93e47ebe25bb41ecc6fafc3cf5197c603cf767a3d44d"}, + {file = "cytoolz-1.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:980c323e626ba298b77ae62871b2de7c50b9d7219e2ddf706f52dd34b8be7349"}, + {file = "cytoolz-1.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:45f6fa1b512bc2a0f2de5123db932df06c7f69d12874fe06d67772b2828e2c8b"}, + {file = "cytoolz-1.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f93f42d9100c415155ad1f71b0de362541afd4ac95e3153467c4c79972521b6b"}, + {file = "cytoolz-1.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a76d20dec9c090cdf4746255bbf06a762e8cc29b5c9c1d138c380bbdb3122ade"}, + {file = "cytoolz-1.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:239039585487c69aa50c5b78f6a422016297e9dea39755761202fb9f0530fe87"}, + {file = "cytoolz-1.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c28307640ca2ab57b9fbf0a834b9bf563958cd9e038378c3a559f45f13c3c541"}, + {file = "cytoolz-1.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:454880477bb901cee3a60f6324ec48c95d45acc7fecbaa9d49a5af737ded0595"}, + {file = "cytoolz-1.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:902115d1b1f360fd81e44def30ac309b8641661150fcbdde18ead446982ada6a"}, + {file = "cytoolz-1.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e68e6b38473a3a79cee431baa22be31cac39f7df1bf23eaa737eaff42e213883"}, + {file = "cytoolz-1.0.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:32fba3f63fcb76095b0a22f4bdcc22bc62a2bd2d28d58bf02fd21754c155a3ec"}, + {file = "cytoolz-1.0.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:0724ba4cf41eb40b6cf75250820ab069e44bdf4183ff78857aaf4f0061551075"}, + {file = "cytoolz-1.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c42420e0686f887040d5230420ed44f0e960ccbfa29a0d65a3acd9ca52459209"}, + {file = "cytoolz-1.0.1-cp39-cp39-win32.whl", hash = "sha256:4ba8b16358ea56b1fe8e637ec421e36580866f2e787910bac1cf0a6997424a34"}, + {file = "cytoolz-1.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:92d27f84bf44586853d9562bfa3610ecec000149d030f793b4cb614fd9da1813"}, + {file = "cytoolz-1.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:83d19d55738ad9c60763b94f3f6d3c6e4de979aeb8d76841c1401081e0e58d96"}, + {file = "cytoolz-1.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f112a71fad6ea824578e6393765ce5c054603afe1471a5c753ff6c67fd872d10"}, + {file = "cytoolz-1.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a515df8f8aa6e1eaaf397761a6e4aff2eef73b5f920aedf271416d5471ae5ee"}, + {file = "cytoolz-1.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92c398e7b7023460bea2edffe5fcd0a76029580f06c3f6938ac3d198b47156f3"}, + {file = "cytoolz-1.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3237e56211e03b13df47435b2369f5df281e02b04ad80a948ebd199b7bc10a47"}, + {file = "cytoolz-1.0.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ba0d1da50aab1909b165f615ba1125c8b01fcc30d606c42a61c42ea0269b5e2c"}, + {file = "cytoolz-1.0.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25b6e8dec29aa5a390092d193abd673e027d2c0b50774ae816a31454286c45c7"}, + {file = "cytoolz-1.0.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:36cd6989ebb2f18fe9af8f13e3c61064b9f741a40d83dc5afeb0322338ad25f2"}, + {file = "cytoolz-1.0.1-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a47394f8ab7fca3201f40de61fdeea20a2baffb101485ae14901ea89c3f6c95d"}, + {file = "cytoolz-1.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d00ac423542af944302e034e618fb055a0c4e87ba704cd6a79eacfa6ac83a3c9"}, + {file = "cytoolz-1.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a5ca923d1fa632f7a4fb33c0766c6fba7f87141a055c305c3e47e256fb99c413"}, + {file = "cytoolz-1.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:058bf996bcae9aad3acaeeb937d42e0c77c081081e67e24e9578a6a353cb7fb2"}, + {file = "cytoolz-1.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69e2a1f41a3dad94a17aef4a5cc003323359b9f0a9d63d4cc867cb5690a2551d"}, + {file = "cytoolz-1.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67daeeeadb012ec2b59d63cb29c4f2a2023b0c4957c3342d354b8bb44b209e9a"}, + {file = "cytoolz-1.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:54d3d36bbf0d4344d1afa22c58725d1668e30ff9de3a8f56b03db1a6da0acb11"}, + {file = "cytoolz-1.0.1.tar.gz", hash = "sha256:89cc3161b89e1bb3ed7636f74ed2e55984fd35516904fc878cae216e42b2c7d6"}, +] + +[package.dependencies] +toolz = ">=0.8.0" + +[package.extras] +cython = ["cython"] + [[package]] name = "dataclasses-json" version = "0.6.7" @@ -983,7 +1719,7 @@ version = "0.3.9" description = "Distribution utilities" optional = false python-versions = "*" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, @@ -1019,7 +1755,7 @@ version = "0.19.1" description = "ECDSA cryptographic signature library (pure python)" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.6" -groups = ["demo"] +groups = ["main", "demo"] files = [ {file = "ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3"}, {file = "ecdsa-0.19.1.tar.gz", hash = "sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61"}, @@ -1032,6 +1768,219 @@ six = ">=1.9.0" gmpy = ["gmpy"] gmpy2 = ["gmpy2"] +[[package]] +name = "ed25519-blake2b" +version = "1.4.1" +description = "Ed25519 public-key signatures (BLAKE2b fork)" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "ed25519-blake2b-1.4.1.tar.gz", hash = "sha256:731e9f93cd1ac1a64649575f3519a99ffe0bb1e4cf7bf5f5f0be513a39df7363"}, +] + +[[package]] +name = "egcd" +version = "2.0.2" +description = "Pure-Python extended Euclidean algorithm implementation that accepts any number of integer arguments." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "egcd-2.0.2-py3-none-any.whl", hash = "sha256:2f0576a651b4aa9e9c4640bba078f9741d1624f386b55cb5363a79ae4b564bd2"}, + {file = "egcd-2.0.2.tar.gz", hash = "sha256:3b05b0feb67549f8f76c97afed36c53252c0d7cb9a65bf4e6ca8b99110fb77f2"}, +] + +[package.extras] +coveralls = ["coveralls (>=4.0,<5.0)"] +docs = ["sphinx (>=5.0,<6.0)", "sphinx-rtd-theme (>=1.1.0,<1.2.0)", "toml (>=0.10.2,<0.11.0)"] +lint = ["pylint (>=2.17.0,<2.18.0) ; python_version < \"3.12\"", "pylint (>=3.2.0,<3.3.0) ; python_version >= \"3.12\""] +publish = ["build (>=0.10,<1.0)", "twine (>=4.0,<5.0)"] +test = ["pytest (>=7.4,<8.0) ; python_version < \"3.12\"", "pytest (>=8.2,<9.0) ; python_version >= \"3.12\"", "pytest-cov (>=4.1,<5.0) ; python_version < \"3.12\"", "pytest-cov (>=5.0,<6.0) ; python_version >= \"3.12\""] + +[[package]] +name = "eth-abi" +version = "5.2.0" +description = "eth_abi: Python utilities for working with Ethereum ABI definitions, especially encoding and decoding" +optional = false +python-versions = "<4,>=3.8" +groups = ["main"] +files = [ + {file = "eth_abi-5.2.0-py3-none-any.whl", hash = "sha256:17abe47560ad753f18054f5b3089fcb588f3e3a092136a416b6c1502cb7e8877"}, + {file = "eth_abi-5.2.0.tar.gz", hash = "sha256:178703fa98c07d8eecd5ae569e7e8d159e493ebb6eeb534a8fe973fbc4e40ef0"}, +] + +[package.dependencies] +eth-typing = ">=3.0.0" +eth-utils = ">=2.0.0" +parsimonious = ">=0.10.0,<0.11.0" + +[package.extras] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "eth-hash[pycryptodome]", "hypothesis (>=6.22.0,<6.108.7)", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-pythonpath (>=0.7.1)", "pytest-timeout (>=2.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] +test = ["eth-hash[pycryptodome]", "hypothesis (>=6.22.0,<6.108.7)", "pytest (>=7.0.0)", "pytest-pythonpath (>=0.7.1)", "pytest-timeout (>=2.0.0)", "pytest-xdist (>=2.4.0)"] +tools = ["hypothesis (>=6.22.0,<6.108.7)"] + +[[package]] +name = "eth-account" +version = "0.13.6" +description = "eth-account: Sign Ethereum transactions and messages with local private keys" +optional = false +python-versions = "<4,>=3.8" +groups = ["main"] +files = [ + {file = "eth_account-0.13.6-py3-none-any.whl", hash = "sha256:27b8c86e134ab10adec5022b55c8005f9fbdccba8b99bd318e45aa56863e1416"}, + {file = "eth_account-0.13.6.tar.gz", hash = "sha256:e496cc4c50fe4e22972f720fda4c13e126e5636d0274163888eb27f08530ac61"}, +] + +[package.dependencies] +bitarray = ">=2.4.0" +ckzg = ">=2.0.0" +eth-abi = ">=4.0.0-b.2" +eth-keyfile = ">=0.7.0,<0.9.0" +eth-keys = ">=0.4.0" +eth-rlp = ">=2.1.0" +eth-utils = ">=2.0.0" +hexbytes = ">=1.2.0" +pydantic = ">=2.0.0" +rlp = ">=1.0.0" + +[package.extras] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "coverage", "hypothesis (>=6.22.0,<6.108.7)", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] +test = ["coverage", "hypothesis (>=6.22.0,<6.108.7)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] + +[[package]] +name = "eth-hash" +version = "0.7.1" +description = "eth-hash: The Ethereum hashing function, keccak256, sometimes (erroneously) called sha3" +optional = false +python-versions = "<4,>=3.8" +groups = ["main"] +files = [ + {file = "eth_hash-0.7.1-py3-none-any.whl", hash = "sha256:0fb1add2adf99ef28883fd6228eb447ef519ea72933535ad1a0b28c6f65f868a"}, + {file = "eth_hash-0.7.1.tar.gz", hash = "sha256:d2411a403a0b0a62e8247b4117932d900ffb4c8c64b15f92620547ca5ce46be5"}, +] + +[package.dependencies] +pycryptodome = {version = ">=3.6.6,<4", optional = true, markers = "extra == \"pycryptodome\""} + +[package.extras] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] +pycryptodome = ["pycryptodome (>=3.6.6,<4)"] +pysha3 = ["pysha3 (>=1.0.0,<2.0.0) ; python_version < \"3.9\"", "safe-pysha3 (>=1.0.0) ; python_version >= \"3.9\""] +test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] + +[[package]] +name = "eth-keyfile" +version = "0.8.1" +description = "eth-keyfile: A library for handling the encrypted keyfiles used to store ethereum private keys" +optional = false +python-versions = "<4,>=3.8" +groups = ["main"] +files = [ + {file = "eth_keyfile-0.8.1-py3-none-any.whl", hash = "sha256:65387378b82fe7e86d7cb9f8d98e6d639142661b2f6f490629da09fddbef6d64"}, + {file = "eth_keyfile-0.8.1.tar.gz", hash = "sha256:9708bc31f386b52cca0969238ff35b1ac72bd7a7186f2a84b86110d3c973bec1"}, +] + +[package.dependencies] +eth-keys = ">=0.4.0" +eth-utils = ">=2" +pycryptodome = ">=3.6.6,<4" + +[package.extras] +dev = ["build (>=0.9.0)", "bumpversion (>=0.5.3)", "ipython", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "towncrier (>=21,<22)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["towncrier (>=21,<22)"] +test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] + +[[package]] +name = "eth-keys" +version = "0.7.0" +description = "eth-keys: Common API for Ethereum key operations" +optional = false +python-versions = "<4,>=3.8" +groups = ["main"] +files = [ + {file = "eth_keys-0.7.0-py3-none-any.whl", hash = "sha256:b0cdda8ffe8e5ba69c7c5ca33f153828edcace844f67aabd4542d7de38b159cf"}, + {file = "eth_keys-0.7.0.tar.gz", hash = "sha256:79d24fd876201df67741de3e3fefb3f4dbcbb6ace66e47e6fe662851a4547814"}, +] + +[package.dependencies] +eth-typing = ">=3" +eth-utils = ">=2" + +[package.extras] +coincurve = ["coincurve (>=17.0.0)"] +dev = ["asn1tools (>=0.146.2)", "build (>=0.9.0)", "bump_my_version (>=0.19.0)", "coincurve (>=17.0.0)", "eth-hash[pysha3]", "factory-boy (>=3.0.1)", "hypothesis (>=5.10.3)", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "pyasn1 (>=0.4.5)", "pytest (>=7.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["towncrier (>=24,<25)"] +test = ["asn1tools (>=0.146.2)", "eth-hash[pysha3]", "factory-boy (>=3.0.1)", "hypothesis (>=5.10.3)", "pyasn1 (>=0.4.5)", "pytest (>=7.0.0)"] + +[[package]] +name = "eth-rlp" +version = "2.2.0" +description = "eth-rlp: RLP definitions for common Ethereum objects in Python" +optional = false +python-versions = "<4,>=3.8" +groups = ["main"] +files = [ + {file = "eth_rlp-2.2.0-py3-none-any.whl", hash = "sha256:5692d595a741fbaef1203db6a2fedffbd2506d31455a6ad378c8449ee5985c47"}, + {file = "eth_rlp-2.2.0.tar.gz", hash = "sha256:5e4b2eb1b8213e303d6a232dfe35ab8c29e2d3051b86e8d359def80cd21db83d"}, +] + +[package.dependencies] +eth-utils = ">=2.0.0" +hexbytes = ">=1.2.0" +rlp = ">=0.6.0" + +[package.extras] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "eth-hash[pycryptodome]", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] +test = ["eth-hash[pycryptodome]", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] + +[[package]] +name = "eth-typing" +version = "5.2.0" +description = "eth-typing: Common type annotations for ethereum python packages" +optional = false +python-versions = "<4,>=3.8" +groups = ["main"] +files = [ + {file = "eth_typing-5.2.0-py3-none-any.whl", hash = "sha256:e1f424e97990fc3c6a1c05a7b0968caed4e20e9c99a4d5f4db3df418e25ddc80"}, + {file = "eth_typing-5.2.0.tar.gz", hash = "sha256:28685f7e2270ea0d209b75bdef76d8ecef27703e1a16399f6929820d05071c28"}, +] + +[package.dependencies] +typing_extensions = ">=4.5.0" + +[package.extras] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] +test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] + +[[package]] +name = "eth-utils" +version = "5.2.0" +description = "eth-utils: Common utility functions for python code that interacts with Ethereum" +optional = false +python-versions = "<4,>=3.8" +groups = ["main"] +files = [ + {file = "eth_utils-5.2.0-py3-none-any.whl", hash = "sha256:4d43eeb6720e89a042ad5b28d4b2111630ae764f444b85cbafb708d7f076da10"}, + {file = "eth_utils-5.2.0.tar.gz", hash = "sha256:17e474eb654df6e18f20797b22c6caabb77415a996b3ba0f3cc8df3437463134"}, +] + +[package.dependencies] +cytoolz = {version = ">=0.10.1", markers = "implementation_name == \"cpython\""} +eth-hash = ">=0.3.1" +eth-typing = ">=5.0.0" +toolz = {version = ">0.8.2", markers = "implementation_name == \"pypy\""} + +[package.extras] +dev = ["build (>=0.9.0)", "bump-my-version (>=0.19.0)", "eth-hash[pycryptodome]", "hypothesis (>=4.43.0)", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=24,<25)"] +test = ["hypothesis (>=4.43.0)", "mypy (==1.10.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] + [[package]] name = "faiss-cpu" version = "1.10.0" @@ -1126,19 +2075,19 @@ sgmllib3k = "*" [[package]] name = "filelock" -version = "3.18.0" +version = "3.16.1" description = "A platform independent file lock." optional = false -python-versions = ">=3.9" +python-versions = ">=3.8" groups = ["main", "dev"] files = [ - {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, - {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, + {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, + {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, ] [package.extras] -docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] [[package]] @@ -1734,6 +2683,23 @@ files = [ {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] +[[package]] +name = "hexbytes" +version = "1.3.0" +description = "hexbytes: Python `bytes` subclass that decodes hex, with a readable console output" +optional = false +python-versions = "<4,>=3.8" +groups = ["main"] +files = [ + {file = "hexbytes-1.3.0-py3-none-any.whl", hash = "sha256:83720b529c6e15ed21627962938dc2dec9bb1010f17bbbd66bf1e6a8287d522c"}, + {file = "hexbytes-1.3.0.tar.gz", hash = "sha256:4a61840c24b0909a6534350e2d28ee50159ca1c9e89ce275fd31c110312cf684"}, +] + +[package.extras] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "eth_utils (>=2.0.0)", "hypothesis (>=3.44.24,<=6.31.6)", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] +test = ["eth_utils (>=2.0.0)", "hypothesis (>=3.44.24,<=6.31.6)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] + [[package]] name = "httpcore" version = "1.0.7" @@ -2056,6 +3022,43 @@ files = [ {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"}, ] +[[package]] +name = "jsonschema" +version = "4.23.0" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, + {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +jsonschema-specifications = ">=2023.03.6" +referencing = ">=0.28.4" +rpds-py = ">=0.7.1" + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=24.6.0)"] + +[[package]] +name = "jsonschema-specifications" +version = "2024.10.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf"}, + {file = "jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272"}, +] + +[package.dependencies] +referencing = ">=0.31.0" + [[package]] name = "kiwisolver" version = "1.4.8" @@ -2471,6 +3474,22 @@ profiling = ["gprof2dot"] rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] +[[package]] +name = "markdownify" +version = "1.1.0" +description = "Convert HTML to markdown." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "markdownify-1.1.0-py3-none-any.whl", hash = "sha256:32a5a08e9af02c8a6528942224c91b933b4bd2c7d078f9012943776fc313eeef"}, + {file = "markdownify-1.1.0.tar.gz", hash = "sha256:449c0bbbf1401c5112379619524f33b63490a8fa479456d41de9dc9e37560ebd"}, +] + +[package.dependencies] +beautifulsoup4 = ">=4.9,<5" +six = ">=1.15,<2" + [[package]] name = "markupsafe" version = "3.0.2" @@ -2867,6 +3886,29 @@ example = ["cairocffi (>=1.7)", "contextily (>=1.6)", "igraph (>=0.11)", "momepy extra = ["lxml (>=4.6)", "pydot (>=3.0.1)", "pygraphviz (>=1.14)", "sympy (>=1.10)"] test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"] +[[package]] +name = "nilql" +version = "0.0.0a12" +description = "Library for working with encrypted data within nilDB queries and replies." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "nilql-0.0.0a12-py3-none-any.whl", hash = "sha256:400b705fa1d9856093f47811062162e6fc8e1d4de8cf1861b2a40aaf78357989"}, + {file = "nilql-0.0.0a12.tar.gz", hash = "sha256:6bd98ec270f06178439875e3251888fe52759bd309ba3639d4c0562e48a12cd8"}, +] + +[package.dependencies] +bcl = ">=2.3,<3.0" +pailliers = ">=0.1,<1.0" + +[package.extras] +coveralls = ["coveralls (>=4.0,<5.0)"] +docs = ["sphinx (>=5.0,<6.0)", "sphinx-rtd-theme (>=2.0.0,<2.1.0)", "toml (>=0.10.2,<0.11.0)"] +lint = ["pylint (>=3.2.0,<3.3.0)"] +publish = ["build (>=0.10,<1.0)", "twine (>=4.0,<5.0)"] +test = ["pytest (>=8.2,<9.0)", "pytest-cov (>=5.0,<6.0)"] + [[package]] name = "nodeenv" version = "1.9.1" @@ -3305,6 +4347,29 @@ files = [ {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] +[[package]] +name = "pailliers" +version = "0.2.0" +description = "Minimal pure-Python implementation of Paillier's additively homomorphic cryptosystem." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "pailliers-0.2.0-py3-none-any.whl", hash = "sha256:ad0ddc72be63f9b3c10200e23178fe527b566c4aa86659ab54a8faeb367ac7d6"}, + {file = "pailliers-0.2.0.tar.gz", hash = "sha256:a1d3d7d840594f51073e531078b3da4dc5a7a527b410102a0f0fa65d6c222871"}, +] + +[package.dependencies] +egcd = ">=2.0,<3.0" +rabinmiller = ">=0.1,<1.0" + +[package.extras] +coveralls = ["coveralls (>=4.0,<5.0)"] +docs = ["sphinx (>=5.0,<6.0)", "sphinx-rtd-theme (>=2.0.0,<2.1.0)", "toml (>=0.10.2,<0.11.0)"] +lint = ["pylint (>=2.17.0,<2.18.0) ; python_version < \"3.12\"", "pylint (>=3.2.0,<3.3.0) ; python_version >= \"3.12\""] +publish = ["build (>=0.10,<1.0)", "twine (>=4.0,<5.0)"] +test = ["pytest (>=7.4,<8.0) ; python_version < \"3.12\"", "pytest (>=8.2,<9.0) ; python_version >= \"3.12\"", "pytest-cov (>=4.1,<5.0) ; python_version < \"3.12\"", "pytest-cov (>=5.0,<6.0) ; python_version >= \"3.12\""] + [[package]] name = "pandas" version = "2.2.3" @@ -3391,6 +4456,43 @@ sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-d test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] xml = ["lxml (>=4.9.2)"] +[[package]] +name = "paramiko" +version = "3.5.1" +description = "SSH2 protocol library" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "paramiko-3.5.1-py3-none-any.whl", hash = "sha256:43b9a0501fc2b5e70680388d9346cf252cfb7d00b0667c39e80eb43a408b8f61"}, + {file = "paramiko-3.5.1.tar.gz", hash = "sha256:b2c665bc45b2b215bd7d7f039901b14b067da00f3a11e6640995fd58f2664822"}, +] + +[package.dependencies] +bcrypt = ">=3.2" +cryptography = ">=3.3" +pynacl = ">=1.5" + +[package.extras] +all = ["gssapi (>=1.4.1) ; platform_system != \"Windows\"", "invoke (>=2.0)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8) ; platform_system == \"Windows\""] +gssapi = ["gssapi (>=1.4.1) ; platform_system != \"Windows\"", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8) ; platform_system == \"Windows\""] +invoke = ["invoke (>=2.0)"] + +[[package]] +name = "parsimonious" +version = "0.10.0" +description = "(Soon to be) the fastest pure-Python PEG parser I could muster" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "parsimonious-0.10.0-py3-none-any.whl", hash = "sha256:982ab435fabe86519b57f6b35610aa4e4e977e9f02a14353edf4bbc75369fc0f"}, + {file = "parsimonious-0.10.0.tar.gz", hash = "sha256:8281600da180ec8ae35427a4ab4f7b82bfec1e3d1e52f80cb60ea82b9512501c"}, +] + +[package.dependencies] +regex = ">=2022.3.15" + [[package]] name = "passlib" version = "1.7.4" @@ -3515,20 +4617,20 @@ xmp = ["defusedxml"] [[package]] name = "platformdirs" -version = "4.3.7" +version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false -python-versions = ">=3.9" +python-versions = ">=3.8" groups = ["main", "dev"] files = [ - {file = "platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94"}, - {file = "platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351"}, + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, ] [package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.14.1)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] [[package]] name = "plotly" @@ -3737,7 +4839,7 @@ version = "7.0.0" description = "Cross-platform lib for process and system monitoring in Python. NOTE: the syntax of this script MUST be kept compatible with Python 2.7." optional = false python-versions = ">=3.6" -groups = ["demo"] +groups = ["main", "demo"] files = [ {file = "psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25"}, {file = "psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da"}, @@ -3755,6 +4857,128 @@ files = [ dev = ["abi3audit", "black (==24.10.0)", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest", "pytest-cov", "pytest-xdist", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "vulture", "wheel"] test = ["pytest", "pytest-xdist", "setuptools"] +[[package]] +name = "py-sr25519-bindings" +version = "0.2.2" +description = "Python bindings for schnorrkel RUST crate" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "py_sr25519_bindings-0.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd29a2ee1dfa55a3e17cf18fe0fa5f5749e0c49c9bd9c423a46e1cc242225447"}, + {file = "py_sr25519_bindings-0.2.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58702966f1547e47bfbf70924eef881087bff969e2dca15953cdc95cb2abb4a2"}, + {file = "py_sr25519_bindings-0.2.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9b5b2cdf08be144f395508acebd5fa41c81dbee1364de75ff80fe0f1308fd969"}, + {file = "py_sr25519_bindings-0.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77f7f4b323c628242909228eaac78bf6376b39b8988704e7910e858c2017b487"}, + {file = "py_sr25519_bindings-0.2.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:58b1e28fc1c57f69d37556b8f3c51cdd84446f643729789b6c0ce19ce2726bd5"}, + {file = "py_sr25519_bindings-0.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:512185a3be65a893208e9c30d11948c8b405532756f3bcab16d1dbe5d8e3355e"}, + {file = "py_sr25519_bindings-0.2.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3e5bf9343f3708cfdf5228dbb1b8a093c64144bb9c4bd02cfb014fb2241dd965"}, + {file = "py_sr25519_bindings-0.2.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e9ac26fd6b8606796fcaee90fe277820efbe168490659d26295fd0fc7b37ee4a"}, + {file = "py_sr25519_bindings-0.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:297e29518a01a24b943333fc9fe5e7102cb7083f2d256270f42476bcf5ba666d"}, + {file = "py_sr25519_bindings-0.2.2-cp310-cp310-win32.whl", hash = "sha256:f1ab3c36d94dec25767e2a54a2fb0eb320fc0c3e1d7ea573288b961d432672ef"}, + {file = "py_sr25519_bindings-0.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:cc40a53600e68848658cf6046cd43ef2ec9f0c8c04ebf8ea3636dd58c1c25296"}, + {file = "py_sr25519_bindings-0.2.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c0931d8fd07e13131e652f3211c1f1c12b7a5153bed9217e4483b195515c76f"}, + {file = "py_sr25519_bindings-0.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:09657937b8f04c034622691c4753fcef0b3857939dbeff72590b7f5de336302d"}, + {file = "py_sr25519_bindings-0.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ca700354e8cc3d082426ca5cdc7dd34a05988adec4edc0cd42d31c4ba16fbc0"}, + {file = "py_sr25519_bindings-0.2.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ddb490e99d5646ba68f5308fed1b92efbc85470b1171a2b78e555b44a7073570"}, + {file = "py_sr25519_bindings-0.2.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8a479a7510f30d2912f552335cb83d321c0be83832a71cd0bcd190f6356a7bf"}, + {file = "py_sr25519_bindings-0.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab25059d290753202f160bb8a4fd3c789ab9663381ca564338015fd3b7625dde"}, + {file = "py_sr25519_bindings-0.2.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:315382f207430143cd748f805f13bf56f36fc66726303b491cd38ce78d8972e9"}, + {file = "py_sr25519_bindings-0.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1a65275421f30e3d563c6f3dec552060f1f85b7840ab8ecf1d48ced008d0ba5f"}, + {file = "py_sr25519_bindings-0.2.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1350c85bdc903105d8fdc7dd369b802bf2821c321fea8aa0929f7a7063437d81"}, + {file = "py_sr25519_bindings-0.2.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3a34fa18885345a0102c3ffbaa17a32cd67d28a60376158508d5ed7f96a478f7"}, + {file = "py_sr25519_bindings-0.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c63b0966b45870f0b1dfc2a366f1763f4a165f3aec2b02e7464cfb2c6ca09e94"}, + {file = "py_sr25519_bindings-0.2.2-cp311-cp311-win32.whl", hash = "sha256:7bf982a7d34f6eb0c7c42b7f59610a527e9b02654079fb78d7eb757c6bd79d9d"}, + {file = "py_sr25519_bindings-0.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:d9ee79ec4e722993da24385a8eb85d97878ef67d48d0e706c098c626d798c7bc"}, + {file = "py_sr25519_bindings-0.2.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f22542738ed98fac0d3da2479dd3f26c695594800877a4d8bb116c47e4fd4b7c"}, + {file = "py_sr25519_bindings-0.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b312b8ac7c8354d5cf1b9aad993bbafbd99cc97b6d246f246e76814f576ed809"}, + {file = "py_sr25519_bindings-0.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c70ff898fa46f380a535c843e3a1a9824d1849216067bbf28eb9ad225b92f0bb"}, + {file = "py_sr25519_bindings-0.2.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:292be23ad53d9f9dbf1703a2a341005629a8f93c57cfad254c8c1230ec7d3fe3"}, + {file = "py_sr25519_bindings-0.2.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:673b31e8f59bc1478814b011921073f8ad4e2c78a1d6580b3ddb1a9d7edc4392"}, + {file = "py_sr25519_bindings-0.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:849f77ab12210e8549e58d444e9199d9aba83a988e99ca8bef04dd53e81f9561"}, + {file = "py_sr25519_bindings-0.2.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf8c1d329275c41836aee5f8789ab14100dbdc2b6f3a0210fac2abb0f7507c24"}, + {file = "py_sr25519_bindings-0.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:48f053c5e8cb66125057b25223ef5ff57bb4383a82871d47089397317c5fd792"}, + {file = "py_sr25519_bindings-0.2.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fea3ce0ac6a26a52735bb48f8daafb82d17147f776bb6d9d3c330bd2ccffe20d"}, + {file = "py_sr25519_bindings-0.2.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f44a0a9cb155af6408e3f73833a935abc98934ce097b2ad07dd13e3a88f82cb8"}, + {file = "py_sr25519_bindings-0.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cc531500823ece8d6889082642e9ea06f2eaffd0ed43d65871cb4727429027c"}, + {file = "py_sr25519_bindings-0.2.2-cp312-cp312-win32.whl", hash = "sha256:840c3ec1fc8dde12421369afa9761943efe377a7bd55a97524587e8b5a6546c2"}, + {file = "py_sr25519_bindings-0.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:c3ee5fd07b2974ce147ac7546b18729d2eb4efebe8eaad178690aaca656487f3"}, + {file = "py_sr25519_bindings-0.2.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3bb2c5fba39a82880c43b0d75e87f4d4a2416717c5fa2122b22e02689c2120e3"}, + {file = "py_sr25519_bindings-0.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1393798a36f74482c53c254969ae8d92f6549767ef69575206eaaf629cbf2a64"}, + {file = "py_sr25519_bindings-0.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29b9ee2e2f8f36676fa2a72af5bdfe257d331b3d83e5a92b45bad2f25a5b975c"}, + {file = "py_sr25519_bindings-0.2.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4e932c33f6b660319c950c300c32ad2c0ba9642743a2e709a2fb886d32c28baf"}, + {file = "py_sr25519_bindings-0.2.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1fce13a3434c57af097b8b07b69e3821b1f10623754204112c14bd544bd961c1"}, + {file = "py_sr25519_bindings-0.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16501bd5b9a37623dbf48aa6b197c57c004f9125e190450e041289a8c3eceac7"}, + {file = "py_sr25519_bindings-0.2.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:beb12471fb76be707fc9213d39e5be4cf4add7e38e08bc1fbf7e786250977e00"}, + {file = "py_sr25519_bindings-0.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:55134f0ba34c27fbb8b489a338c6cb6a31465813f615ed93afbd67e844ef3aed"}, + {file = "py_sr25519_bindings-0.2.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:785521c868738a2345e3625ad9166ede228f63e9d3f0c7ff8e35f49d636bce04"}, + {file = "py_sr25519_bindings-0.2.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c8cab5620a4ef4cc69a314c9e9ac17af1c0d4d11e297fcefe5d71d827fd7ee21"}, + {file = "py_sr25519_bindings-0.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15ae6f86f112c6b23d357b5a98a6cb493f5c2734fabff354a8198be9dea0e90e"}, + {file = "py_sr25519_bindings-0.2.2-cp313-cp313-win32.whl", hash = "sha256:cba9efa48f48bf56e73a528005978b6f05cb2c847e21eb9645bbc6581619482f"}, + {file = "py_sr25519_bindings-0.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:9cdb4e0f231fd5824f73361a37a102871866d29752f96d88b1da958f1e5ff2d4"}, + {file = "py_sr25519_bindings-0.2.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1d436db7f48dabd4201bb1a88c66a6a3cd15a40e89a236ec1b8cb60037dc1a9"}, + {file = "py_sr25519_bindings-0.2.2-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a9b8c9a81f90dc330eabbdc3ec5f9fdf84a34cd37a1e660cbf5c5daec7b2d08f"}, + {file = "py_sr25519_bindings-0.2.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f496da3eb2d843bd12ccff871d22d086b08cfe95852ca91dcdbd91e350aca8d"}, + {file = "py_sr25519_bindings-0.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:862fa69f948cb3028051a71ce0d2d88cbe8b52723c782f0972d12f5f85a25637"}, + {file = "py_sr25519_bindings-0.2.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:1111597744d7993ce732f785e97e0d2e4f9554509d90ba4b0e99829dbf1c2e6d"}, + {file = "py_sr25519_bindings-0.2.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c4518b553335f70f18b8167eb2b7f533a66eb703f251d4d4b36c4a03d14cd75e"}, + {file = "py_sr25519_bindings-0.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c917a8f365450be06e051f8d8671c182057cdda42bd5f6883c5f537a2bac4f5a"}, + {file = "py_sr25519_bindings-0.2.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b8b2666d381416fb07336c6596a5554dd0e0f1ec50ff32bcc975ae29df79961"}, + {file = "py_sr25519_bindings-0.2.2-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5644353660fd9f97318d70fb7cf362f969a5ee572b61df8f18eda5fea80a6514"}, + {file = "py_sr25519_bindings-0.2.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0728fff2a29d4cc76c4cf22142cd2e2e8dc37745b213a866412980191e1260c"}, + {file = "py_sr25519_bindings-0.2.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b74c31e2960c4af5b709b562aaf610989af532aee771fcdf175533de60441607"}, + {file = "py_sr25519_bindings-0.2.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3d3c6836df2d67008e3f11080fb216e414cc924de256dd118e50a92cd334f143"}, + {file = "py_sr25519_bindings-0.2.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:dc5a57b67384244083b8d0831f9490dadca34e0543c1bf2f3a876aa4e7081961"}, + {file = "py_sr25519_bindings-0.2.2-cp37-cp37m-musllinux_1_2_armv7l.whl", hash = "sha256:ea139e7bf80ddc1c682db439825bec56baf745d643c146a783e9ddb737af266a"}, + {file = "py_sr25519_bindings-0.2.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:727f91cff7901db2d4e0f762dafd48c2b1086945b4903dcdd0a3eb65624c17c8"}, + {file = "py_sr25519_bindings-0.2.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0e4217f59936ae13fa4215838d2da59c130572408e3f29a9f7ca436924f4b356"}, + {file = "py_sr25519_bindings-0.2.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e6f2c026aa910cac7f16723b6b3e9736fe805e51b6ba41cfb4e25a4c0a6442"}, + {file = "py_sr25519_bindings-0.2.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:776d1a1f114b1f0553c9c8336545daaf20443d0b681c47c499377f69406f7a56"}, + {file = "py_sr25519_bindings-0.2.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:314893d4ea96877560bc12446956d61707ca46fb99040ffad751a0710a7aa87f"}, + {file = "py_sr25519_bindings-0.2.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:277e8ef38c9d899b1855fdcde07ae73a9917e06c46df556b8ca3216ae585b532"}, + {file = "py_sr25519_bindings-0.2.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:695e80ccdb710efba2f909235b18eaf230cf0b3f60e8d52a1c904eaeeff839ba"}, + {file = "py_sr25519_bindings-0.2.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2777013ff914bcc87e657657e99922fa48f3bb674734550989fb210fb3d878a2"}, + {file = "py_sr25519_bindings-0.2.2-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:30f1af9306fda911f296db29b4fff06197d3f38de5643b3d95862d3833db1e41"}, + {file = "py_sr25519_bindings-0.2.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:56eeafc92d3c66990ab97ff91c09b3295aea6dac9b64af0227750a8192aeaeec"}, + {file = "py_sr25519_bindings-0.2.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f7126db740eb190cf1ba993e066f03c2914edaf08e6963d10bbdd740922c95e6"}, + {file = "py_sr25519_bindings-0.2.2-cp38-cp38-win32.whl", hash = "sha256:7b20210a0a0b39e3f0bcb7832a3df736eeea2fcc5776dba1ce5b0c050e489145"}, + {file = "py_sr25519_bindings-0.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:5329e2e54eb9850c2eb84d6a226bd98cdc3597535453eced920035e1e026dced"}, + {file = "py_sr25519_bindings-0.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b32743bba0a53225097120b212da88c14584022a357d7e91cf19ed0a3adad9f6"}, + {file = "py_sr25519_bindings-0.2.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd56849d9195693c6a8e7c48efe4256918b6afeec090915f3f8f883cdb8addda"}, + {file = "py_sr25519_bindings-0.2.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f46c87fd7a8e55c4fce272d4e34663d3c7c3ffee906826a2a16a1400027aa5b9"}, + {file = "py_sr25519_bindings-0.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ee25565a287690d6e48302f4584775622ce3329d2ab92fd3b0a4f063d4ca91f"}, + {file = "py_sr25519_bindings-0.2.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b1199dab44704aa34401428ca3170da5b7ffdc8c65208a2c75a3c1fe5298b20e"}, + {file = "py_sr25519_bindings-0.2.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702a28e8ead7d1d664bea22168158edcc0e3d36e5cc1a79c7373ab1636f89cc2"}, + {file = "py_sr25519_bindings-0.2.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:3de67c503155019c494e5887d1797f046afe1aeb99ee4b3cf86c15386330f034"}, + {file = "py_sr25519_bindings-0.2.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e1a48b295b562836d72dee7136c0503ea63cd89fec85d418b91ba040471c37ed"}, + {file = "py_sr25519_bindings-0.2.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:acebe18639616cd00ad0544d9bdaa73c545f00c4fc0d29b8a4e1e6c447f7a607"}, + {file = "py_sr25519_bindings-0.2.2-cp39-cp39-win32.whl", hash = "sha256:62e4bdd094589446a24dc1029cf2ef6c869e1f4fede04e17335bc92e60640fc5"}, + {file = "py_sr25519_bindings-0.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:0c2483031813f908da35c380196bc88410e2542482a5b4b51d265c6566de116c"}, + {file = "py_sr25519_bindings-0.2.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c9ee94a9fa1625f3192c89ecacc2bd012e09b57e6d2ad8ede027b31381609d3"}, + {file = "py_sr25519_bindings-0.2.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e4c95b3b6230faf2e89f6e5407d63a9d0d52385e6b7d42205570f5ee2f927940"}, + {file = "py_sr25519_bindings-0.2.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ab2b063129babc8f1d9fe6cf5c2d8cc434b6797c553440302da1fab987d74ab"}, + {file = "py_sr25519_bindings-0.2.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1bf93d46ab717089f4fac9b764ea4e7be9f4a45a62bd9919ef850ae8d2ae433"}, + {file = "py_sr25519_bindings-0.2.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5e927dd15d61e76fde3fa8bd6d776148ea987944fee1fe815fbc40c6a77f61ad"}, + {file = "py_sr25519_bindings-0.2.2-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:6584cf4c87fae9bdd64bc50dd786d9c805165bb6bc7a1ff545e77b29a78acb8d"}, + {file = "py_sr25519_bindings-0.2.2-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:a26b1add87dc6086463975aa1d889f39f90b0d22949d4de52e8a53e516bd2ac4"}, + {file = "py_sr25519_bindings-0.2.2-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:84cb2c645ce60a04688dedf61ed289d4fb716aef4129313814be1a2d47e268e7"}, + {file = "py_sr25519_bindings-0.2.2-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:76e660007dd415de22b1d99a884ee39cb6abf03f24377f58e4498533856c2bac"}, + {file = "py_sr25519_bindings-0.2.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d0afcb14cd493485175dc9bf8c57b8b37581bbb29f55b6e4f3ce1f803222488"}, + {file = "py_sr25519_bindings-0.2.2-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b5c5f9080e879badefff596361520b2d9de9d9c4be7c14b36a017d798c451e2"}, + {file = "py_sr25519_bindings-0.2.2-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a42353737c22a3fa425ac350f5fac74d5b2f9c3cdb8ad44dbb367bd7869774cc"}, + {file = "py_sr25519_bindings-0.2.2-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:3ea3dc2c6a2a38b791114bd50021f10db2dd2a1d7a1a1aac0c7e80d885c0d3b5"}, + {file = "py_sr25519_bindings-0.2.2-pp38-pypy38_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:888d98a2d7736e0f269c8ab1f09dfac04af2d024b18c0485adc3615277f3442b"}, + {file = "py_sr25519_bindings-0.2.2-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:50976e9b22328df5696fcbfded27215a35225ee41e0b3f1b26a01ab00ad08143"}, + {file = "py_sr25519_bindings-0.2.2-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6449963cd6b300b224a1bc1fec77df012e30d609e54ccffe4f44f4563eab27c2"}, + {file = "py_sr25519_bindings-0.2.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:726346118fc2b785525945ee71ea1acf9be84c41e266c2345c93c7d4d6132cbc"}, + {file = "py_sr25519_bindings-0.2.2-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4dd5cfe48faafa7e112a021284663db8b64efd9b2225e69906f0bf7f3159a3ce"}, + {file = "py_sr25519_bindings-0.2.2-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:85432a949143d7a20e452b4c89d5f0ad9a0162e1ce5a904fc157fe204cbe5ded"}, + {file = "py_sr25519_bindings-0.2.2-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:04a2f2eac269bb2f9bf30c795990211cd8d4cfdd28eafbd73b2dfc77a9ef940f"}, + {file = "py_sr25519_bindings-0.2.2-pp39-pypy39_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:bc7ecd25700a664d835cc9db5f6a4f65fef62a395762487c8c2a661566316e8f"}, + {file = "py_sr25519_bindings-0.2.2-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:9efc5526c0eb74c2f8df809c47e22d62febc31db8f38b5c6b1253e810e0ed71f"}, + {file = "py_sr25519_bindings-0.2.2-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:daa74fdd7bac2d97fbbbbb1ca40a0c02102220d09cfa9695cbde8d2cbedfadb7"}, + {file = "py_sr25519_bindings-0.2.2.tar.gz", hash = "sha256:192d65d3bc43c6f4121a0732e1f6eb6ad869897ca26368ba032e96a82b3b7606"}, +] + [[package]] name = "pyasn1" version = "0.4.8" @@ -3807,16 +5031,55 @@ files = [ ] markers = {demo = "platform_python_implementation != \"PyPy\""} +[[package]] +name = "pycryptodome" +version = "3.22.0" +description = "Cryptographic library for Python" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +files = [ + {file = "pycryptodome-3.22.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:96e73527c9185a3d9b4c6d1cfb4494f6ced418573150be170f6580cb975a7f5a"}, + {file = "pycryptodome-3.22.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:9e1bb165ea1dc83a11e5dbbe00ef2c378d148f3a2d3834fb5ba4e0f6fd0afe4b"}, + {file = "pycryptodome-3.22.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:d4d1174677855c266eed5c4b4e25daa4225ad0c9ffe7584bb1816767892545d0"}, + {file = "pycryptodome-3.22.0-cp27-cp27m-win32.whl", hash = "sha256:9dbb749cef71c28271484cbef684f9b5b19962153487735411e1020ca3f59cb1"}, + {file = "pycryptodome-3.22.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f1ae7beb64d4fc4903a6a6cca80f1f448e7a8a95b77d106f8a29f2eb44d17547"}, + {file = "pycryptodome-3.22.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:a26bcfee1293b7257c83b0bd13235a4ee58165352be4f8c45db851ba46996dc6"}, + {file = "pycryptodome-3.22.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:009e1c80eea42401a5bd5983c4bab8d516aef22e014a4705622e24e6d9d703c6"}, + {file = "pycryptodome-3.22.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3b76fa80daeff9519d7e9f6d9e40708f2fce36b9295a847f00624a08293f4f00"}, + {file = "pycryptodome-3.22.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a31fa5914b255ab62aac9265654292ce0404f6b66540a065f538466474baedbc"}, + {file = "pycryptodome-3.22.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0092fd476701eeeb04df5cc509d8b739fa381583cda6a46ff0a60639b7cd70d"}, + {file = "pycryptodome-3.22.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d5b0ddc7cf69231736d778bd3ae2b3efb681ae33b64b0c92fb4626bb48bb89"}, + {file = "pycryptodome-3.22.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f6cf6aa36fcf463e622d2165a5ad9963b2762bebae2f632d719dfb8544903cf5"}, + {file = "pycryptodome-3.22.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:aec7b40a7ea5af7c40f8837adf20a137d5e11a6eb202cde7e588a48fb2d871a8"}, + {file = "pycryptodome-3.22.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d21c1eda2f42211f18a25db4eaf8056c94a8563cd39da3683f89fe0d881fb772"}, + {file = "pycryptodome-3.22.0-cp37-abi3-win32.whl", hash = "sha256:f02baa9f5e35934c6e8dcec91fcde96612bdefef6e442813b8ea34e82c84bbfb"}, + {file = "pycryptodome-3.22.0-cp37-abi3-win_amd64.whl", hash = "sha256:d086aed307e96d40c23c42418cbbca22ecc0ab4a8a0e24f87932eeab26c08627"}, + {file = "pycryptodome-3.22.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:98fd9da809d5675f3a65dcd9ed384b9dc67edab6a4cda150c5870a8122ec961d"}, + {file = "pycryptodome-3.22.0-pp27-pypy_73-win32.whl", hash = "sha256:37ddcd18284e6b36b0a71ea495a4c4dca35bb09ccc9bfd5b91bfaf2321f131c1"}, + {file = "pycryptodome-3.22.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b4bdce34af16c1dcc7f8c66185684be15f5818afd2a82b75a4ce6b55f9783e13"}, + {file = "pycryptodome-3.22.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2988ffcd5137dc2d27eb51cd18c0f0f68e5b009d5fec56fbccb638f90934f333"}, + {file = "pycryptodome-3.22.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e653519dedcd1532788547f00eeb6108cc7ce9efdf5cc9996abce0d53f95d5a9"}, + {file = "pycryptodome-3.22.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5810bc7494e4ac12a4afef5a32218129e7d3890ce3f2b5ec520cc69eb1102ad"}, + {file = "pycryptodome-3.22.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7514a1aebee8e85802d154fdb261381f1cb9b7c5a54594545145b8ec3056ae6"}, + {file = "pycryptodome-3.22.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:56c6f9342fcb6c74e205fbd2fee568ec4cdbdaa6165c8fde55dbc4ba5f584464"}, + {file = "pycryptodome-3.22.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87a88dc543b62b5c669895caf6c5a958ac7abc8863919e94b7a6cafd2f64064f"}, + {file = "pycryptodome-3.22.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7a683bc9fa585c0dfec7fa4801c96a48d30b30b096e3297f9374f40c2fedafc"}, + {file = "pycryptodome-3.22.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f4f6f47a7f411f2c157e77bbbda289e0c9f9e1e9944caa73c1c2e33f3f92d6e"}, + {file = "pycryptodome-3.22.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6cf9553b29624961cab0785a3177a333e09e37ba62ad22314ebdbb01ca79840"}, + {file = "pycryptodome-3.22.0.tar.gz", hash = "sha256:fd7ab568b3ad7b77c908d7c3f7e167ec5a8f035c64ff74f10d47a4edd043d723"}, +] + [[package]] name = "pydantic" -version = "2.10.6" +version = "2.10.4" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" groups = ["main", "demo"] files = [ - {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"}, - {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"}, + {file = "pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d"}, + {file = "pydantic-2.10.4.tar.gz", hash = "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06"}, ] [package.dependencies] @@ -4017,6 +5280,27 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pyjwt" +version = "2.10.1" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, + {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, +] + +[package.dependencies] +cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + [[package]] name = "pylint" version = "3.3.3" @@ -4045,6 +5329,33 @@ tomlkit = ">=0.10.1" spelling = ["pyenchant (>=3.2,<4.0)"] testutils = ["gitpython (>3)"] +[[package]] +name = "pynacl" +version = "1.5.0" +description = "Python binding to the Networking and Cryptography (NaCl) library" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, + {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, +] + +[package.dependencies] +cffi = ">=1.4.1" + +[package.extras] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] +tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] + [[package]] name = "pyparsing" version = "3.2.3" @@ -4060,6 +5371,25 @@ files = [ [package.extras] diagrams = ["jinja2", "railroad-diagrams"] +[[package]] +name = "pyproject-api" +version = "1.8.0" +description = "API to interact with the python pyproject.toml based projects" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pyproject_api-1.8.0-py3-none-any.whl", hash = "sha256:3d7d347a047afe796fd5d1885b1e391ba29be7169bd2f102fcd378f04273d228"}, + {file = "pyproject_api-1.8.0.tar.gz", hash = "sha256:77b8049f2feb5d33eefcc21b57f1e279636277a8ac8ad6b5871037b243778496"}, +] + +[package.dependencies] +packaging = ">=24.1" + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx-autodoc-typehints (>=2.4.1)"] +testing = ["covdefaults (>=2.3)", "pytest (>=8.3.3)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "setuptools (>=75.1)"] + [[package]] name = "pytest" version = "8.3.4" @@ -4178,6 +5508,45 @@ files = [ {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, ] +[[package]] +name = "pyunormalize" +version = "16.0.0" +description = "Unicode normalization forms (NFC, NFKC, NFD, NFKD). A library independent of the Python core Unicode database." +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "pyunormalize-16.0.0-py3-none-any.whl", hash = "sha256:c647d95e5d1e2ea9a2f448d1d95d8518348df24eab5c3fd32d2b5c3300a49152"}, + {file = "pyunormalize-16.0.0.tar.gz", hash = "sha256:2e1dfbb4a118154ae26f70710426a52a364b926c9191f764601f5a8cb12761f7"}, +] + +[[package]] +name = "pywin32" +version = "310" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +groups = ["main"] +markers = "platform_system == \"Windows\"" +files = [ + {file = "pywin32-310-cp310-cp310-win32.whl", hash = "sha256:6dd97011efc8bf51d6793a82292419eba2c71cf8e7250cfac03bba284454abc1"}, + {file = "pywin32-310-cp310-cp310-win_amd64.whl", hash = "sha256:c3e78706e4229b915a0821941a84e7ef420bf2b77e08c9dae3c76fd03fd2ae3d"}, + {file = "pywin32-310-cp310-cp310-win_arm64.whl", hash = "sha256:33babed0cf0c92a6f94cc6cc13546ab24ee13e3e800e61ed87609ab91e4c8213"}, + {file = "pywin32-310-cp311-cp311-win32.whl", hash = "sha256:1e765f9564e83011a63321bb9d27ec456a0ed90d3732c4b2e312b855365ed8bd"}, + {file = "pywin32-310-cp311-cp311-win_amd64.whl", hash = "sha256:126298077a9d7c95c53823934f000599f66ec9296b09167810eb24875f32689c"}, + {file = "pywin32-310-cp311-cp311-win_arm64.whl", hash = "sha256:19ec5fc9b1d51c4350be7bb00760ffce46e6c95eaf2f0b2f1150657b1a43c582"}, + {file = "pywin32-310-cp312-cp312-win32.whl", hash = "sha256:8a75a5cc3893e83a108c05d82198880704c44bbaee4d06e442e471d3c9ea4f3d"}, + {file = "pywin32-310-cp312-cp312-win_amd64.whl", hash = "sha256:bf5c397c9a9a19a6f62f3fb821fbf36cac08f03770056711f765ec1503972060"}, + {file = "pywin32-310-cp312-cp312-win_arm64.whl", hash = "sha256:2349cc906eae872d0663d4d6290d13b90621eaf78964bb1578632ff20e152966"}, + {file = "pywin32-310-cp313-cp313-win32.whl", hash = "sha256:5d241a659c496ada3253cd01cfaa779b048e90ce4b2b38cd44168ad555ce74ab"}, + {file = "pywin32-310-cp313-cp313-win_amd64.whl", hash = "sha256:667827eb3a90208ddbdcc9e860c81bde63a135710e21e4cb3348968e4bd5249e"}, + {file = "pywin32-310-cp313-cp313-win_arm64.whl", hash = "sha256:e308f831de771482b7cf692a1f308f8fca701b2d8f9dde6cc440c7da17e47b33"}, + {file = "pywin32-310-cp38-cp38-win32.whl", hash = "sha256:0867beb8addefa2e3979d4084352e4ac6e991ca45373390775f7084cc0209b9c"}, + {file = "pywin32-310-cp38-cp38-win_amd64.whl", hash = "sha256:30f0a9b3138fb5e07eb4973b7077e1883f558e40c578c6925acc7a94c34eaa36"}, + {file = "pywin32-310-cp39-cp39-win32.whl", hash = "sha256:851c8d927af0d879221e616ae1f66145253537bbdd321a77e8ef701b443a9a1a"}, + {file = "pywin32-310-cp39-cp39-win_amd64.whl", hash = "sha256:96867217335559ac619f00ad70e513c0fcf84b8a3af9fc2bba3b59b97da70475"}, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -4241,6 +5610,25 @@ files = [ {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] +[[package]] +name = "rabinmiller" +version = "0.1.0" +description = "Pure-Python implementation of the Miller-Rabin primality test." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "rabinmiller-0.1.0-py3-none-any.whl", hash = "sha256:3fec2d26fc210772ced965a8f0e2870e5582cadf255bc665ef3f4932752ada5f"}, + {file = "rabinmiller-0.1.0.tar.gz", hash = "sha256:a9873aa6fdd0c26d5205d99e126fd94e6e1bb2aa966e167e136dfbfab0d0556d"}, +] + +[package.extras] +coveralls = ["coveralls (>=4.0,<5.0)"] +docs = ["sphinx (>=5.0,<6.0)", "sphinx-autodoc-typehints (>=1.23.0,<1.24.0)", "sphinx-rtd-theme (>=2.0.0,<2.1.0)", "toml (>=0.10.2,<0.11.0)"] +lint = ["pylint (>=2.17.0,<2.18.0) ; python_version < \"3.12\"", "pylint (>=3.2.0,<3.3.0) ; python_version >= \"3.12\""] +publish = ["build (>=0.10,<1.0)", "twine (>=4.0,<5.0)"] +test = ["pytest (>=7.4,<8.0) ; python_version < \"3.12\"", "pytest (>=8.2,<9.0) ; python_version >= \"3.12\"", "pytest-cov (>=4.1,<5.0) ; python_version < \"3.12\"", "pytest-cov (>=5.0,<6.0) ; python_version >= \"3.12\""] + [[package]] name = "redis" version = "5.2.1" @@ -4260,6 +5648,23 @@ async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\ hiredis = ["hiredis (>=3.0.0)"] ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==23.2.1)", "requests (>=2.31.0)"] +[[package]] +name = "referencing" +version = "0.36.2" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0"}, + {file = "referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" +typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} + [[package]] name = "regex" version = "2024.11.6" @@ -4401,6 +5806,27 @@ files = [ [package.dependencies] requests = ">=2.0.1,<3.0.0" +[[package]] +name = "rlp" +version = "4.1.0" +description = "rlp: A package for Recursive Length Prefix encoding and decoding" +optional = false +python-versions = "<4,>=3.8" +groups = ["main"] +files = [ + {file = "rlp-4.1.0-py3-none-any.whl", hash = "sha256:8eca394c579bad34ee0b937aecb96a57052ff3716e19c7a578883e767bc5da6f"}, + {file = "rlp-4.1.0.tar.gz", hash = "sha256:be07564270a96f3e225e2c107db263de96b5bc1f27722d2855bd3459a08e95a9"}, +] + +[package.dependencies] +eth-utils = ">=2" + +[package.extras] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "hypothesis (>=6.22.0,<6.108.7)", "ipython", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] +rust-backend = ["rusty-rlp (>=0.2.1)"] +test = ["hypothesis (>=6.22.0,<6.108.7)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] + [[package]] name = "roman-numerals-py" version = "3.1.0" @@ -4417,6 +5843,130 @@ files = [ lint = ["mypy (==1.15.0)", "pyright (==1.1.394)", "ruff (==0.9.7)"] test = ["pytest (>=8)"] +[[package]] +name = "rpds-py" +version = "0.24.0" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "rpds_py-0.24.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:006f4342fe729a368c6df36578d7a348c7c716be1da0a1a0f86e3021f8e98724"}, + {file = "rpds_py-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2d53747da70a4e4b17f559569d5f9506420966083a31c5fbd84e764461c4444b"}, + {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8acd55bd5b071156bae57b555f5d33697998752673b9de554dd82f5b5352727"}, + {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7e80d375134ddb04231a53800503752093dbb65dad8dabacce2c84cccc78e964"}, + {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60748789e028d2a46fc1c70750454f83c6bdd0d05db50f5ae83e2db500b34da5"}, + {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e1daf5bf6c2be39654beae83ee6b9a12347cb5aced9a29eecf12a2d25fff664"}, + {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b221c2457d92a1fb3c97bee9095c874144d196f47c038462ae6e4a14436f7bc"}, + {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:66420986c9afff67ef0c5d1e4cdc2d0e5262f53ad11e4f90e5e22448df485bf0"}, + {file = "rpds_py-0.24.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:43dba99f00f1d37b2a0265a259592d05fcc8e7c19d140fe51c6e6f16faabeb1f"}, + {file = "rpds_py-0.24.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a88c0d17d039333a41d9bf4616bd062f0bd7aa0edeb6cafe00a2fc2a804e944f"}, + {file = "rpds_py-0.24.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc31e13ce212e14a539d430428cd365e74f8b2d534f8bc22dd4c9c55b277b875"}, + {file = "rpds_py-0.24.0-cp310-cp310-win32.whl", hash = "sha256:fc2c1e1b00f88317d9de6b2c2b39b012ebbfe35fe5e7bef980fd2a91f6100a07"}, + {file = "rpds_py-0.24.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0145295ca415668420ad142ee42189f78d27af806fcf1f32a18e51d47dd2052"}, + {file = "rpds_py-0.24.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2d3ee4615df36ab8eb16c2507b11e764dcc11fd350bbf4da16d09cda11fcedef"}, + {file = "rpds_py-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e13ae74a8a3a0c2f22f450f773e35f893484fcfacb00bb4344a7e0f4f48e1f97"}, + {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf86f72d705fc2ef776bb7dd9e5fbba79d7e1f3e258bf9377f8204ad0fc1c51e"}, + {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c43583ea8517ed2e780a345dd9960896afc1327e8cf3ac8239c167530397440d"}, + {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4cd031e63bc5f05bdcda120646a0d32f6d729486d0067f09d79c8db5368f4586"}, + {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34d90ad8c045df9a4259c47d2e16a3f21fdb396665c94520dbfe8766e62187a4"}, + {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e838bf2bb0b91ee67bf2b889a1a841e5ecac06dd7a2b1ef4e6151e2ce155c7ae"}, + {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04ecf5c1ff4d589987b4d9882872f80ba13da7d42427234fce8f22efb43133bc"}, + {file = "rpds_py-0.24.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:630d3d8ea77eabd6cbcd2ea712e1c5cecb5b558d39547ac988351195db433f6c"}, + {file = "rpds_py-0.24.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ebcb786b9ff30b994d5969213a8430cbb984cdd7ea9fd6df06663194bd3c450c"}, + {file = "rpds_py-0.24.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:174e46569968ddbbeb8a806d9922f17cd2b524aa753b468f35b97ff9c19cb718"}, + {file = "rpds_py-0.24.0-cp311-cp311-win32.whl", hash = "sha256:5ef877fa3bbfb40b388a5ae1cb00636a624690dcb9a29a65267054c9ea86d88a"}, + {file = "rpds_py-0.24.0-cp311-cp311-win_amd64.whl", hash = "sha256:e274f62cbd274359eff63e5c7e7274c913e8e09620f6a57aae66744b3df046d6"}, + {file = "rpds_py-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d8551e733626afec514b5d15befabea0dd70a343a9f23322860c4f16a9430205"}, + {file = "rpds_py-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e374c0ce0ca82e5b67cd61fb964077d40ec177dd2c4eda67dba130de09085c7"}, + {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d69d003296df4840bd445a5d15fa5b6ff6ac40496f956a221c4d1f6f7b4bc4d9"}, + {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8212ff58ac6dfde49946bea57474a386cca3f7706fc72c25b772b9ca4af6b79e"}, + {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:528927e63a70b4d5f3f5ccc1fa988a35456eb5d15f804d276709c33fc2f19bda"}, + {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a824d2c7a703ba6daaca848f9c3d5cb93af0505be505de70e7e66829affd676e"}, + {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44d51febb7a114293ffd56c6cf4736cb31cd68c0fddd6aa303ed09ea5a48e029"}, + {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3fab5f4a2c64a8fb64fc13b3d139848817a64d467dd6ed60dcdd6b479e7febc9"}, + {file = "rpds_py-0.24.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9be4f99bee42ac107870c61dfdb294d912bf81c3c6d45538aad7aecab468b6b7"}, + {file = "rpds_py-0.24.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:564c96b6076a98215af52f55efa90d8419cc2ef45d99e314fddefe816bc24f91"}, + {file = "rpds_py-0.24.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:75a810b7664c17f24bf2ffd7f92416c00ec84b49bb68e6a0d93e542406336b56"}, + {file = "rpds_py-0.24.0-cp312-cp312-win32.whl", hash = "sha256:f6016bd950be4dcd047b7475fdf55fb1e1f59fc7403f387be0e8123e4a576d30"}, + {file = "rpds_py-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:998c01b8e71cf051c28f5d6f1187abbdf5cf45fc0efce5da6c06447cba997034"}, + {file = "rpds_py-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2d8e4508e15fc05b31285c4b00ddf2e0eb94259c2dc896771966a163122a0c"}, + {file = "rpds_py-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f00c16e089282ad68a3820fd0c831c35d3194b7cdc31d6e469511d9bffc535c"}, + {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951cc481c0c395c4a08639a469d53b7d4afa252529a085418b82a6b43c45c240"}, + {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9ca89938dff18828a328af41ffdf3902405a19f4131c88e22e776a8e228c5a8"}, + {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0ef550042a8dbcd657dfb284a8ee00f0ba269d3f2286b0493b15a5694f9fe8"}, + {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b2356688e5d958c4d5cb964af865bea84db29971d3e563fb78e46e20fe1848b"}, + {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78884d155fd15d9f64f5d6124b486f3d3f7fd7cd71a78e9670a0f6f6ca06fb2d"}, + {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a4a535013aeeef13c5532f802708cecae8d66c282babb5cd916379b72110cf7"}, + {file = "rpds_py-0.24.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:84e0566f15cf4d769dade9b366b7b87c959be472c92dffb70462dd0844d7cbad"}, + {file = "rpds_py-0.24.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:823e74ab6fbaa028ec89615ff6acb409e90ff45580c45920d4dfdddb069f2120"}, + {file = "rpds_py-0.24.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c61a2cb0085c8783906b2f8b1f16a7e65777823c7f4d0a6aaffe26dc0d358dd9"}, + {file = "rpds_py-0.24.0-cp313-cp313-win32.whl", hash = "sha256:60d9b630c8025b9458a9d114e3af579a2c54bd32df601c4581bd054e85258143"}, + {file = "rpds_py-0.24.0-cp313-cp313-win_amd64.whl", hash = "sha256:6eea559077d29486c68218178ea946263b87f1c41ae7f996b1f30a983c476a5a"}, + {file = "rpds_py-0.24.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:d09dc82af2d3c17e7dd17120b202a79b578d79f2b5424bda209d9966efeed114"}, + {file = "rpds_py-0.24.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5fc13b44de6419d1e7a7e592a4885b323fbc2f46e1f22151e3a8ed3b8b920405"}, + {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c347a20d79cedc0a7bd51c4d4b7dbc613ca4e65a756b5c3e57ec84bd43505b47"}, + {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20f2712bd1cc26a3cc16c5a1bfee9ed1abc33d4cdf1aabd297fe0eb724df4272"}, + {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aad911555286884be1e427ef0dc0ba3929e6821cbeca2194b13dc415a462c7fd"}, + {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0aeb3329c1721c43c58cae274d7d2ca85c1690d89485d9c63a006cb79a85771a"}, + {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a0f156e9509cee987283abd2296ec816225145a13ed0391df8f71bf1d789e2d"}, + {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aa6800adc8204ce898c8a424303969b7aa6a5e4ad2789c13f8648739830323b7"}, + {file = "rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a18fc371e900a21d7392517c6f60fe859e802547309e94313cd8181ad9db004d"}, + {file = "rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9168764133fd919f8dcca2ead66de0105f4ef5659cbb4fa044f7014bed9a1797"}, + {file = "rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f6e3cec44ba05ee5cbdebe92d052f69b63ae792e7d05f1020ac5e964394080c"}, + {file = "rpds_py-0.24.0-cp313-cp313t-win32.whl", hash = "sha256:8ebc7e65ca4b111d928b669713865f021b7773350eeac4a31d3e70144297baba"}, + {file = "rpds_py-0.24.0-cp313-cp313t-win_amd64.whl", hash = "sha256:675269d407a257b8c00a6b58205b72eec8231656506c56fd429d924ca00bb350"}, + {file = "rpds_py-0.24.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a36b452abbf29f68527cf52e181fced56685731c86b52e852053e38d8b60bc8d"}, + {file = "rpds_py-0.24.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b3b397eefecec8e8e39fa65c630ef70a24b09141a6f9fc17b3c3a50bed6b50e"}, + {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdabcd3beb2a6dca7027007473d8ef1c3b053347c76f685f5f060a00327b8b65"}, + {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5db385bacd0c43f24be92b60c857cf760b7f10d8234f4bd4be67b5b20a7c0b6b"}, + {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8097b3422d020ff1c44effc40ae58e67d93e60d540a65649d2cdaf9466030791"}, + {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:493fe54318bed7d124ce272fc36adbf59d46729659b2c792e87c3b95649cdee9"}, + {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8aa362811ccdc1f8dadcc916c6d47e554169ab79559319ae9fae7d7752d0d60c"}, + {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d8f9a6e7fd5434817526815f09ea27f2746c4a51ee11bb3439065f5fc754db58"}, + {file = "rpds_py-0.24.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8205ee14463248d3349131bb8099efe15cd3ce83b8ef3ace63c7e976998e7124"}, + {file = "rpds_py-0.24.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:921ae54f9ecba3b6325df425cf72c074cd469dea843fb5743a26ca7fb2ccb149"}, + {file = "rpds_py-0.24.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:32bab0a56eac685828e00cc2f5d1200c548f8bc11f2e44abf311d6b548ce2e45"}, + {file = "rpds_py-0.24.0-cp39-cp39-win32.whl", hash = "sha256:f5c0ed12926dec1dfe7d645333ea59cf93f4d07750986a586f511c0bc61fe103"}, + {file = "rpds_py-0.24.0-cp39-cp39-win_amd64.whl", hash = "sha256:afc6e35f344490faa8276b5f2f7cbf71f88bc2cda4328e00553bd451728c571f"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:619ca56a5468f933d940e1bf431c6f4e13bef8e688698b067ae68eb4f9b30e3a"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:4b28e5122829181de1898c2c97f81c0b3246d49f585f22743a1246420bb8d399"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e5ab32cf9eb3647450bc74eb201b27c185d3857276162c101c0f8c6374e098"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:208b3a70a98cf3710e97cabdc308a51cd4f28aa6e7bb11de3d56cd8b74bab98d"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbc4362e06f950c62cad3d4abf1191021b2ffaf0b31ac230fbf0526453eee75e"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ebea2821cdb5f9fef44933617be76185b80150632736f3d76e54829ab4a3b4d1"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4df06c35465ef4d81799999bba810c68d29972bf1c31db61bfdb81dd9d5bb"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3aa13bdf38630da298f2e0d77aca967b200b8cc1473ea05248f6c5e9c9bdb44"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:041f00419e1da7a03c46042453598479f45be3d787eb837af382bfc169c0db33"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:d8754d872a5dfc3c5bf9c0e059e8107451364a30d9fd50f1f1a85c4fb9481164"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:896c41007931217a343eff197c34513c154267636c8056fb409eafd494c3dcdc"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:92558d37d872e808944c3c96d0423b8604879a3d1c86fdad508d7ed91ea547d5"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f9e0057a509e096e47c87f753136c9b10d7a91842d8042c2ee6866899a717c0d"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6e109a454412ab82979c5b1b3aee0604eca4bbf9a02693bb9df027af2bfa91a"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc1c892b1ec1f8cbd5da8de287577b455e388d9c328ad592eabbdcb6fc93bee5"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c39438c55983d48f4bb3487734d040e22dad200dab22c41e331cee145e7a50d"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d7e8ce990ae17dda686f7e82fd41a055c668e13ddcf058e7fb5e9da20b57793"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9ea7f4174d2e4194289cb0c4e172d83e79a6404297ff95f2875cf9ac9bced8ba"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb2954155bb8f63bb19d56d80e5e5320b61d71084617ed89efedb861a684baea"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04f2b712a2206e13800a8136b07aaedc23af3facab84918e7aa89e4be0260032"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:eda5c1e2a715a4cbbca2d6d304988460942551e4e5e3b7457b50943cd741626d"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:9abc80fe8c1f87218db116016de575a7998ab1629078c90840e8d11ab423ee25"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6a727fd083009bc83eb83d6950f0c32b3c94c8b80a9b667c87f4bd1274ca30ba"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e0f3ef95795efcd3b2ec3fe0a5bcfb5dadf5e3996ea2117427e524d4fbf309c6"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:2c13777ecdbbba2077670285dd1fe50828c8742f6a4119dbef6f83ea13ad10fb"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79e8d804c2ccd618417e96720ad5cd076a86fa3f8cb310ea386a3e6229bae7d1"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd822f019ccccd75c832deb7aa040bb02d70a92eb15a2f16c7987b7ad4ee8d83"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0047638c3aa0dbcd0ab99ed1e549bbf0e142c9ecc173b6492868432d8989a046"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5b66d1b201cc71bc3081bc2f1fc36b0c1f268b773e03bbc39066651b9e18391"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbcbb6db5582ea33ce46a5d20a5793134b5365110d84df4e30b9d37c6fd40ad3"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63981feca3f110ed132fd217bf7768ee8ed738a55549883628ee3da75bb9cb78"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:3a55fc10fdcbf1a4bd3c018eea422c52cf08700cf99c28b5cb10fe97ab77a0d3"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:c30ff468163a48535ee7e9bf21bd14c7a81147c0e58a36c1078289a8ca7af0bd"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:369d9c6d4c714e36d4a03957b4783217a3ccd1e222cdd67d464a3a479fc17796"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:24795c099453e3721fda5d8ddd45f5dfcc8e5a547ce7b8e9da06fecc3832e26f"}, + {file = "rpds_py-0.24.0.tar.gz", hash = "sha256:772cc1b2cd963e7e17e6cc55fe0371fb9c704d63e44cacec7b9b7f523b78919e"}, +] + [[package]] name = "rsa" version = "4.9" @@ -4836,7 +6386,7 @@ version = "2.6" description = "A modern CSS selector implementation for Beautiful Soup." optional = false python-versions = ">=3.8" -groups = ["dev", "docs", "research"] +groups = ["main", "dev", "docs", "research"] files = [ {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, @@ -5469,6 +7019,19 @@ files = [ {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, ] +[[package]] +name = "toolz" +version = "1.0.0" +description = "List processing tools and functional utilities" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "implementation_name == \"cpython\" or implementation_name == \"pypy\"" +files = [ + {file = "toolz-1.0.0-py3-none-any.whl", hash = "sha256:292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236"}, + {file = "toolz-1.0.0.tar.gz", hash = "sha256:2c86e3d9a04798ac556793bced838816296a2f085017664e4995cb40a1047a02"}, +] + [[package]] name = "torch" version = "2.6.0" @@ -5526,6 +7089,32 @@ typing-extensions = ">=4.10.0" opt-einsum = ["opt-einsum (>=3.3)"] optree = ["optree (>=0.13.0)"] +[[package]] +name = "tox" +version = "4.23.2" +description = "tox is a generic virtualenv management and test command line tool" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "tox-4.23.2-py3-none-any.whl", hash = "sha256:452bc32bb031f2282881a2118923176445bac783ab97c874b8770ab4c3b76c38"}, + {file = "tox-4.23.2.tar.gz", hash = "sha256:86075e00e555df6e82e74cfc333917f91ecb47ffbc868dcafbd2672e332f4a2c"}, +] + +[package.dependencies] +cachetools = ">=5.5" +chardet = ">=5.2" +colorama = ">=0.4.6" +filelock = ">=3.16.1" +packaging = ">=24.1" +platformdirs = ">=4.3.6" +pluggy = ">=1.5" +pyproject-api = ">=1.8" +virtualenv = ">=20.26.6" + +[package.extras] +test = ["devpi-process (>=1.0.2)", "pytest (>=8.3.3)", "pytest-mock (>=3.14)"] + [[package]] name = "tqdm" version = "4.67.1" @@ -5642,16 +7231,31 @@ build = ["cmake (>=3.20)", "lit"] tests = ["autopep8", "flake8", "isort", "llnl-hatchet", "numpy", "pytest", "scipy (>=1.7.1)"] tutorials = ["matplotlib", "pandas", "tabulate"] +[[package]] +name = "types-requests" +version = "2.32.0.20250328" +description = "Typing stubs for requests" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "types_requests-2.32.0.20250328-py3-none-any.whl", hash = "sha256:72ff80f84b15eb3aa7a8e2625fffb6a93f2ad5a0c20215fc1dcfa61117bcb2a2"}, + {file = "types_requests-2.32.0.20250328.tar.gz", hash = "sha256:c9e67228ea103bd811c96984fac36ed2ae8da87a36a633964a21f199d60baf32"}, +] + +[package.dependencies] +urllib3 = ">=2" + [[package]] name = "typing-extensions" -version = "4.13.1" +version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" groups = ["main", "demo", "dev", "docs", "research"] files = [ - {file = "typing_extensions-4.13.1-py3-none-any.whl", hash = "sha256:4b6cf02909eb5495cfbc3f6e8fd49217e6cc7944e145cdda8caa3734777f9e69"}, - {file = "typing_extensions-4.13.1.tar.gz", hash = "sha256:98795af00fb9640edec5b8e31fc647597b4691f099ad75f469a2616be1a76dff"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] @@ -5733,14 +7337,14 @@ standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3) [[package]] name = "virtualenv" -version = "20.30.0" +version = "20.28.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" -groups = ["dev"] +groups = ["main", "dev"] files = [ - {file = "virtualenv-20.30.0-py3-none-any.whl", hash = "sha256:e34302959180fca3af42d1800df014b35019490b119eba981af27f2fa486e5d6"}, - {file = "virtualenv-20.30.0.tar.gz", hash = "sha256:800863162bcaa5450a6e4d721049730e7f2dae07720e0902b0e4040bd6f9ada8"}, + {file = "virtualenv-20.28.1-py3-none-any.whl", hash = "sha256:412773c85d4dab0409b83ec36f7a6499e72eaf08c80e81e9576bca61831c71cb"}, + {file = "virtualenv-20.28.1.tar.gz", hash = "sha256:5d34ab240fdb5d21549b76f9e8ff3af28252f5499fb6d6f031adac4e5a8c5329"}, ] [package.dependencies] @@ -5750,7 +7354,41 @@ platformdirs = ">=3.9.1,<5" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] + +[[package]] +name = "web3" +version = "7.10.0" +description = "web3: A Python library for interacting with Ethereum" +optional = false +python-versions = "<4,>=3.8" +groups = ["main"] +files = [ + {file = "web3-7.10.0-py3-none-any.whl", hash = "sha256:06fcab920554450e9f7d108da5e6b9d29c0d1a981a59a5551cc82d2cb2233b34"}, + {file = "web3-7.10.0.tar.gz", hash = "sha256:0cace05ea14f800a4497649ecd99332ca4e85c8a90ea577e05ae909cb08902b9"}, +] + +[package.dependencies] +aiohttp = ">=3.7.4.post0" +eth-abi = ">=5.0.1" +eth-account = ">=0.13.1" +eth-hash = {version = ">=0.5.1", extras = ["pycryptodome"]} +eth-typing = ">=5.0.0" +eth-utils = ">=5.0.0" +hexbytes = ">=1.2.0" +pydantic = ">=2.4.0" +pyunormalize = ">=15.0.0" +pywin32 = {version = ">=223", markers = "platform_system == \"Windows\""} +requests = ">=2.23.0" +types-requests = ">=2.0.0" +typing-extensions = ">=4.0.1" +websockets = ">=10.0.0,<16.0.0" + +[package.extras] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "eth-tester[py-evm] (>=0.12.0b1,<0.13.0b1)", "flaky (>=3.7.0)", "hypothesis (>=3.31.2)", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "py-geth (>=5.1.0)", "pytest (>=7.0.0)", "pytest-asyncio (>=0.18.1,<0.23)", "pytest-mock (>=1.10)", "pytest-xdist (>=2.4.0)", "setuptools (>=38.6.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "tqdm (>4.32)", "twine (>=1.13)", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] +test = ["eth-tester[py-evm] (>=0.12.0b1,<0.13.0b1)", "flaky (>=3.7.0)", "hypothesis (>=3.31.2)", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "py-geth (>=5.1.0)", "pytest (>=7.0.0)", "pytest-asyncio (>=0.18.1,<0.23)", "pytest-mock (>=1.10)", "pytest-xdist (>=2.4.0)", "tox (>=4.0.0)"] +tester = ["eth-tester[py-evm] (>=0.12.0b1,<0.13.0b1)", "py-geth (>=5.1.0)"] [[package]] name = "websockets" @@ -5758,7 +7396,7 @@ version = "15.0.1" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" optional = false python-versions = ">=3.9" -groups = ["demo"] +groups = ["main", "demo"] files = [ {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"}, {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"}, @@ -6168,4 +7806,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.1" python-versions = ">=3.11,<3.13" -content-hash = "1c59588bd2a44d6870e86c0636b8ec40786daf3aaf331c3663a6846050ef115c" +content-hash = "5e3b7137f4214fd98c8d8ba8fc3fbcb783cb9df5bd05fc86e52f42ec2446402e" diff --git a/pyproject.toml b/pyproject.toml index 76d4e91..1b15c5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,11 @@ dependencies = [ "aiogram (>=3.19.0,<4.0.0)", "faiss-cpu (>=1.10.0,<2.0.0)", "simsimd (>=6.2.1,<7.0.0)", + "cdp-sdk (>=0.21.0,<0.22.0)", + "coinbase-agentkit (>=0.4.0,<0.5.0)", + "coinbase-agentkit-langchain (>=0.3.0,<0.4.0)", + "accelerate (>=1.6.0,<2.0.0)", + "markdownify (>=1.1.0,<2.0.0)", ] [project.scripts] @@ -67,7 +72,7 @@ aioconsole = "^0.8.1" fastapi = "^0.115.8" uvicorn = "^0.34.0" websockets = "^15.0" -pydantic = "^2.10.6" +pydantic = "^2.10.4" pydantic-settings = "^2.7.1" httpx = "^0.28.1" python-multipart = "^0.0.20" From 941dd4f43304ee01d5402590bae9fded22cc232d Mon Sep 17 00:00:00 2001 From: Akshat Date: Mon, 21 Apr 2025 00:48:39 -0400 Subject: [PATCH 05/20] feat: enhance callbacks to trace payment tool activity --- agentconnect/utils/callbacks.py | 462 ++++++++++++++++++++++++++++++++ 1 file changed, 462 insertions(+) create mode 100644 agentconnect/utils/callbacks.py diff --git a/agentconnect/utils/callbacks.py b/agentconnect/utils/callbacks.py new file mode 100644 index 0000000..aabb103 --- /dev/null +++ b/agentconnect/utils/callbacks.py @@ -0,0 +1,462 @@ +""" +Agent activity tracing and status update callback handler for AgentConnect. + +This module provides a LangChain callback handler for tracking agent activity, +including tool usage, LLM calls, chain steps, and the LLM's generated text output +(which may contain reasoning steps), with configurable console output. +""" + +import logging +import json +import re # Add re import at the top +from typing import Any, Dict, List, Optional, Union +from uuid import UUID + +from colorama import Fore, Style +from langchain_core.agents import AgentAction +from langchain_core.callbacks import BaseCallbackHandler +from langchain_core.messages import ToolMessage +from langchain_core.outputs import LLMResult, ChatGeneration + +# Configure logger +logger = logging.getLogger(__name__) + +# Define max lengths for logging to prevent clutter +MAX_INPUT_DETAIL_LENGTH = 150 +MAX_OUTPUT_PREVIEW_LENGTH = 100 +MAX_ERROR_MESSAGE_LENGTH = 200 + +# Define colors for logging +TOOL_COLOR = Fore.MAGENTA +TOOL_SUCCESS_COLOR = Fore.GREEN +TOOL_ERROR_COLOR = Fore.RED +LLM_COLOR = Fore.BLUE # Color for LLM activity +CHAIN_COLOR = Fore.CYAN # Color for chain/step activity +REASONING_COLOR = Fore.LIGHTBLUE_EX + + +class ToolTracerCallbackHandler(BaseCallbackHandler): + """ + Callback handler for tracing agent activity. Logs detailed activity and + optionally prints concise updates (like LLM generation text, tool usage, etc.) + to the console based on configuration. + """ + + def __init__( + self, + agent_id: str, + print_tool_activity: bool = False, # Default OFF + print_reasoning_steps: bool = True, # Default ON - Print LLM generation text + print_llm_activity: bool = False, # Default OFF + ): + """ + Initialize the callback handler. + + Args: + agent_id: The ID of the agent this handler is tracking. + print_tool_activity: If True, print tool start/end/error to console. + print_reasoning_steps: If True, print the text generated by the LLM in on_llm_end. + This often contains reasoning or thought processes. + print_llm_activity: If True, print LLM start events to console. + """ + super().__init__() + self.agent_id = agent_id + self.print_tool_activity = print_tool_activity + self.print_reasoning_steps = print_reasoning_steps + self.print_llm_activity = print_llm_activity + + # Initial message is logged only once + init_msg = ( + f"AgentActivityMonitor initialized for Agent ID: {self.agent_id} " + f"(Tools: {print_tool_activity}, Reasoning Text: {print_reasoning_steps}, " + f"LLM Start: {print_llm_activity})" + ) + logger.info(init_msg) + + def _format_details(self, details: Any, max_length: int) -> str: + """Formats details for logging, handling different types and truncating.""" + if isinstance(details, dict): + try: + detail_str = json.dumps(details) + except TypeError: + detail_str = str(details) + else: + detail_str = str(details) + if len(detail_str) > max_length: + return f"{detail_str[:max_length - 3]}..." + return detail_str + + def _get_short_snippet(self, text: str, max_length: int = 50) -> str: + """Create a short snippet from the text.""" + if not text: + return "..." + text_str = str(text) + if len(text_str) > max_length: + return f"{text_str[:max_length]}..." + return text_str + + def on_tool_start( + self, + serialized: Dict[str, Any], + input_str: str, + *, + run_id: UUID, + parent_run_id: Optional[UUID] = None, + tags: Optional[List[str]] = None, + metadata: Optional[Dict[str, Any]] = None, + inputs: Optional[Dict[str, Any]] = None, + **kwargs: Any, + ) -> Any: + """ + Run when the tool starts running. + + Args: + serialized (Dict[str, Any]): The serialized tool. + input_str (str): The input string. + run_id (UUID): The run ID. This is the ID of the current run. + parent_run_id (UUID): The parent run ID. This is the ID of the parent run. + tags (Optional[List[str]]): The tags. + metadata (Optional[Dict[str, Any]]): The metadata. + inputs (Optional[Dict[str, Any]]): The inputs. + kwargs (Any): Additional keyword arguments. + """ + + tool_name = serialized.get("name", "UnknownTool") + input_details_raw = inputs if inputs is not None else input_str + input_details_formatted = self._format_details( + input_details_raw, MAX_INPUT_DETAIL_LENGTH + ) + log_message = f"[TOOL START] Agent: {self.agent_id} | Tool: {tool_name} | Input: {input_details_formatted}" + logger.info(log_message) + + if self.print_tool_activity: + print_msg = "" + # Safely get inputs, providing defaults if they don't exist + safe_inputs = inputs if isinstance(inputs, dict) else {} + + if tool_name == "search_for_agents": + capability = safe_inputs.get("capability_name", "unknown capability") + print_msg = f"{TOOL_COLOR}🔎 Searching for agents with capability: {capability}...{Style.RESET_ALL}" + elif tool_name == "send_collaboration_request": + target_agent = safe_inputs.get("target_agent_id", "unknown agent") + task_snippet = self._get_short_snippet( + safe_inputs.get("task", "unknown task"), 50 + ) + print_msg = f"{TOOL_COLOR}🤝 Interacting with {target_agent}: {task_snippet}...{Style.RESET_ALL}" + elif tool_name == "WalletActionProvider_native_transfer": + amount = safe_inputs.get("amount", "unknown amount") + amount = int(amount) / 10**18 + asset = safe_inputs.get("asset_id", "ETH") + dest = self._get_short_snippet( + safe_inputs.get("destination", "unknown destination"), 20 + ) + print_msg = f"{TOOL_COLOR}💸 Initiating transfer of {amount} {asset} to {dest}...{Style.RESET_ALL}" + elif ( + tool_name == "ERC20ActionProvider_transfer" + ): # Assuming this is for ERC20 transfer + amount = safe_inputs.get("amount", "unknown amount") + amount = int(amount) / 10**6 + asset = safe_inputs.get( + "asset_id", "USDC" + ) # Asset ID usually IS the token address/symbol here + dest = self._get_short_snippet( + safe_inputs.get("destination", "unknown destination"), 20 + ) + print_msg = f"{TOOL_COLOR}💸 Initiating transfer of {amount} {asset} to {dest}...{Style.RESET_ALL}" + elif tool_name == "WalletActionProvider_get_balance": + print_msg = ( + f"{TOOL_COLOR}💰 Checking native token balance...{Style.RESET_ALL}" + ) + elif tool_name == "WalletActionProvider_get_wallet_details": + print_msg = f"{TOOL_COLOR}ℹ️ Fetching wallet details (address, network, balances)...{Style.RESET_ALL}" + elif tool_name == "CdpApiActionProvider_address_reputation": + address = self._get_short_snippet( + safe_inputs.get("address", "unknown address"), 20 + ) + print_msg = f"{TOOL_COLOR}🛡️ Checking reputation for address: {address}...{Style.RESET_ALL}" + elif tool_name == "CdpApiActionProvider_request_faucet_funds": + asset = safe_inputs.get("asset_id", "ETH") + print_msg = f"{TOOL_COLOR}🚰 Requesting faucet funds ({asset})...{Style.RESET_ALL}" + elif tool_name == "ERC20ActionProvider_get_balance": + token = self._get_short_snippet( + safe_inputs.get("contract_address", "unknown token"), 15 + ) + owner = self._get_short_snippet( + safe_inputs.get("address", "wallet"), 15 + ) + print_msg = f"{TOOL_COLOR}💰 Checking balance of token {token} for {owner}...{Style.RESET_ALL}" + else: + # Fallback to generic message for other tools + input_snippet = self._get_short_snippet( + ( + json.dumps(input_details_raw) + if isinstance(input_details_raw, dict) + else input_details_raw + ), + 80, + ) + print_msg = f"{TOOL_COLOR}🛠️ [Tool Start] {tool_name}({input_snippet}...){Style.RESET_ALL}" + + if print_msg: + print(print_msg) + + def on_tool_end( + self, + output: Any, + *, + run_id: UUID, + parent_run_id: Optional[UUID] = None, + tags: Optional[List[str]] = None, + name: str = "UnknownTool", + **kwargs: Any, + ) -> Any: + """ + Run when the tool ends running. + + Args: + output (Any): The output of the tool. + run_id (UUID): The run ID. This is the ID of the current run. + parent_run_id (UUID): The parent run ID. This is the ID of the parent run. + kwargs (Any): Additional keyword arguments. + """ + + tool_name = kwargs.get("name", name) + output_type = type(output).__name__ + output_preview = self._format_details(output, MAX_OUTPUT_PREVIEW_LENGTH) + log_message = f"[TOOL END] Agent: {self.agent_id} | Tool: {tool_name} | Status: Success | Output Type: {output_type} | Preview: {output_preview}" + logger.debug(log_message) + + if self.print_tool_activity: + print_msg = "" + output_str = str(output) # Keep for general logging if needed + + if tool_name == "send_collaboration_request": + status = "processed." + # response_snippet = "" + success = None # Track success status explicitly + json_data = None + + # Primary Case: Handle ToolMessage output + if isinstance(output, ToolMessage): + content_str = output.content + if isinstance(content_str, str): + try: + json_data = json.loads(content_str) + except json.JSONDecodeError as e: + logger.warning( + f"ToolMessage content is not valid JSON for {tool_name}: {content_str[:100]}... Error: {e}" + ) + status = "returned unparsable content." + # response_snippet = f" Raw content: {self._get_short_snippet(content_str, 60)}..." + else: + logger.warning( + f"ToolMessage content is not a string for {tool_name}: {type(content_str)}" + ) + status = "returned non-string content." + # response_snippet = f" Content: {self._get_short_snippet(str(content_str), 60)}..." + + # Fallback 1: Attempt to parse JSON from string representation + elif isinstance(output, str): + logger.debug( + f"Fallback: {tool_name} output is a string, attempting parse." + ) + # Try to find JSON within content='...' or content="..." + match = re.search(r'content=(["\'])(.*?)\1', output, re.DOTALL) + if match: + json_str = match.group(2) + try: + json_data = json.loads(json_str) + except json.JSONDecodeError as e: + logger.debug( + f"Failed to parse JSON from content in string: {e}" + ) + status = "returned unparsable content string." + # response_snippet = ( + # f" Raw output: {self._get_short_snippet(output, 60)}..." + # ) + else: + # If no content= pattern, try parsing the whole string as JSON + try: + json_data = json.loads(output) + except json.JSONDecodeError: + logger.debug( + f"Output string is not valid JSON: {output_str[:100]}..." + ) + status = "completed with non-JSON string output." + # response_snippet = ( + # f" Output: {self._get_short_snippet(output, 60)}..." + # ) + + # Fallback 2: Handle dictionary output directly + elif isinstance(output, dict): + logger.debug(f"Fallback: {tool_name} output is a dict.") + json_data = output + + # Process extracted/provided JSON data (if successfully parsed) + if json_data and isinstance(json_data, dict): + success = json_data.get("success") + if success is True: + status = "completed successfully. Please make the payment." + # response_content = json_data.get("response") + elif success is False: + status = "failed." + # error_reason = json_data.get("response", "Unknown reason") + # response_snippet = f" Reason: {self._get_short_snippet(str(error_reason), 60)}..." + else: # Success field missing or not boolean + status = "completed with unexpected JSON structure." + # response_snippet = f" Data: {self._get_short_snippet(json.dumps(json_data), 60)}..." + # Final Fallback: If output wasn't ToolMessage, str, or dict, or if JSON parsing failed earlier + elif status == "processed.": # Only trigger if no other status was set + status = "completed with unexpected output type." + # response_snippet = f" Type: {output_type}, Output: {self._get_short_snippet(output_str, 60)}..." + logger.warning( + f"Unexpected output type {output_type} for {tool_name}. Output: {output_str[:100]}..." + ) + + # Determine color based on final success status + color = ( + TOOL_SUCCESS_COLOR + if success is True + else TOOL_ERROR_COLOR if success is False else Fore.YELLOW + ) # Yellow for unknown/unexpected + + print_msg = f"{color}➡️ Collaboration request {status}{Style.RESET_ALL}" + + if print_msg: + print(print_msg) + + def on_tool_error( + self, + error: Union[Exception, KeyboardInterrupt], + *, + run_id: UUID, + parent_run_id: Optional[UUID] = None, + tags: Optional[List[str]] = None, + name: str = "UnknownTool", + **kwargs: Any, + ) -> Any: + """ + Run when tool errors. + + Args: + error (BaseException): The error that occurred. + run_id (UUID): The run ID. This is the ID of the current run. + parent_run_id (UUID): The parent run ID. This is the ID of the parent run. + kwargs (Any): Additional keyword arguments. + """ + + tool_name = kwargs.get("name", name) + error_type = type(error).__name__ + error_message_raw = str(error) + error_message_formatted_log = self._format_details( + error_message_raw, MAX_ERROR_MESSAGE_LENGTH + ) + log_message = f"[TOOL ERROR] Agent: {self.agent_id} | Tool: {tool_name} | Status: Failed | Error: {error_type} - {error_message_formatted_log}" + logger.error(log_message) + + # --- Commented out console printing for tool error --- + # if self.print_tool_activity: + # print_msg = "" + # error_message_snippet = self._get_short_snippet(error_message_raw, 100) + # + # if tool_name == "search_for_agents": + # print_msg = f"{TOOL_ERROR_COLOR}❌ Agent search failed unexpectedly: {error_message_snippet}...{Style.RESET_ALL}" + # elif tool_name == "send_collaboration_request": + # print_msg = f"{TOOL_ERROR_COLOR}❌ Collaboration request failed unexpectedly during execution: {error_message_snippet}...{Style.RESET_ALL}" + # elif tool_name == "WalletActionProvider_native_transfer" or tool_name == "ERC20ActionProvider_transfer": + # payment_type = "Payment" if tool_name == "WalletActionProvider_native_transfer" else "Token Transfer" + # print_msg = f"{TOOL_ERROR_COLOR}❌ {payment_type} failed during execution: {error_message_snippet}...{Style.RESET_ALL}" + # else: + # # Fallback for generic errors + # print_msg = f"{TOOL_ERROR_COLOR}❌ [Tool Error] {tool_name} failed: {error_type} - {error_message_snippet}...{Style.RESET_ALL}" + # + # if print_msg: + # print(print_msg) + + def on_agent_action( + self, + action: AgentAction, + *, + run_id: UUID, + parent_run_id: UUID | None = None, + **kwargs: Any, + ) -> Any: + """ + Run on agent action. + + Args: + action (AgentAction): The agent action. + run_id (UUID): The run ID. This is the ID of the current run. + parent_run_id (UUID): The parent run ID. This is the ID of the parent run. + kwargs (Any): Additional keyword arguments. + """ + + print(f"Agent action: {action}") + print(f"{REASONING_COLOR}{action.log}...{Style.RESET_ALL}") + + return super().on_agent_action( + action, run_id=run_id, parent_run_id=parent_run_id, **kwargs + ) + + def on_llm_end( + self, + response: LLMResult, # Use the correct type hint + *, + run_id: UUID, + parent_run_id: Optional[UUID] = None, + tags: Optional[List[str]] = None, + **kwargs: Any, + ) -> Any: + """ + Run when LLM ends running. + + Args: + response (LLMResult): The response which was generated. + run_id (UUID): The run ID. This is the ID of the current run. + parent_run_id (UUID): The parent run ID. This is the ID of the parent run. + kwargs (Any): Additional keyword arguments + """ + + log_message = f"[LLM END] Agent: {self.agent_id}" + logger.debug(log_message) + + # Check if reasoning steps should be printed + if self.print_reasoning_steps: + try: + # Extract the generated text from the first generation + if response.generations and response.generations[0]: + first_generation = response.generations[0][0] + if isinstance(first_generation, ChatGeneration): + generated_text = first_generation.text.strip() + if generated_text: # Only process if there's text + # Split into lines and print ONLY thought/action lines + lines = generated_text.splitlines() + printed_something = ( + False # Keep track if we printed any thought/action + ) + for line in lines: + if "🤔" in line: + print( + f"{REASONING_COLOR}{line[line.find("🤔")+len("🤔"):].strip()}...{Style.RESET_ALL}" + ) + printed_something = True + # Else: ignore other lines (like the final answer block) + + # Add a separator only if thoughts/actions were printed + if printed_something: + print( + f"{REASONING_COLOR}---" + ) # Use reasoning color for separator + + else: + logger.warning( + f"Unexpected generation type in on_llm_end: {type(first_generation)}" + ) + else: + logger.warning( + "LLM response structure unexpected or empty in on_llm_end." + ) + except Exception as e: + logger.error(f"Error processing LLM response in callback: {e}") + # Fallback: print the raw response object for debugging if needed + # print(f"{REASONING_COLOR}Raw LLM Response: {response}{Style.RESET_ALL}") From 495567d76dd9c5f37cd767f621b984220cd2440f Mon Sep 17 00:00:00 2001 From: Akshat Date: Mon, 21 Apr 2025 00:49:53 -0400 Subject: [PATCH 06/20] feat: add autonomous workflow demo showcasing payment capabilities --- .../autonomous_workflow/run_workflow_demo.py | 349 ++++++++++++++++++ 1 file changed, 349 insertions(+) create mode 100644 examples/autonomous_workflow/run_workflow_demo.py diff --git a/examples/autonomous_workflow/run_workflow_demo.py b/examples/autonomous_workflow/run_workflow_demo.py new file mode 100644 index 0000000..f728c2d --- /dev/null +++ b/examples/autonomous_workflow/run_workflow_demo.py @@ -0,0 +1,349 @@ +#!/usr/bin/env python +""" +Autonomous Workflow Demo for AgentConnect + +This script demonstrates a multi-agent workflow using the AgentConnect framework. +It features three agents: +1. User Proxy Agent - Orchestrates the workflow based on user requests +2. Research Agent - Performs company research using web search tools +3. Telegram Broadcast Agent - Broadcasts messages to a Telegram group + +The demo showcases autonomous service discovery, execution, and payment between agents +using AgentKit/CDP SDK integration within the AgentConnect framework. +""" + +import asyncio +import os +from typing import List, Tuple + +from dotenv import load_dotenv +from langchain_community.tools.tavily_search import TavilySearchResults +from langchain_community.tools.requests.tool import RequestsGetTool +from langchain_community.utilities import TextRequestsWrapper +from colorama import init, Fore, Style + +from agentconnect.agents.ai_agent import AIAgent +from agentconnect.agents.human_agent import HumanAgent +from agentconnect.agents.telegram.telegram_agent import TelegramAIAgent +from agentconnect.communication.hub import CommunicationHub +from agentconnect.core.agent import BaseAgent +from agentconnect.core.types import ( + AgentIdentity, + Capability, + ModelProvider, + ModelName, +) +from agentconnect.core.registry import AgentRegistry +from agentconnect.utils.logging_config import ( + setup_logging, + LogLevel, + disable_all_logging, +) +from agentconnect.utils.callbacks import ToolTracerCallbackHandler + +# Initialize colorama for cross-platform colored output +init() + +# Define colors for different message types +COLORS = { + "SYSTEM": Fore.YELLOW, + "USER_PROXY": Fore.CYAN, + "RESEARCH": Fore.BLUE, + "TELEGRAM": Fore.MAGENTA, + "HUMAN": Fore.GREEN, + "ERROR": Fore.RED, + "INFO": Fore.WHITE, +} + +def print_colored(message: str, color_type: str = "SYSTEM") -> None: + """Print a message with specified color""" + color = COLORS.get(color_type.upper(), Fore.WHITE) + print(f"{color}{message}{Style.RESET_ALL}") + +# Define Base Sepolia USDC Contract Address +BASE_SEPOLIA_USDC_ADDRESS = "0x036CbD53842c5426634e7929541eC2318f3dCF7e" + +# Define Capabilities +GENERAL_RESEARCH = Capability( + name="general_research", + description="Performs detailed research on a given topic, project, or URL, providing a structured report.", +) + +TELEGRAM_BROADCAST = Capability( + name="telegram_broadcast", + description="Broadcasts a given message summary to pre-configured Telegram groups.", +) + + +async def setup_agents() -> Tuple[AIAgent, AIAgent, TelegramAIAgent, HumanAgent]: + """ + Set up and configure all agents needed for the workflow. + + Returns: + Tuple containing (user_proxy_agent, research_agent, telegram_broadcaster, human_agent) + """ + # Load environment variables + load_dotenv() + + # Retrieve API keys from environment + google_api_key = os.getenv("GOOGLE_API_KEY") + openai_api_key = os.getenv("OPENAI_API_KEY") + tavily_api_key = os.getenv("TAVILY_API_KEY") + telegram_token = os.getenv("TELEGRAM_BOT_TOKEN") + + # Check for required environment variables + missing_vars = [] + if not google_api_key and not openai_api_key: + missing_vars.append("GOOGLE_API_KEY or OPENAI_API_KEY") + if not os.getenv("CDP_API_KEY_NAME"): + missing_vars.append("CDP_API_KEY_NAME") + if not os.getenv("CDP_API_KEY_PRIVATE_KEY"): + missing_vars.append("CDP_API_KEY_PRIVATE_KEY") + if not telegram_token: + missing_vars.append("TELEGRAM_BOT_TOKEN") + if not tavily_api_key: + missing_vars.append("TAVILY_API_KEY") + + if missing_vars: + raise ValueError( + f"Missing required environment variables: {', '.join(missing_vars)}" + ) + + # Determine which LLM to use based on available API keys + if google_api_key: + provider_type = ModelProvider.GOOGLE + model_name = ModelName.GEMINI2_FLASH + api_key = google_api_key + else: + provider_type = ModelProvider.OPENAI + model_name = ModelName.GPT4O + api_key = openai_api_key + + print_colored(f"Using {provider_type.value}: {model_name.value}", "INFO") + + # Configure Callback Handler + monitor_callback = ToolTracerCallbackHandler( + agent_id="user_proxy_agent", + print_tool_activity=True, # OFF - Keep demo clean + print_reasoning_steps=True, # ON - Print LLM's generated text (reasoning) + print_llm_activity=True, # OFF - Reasoning print implies LLM activity + ) + + # Create User Proxy Agent (Workflow Orchestrator) + user_proxy_agent = AIAgent( + agent_id="user_proxy_agent", + name="Workflow Orchestrator", + provider_type=provider_type, + model_name=model_name, + api_key=api_key, + identity=AgentIdentity.create_key_based(), + capabilities=[], # No specific capabilities - it orchestrates + enable_payments=True, + external_callbacks=[monitor_callback], + personality =f"""You are a workflow orchestrator. You interact with other agents to complete tasks. You are responsible for managing payments and returning results. + If a payment is made, provide the amount and the transaction hash in your response. + + **Payment Details (USDC on Base Sepolia):** + - Contract: {BASE_SEPOLIA_USDC_ADDRESS} + - Amount: 6 decimals. 1 USDC = '1000000'. + """ + ) + + # Create Research Agent + research_agent = AIAgent( + agent_id="research_agent", + name="Research Specialist", + provider_type=provider_type, + model_name=model_name, + api_key=api_key, + identity=AgentIdentity.create_key_based(), + capabilities=[GENERAL_RESEARCH], + enable_payments=True, + personality="""You are a Research Specialist. You provide detailed, well-structured reports on any given topic, project, or URL using web search tools. + +**Report Structure:** +- **For companies/projects/organizations:** Aim to structure your report using these sections when applicable: Topic/Project Name, Executive Summary, Key Personnel/Founders, Offerings/Products, Ecosystem/Partners, Asset/Token Details, Community Sentiment, Sources Consulted, Closing Summary. +- **For conceptual topics or general questions:** Adapt the structure logically. Focus on defining the concept, explaining key aspects, providing examples, discussing benefits/drawbacks, listing sources, and offering a concluding summary. +- **Always include Sources Consulted.** + +Your fee is 2 USDC (Base Sepolia). When responding, state your fee.""", + custom_tools=[ + TavilySearchResults(api_key=tavily_api_key, max_results=5), + RequestsGetTool( + requests_wrapper=TextRequestsWrapper(), allow_dangerous_requests=True + ), + ], + ) + + # Create Telegram Broadcast Agent + telegram_broadcaster = TelegramAIAgent( + agent_id="telegram_broadcaster_agent", + name="Telegram Broadcaster", + provider_type=provider_type, + model_name=model_name, + api_key=api_key, + identity=AgentIdentity.create_key_based(), + capabilities=[TELEGRAM_BROADCAST], + enable_payments=True, + personality="""You are a Telegram Broadcast Specialist. You broadcast messages to all regestered Telegram groups. \ + Your fee is 1 USDC (Base Sepolia). After broadcasting, state your fee in your response.""", + telegram_token=telegram_token, + ) + + # Create Human Agent + human_agent = HumanAgent( + agent_id="human_user", + name="Human User", + identity=AgentIdentity.create_key_based(), + organization_id="demo_org", + ) + + return user_proxy_agent, research_agent, telegram_broadcaster, human_agent + + +async def main(enable_logging: bool = False): + """ + Main execution flow for the autonomous workflow demo. + + Sets up the agents, registers them with the communication hub, + and handles user input for research and broadcast requests. + + Args: + enable_logging: Whether to enable verbose logging + """ + + if not enable_logging: + disable_all_logging() + else: + # Keep logging setup simple if enabled, main feedback via print_colored + setup_logging(level=LogLevel.WARNING) + + try: + print_colored("\nSetting up agents...", "SYSTEM") + # Set up agents + user_proxy_agent, research_agent, telegram_broadcaster, human_agent = ( + await setup_agents() + ) + agents: List[BaseAgent] = [ + user_proxy_agent, + research_agent, + telegram_broadcaster, + human_agent, + ] + + # Create registry and communication hub + registry = AgentRegistry() + hub = CommunicationHub(registry) + + print_colored("Registering agents with Communication Hub...", "SYSTEM") + # Register all agents + for agent in agents: + if not await hub.register_agent(agent): + print_colored(f"Failed to register {agent.agent_id}", "ERROR") + return + print_colored(f" ✓ Registered: {agent.name} ({agent.agent_id})", "INFO") + + # Display payment address if available + if hasattr(agent, "metadata") and hasattr( + agent.metadata, "payment_address" + ): + if agent.metadata.payment_address: # Check if address is not None or empty + print_colored( + f" Payment Address ({agent.name}): {agent.metadata.payment_address}", "INFO" + ) + else: + print_colored(f" Payment address pending initialization for {agent.name}...", "INFO") + + print_colored("All agents registered. Waiting for initialization...", "SYSTEM") + + # Start agent processing loops + tasks = [] + try: + print_colored("Starting agent processing loops...", "SYSTEM") + # Start the AI agents + telegram_task = asyncio.create_task(telegram_broadcaster.run()) + tasks.append(telegram_task) + + research_task = asyncio.create_task(research_agent.run()) + tasks.append(research_task) + + user_proxy_task = asyncio.create_task(user_proxy_agent.run()) + tasks.append(user_proxy_task) + + # Allow some time for agents to initialize + await asyncio.sleep(3) + + # Print welcome message and instructions + print_colored("\n=== AgentConnect Autonomous Workflow Demo ===", "SYSTEM") + print_colored( + "This demo showcases multi-agent workflows with service discovery and payments.", + "SYSTEM", + ) + print_colored("Available agents:", "INFO") + print_colored(" - User Proxy (Orchestrator)", "USER_PROXY") + print_colored(" - Research Agent (2 USDC per request)", "RESEARCH") + print_colored(" - Telegram Broadcaster (1.0 USDC per broadcast)", "TELEGRAM") + print_colored("\nExample commands:", "INFO") + print_colored(" - Research X and broadcast the summary", "INFO") + print_colored( + " - Find information about Y and share it on Telegram", "INFO" + ) + print_colored("\nType 'exit' or 'quit' to end the demo", "INFO") + + # Start human interaction with the user proxy agent + # HumanAgent will handle its own colored printing for the chat + print_colored("\n▶️ Starting interactive session with Workflow Orchestrator...", "SYSTEM") + await human_agent.start_interaction(user_proxy_agent) + + except asyncio.CancelledError: + print_colored("Tasks cancelled", "SYSTEM") + except Exception as e: + print_colored(f"Error in main execution: {e}", "ERROR") + finally: + # Cleanup + print_colored("\nCleaning up...", "SYSTEM") + + # Cancel all tasks + for task in tasks: + if not task.done(): + task.cancel() + + # Wait for tasks to finish + if tasks: + await asyncio.gather(*tasks, return_exceptions=True) + + # Stop the Telegram bot explicitly + if telegram_broadcaster: + print_colored("Stopping Telegram bot...", "SYSTEM") + await telegram_broadcaster.stop_telegram_bot() + + # Unregister agents + print_colored("Unregistering agents...", "SYSTEM") + for agent in agents: + # Skip human agent as it doesn't run a loop + if agent.agent_id == "human_user": + continue + try: + await hub.unregister_agent(agent.agent_id) + print_colored(f" ✓ Unregistered {agent.agent_id}", "INFO") + except Exception as e: + print_colored(f" ✗ Error unregistering {agent.agent_id}: {e}", "ERROR") + + except ValueError as e: + print_colored(f"Setup error: {e}", "ERROR") + except Exception as e: + print_colored(f"Unexpected error: {e}", "ERROR") + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + print_colored("\nDemo interrupted by user. Shutting down...", "SYSTEM") + except Exception as e: + print_colored(f"Fatal error: {e}", "ERROR") + finally: + print_colored("\nDemo shutdown complete.", "SYSTEM") + + +# Research the Uniswap protocol (uniswap.org), summarize its core function and tokenomics, and broadcast the summary on telegram. \ No newline at end of file From c40c2b6fdb4d214a48f82e51ee86d6c259b4e1db Mon Sep 17 00:00:00 2001 From: Akshat Date: Mon, 21 Apr 2025 00:54:56 -0400 Subject: [PATCH 07/20] refactor: minor code refinements and fixes discovered during review --- agentconnect/agents/human_agent.py | 5 +- .../agents/telegram/message_processor.py | 2 +- .../agents/telegram/telegram_agent.py | 9 ++ agentconnect/cli.py | 62 +++++++-- .../core/registry/capability_discovery.py | 120 +++++++++++++++--- agentconnect/utils/__init__.py | 20 +++ agentconnect/utils/interaction_control.py | 19 +-- examples/example_usage.py | 26 +++- 8 files changed, 222 insertions(+), 41 deletions(-) diff --git a/agentconnect/agents/human_agent.py b/agentconnect/agents/human_agent.py index 218b543..4367c67 100644 --- a/agentconnect/agents/human_agent.py +++ b/agentconnect/agents/human_agent.py @@ -169,6 +169,7 @@ async def start_interaction(self, target_agent: BaseAgent) -> None: print( f"{Fore.RED}❌ Error: {response.content}{Style.RESET_ALL}" ) + print("-" * 40) logger.error( f"Human Agent {self.agent_id} received error message: {response.content[:50]}..." ) @@ -191,8 +192,10 @@ async def start_interaction(self, target_agent: BaseAgent) -> None: f"Human Agent {self.agent_id} received processing status message: {response.content[:50]}..." ) else: - print(f"\n{Fore.CYAN}{target_agent.name}:{Style.RESET_ALL}") + print("-" * 40) + print(f"{Fore.CYAN}{target_agent.name}:{Style.RESET_ALL}") print(f"{response.content}") + print("-" * 40) logger.info( f"Human Agent {self.agent_id} received and displayed response: {response.content[:50]}..." ) diff --git a/agentconnect/agents/telegram/message_processor.py b/agentconnect/agents/telegram/message_processor.py index b7b57c3..bbc9993 100644 --- a/agentconnect/agents/telegram/message_processor.py +++ b/agentconnect/agents/telegram/message_processor.py @@ -365,7 +365,7 @@ async def process_agent_response( "configurable": { "thread_id": conversation_id, }, - "callbacks": interaction_control.get_callback_manager(), + "callbacks": interaction_control.get_callback_handlers(), } logger.debug( diff --git a/agentconnect/agents/telegram/telegram_agent.py b/agentconnect/agents/telegram/telegram_agent.py index 66aae4d..1b88f62 100644 --- a/agentconnect/agents/telegram/telegram_agent.py +++ b/agentconnect/agents/telegram/telegram_agent.py @@ -13,6 +13,7 @@ from dotenv import load_dotenv from aiogram import types from langchain.tools import BaseTool +from langchain_core.callbacks import BaseCallbackHandler from agentconnect.agents.ai_agent import AIAgent from agentconnect.agents.telegram.bot_manager import TelegramBotManager @@ -131,6 +132,10 @@ def __init__( max_tokens_per_minute: int = 5500, max_tokens_per_hour: int = 100000, telegram_token: Optional[str] = None, + enable_payments: bool = False, + verbose: bool = False, + wallet_data_dir: Optional[str] = None, + external_callbacks: Optional[List[BaseCallbackHandler]] = None, ): """ Initialize a Telegram AI Agent. @@ -249,6 +254,10 @@ def __init__( max_tokens_per_minute=max_tokens_per_minute, max_tokens_per_hour=max_tokens_per_hour, custom_tools=self._get_custom_tools(), # Pass the custom tools to AIAgent + enable_payments=enable_payments, + verbose=verbose, + wallet_data_dir=wallet_data_dir, + external_callbacks=external_callbacks, ) def _initialize_telegram_components(self): diff --git a/agentconnect/cli.py b/agentconnect/cli.py index 213ea7c..742e5a0 100644 --- a/agentconnect/cli.py +++ b/agentconnect/cli.py @@ -15,16 +15,18 @@ agentconnect --example research agentconnect --example data agentconnect --example telegram + agentconnect --example agent_economy agentconnect --demo # UI compatibility under development (Windows only) agentconnect --check-env agentconnect --help Available examples: - chat - Simple chat with an AI assistant - multi - Multi-agent e-commerce analysis - research - Research assistant with multiple agents - data - Data analysis and visualization assistant - telegram - Modular multi-agent system with Telegram integration + chat - Simple chat with an AI assistant + multi - Multi-agent e-commerce analysis + research - Research assistant with multiple agents + data - Data analysis and visualization assistant + telegram - Modular multi-agent system with Telegram integration + agent_economy - Autonomous workflow with agent payments system Note: The demo UI is currently under development and only supported on Windows. For the best experience, please use the examples instead. @@ -85,11 +87,12 @@ def parse_args(args: Optional[List[str]] = None) -> argparse.Namespace: formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Available examples: - chat - Simple chat with an AI assistant - multi - Multi-agent e-commerce analysis - research - Research assistant with multiple agents - data - Data analysis and visualization assistant - telegram - Modular multi-agent system with Telegram integration + chat - Simple chat with an AI assistant + multi - Multi-agent e-commerce analysis + research - Research assistant with multiple agents + data - Data analysis and visualization assistant + telegram - Modular multi-agent system with Telegram integration + agent_economy - Autonomous workflow with agent payments system Examples: agentconnect --example chat @@ -105,8 +108,16 @@ def parse_args(args: Optional[List[str]] = None) -> argparse.Namespace: parser.add_argument( "--example", "-e", - choices=["chat", "multi", "research", "data", "telegram"], - help="Run a specific example: chat (simple AI assistant), multi (multi-agent ecommerce analysis), research (research assistant), data (data analysis assistant), or telegram (modular multi-agent system with Telegram integration)", + choices=[ + "chat", + "multi", + "research", + "data", + "telegram", + "workflow", + "agent_economy", + ], + help="Run a specific example: chat (simple AI assistant), multi (multi-agent ecommerce analysis), research (research assistant), data (data analysis assistant), telegram (modular multi-agent system with Telegram integration), agent_economy (autonomous workflow with payments), or workflow (legacy name, same as agent_economy)", ) parser.add_argument( @@ -153,6 +164,27 @@ async def run_example(example_name: str, verbose: bool = False) -> None: "The example will run, but research capabilities will be limited" ) + # Check for workflow dependencies + if example_name in ["workflow", "agent_economy"]: + try: + from langchain_community.tools.tavily_search import ( # noqa: F401 + TavilySearchResults, # noqa: F401 + ) + from langchain_community.tools.requests.tool import ( # noqa: F401 + RequestsGetTool, # noqa: F401 + ) + from langchain_community.utilities import TextRequestsWrapper # noqa: F401 + from colorama import init, Fore, Style # noqa: F401 + except ImportError: + logger.warning("Dependencies are missing for the agent economy demo") + logger.info("To install the required dependencies:") + logger.info(" poetry install --with demo") + logger.info( + " or: pip install langchain-community colorama tavily-python python-dotenv" + ) + logger.info("Please install the missing dependencies and try again") + sys.exit(1) + try: if example_name == "chat": from examples import run_chat_example @@ -174,6 +206,12 @@ async def run_example(example_name: str, verbose: bool = False) -> None: from examples import run_telegram_assistant await run_telegram_assistant(enable_logging=verbose) + elif example_name in ["workflow", "agent_economy"]: + from examples.autonomous_workflow.run_workflow_demo import ( + main as run_workflow_demo, + ) + + await run_workflow_demo(enable_logging=verbose) else: logger.error(f"Unknown example: {example_name}") except ImportError as e: diff --git a/agentconnect/core/registry/capability_discovery.py b/agentconnect/core/registry/capability_discovery.py index 9d62465..707b786 100644 --- a/agentconnect/core/registry/capability_discovery.py +++ b/agentconnect/core/registry/capability_discovery.py @@ -12,6 +12,7 @@ import warnings from typing import Dict, List, Set, Tuple, Any, Optional from langchain_core.vectorstores import VectorStore +from langchain_huggingface import HuggingFaceEmbeddings # Absolute imports from agentconnect package from agentconnect.core.registry.registration import AgentRegistration @@ -19,6 +20,32 @@ # Set up logging logger = logging.getLogger("CapabilityDiscovery") +# Permanently filter out UserWarnings about relevance scores from any source +warnings.filterwarnings( + "ignore", message="Relevance scores must be between 0 and 1", category=UserWarning +) +warnings.filterwarnings("ignore", message=".*elevance scores.*", category=UserWarning) + +# Monkeypatch the showwarning function to completely suppress relevance score warnings +original_showwarning = warnings.showwarning + + +def custom_showwarning(message, category, filename, lineno, file=None, line=None): + """ + Custom warning handler to suppress relevance score warnings. + + Args: + message: The warning message + category: The warning category + """ + if category == UserWarning and "relevance scores" in str(message).lower(): + return # Suppress the warning completely + # For all other warnings, use the original function + return original_showwarning(message, category, filename, lineno, file, line) + + +warnings.showwarning = custom_showwarning + def check_semantic_search_requirements() -> Dict[str, bool]: """ @@ -186,9 +213,6 @@ async def initialize_embeddings_model(self): ) return - # Import the necessary modules - from langchain_huggingface import HuggingFaceEmbeddings - # Get model name from config or use default model_name = self._vector_store_config.get( "model_name", "sentence-transformers/all-mpnet-base-v2" @@ -203,10 +227,54 @@ async def initialize_embeddings_model(self): "cache_folder", "./.cache/huggingface/embeddings" ) - self._embeddings_model = HuggingFaceEmbeddings( - model_name=model_name, - cache_folder=cache_folder, - ) + # Try with explicit model_kwargs and encode_kwargs first + try: + self._embeddings_model = HuggingFaceEmbeddings( + model_name=model_name, + cache_folder=cache_folder, + model_kwargs={"device": "cpu", "revision": "main"}, + encode_kwargs={"normalize_embeddings": True}, + ) + except Exception as model_error: + logger.warning( + f"First embedding initialization attempt failed: {str(model_error)}" + ) + + # Try alternative initialization approach + try: + # Import directly from sentence_transformers as fallback + import sentence_transformers + + # Create the model directly first + st_model = sentence_transformers.SentenceTransformer( + model_name, + cache_folder=cache_folder, + device="cpu", + revision="main", # Use main branch which is more stable + ) + + # Then create embeddings with the pre-initialized model + self._embeddings_model = HuggingFaceEmbeddings( + model=st_model, encode_kwargs={"normalize_embeddings": True} + ) + + logger.info( + "Initialized embeddings using pre-loaded sentence transformer model" + ) + except Exception as fallback_error: + # If that fails too, try with minimal parameters + logger.warning( + f"Fallback embedding initialization failed: {str(fallback_error)}" + ) + + # Last attempt with minimal configuration + self._embeddings_model = HuggingFaceEmbeddings( + model_name="all-MiniLM-L6-v2", # Try with a smaller model + ) + + logger.info( + "Initialized embeddings with minimal configuration and smaller model" + ) # Reset capability map self._capability_to_agent_map = {} @@ -221,7 +289,9 @@ async def initialize_embeddings_model(self): logger.warning(traceback.format_exc()) - async def _init_vector_store(self, documents: List, embeddings_model: Any) -> Any: + async def _init_vector_store( + self, documents: List, embeddings_model: "HuggingFaceEmbeddings" + ) -> Any: """ Initialize vector store with the preferred backend. @@ -579,14 +649,9 @@ async def find_by_capability_semantic( and self._capability_to_agent_map ): try: - # Suppress the specific UserWarning from LangChain during the search call - with warnings.catch_warnings(record=False): - warnings.filterwarnings( - "ignore", - # Use regex to match the start of the message reliably - message=r"^Relevance scores must be between 0 and 1", - category=UserWarning, - ) + # Completely disable all warnings during search call - no matter what, don't show warnings + with warnings.catch_warnings(): + warnings.simplefilter("ignore") # Try using async similarity search with scores try: kwargs = {} @@ -602,11 +667,32 @@ async def find_by_capability_semantic( ) ) + # Handle any potential issues with the search results format + cleaned_search_results = [] + for item in search_results: + # Make sure each result is a proper tuple of (doc, score) + if not isinstance(item, tuple) or len(item) != 2: + logger.warning(f"Skipping malformed search result: {item}") + continue + + doc, score = item + # Convert score to float if necessary + if hasattr(score, "item"): # Convert numpy types + score = float(score.item()) + elif not isinstance(score, (int, float)): + try: + score = float(score) + except (ValueError, TypeError): + logger.warning(f"Could not convert score to float: {score}") + continue + + cleaned_search_results.append((doc, score)) + # Process results seen_agent_ids = set() processed_results = [] - for doc, original_score in search_results: + for doc, original_score in cleaned_search_results: # --- Filter 1: Exclude non-positive cosine scores --- # Scores <= 0 indicate orthogonality or dissimilarity in cosine similarity. if original_score <= 0: diff --git a/agentconnect/utils/__init__.py b/agentconnect/utils/__init__.py index 6d64bf9..d9d7540 100644 --- a/agentconnect/utils/__init__.py +++ b/agentconnect/utils/__init__.py @@ -10,6 +10,7 @@ - **InteractionState**: Enum for interaction states (CONTINUE, STOP, WAIT) - **TokenConfig**: Configuration for token-based rate limiting - **Logging utilities**: Configurable logging setup with colored output +- **Wallet management**: Functions for handling agent wallet configurations and data """ # Interaction control components @@ -28,6 +29,17 @@ setup_logging, ) +# Wallet management +from agentconnect.utils.wallet_manager import ( + load_wallet_data, + save_wallet_data, + set_wallet_data_dir, + set_default_data_dir, + wallet_exists, + delete_wallet_data, + get_all_wallets, +) + __all__ = [ # Interaction control "InteractionControl", @@ -39,4 +51,12 @@ "LogLevel", "disable_all_logging", "get_module_levels_for_development", + # Wallet management + "load_wallet_data", + "save_wallet_data", + "set_wallet_data_dir", + "set_default_data_dir", + "wallet_exists", + "delete_wallet_data", + "get_all_wallets", ] diff --git a/agentconnect/utils/interaction_control.py b/agentconnect/utils/interaction_control.py index 2622323..9f7ae7f 100644 --- a/agentconnect/utils/interaction_control.py +++ b/agentconnect/utils/interaction_control.py @@ -15,7 +15,6 @@ from typing import Any, Callable, Dict, List, Optional # Third-party imports -from langchain_core.callbacks import CallbackManager from langchain_core.callbacks.base import BaseCallbackHandler # Set up logging @@ -293,6 +292,7 @@ class InteractionControl: for agent interactions. Attributes: + agent_id: The ID of the agent this control belongs to. token_config: Configuration for token-based rate limiting max_turns: Maximum number of turns in a conversation current_turn: Current turn number @@ -301,6 +301,7 @@ class InteractionControl: _conversation_stats: Dictionary of conversation statistics """ + agent_id: str token_config: TokenConfig max_turns: int = 20 current_turn: int = 0 @@ -311,7 +312,9 @@ class InteractionControl: def __post_init__(self): """Initialize conversation stats dictionary.""" self._conversation_stats = {} - logger.debug(f"InteractionControl initialized with max_turns={self.max_turns}") + logger.debug( + f"InteractionControl initialized for agent {self.agent_id} with max_turns={self.max_turns}" + ) async def process_interaction( self, token_count: int, conversation_id: Optional[str] = None @@ -412,16 +415,16 @@ def get_conversation_stats( return self._conversation_stats.get(conversation_id, {}) return self._conversation_stats - def get_callback_manager(self) -> CallbackManager: + def get_callback_handlers(self) -> List[BaseCallbackHandler]: """ - Create a callback manager with rate limiting for LangChain/LangGraph. + Create a list of callback handlers managed by InteractionControl (primarily rate limiting). Returns: - CallbackManager with rate limiting callbacks + List containing configured BaseCallbackHandler instances. """ - callbacks = [] + callbacks: List[BaseCallbackHandler] = [] - # Add rate limiting callback + # 1. Add rate limiting callback rate_limiter = RateLimitingCallbackHandler( max_tokens_per_minute=self.token_config.max_tokens_per_minute, max_tokens_per_hour=self.token_config.max_tokens_per_hour, @@ -433,4 +436,4 @@ def get_callback_manager(self) -> CallbackManager: # We're not adding a LangChain tracer here to avoid duplicate traces in LangSmith # LangSmith will automatically trace the workflow if LANGCHAIN_TRACING is enabled - return CallbackManager(callbacks) + return callbacks diff --git a/examples/example_usage.py b/examples/example_usage.py index 78972f5..8d5d287 100644 --- a/examples/example_usage.py +++ b/examples/example_usage.py @@ -11,9 +11,11 @@ - Secure agent registration and communication with cryptographic verification - Real-time message exchange with proper error handling - Graceful session management and cleanup +- Optional payment capabilities using blockchain technology Required environment variables: - At least one provider API key (OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) +- For payment capabilities: CDP_API_KEY_NAME, CDP_API_KEY_PRIVATE (Coinbase Developer Platform) """ import asyncio @@ -56,7 +58,7 @@ def print_colored(message: str, color_type: str = "SYSTEM") -> None: print(f"{color}{message}{Style.RESET_ALL}") -async def main(enable_logging: bool = False) -> None: +async def main(enable_logging: bool = False, enable_payments: bool = False) -> None: """ Run an interactive demo between a human user and an AI assistant. @@ -65,9 +67,11 @@ async def main(enable_logging: bool = False) -> None: 2. Secure agent registration and communication 3. Real-time message exchange with proper error handling 4. Graceful session management and cleanup + 5. Optional blockchain payment capabilities Args: enable_logging (bool): Enable detailed logging for debugging. Defaults to False. + enable_payments (bool): Enable blockchain payment capabilities. Defaults to False. """ # Load environment variables from .env file load_dotenv() @@ -186,6 +190,17 @@ async def main(enable_logging: bool = False) -> None: ) ] + # Initialize wallet configuration if payments are enabled + if enable_payments: + print_colored( + "Payment capabilities enabled. Environment will be validated during agent initialization.", + "INFO" + ) + print_colored( + "Required environment variables: CDP_API_KEY_NAME, CDP_API_KEY_PRIVATE_KEY, (optional) CDP_NETWORK_ID", + "INFO" + ) + ai_assistant = AIAgent( agent_id="ai1", name="Assistant", @@ -197,6 +212,7 @@ async def main(enable_logging: bool = False) -> None: interaction_modes=[InteractionMode.HUMAN_TO_AGENT], personality="helpful and professional", organization_id="org2", + enable_payments=enable_payments, # Enable payment capabilities if requested ) # --- End AI Agent Setup --- @@ -213,6 +229,10 @@ async def main(enable_logging: bool = False) -> None: # Start AI processing ai_task = asyncio.create_task(ai_assistant.run()) + # Display payment address if payment capabilities are enabled + if enable_payments and ai_assistant.payments_enabled: + print_colored(f"\nAI Assistant Payment Address: {ai_assistant.metadata.payment_address}", "INFO") + print_colored("\n=== Starting Interactive Session ===", "SYSTEM") print_colored("Type your messages and press Enter to send", "INFO") print_colored("Type 'exit' to end the session", "INFO") @@ -248,4 +268,6 @@ async def main(enable_logging: bool = False) -> None: if __name__ == "__main__": - asyncio.run(main()) + # By default, run without payments enabled for simpler setup + # To enable payments, you would call main(enable_payments=True) + asyncio.run(main(enable_payments=False)) From 1226200d51dba155ac91cb7c03f4c94f1ddd971b Mon Sep 17 00:00:00 2001 From: Akshat Date: Mon, 21 Apr 2025 00:55:31 -0400 Subject: [PATCH 08/20] docs: update CHANGELOG for agent payment features --- CHANGELOG.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6b48c8..7685189 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,16 +8,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Agent payment capabilities using Coinbase Developer Platform (CDP) SDK and AgentKit. +- Wallet persistence for agents (`agentconnect.utils.wallet_manager`). +- CDP environment validation and payment readiness checks (`agentconnect.utils.payment_helper`). +- Payment-related dependencies (`cdp-sdk`, `coinbase-agentkit`, `coinbase-agentkit-langchain`) to `pyproject.toml`. +- Payment address (`payment_address`) field to `AgentMetadata` and `AgentRegistration`. +- Payment capability template (`PAYMENT_CAPABILITY_TEMPLATE`) for agent prompts. +- Autonomous workflow demo (`examples/autonomous_workflow`) showcasing inter-agent payments. ### Changed +- `BaseAgent` and `AIAgent` to initialize wallet provider and AgentKit conditionally based on `enable_payments` flag. +- `AIAgent` workflow initialization to include AgentKit tools when payments are enabled. +- Agent prompts (`CORE_DECISION_LOGIC`, ReAct prompts) updated to include payment instructions and context. +- `CommunicationHub` registration updated to include `payment_address`. +- `ToolTracerCallbackHandler` enhanced to provide specific tracing for payment tool actions. ### Deprecated - -### Removed +- `examples/run_example.py` in favor of using the official CLI tool (`agentconnect` command) ### Fixed +- Suppressed unnecessary warnings in capability discovery module +- Improved error handling in agent communication ### Security +- Enhanced validation for inter-agent messages +- Implemented secure API key management +- Added rate limiting for API calls +- Set up environment variable handling +- Added input validation for all API endpoints ## [0.2.0] - 2025-04-01 From 0db8b2f6e5249e224f100835c79b370bad3e59fe Mon Sep 17 00:00:00 2001 From: Akshat Date: Tue, 22 Apr 2025 02:13:04 -0400 Subject: [PATCH 09/20] docs: Add agent payment documentation --- .gitignore | 3 + README.md | 39 +++++++- agentconnect/agents/README.md | 12 +++ agentconnect/agents/ai_agent.py | 2 +- .../agents/telegram/telegram_agent.py | 4 + agentconnect/core/README.md | 14 +++ agentconnect/core/registry/README.md | 1 + agentconnect/core/registry/registry_base.py | 2 +- agentconnect/prompts/README.md | 4 +- agentconnect/utils/README.md | 75 ++++++++++++++++ example.env | 83 +++++++++++++----- requirements.txt | Bin 2209 -> 2208 bytes 12 files changed, 213 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index 81c38f9..6fca6c9 100644 --- a/.gitignore +++ b/.gitignore @@ -190,6 +190,9 @@ examples/visualizations/ # telegram groups.txt +# Agent wallet data +data/ + # VS Code .vscode/ .idea/ diff --git a/README.md b/README.md index 2635dff..859ce1b 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,14 @@ AgentConnect empowers developers to create a truly decentralized ecosystem of AI
  • Ensures secure interactions
  • + +

    💰 Agent Economy

    +
      +
    • Agent-to-agent payments
    • +
    • Automated service transactions
    • +
    • Coinbase CDP integration
    • +
    +

    🔌 Multi-Provider Support

      @@ -88,6 +96,8 @@ AgentConnect empowers developers to create a truly decentralized ecosystem of AI
    • Google AI
    + +

    📊 Monitoring (LangSmith)

      @@ -96,6 +106,22 @@ AgentConnect empowers developers to create a truly decentralized ecosystem of AI
    • Performance analysis
    + +

    🌐 Capability Advertising

    +
      +
    • Service discovery
    • +
    • Skill broadcasting
    • +
    • Dynamic fee negotiation
    • +
    + + +

    🔗 Blockchain Integration

    +
      +
    • Cryptocurrency transactions
    • +
    • Transaction verification
    • +
    • Base Sepolia support
    • +
    + @@ -114,6 +140,15 @@ copy example.env .env # Windows cp example.env .env # Linux/Mac ``` +Set required environment variables in your `.env` file: +``` +# Required for AI providers (at least one) +OPENAI_API_KEY=your_openai_api_key +# Optional for payment capabilities +CDP_API_KEY_NAME=your_cdp_api_key_name +CDP_API_KEY_PRIVATE_KEY=your_cdp_api_key_private_key +``` + For detailed installation instructions and configuration options, see the [QuickStart Guide](docs/source/quickstart.md) and [Installation Guide](docs/source/installation.md). ## 🎮 Usage @@ -132,6 +167,7 @@ AgentConnect includes several example applications to demonstrate different feat - **Research Assistant**: Task delegation and information retrieval - **Data Analysis**: Specialized data processing - **Telegram Assistant**: Telegram AI agent with multi-agent collaboration +- **Agent Economy**: Autonomous workflow with automatic cryptocurrency payments between agents For code examples and detailed descriptions, see the [Examples Directory](examples/README.md). @@ -180,6 +216,7 @@ AgentConnect integrates with LangSmith for comprehensive monitoring: * View detailed traces of agent interactions * Debug complex reasoning chains * Analyze token usage and performance + * Track payment tool calls from AgentKit integration ## 🛠️ Development @@ -225,7 +262,7 @@ AgentConnect/ - ✅ **MVP with basic agent-to-agent interactions** - ✅ **Autonomous communication between agents** - ✅ **Capability-based agent discovery** -- ⬜ **Coinbase AgentKit Payment Integration** +- ✅ **Coinbase AgentKit Payment Integration** - ⬜ **Agent Identity & Reputation System** - ⬜ **Marketplace-Style Agent Discovery** - ⬜ **MCP Integration** diff --git a/agentconnect/agents/README.md b/agentconnect/agents/README.md index f68827d..b4b9074 100644 --- a/agentconnect/agents/README.md +++ b/agentconnect/agents/README.md @@ -28,9 +28,20 @@ The `AIAgent` class is an autonomous, independent AI implementation that can ope - Rate limiting and cooldown mechanisms - Workflow-based processing that can include its own internal agent system - Tool integration for enhanced capabilities +- Optional payment capabilities for agent-to-agent transactions Each AI agent can operate completely independently, potentially with its own internal multi-agent structure, while still being able to discover and communicate with other independent agents across the network. +#### Payment Integration + +When created with `enable_payments=True`, the `AIAgent` integrates payment capabilities: + +- **Wallet Setup**: Triggers wallet initialization in `BaseAgent.__init__` +- **AgentKit Tools**: Payment tools (e.g., `native_transfer`, `erc20_transfer`) are automatically added to the agent's workflow in `AIAgent._initialize_workflow` +- **LLM Decision Making**: The agent's LLM decides when to use payment tools based on prompt instructions in templates like `CORE_DECISION_LOGIC` and `PAYMENT_CAPABILITY_TEMPLATE` +- **Network Support**: Default support for Base Sepolia testnet, configurable to other networks +- **Transaction Verification**: Built-in transaction verification and confirmation + ### HumanAgent The `HumanAgent` class provides an interface for human users to interact with AI agents. It offers: @@ -243,3 +254,4 @@ if not message.verify(agent.identity): 5. **Resource Management**: Be mindful of resource usage when creating multiple independent AI agents. 6. **Secure Communication**: Always verify message signatures to maintain security in the decentralized network. 7. **Autonomous Operation**: Design agents that can make independent decisions without central control. +8. **Secure CDP Keys**: When using `enable_payments=True`, ensure CDP API keys are handled securely and never exposed. diff --git a/agentconnect/agents/ai_agent.py b/agentconnect/agents/ai_agent.py index 7149b97..e809118 100644 --- a/agentconnect/agents/ai_agent.py +++ b/agentconnect/agents/ai_agent.py @@ -131,7 +131,7 @@ def __init__( custom_tools: Optional list of custom LangChain tools for the agent agent_type: Type of agent workflow to create enable_payments: Whether to enable payment capabilities - verbose: + verbose: Whether to enable verbose logging wallet_data_dir: Optional custom directory for wallet data storage external_callbacks: Optional list of external callback handlers to include """ diff --git a/agentconnect/agents/telegram/telegram_agent.py b/agentconnect/agents/telegram/telegram_agent.py index 1b88f62..e3b096e 100644 --- a/agentconnect/agents/telegram/telegram_agent.py +++ b/agentconnect/agents/telegram/telegram_agent.py @@ -155,6 +155,10 @@ def __init__( max_tokens_per_minute: Rate limiting for token usage per minute max_tokens_per_hour: Rate limiting for token usage per hour telegram_token: Telegram Bot API token (can also use TELEGRAM_BOT_TOKEN env var) + enable_payments: Whether to enable payments + verbose: Whether to enable verbose logging + wallet_data_dir: Directory to store wallet data + external_callbacks: List of external callbacks to use """ # Define Telegram-specific capabilities telegram_capabilities = [ diff --git a/agentconnect/core/README.md b/agentconnect/core/README.md index deb70dd..52772eb 100644 --- a/agentconnect/core/README.md +++ b/agentconnect/core/README.md @@ -51,6 +51,7 @@ The central registry for agent discovery and management: - **Agent Lifecycle Management**: Track agent status and handle registration/unregistration - **Organization Management**: Group agents by organization - **Vector Search Integration**: Coordinate with the capability discovery service +- **Payment Address Storage**: Store and provide agent payment addresses during discovery Key methods: - `register()`: Register an agent with the registry @@ -96,6 +97,7 @@ Data structure for agent registration information: - **Capabilities**: List of agent capabilities - **Identity Information**: Agent identity credentials - **Organization Details**: Information about the agent's organization +- **Payment Address**: Optional cryptocurrency address for agent-to-agent payments ### Message (`message.py`) @@ -123,6 +125,18 @@ The `types.py` file defines core types used throughout the framework: - **AgentIdentity**: Decentralized identity for agents - **MessageType**: Types of messages that can be exchanged - **ProtocolVersion**: Supported protocol versions +- **AgentMetadata**: Agent information including optional payment address + +### Payment Integration + +The core module integrates with the Coinbase Developer Platform (CDP) for payment capabilities: + +- **BaseAgent Wallet Setup**: `BaseAgent.__init__` conditionally initializes agent wallets when `enable_payments=True` +- **Payment Address Storage**: `payment_address` field in `AgentMetadata` and `AgentRegistration` +- **Payment Constants**: Default token symbol and amounts defined in `payment_constants.py` +- **Capability Discovery**: Payment addresses are included in agent search results + +For details on how agents use payment capabilities, see `agentconnect/agents/README.md`. ### Exceptions (`exceptions.py`) diff --git a/agentconnect/core/registry/README.md b/agentconnect/core/registry/README.md index eb9d1a7..cf5994a 100644 --- a/agentconnect/core/registry/README.md +++ b/agentconnect/core/registry/README.md @@ -24,6 +24,7 @@ The central registry for agent discovery and management: - **Capability Lookup**: Provides both exact and semantic matching of capabilities - **Agent Lifecycle Management**: Tracks agent availability and status - **Organization Grouping**: Organizes agents by organization +- **Payment Address Handling**: Stores the optional `payment_address` provided during registration and makes it available during discovery, facilitating agent economy features. The `AgentRegistry` class acts as a facade for the entire registry subsystem, coordinating between the specialized components and providing a unified API for agent registration and discovery. diff --git a/agentconnect/core/registry/registry_base.py b/agentconnect/core/registry/registry_base.py index 4802bac..3f31c92 100644 --- a/agentconnect/core/registry/registry_base.py +++ b/agentconnect/core/registry/registry_base.py @@ -36,7 +36,7 @@ class AgentRegistry: by capability, and verifying agent identities. """ - def __init__(self, vector_search_config: Dict[str, Any] = None): + def __init__(self, vector_search_config: Optional[Dict[str, Any]] = None): """ Initialize the agent registry. diff --git a/agentconnect/prompts/README.md b/agentconnect/prompts/README.md index 1f33d4f..a1137a3 100644 --- a/agentconnect/prompts/README.md +++ b/agentconnect/prompts/README.md @@ -69,6 +69,7 @@ The tools system provides agents with the ability to perform specific actions, p - **`search_for_agents`**: Searches for agents with specific capabilities using semantic matching. This tool helps agents find other specialized agents that can assist with tasks outside their capabilities. - **`send_collaboration_request`**: Sends a request to a specific agent to perform a task and waits for a response. This tool enables agent-to-agent delegation and collaboration. - **`decompose_task`**: Breaks down a complex task into smaller, manageable subtasks. This helps agents organize and tackle complex requests more effectively. +- **AgentKit Payment Tools (e.g., `native_transfer`, `erc20_transfer`)**: When payment capabilities are enabled for an `AIAgent`, tools provided by Coinbase AgentKit are automatically added. These allow the agent to initiate and manage cryptocurrency transactions based on LLM decisions guided by payment prompts. ### Tool Architecture @@ -193,7 +194,8 @@ Prompt templates are used to create different types of prompts for agents. The ` - **System Prompts**: Define the agent's role, capabilities, and personality. - **Collaboration Prompts**: Used for collaboration requests and responses. -- **ReAct Prompts**: Used for the ReAct agent, which makes decisions and calls tools. +- **ReAct Prompts**: Used for the ReAct agent, which makes decisions and calls tools. This includes the `CORE_DECISION_LOGIC` template, which incorporates instructions for when to consider collaboration or payments. +- **Payment Capability Prompts**: Includes the `PAYMENT_CAPABILITY_TEMPLATE`, which provides specific instructions and context to the LLM regarding the available payment tools and when it might be appropriate to use them for agent-to-agent transactions. ### ReAct Integration diff --git a/agentconnect/utils/README.md b/agentconnect/utils/README.md index d6a33a7..9ae14d3 100644 --- a/agentconnect/utils/README.md +++ b/agentconnect/utils/README.md @@ -9,6 +9,8 @@ utils/ ├── __init__.py # Package initialization and API exports ├── interaction_control.py # Rate limiting and interaction tracking ├── logging_config.py # Logging configuration +├── payment_helper.py # Payment utilities for CDP validation and agent payment readiness +├── wallet_manager.py # Agent wallet persistence utilities └── README.md # This file ``` @@ -46,6 +48,37 @@ Key classes and functions: - `get_module_levels_for_development()`: Get recommended log levels for development - `setup_langgraph_logging()`: Configure logging specifically for LangGraph components +### Payment Helper (`payment_helper.py`) + +The payment helper module provides utility functions for setting up and managing payment capabilities: + +- **CDP Environment Validation**: Verify CDP API keys and required packages +- **Agent Payment Readiness**: Check if an agent is ready for payments +- **Wallet Metadata Retrieval**: Get metadata about agent wallets +- **Backup Utilities**: Create backups of wallet data + +Key functions: +- `verify_payment_environment()`: Check required environment variables +- `validate_cdp_environment()`: Validate the entire CDP setup including packages +- `check_agent_payment_readiness()`: Check if an agent can make payments +- `backup_wallet_data()`: Create backup of wallet data + +### Wallet Manager (`wallet_manager.py`) + +The wallet manager provides wallet data persistence for agents: + +- **Wallet Data Storage**: Save and load wallet data securely +- **Wallet Existence Checking**: Check if wallet data exists +- **Wallet Data Management**: Delete and backup wallet data +- **Configuration Management**: Set custom data directories + +Key functions: +- `save_wallet_data()`: Persist wallet data for an agent +- `load_wallet_data()`: Load wallet data for an agent +- `wallet_exists()`: Check if wallet data exists +- `get_all_wallets()`: List all wallet files +- `delete_wallet_data()`: Delete wallet data + ## Usage Examples ### Interaction Control @@ -127,6 +160,48 @@ setup_langgraph_logging(level=LogLevel.INFO) disable_all_logging() ``` +### Payment Utilities + +```python +from agentconnect.utils import payment_helper, wallet_manager + +# Validate CDP environment +is_valid, message = payment_helper.validate_cdp_environment() +if not is_valid: + print(f"CDP environment is not properly configured: {message}") + # Set up environment... + +# Check agent payment readiness +status = payment_helper.check_agent_payment_readiness(agent) +if status["ready"]: + print(f"Agent is ready for payments with address: {status['payment_address']}") +else: + print(f"Agent is not ready for payments: {status}") + +# Save wallet data +wallet_manager.save_wallet_data( + agent_id="agent123", + wallet_data=agent.wallet_provider.export_wallet(), + data_dir="custom/wallet/dir" # Optional +) + +# Load wallet data +wallet_json = wallet_manager.load_wallet_data("agent123") +if wallet_json: + print("Wallet data loaded successfully") + +# Back up wallet data +backup_path = payment_helper.backup_wallet_data( + agent_id="agent123", + backup_dir="wallet_backups" +) +print(f"Wallet backed up to: {backup_path}") +``` + +## Wallet Security Note + +IMPORTANT: The default wallet data storage implementation in `wallet_manager.py` stores wallet data unencrypted on disk, which is suitable for testing/demo purposes but NOT secure for production environments holding real assets. For production use, implement proper encryption or use a secure key management system. + ## Integration with LangGraph The rate limiting system is designed to work seamlessly with LangGraph: diff --git a/example.env b/example.env index 8133869..33e3369 100644 --- a/example.env +++ b/example.env @@ -1,61 +1,100 @@ # ============================================= -# REQUIRED SETTINGS +# CORE PROVIDER SETTINGS (REQUIRED) # ============================================= -# You only need to set the API key for your chosen default provider -DEFAULT_PROVIDER=groq # Choose one of: groq, anthropic, openai, google +# Choose your default LLM provider +DEFAULT_PROVIDER=groq # Options: groq, anthropic, openai, google -# Provider API Keys -# At least one is required (matching DEFAULT_PROVIDER) +# API Keys for LLM Providers (At least ONE matching DEFAULT_PROVIDER is REQUIRED) GROQ_API_KEY= OPENAI_API_KEY= ANTHROPIC_API_KEY= GOOGLE_API_KEY= +# Default Model Name (Optional - uses provider default if unset) +# Example: For Groq, you might use llama3-70b-8192 +# DEFAULT_MODEL= + # ============================================= -# OPTIONAL SETTINGS (all have sensible defaults) +# MONITORING (OPTIONAL) # ============================================= - # LangSmith Settings (for monitoring and debugging) # LANGSMITH_TRACING=true # LANGSMITH_ENDPOINT="https://api.smith.langchain.com" # LANGSMITH_API_KEY=your_langsmith_api_key # LANGSMITH_PROJECT=AgentConnect -# For advanced agent usage (search, telegram) +# ============================================= +# ADVANCED FEATURES (OPTIONAL) +# ============================================= +# For Telegram Agent Functionality # TELEGRAM_BOT_TOKEN= + +# For Web Search Capabilities (e.g., Research Agent) # TAVILY_API_KEY= -# API Settings (defaults shown) +# ============================================= +# PAYMENT CAPABILITIES (OPTIONAL) +# ============================================= +# Coinbase Developer Platform (CDP) API Key for Agent Payments +# Required only if you enable payment features +# CDP_API_KEY_NAME=your_cdp_api_key_name +# CDP_API_KEY_PRIVATE_KEY=your_cdp_api_key_private_key + +# ============================================= +# API SERVER SETTINGS (OPTIONAL) +# ============================================= # API_HOST=127.0.0.1 # API_PORT=8000 # DEBUG=True -# ALLOWED_ORIGINS=http://localhost:5173 +# ALLOWED_ORIGINS=http://localhost:5173 # Frontend URL for CORS + +# ============================================= +# RATE LIMITING SETTINGS (OPTIONAL) +# ============================================= +# LLM Token Limits (applied per agent) +# MAX_TOKENS_PER_MINUTE=5500 +# MAX_TOKENS_PER_HOUR=100000 -# Rate Limiting Settings (defaults shown) +# WebSocket Rate Limiting (per connection) # WS_RATE_LIMIT_TIMES=30 # WS_RATE_LIMIT_SECONDS=60 + +# API Endpoint Rate Limiting (per IP) # API_RATE_LIMIT_TIMES=100 # API_RATE_LIMIT_SECONDS=60 -# Session Settings (defaults shown) +# ============================================= +# SESSION MANAGEMENT (OPTIONAL) +# ============================================= +# Session Timeout (seconds) # SESSION_TIMEOUT=3600 + +# WebSocket Timeout (seconds) # WEBSOCKET_TIMEOUT=300 + +# Maximum messages allowed per session # MAX_MESSAGES_PER_SESSION=1000 + +# Maximum inactive time before session closure (seconds) # MAX_INACTIVE_TIME=1800 + +# Maximum total duration of a session (seconds) # MAX_SESSION_DURATION=86400 -# MAX_SESSIONS_PER_USER=5 -# Model Settings (defaults shown) -# DEFAULT_MODEL=llama-3.3-70b-versatile -# MAX_TOKENS_PER_MINUTE=5500 -# MAX_TOKENS_PER_HOUR=100000 +# Maximum concurrent sessions allowed per user +# MAX_SESSIONS_PER_USER=5 -# Authentication Settings (defaults shown) +# ============================================= +# AUTHENTICATION (OPTIONAL) +# ============================================= # A random secret key will be generated if not provided -# AUTH_SECRET_KEY=your_secret_key_here +# For persistent sessions across restarts, set a fixed key +# AUTH_SECRET_KEY=your_very_secure_random_secret_key_here # ACCESS_TOKEN_EXPIRE_MINUTES=30 # REFRESH_TOKEN_EXPIRE_DAYS=7 -# Logging Settings (defaults shown) -# LOG_LEVEL=INFO -# LOG_FORMAT=%(asctime)s - %(name)s - %(levelname)s - %(message)s +# ============================================= +# LOGGING (OPTIONAL) +# ============================================= +# LOG_LEVEL=INFO # Options: DEBUG, INFO, WARNING, ERROR, CRITICAL +# LOG_FORMAT='%(asctime)s - %(name)s - %(levelname)s - %(message)s' # Example format diff --git a/requirements.txt b/requirements.txt index 9efaefa5924dc3ab4e7046305203975543084655..f71e794ff312070993b98cc146ea542a37ffeaf1 100644 GIT binary patch delta 7 OcmZ1|xIl2j0uBHRkOI5_ delta 9 QcmZ1=xKMDz0uDwl01*-b#{d8T From 86a15bb5d746c634c552148da8038a71e3a5c101 Mon Sep 17 00:00:00 2001 From: Akshat Date: Fri, 2 May 2025 05:51:11 -0400 Subject: [PATCH 10/20] feat: enhance agent core with standalone chat and stop methods --- agentconnect/agents/ai_agent.py | 610 +++++++++++++++-------------- agentconnect/agents/human_agent.py | 112 +++++- agentconnect/core/agent.py | 49 +++ 3 files changed, 482 insertions(+), 289 deletions(-) diff --git a/agentconnect/agents/ai_agent.py b/agentconnect/agents/ai_agent.py index e809118..37aa8c6 100644 --- a/agentconnect/agents/ai_agent.py +++ b/agentconnect/agents/ai_agent.py @@ -13,7 +13,7 @@ import logging from datetime import datetime from enum import Enum -from typing import List, Optional, Union +from typing import List, Optional, Union, Dict, Any from pathlib import Path # Third-party imports @@ -99,15 +99,13 @@ def __init__( memory_type: MemoryType = MemoryType.BUFFER, prompt_tools: Optional[PromptTools] = None, prompt_templates: Optional[PromptTemplates] = None, - # Custom tools parameter custom_tools: Optional[List[BaseTool]] = None, agent_type: str = "ai", - # Payment capabilities parameters enable_payments: bool = False, verbose: bool = False, wallet_data_dir: Optional[Union[str, Path]] = None, - # External callbacks parameter external_callbacks: Optional[List[BaseCallbackHandler]] = None, + model_config: Optional[Dict[str, Any]] = None, ): """Initialize the AI agent. @@ -134,11 +132,11 @@ def __init__( verbose: Whether to enable verbose logging wallet_data_dir: Optional custom directory for wallet data storage external_callbacks: Optional list of external callback handlers to include + model_config: Optional dict of default model parameters (e.g., temperature, max_tokens) """ # Validate CDP environment if payments are requested actual_enable_payments = enable_payments if enable_payments: - # The validation function will load dotenv for us is_valid, message = validate_cdp_environment() if not is_valid: logger.warning( @@ -147,12 +145,15 @@ def __init__( logger.warning( f"Payment capabilities will be disabled for agent {agent_id}" ) - actual_enable_payments = False # Disable payments since the environment is not properly configured + actual_enable_payments = False else: logger.info( f"CDP environment validation passed for agent {agent_id}: {message}" ) + # Store the model config before initializing LLM + self.model_config = model_config or {} + # Initialize base agent super().__init__( agent_id=agent_id, @@ -176,18 +177,15 @@ def __init__( self.memory_type = memory_type self.workflow_agent_type = agent_type self.verbose = verbose - - # Store the custom tools list if provided self.custom_tools = custom_tools or [] - - # Store the prompt_tools instance if provided self._prompt_tools = prompt_tools - - # Store external callbacks if provided self.external_callbacks = external_callbacks or [] - - # Create a new PromptTemplates instance for this agent self.prompt_templates = prompt_templates or PromptTemplates() + self.workflow = None + + # Initialize hub and registry references + self._hub = None + self._registry = None # Initialize token tracking and rate limiting token_config = TokenConfig( @@ -198,25 +196,15 @@ def __init__( self.interaction_control = InteractionControl( agent_id=self.agent_id, token_config=token_config, max_turns=max_turns ) - - # Set cooldown callback to update agent's cooldown state self.interaction_control.set_cooldown_callback(self.set_cooldown) - # Initialize the LLM (This will now use the tool_tracer_handler) + # Initialize the LLM self.llm = self._initialize_llm() logger.debug(f"Initialized LLM for AI Agent {self.agent_id}: {self.llm}") - - # Initialize the workflow to None - will be set when registry and hub are available - self.workflow = None - - # Initialize the conversation chain (kept for consistency) - self.conversation_chain = None - logger.info( f"AI Agent {self.agent_id} initialized with {len(self.capabilities)} capabilities" ) - # Property setter for hub that initializes workflow when both hub and registry are set @property def hub(self): """Get the hub property.""" @@ -228,7 +216,6 @@ def hub(self, value): self._hub = value self._initialize_workflow_if_ready() - # Property setter for registry that initializes workflow when both hub and registry are set @property def registry(self): """Get the registry property.""" @@ -240,6 +227,16 @@ def registry(self, value): self._registry = value self._initialize_workflow_if_ready() + @property + def prompt_tools(self): + """Get the prompt_tools property.""" + return self._prompt_tools + + @prompt_tools.setter + def prompt_tools(self, value): + """Set the prompt_tools property.""" + self._prompt_tools = value + def _initialize_workflow_if_ready(self): """Initialize the workflow if both registry and hub are set.""" if ( @@ -247,70 +244,80 @@ def _initialize_workflow_if_ready(self): and self._hub is not None and hasattr(self, "_registry") and self._registry is not None + and self.workflow is None ): - if self.workflow is None: - logger.debug( - f"AI Agent {self.agent_id}: Registry and hub are set, initializing workflow" - ) - self.workflow = self._initialize_workflow() - logger.debug(f"AI Agent {self.agent_id}: Workflow initialized") + logger.debug( + f"AI Agent {self.agent_id}: Registry and hub are set, initializing workflow" + ) + self.workflow = self._initialize_workflow() + logger.debug(f"AI Agent {self.agent_id}: Workflow initialized") + + def _initialize_llm(self): + """Initialize the LLM based on the provider type and model name.""" + from agentconnect.providers import ProviderFactory + + provider = ProviderFactory.create_provider(self.provider_type, self.api_key) + logger.debug(f"AI Agent {self.agent_id}: LLM provider created: {provider}") + return provider.get_langchain_llm( + model_name=self.model_name, **self.model_config or {} + ) def _initialize_workflow(self) -> Runnable: """Initialize the workflow for the agent.""" + # Determine if we're in standalone mode + is_standalone = ( + not hasattr(self, "_registry") + or self._registry is None + or not hasattr(self, "_hub") + or self._hub is None + ) - # Create a new PromptTools instance for this agent if not provided + # Create a PromptTools instance if not already provided if self._prompt_tools is None: self._prompt_tools = PromptTools( - agent_registry=self.registry, communication_hub=self.hub, llm=self.llm + agent_registry=self._registry, communication_hub=self._hub, llm=self.llm + ) + logger.debug( + f"AI Agent {self.agent_id}: Created {'standalone' if is_standalone else 'connected'} PromptTools instance." ) - logger.debug(f"AI Agent {self.agent_id}: Created new PromptTools instance.") # Set the current agent context for the tools self._prompt_tools.set_current_agent(self.agent_id) - logger.debug(f"AI Agent {self.agent_id}: Current agent context set in tools.") - - # Get the tools from PromptTools - tools = self._prompt_tools - logger.debug(f"AI Agent {self.agent_id}: Tools initialized or provided.") - # Create prompt templates if not provided - prompt_templates = self.prompt_templates or PromptTemplates() - logger.debug( - f"AI Agent {self.agent_id}: Prompt templates initialized or provided." - ) + # Create system config if not already created + if not hasattr(self, "system_config"): + # Add standalone mode note to system config if in standalone mode + additional_context = {} + if is_standalone: + additional_context["standalone_mode"] = ( + "You are operating in standalone mode without connections to other agents. " + "Focus on using your internal capabilities to help the user directly. " + "If collaboration would normally be useful, explain why it's not available " + "and offer the best alternative solutions you can provide on your own." + ) - # Create system config - Pass the full Capability objects - self.system_config = SystemPromptConfig( - name=self.name, - capabilities=self.capabilities, # Pass full Capability objects - personality=self.personality, - ) - logger.debug( - f"AI Agent {self.agent_id}: System config created with capabilities: {self.capabilities}" - ) + self.system_config = SystemPromptConfig( + name=self.name, + capabilities=self.capabilities, + personality=self.personality, + additional_context=additional_context, + ) - # Initialize custom tools from AgentKit if payments are enabled + # Initialize custom tools list custom_tools_list = list(self.custom_tools) if self.custom_tools else [] - # Check if payments are enabled and AgentKit is available - if self.agent_kit is not None: + # Add payment tools if enabled + if self.enable_payments and self.agent_kit is not None: try: - # Import AgentKit LangChain integration from coinbase_agentkit_langchain import get_langchain_tools - # Get the AgentKit tools agentkit_tools = get_langchain_tools(self.agent_kit) - - # Add the tools to the custom tools list custom_tools_list.extend(agentkit_tools) - # Log the available payment tools tool_names = [tool.name for tool in agentkit_tools] logger.info( f"AI Agent {self.agent_id}: Added {len(agentkit_tools)} AgentKit payment tools: {tool_names}" ) - - # Determine which payment tool to use based on token symbol payment_tool = ( "native_transfer" if POC_PAYMENT_TOKEN_SYMBOL == "ETH" @@ -326,7 +333,6 @@ def _initialize_workflow(self) -> Runnable: logger.info( f"AI Agent {self.agent_id}: Enabled payment capabilities in system prompt" ) - except ImportError as e: logger.warning( f"AI Agent {self.agent_id}: Could not import AgentKit LangChain tools: {e}" @@ -339,32 +345,46 @@ def _initialize_workflow(self) -> Runnable: f"AI Agent {self.agent_id}: Error initializing AgentKit tools: {e}" ) - # Create and compile the workflow with business logic info + # Create the workflow with all components workflow = create_workflow_for_agent( agent_type=self.workflow_agent_type, system_config=self.system_config, llm=self.llm, - tools=tools, - prompt_templates=prompt_templates, + tools=self._prompt_tools, + prompt_templates=self.prompt_templates, agent_id=self.agent_id, custom_tools=custom_tools_list, verbose=self.verbose, ) - logger.debug( - f"AI Agent {self.agent_id}: Workflow created with {len(custom_tools_list)} custom tools." - ) - compiled_workflow = workflow.compile() - logger.debug(f"AI Agent {self.agent_id}: Workflow compiled.") - return compiled_workflow + return workflow.compile() - def _initialize_llm(self): - """Initialize the LLM based on the provider type and model name.""" - from agentconnect.providers import ProviderFactory + def _create_error_response( + self, + message: Message, + error_msg: str, + error_type: str, + is_collaboration_request: bool = False, + ) -> Message: + """Create a standardized error response message.""" + message_type = ( + MessageType.COLLABORATION_RESPONSE + if is_collaboration_request + else MessageType.ERROR + ) - provider = ProviderFactory.create_provider(self.provider_type, self.api_key) - logger.debug(f"AI Agent {self.agent_id}: LLM provider created: {provider}") - return provider.get_langchain_llm(model_name=self.model_name) + metadata = {"error_type": error_type} + if is_collaboration_request: + metadata["original_message_type"] = "ERROR" + + return Message.create( + sender_id=self.agent_id, + receiver_id=message.sender_id, + content=error_msg, + sender_identity=self.identity, + message_type=message_type, + metadata=metadata, + ) async def process_message(self, message: Message) -> Optional[Message]: """ @@ -382,7 +402,7 @@ async def process_message(self, message: Message) -> Optional[Message]: to generate appropriate responses and handle complex tasks that may require collaboration with other independent agents in the decentralized network. """ - # Check if this is a collaboration request before calling super().process_message + # Check if this is a collaboration request is_collaboration_request = ( message.message_type == MessageType.REQUEST_COLLABORATION ) @@ -396,7 +416,7 @@ async def process_message(self, message: Message) -> Optional[Message]: return response try: - # Initialize workflow if it wasn't initialized in the constructor + # Initialize workflow if needed if self.workflow is None: if ( hasattr(self, "_hub") @@ -412,31 +432,11 @@ async def process_message(self, message: Message) -> Optional[Message]: logger.error( f"AI Agent {self.agent_id}: Cannot initialize workflow, registry or hub not set" ) - - error_msg = "I'm sorry, I'm not fully initialized yet. Please try again later." - error_type = "initialization_error" - - # Use COLLABORATION_RESPONSE for collaboration requests - message_type = ( - MessageType.COLLABORATION_RESPONSE - if is_collaboration_request - else MessageType.ERROR - ) - - return Message.create( - sender_id=self.agent_id, - receiver_id=message.sender_id, - content=error_msg, - sender_identity=self.identity, - message_type=message_type, - metadata={ - "error_type": error_type, - **( - {"original_message_type": "ERROR"} - if is_collaboration_request - else {} - ), - }, + return self._create_error_response( + message, + "I'm sorry, I'm not fully initialized yet. Please try again later.", + "initialization_error", + is_collaboration_request, ) # If workflow is still None, return an error @@ -444,33 +444,11 @@ async def process_message(self, message: Message) -> Optional[Message]: logger.error( f"AI Agent {self.agent_id}: Cannot process message, workflow not initialized" ) - - error_msg = ( - "I'm sorry, I'm not fully initialized yet. Please try again later." - ) - error_type = "initialization_error" - - # Use COLLABORATION_RESPONSE for collaboration requests - message_type = ( - MessageType.COLLABORATION_RESPONSE - if is_collaboration_request - else MessageType.ERROR - ) - - return Message.create( - sender_id=self.agent_id, - receiver_id=message.sender_id, - content=error_msg, - sender_identity=self.identity, - message_type=message_type, - metadata={ - "error_type": error_type, - **( - {"original_message_type": "ERROR"} - if is_collaboration_request - else {} - ), - }, + return self._create_error_response( + message, + "I'm sorry, I'm not fully initialized yet. Please try again later.", + "initialization_error", + is_collaboration_request, ) # Check if this is an error message that needs special handling @@ -507,47 +485,28 @@ async def process_message(self, message: Message) -> Optional[Message]: metadata={"handled_error": error_type}, ) - # Special handling for collaboration requests - # This ensures responses are properly correlated with the original request - # Get the conversation ID for this sender conversation_id = self._get_conversation_id(message.sender_id) - # Get the base callback manager from interaction_control (rate limiting + tool tracing) + # Setup callbacks - combine rate limiting callbacks with any external ones callbacks = self.interaction_control.get_callback_handlers() - - # Add any external callbacks if self.external_callbacks: callbacks.extend(self.external_callbacks) - # Set up the configuration with the thread ID for memory persistence and ALL handlers - config = { - "configurable": { - "thread_id": conversation_id, - "run_name": f"Agent {self.agent_id} - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", - }, - "callbacks": callbacks, - } - # Ensure the prompt_tools has the correct agent_id set if ( self._prompt_tools and self._prompt_tools._current_agent_id != self.agent_id ): self._prompt_tools.set_current_agent(self.agent_id) - logger.debug( - f"AI Agent {self.agent_id}: Reset current agent ID in tools before workflow invocation" - ) - # Create the initial state for the workflow - # --- Add context prefix based on sender/message type --- + # Add context prefix based on sender/message type sender_type = ( - "Human" if message.sender_id.startswith("human_") else "AI Agent" + "Human" if message.sender_id.startswith("human") else "AI Agent" ) is_collab_request = ( message.message_type == MessageType.REQUEST_COLLABORATION ) - # Check metadata for response correlation, assuming 'response_to' indicates a collab response is_collab_response = "response_to" in (message.metadata or {}) context_prefix = "" @@ -556,63 +515,50 @@ async def process_message(self, message: Message) -> Optional[Message]: context_prefix = f"[Incoming Collaboration Request from AI Agent {message.sender_id}]:\n" elif is_collab_response: context_prefix = f"[Incoming Response from Collaborating AI Agent {message.sender_id}]:\n" - else: # General message from AI + else: context_prefix = ( f"[Incoming Message from AI Agent {message.sender_id}]:\n" ) - # No prefix for direct Human messages workflow_input_content = f"{context_prefix}{message.content}" - # --- End context prefix logic --- + # Create the initial state and config for the workflow initial_state = { "messages": [HumanMessage(content=workflow_input_content)], "sender": message.sender_id, "receiver": self.agent_id, "message_type": message.message_type, "metadata": message.metadata or {}, - "max_retries": 2, # Set a maximum number of retries for collaboration - "retry_count": 0, # Initialize retry count + "max_retries": 2, + "retry_count": 0, + } + + config = { + "configurable": { + "thread_id": conversation_id, + "run_name": f"Agent {self.agent_id} - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", + }, + "callbacks": callbacks, } + logger.debug( f"AI Agent {self.agent_id} invoking workflow with conversation ID: {conversation_id}" ) - # Use the provided runnable with a timeout + # Invoke the workflow with a timeout try: - # Invoke the workflow with a timeout and the combined callbacks response_state = await asyncio.wait_for( self.workflow.ainvoke(initial_state, config), - timeout=180.0, # 3 minute timeout for workflow execution + timeout=180.0, # 3 minute timeout ) logger.debug(f"AI Agent {self.agent_id} workflow invocation complete.") except asyncio.TimeoutError: logger.error(f"AI Agent {self.agent_id} workflow execution timed out") - - # Create a timeout response - timeout_message = "I'm sorry, but this request is taking too long to process. Please try again with a simpler request or break it down into smaller parts." - - # Use COLLABORATION_RESPONSE for collaboration requests - message_type = ( - MessageType.COLLABORATION_RESPONSE - if is_collaboration_request - else MessageType.ERROR - ) - - return Message.create( - sender_id=self.agent_id, - receiver_id=message.sender_id, - content=timeout_message, - sender_identity=self.identity, - message_type=message_type, - metadata={ - "error_type": "workflow_timeout", - **( - {"original_message_type": "ERROR"} - if is_collaboration_request - else {} - ), - }, + return self._create_error_response( + message, + "I'm sorry, but this request is taking too long to process. Please try again with a simpler request or break it down into smaller parts.", + "workflow_timeout", + is_collaboration_request, ) # Extract the last message from the workflow response state @@ -620,33 +566,21 @@ async def process_message(self, message: Message) -> Optional[Message]: logger.error( f"AI Agent {self.agent_id}: Workflow returned empty or invalid messages state." ) - # Handle error appropriately, maybe return an error message - return Message.create( - sender_id=self.agent_id, - receiver_id=message.sender_id, - content="Internal error: Could not retrieve response.", - sender_identity=self.identity, - message_type=( - MessageType.ERROR - if not is_collaboration_request - else MessageType.COLLABORATION_RESPONSE - ), - metadata={"error_type": "empty_workflow_response"}, + return self._create_error_response( + message, + "Internal error: Could not retrieve response.", + "empty_workflow_response", + is_collaboration_request, ) last_message = response_state["messages"][-1] - logger.debug( - f"AI Agent {self.agent_id} extracted last message from workflow response." - ) # Token counting and rate limiting total_tokens = 0 if hasattr(last_message, "usage_metadata") and last_message.usage_metadata: total_tokens = last_message.usage_metadata.get("total_tokens", 0) - logger.debug(f"AI Agent {self.agent_id} token count: {total_tokens}") - # Update token count after response - this will automatically trigger cooldown if needed - # through the callback we set earlier + # Update token count and handle rate limiting state = await self.interaction_control.process_interaction( token_count=total_tokens, conversation_id=conversation_id ) @@ -656,73 +590,44 @@ async def process_message(self, message: Message) -> Optional[Message]: logger.info( f"AI Agent {self.agent_id} reached maximum turns with {message.sender_id}. Ending conversation." ) - # End the conversation self.end_conversation(message.sender_id) last_message.content = f"{last_message.content}\n\nWe've reached the maximum number of turns for this conversation. If you need further assistance, please start a new conversation." - elif state == InteractionState.WAIT: - logger.info( - f"AI Agent {self.agent_id} is in cooldown state with {message.sender_id}." - ) - # We don't need to create a special message here as the cooldown callback - # will have already set the agent's cooldown state, which will be handled - # by the BaseAgent.process_message method on the next interaction - # Update conversation tracking + current_time = datetime.now() if message.sender_id in self.active_conversations: self.active_conversations[message.sender_id]["message_count"] += 1 self.active_conversations[message.sender_id][ "last_message_time" - ] = datetime.now() - logger.debug( - f"AI Agent {self.agent_id} updated active conversation with {message.sender_id}." - ) + ] = current_time else: self.active_conversations[message.sender_id] = { "message_count": 1, - "last_message_time": datetime.now(), + "last_message_time": current_time, } - logger.debug( - f"AI Agent {self.agent_id} created new active conversation with {message.sender_id}." - ) # Determine the appropriate message type for the response - # Always use COLLABORATION_RESPONSE for collaboration requests response_message_type = ( MessageType.COLLABORATION_RESPONSE if is_collaboration_request else MessageType.RESPONSE ) - if is_collaboration_request: - logger.info( - f"AI Agent {self.agent_id} sending collaboration response to {message.sender_id}" - ) # Create response metadata - response_metadata = { - "token_count": total_tokens, - } + response_metadata = {"token_count": total_tokens} # Add response_to if this is a response to a request with an ID if message.metadata and "request_id" in message.metadata: response_metadata["response_to"] = message.metadata["request_id"] - logger.debug( - f"AI Agent {self.agent_id} adding response correlation: {message.metadata['request_id']}" - ) elif ( hasattr(self, "pending_requests") and message.sender_id in self.pending_requests ): - # If we don't have a request_id in the message metadata, but we have one stored in pending_requests, - # use that one instead request_id = self.pending_requests[message.sender_id].get("request_id") if request_id: response_metadata["response_to"] = request_id - logger.debug( - f"AI Agent {self.agent_id} adding response correlation from pending_requests: {request_id}" - ) - # Create the response message + # Create and return the response message response_message = Message.create( sender_id=self.agent_id, receiver_id=message.sender_id, @@ -740,57 +645,22 @@ async def process_message(self, message: Message) -> Optional[Message]: logger.exception( f"AI Agent {self.agent_id} error processing message: {str(e)}" ) - - # Create an error response - # Use COLLABORATION_RESPONSE for collaboration requests - message_type = ( - MessageType.COLLABORATION_RESPONSE - if is_collaboration_request - else MessageType.ERROR - ) - - return Message.create( - sender_id=self.agent_id, - receiver_id=message.sender_id, - content=f"I encountered an unexpected error while processing your request: {str(e)}\n\nPlease try again with a different approach.", - sender_identity=self.identity, - message_type=message_type, - metadata={ - "error_type": "processing_error", - **( - {"original_message_type": "ERROR"} - if is_collaboration_request - else {} - ), - }, + return self._create_error_response( + message, + f"I encountered an unexpected error while processing your request: {str(e)}\n\nPlease try again with a different approach.", + "processing_error", + is_collaboration_request, ) - # Property for prompt_tools to ensure consistent access - @property - def prompt_tools(self): - """Get the prompt_tools property.""" - return self._prompt_tools - - @prompt_tools.setter - def prompt_tools(self, value): - """Set the prompt_tools property.""" - self._prompt_tools = value - def set_cooldown(self, duration: int) -> None: - """Set a cooldown period for the agent. - - Args: - duration: Cooldown duration in seconds - """ + """Set a cooldown period for the agent.""" # Call the parent class method to set the cooldown super().set_cooldown(duration) - - # Log detailed information about the cooldown logger.warning( f"AI Agent {self.agent_id} entered cooldown for {duration} seconds due to rate limiting." ) - # If this is a UI agent, we might want to send a notification to the UI + # UI notification if in UI mode if self.is_ui_mode: # TODO: This would be implemented by a UI notification system logger.info( @@ -798,9 +668,8 @@ def set_cooldown(self, duration: int) -> None: ) def reset_interaction_state(self) -> None: - """Reset the interaction state of the agent. - - This resets both the cooldown state and the turn counter. + """ + Reset the interaction state of the agent. This resets both the cooldown state and the turn counter. """ # Reset the cooldown state self.reset_cooldown() @@ -823,3 +692,172 @@ def reset_interaction_state(self) -> None: logger.info( f"Conversation {conv_id}: {conv_stats['total_tokens']} tokens, {conv_stats['turn_count']} turns" ) + + async def chat( + self, + query: str, + conversation_id: str = "standalone_chat", + metadata: Optional[Dict] = None, + ) -> str: + """ + Allows direct interaction with the agent without needing a CommunicationHub or AgentRegistry. + + This method is useful for testing or using a single agent instance directly. + It simulates a user query and returns the agent's response, maintaining + conversation history based on the conversation_id if memory is configured. + + Args: + query: The user's input/query to the agent. + conversation_id: An identifier for the conversation thread. Defaults to "standalone_chat". + Use different IDs to maintain separate conversation histories. + metadata: Optional metadata to pass to the workflow. + + Returns: + The agent's response as a string. + + Raises: + RuntimeError: If the workflow cannot be initialized or fails unexpectedly. + asyncio.TimeoutError: If the workflow execution times out. + """ + logger.info( + f"AI Agent {self.agent_id} received direct chat query: {query[:50]}..." + ) + + # Initialize workflow if not already done + if self.workflow is None: + try: + # Ensure hub and registry attributes exist (as None) for standalone mode + if not hasattr(self, "_registry"): + self._registry = None + if not hasattr(self, "_hub"): + self._hub = None + + # Create PromptTools if needed for standalone mode + if not hasattr(self, "_prompt_tools") or self._prompt_tools is None: + self._prompt_tools = PromptTools( + agent_registry=None, + communication_hub=None, + llm=self._initialize_llm(), + ) + logger.info( + f"AI Agent {self.agent_id}: Created standalone PromptTools instance." + ) + + # Set current agent context + self._prompt_tools.set_current_agent(self.agent_id) + + # Create standalone system config + self.system_config = SystemPromptConfig( + name=self.name, + capabilities=self.capabilities, + personality=self.personality, + additional_context={ + "standalone_mode": ( + "You are operating in standalone mode without connections to other agents. " + "Focus on using your internal capabilities to help the user directly. " + "If collaboration would normally be useful, explain why it's not available " + "and offer the best alternative solutions you can provide on your own." + ) + }, + ) + + # Initialize workflow + self.workflow = self._initialize_workflow() + if self.workflow is None: + raise RuntimeError("Workflow initialization failed.") + + logger.info( + f"AI Agent {self.agent_id}: Workflow initialized for standalone chat." + ) + except Exception as e: + logger.exception( + f"AI Agent {self.agent_id}: Failed to initialize workflow for chat: {e}" + ) + raise RuntimeError(f"Failed to initialize agent workflow: {e}") from e + + # Set up workflow input and configuration + initial_state = { + "messages": [HumanMessage(content=query)], + "sender": "user_standalone", + "receiver": self.agent_id, + "message_type": MessageType.TEXT, + "metadata": metadata or {}, + "max_retries": 0, + "retry_count": 0, + } + + # Prepare callbacks + callbacks = self.interaction_control.get_callback_handlers() + if self.external_callbacks: + callbacks.extend(self.external_callbacks) + + # Create workflow configuration + config = { + "configurable": { + "thread_id": conversation_id, + "run_name": f"Agent {self.agent_id} - Standalone Chat - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", + }, + "callbacks": callbacks, + } + + # Ensure prompt_tools has current agent context + if ( + hasattr(self, "_prompt_tools") + and self._prompt_tools + and self._prompt_tools._current_agent_id != self.agent_id + ): + self._prompt_tools.set_current_agent(self.agent_id) + + # Invoke workflow + try: + logger.debug( + f"AI Agent {self.agent_id} invoking workflow for chat with conversation ID: {conversation_id}" + ) + response_state = await asyncio.wait_for( + self.workflow.ainvoke(initial_state, config), + timeout=180.0, + ) + logger.debug( + f"AI Agent {self.agent_id}: Chat workflow invocation complete." + ) + except asyncio.TimeoutError as e: + logger.error( + f"AI Agent {self.agent_id}: Chat workflow execution timed out." + ) + raise e + except Exception as e: + logger.exception( + f"AI Agent {self.agent_id}: Error during chat workflow invocation: {e}" + ) + raise RuntimeError(f"Agent workflow failed during chat: {e}") from e + + # Extract response + if "messages" not in response_state or not response_state["messages"]: + logger.error( + f"AI Agent {self.agent_id}: Chat workflow returned empty or invalid messages state." + ) + raise RuntimeError("Agent workflow returned no response message.") + + # Get response content + last_message = response_state["messages"][-1] + if hasattr(last_message, "content"): + response_content = last_message.content + else: + logger.error( + f"AI Agent {self.agent_id}: Last message in chat response has no content: {last_message}" + ) + raise RuntimeError("Agent workflow returned unexpected message format.") + + # Handle token tracking and rate limiting + total_tokens = 0 + if hasattr(last_message, "usage_metadata") and last_message.usage_metadata: + total_tokens = last_message.usage_metadata.get("total_tokens", 0) + + await self.interaction_control.process_interaction( + token_count=total_tokens, conversation_id=conversation_id + ) + + logger.info( + f"AI Agent {self.agent_id} generated chat response: {response_content[:50]}..." + ) + return response_content diff --git a/agentconnect/agents/human_agent.py b/agentconnect/agents/human_agent.py index 4367c67..72f7fdf 100644 --- a/agentconnect/agents/human_agent.py +++ b/agentconnect/agents/human_agent.py @@ -8,7 +8,7 @@ # Standard library imports import asyncio import logging -from typing import Optional +from typing import Optional, Callable, List, Dict, Any # Third-party imports import aioconsole @@ -47,6 +47,7 @@ def __init__( name: str, identity: AgentIdentity, organization_id: Optional[str] = None, + response_callbacks: Optional[List[Callable]] = None, ): """Initialize the human agent. @@ -55,6 +56,7 @@ def __init__( name: Human-readable name for the agent identity: Identity information for the agent organization_id: ID of the organization the agent belongs to + response_callbacks: Optional list of callbacks to be called when human responds """ # Create Capability objects for human capabilities capabilities = [ @@ -82,6 +84,8 @@ def __init__( ) self.name = name self.is_active = True + self.response_callbacks = response_callbacks or [] + self.last_response_data = {} logger.info(f"Human Agent {self.agent_id} initialized.") def _initialize_llm(self): @@ -118,6 +122,11 @@ async def start_interaction(self, target_agent: BaseAgent) -> None: ) return + print( + f"{Fore.GREEN}Human Agent {self.agent_id} starting interaction with {target_agent.agent_id}{Style.RESET_ALL}" + ) + print(f"{Fore.GREEN}Exit with 'exit', 'quit', or 'bye'{Style.RESET_ALL}") + print(f"{Fore.GREEN}Loading...{Style.RESET_ALL}") while self.is_active: try: # Get user input @@ -260,8 +269,105 @@ async def process_message(self, message: Message) -> Optional[Message]: # Display received message print(f"\n{Fore.CYAN}{message.sender_id}:{Style.RESET_ALL}") print(f"{message.content}") + print("-" * 40) logger.info( f"Human Agent {self.agent_id} displayed received message from {message.sender_id}." ) - self.message_queue.task_done() - return None + + # Prompt for and get user response + print( + f"{Fore.YELLOW}Type your response or use these commands:{Style.RESET_ALL}" + ) + print( + f"{Fore.YELLOW}- 'exit', 'quit', or 'bye' to end the conversation{Style.RESET_ALL}" + ) + print( + f"{Fore.YELLOW}- Press Enter without typing to skip responding{Style.RESET_ALL}" + ) + user_input = await aioconsole.ainput(f"\n{Fore.GREEN}You: {Style.RESET_ALL}") + + # Check for exit commands + if user_input.lower().strip() in ["exit", "quit", "bye"]: + logger.info( + f"Human Agent {self.agent_id} ending conversation with {message.sender_id}" + ) + print( + f"{Fore.YELLOW}Ending conversation with {message.sender_id}{Style.RESET_ALL}" + ) + + # Send an exit message to the AI + return Message.create( + sender_id=self.agent_id, + receiver_id=message.sender_id, + content="__EXIT__", + sender_identity=self.identity, + message_type=MessageType.STOP, + metadata={"reason": "user_exit"}, + ) + + # Log the user input + if user_input.strip(): + logger.info( + f"Human Agent {self.agent_id} received user input: {user_input[:50]}..." + ) + + # Send response back to the sender + logger.info( + f"Human Agent {self.agent_id} sending response to {message.sender_id}: {user_input[:50]}..." + ) + return Message.create( + sender_id=self.agent_id, + receiver_id=message.sender_id, + content=user_input, + sender_identity=self.identity, + message_type=MessageType.TEXT, + ) + else: + # If the user didn't enter any text, log it but don't send a response + logger.info( + f"Human Agent {self.agent_id} skipped responding to {message.sender_id}" + ) + print(f"{Fore.YELLOW}No response sent.{Style.RESET_ALL}") + return None + + async def send_message( + self, + receiver_id: str, + content: str, + message_type: MessageType = MessageType.TEXT, + metadata: Optional[Dict[str, Any]] = None, + ) -> Message: + """Override send_message to track human responses and notify callbacks""" + # Call the original method in the parent class + message = await super().send_message( + receiver_id, content, message_type, metadata + ) + + # Store information about this response + self.last_response_data = { + "receiver_id": receiver_id, + "content": content, + "message_type": message_type, + "timestamp": asyncio.get_event_loop().time(), + } + + # Notify any registered callbacks + for callback in self.response_callbacks: + try: + callback(self.last_response_data) + except Exception as e: + logger.error(f"Error in response callback: {str(e)}") + + return message + + def add_response_callback(self, callback: Callable) -> None: + """Add a callback to be notified when the human sends a response""" + if callback not in self.response_callbacks: + self.response_callbacks.append(callback) + logger.debug(f"Human Agent {self.agent_id}: Added response callback") + + def remove_response_callback(self, callback: Callable) -> None: + """Remove a previously registered callback""" + if callback in self.response_callbacks: + self.response_callbacks.remove(callback) + logger.debug(f"Human Agent {self.agent_id}: Removed response callback") diff --git a/agentconnect/core/agent.py b/agentconnect/core/agent.py index e3b3b19..74f856f 100644 --- a/agentconnect/core/agent.py +++ b/agentconnect/core/agent.py @@ -878,6 +878,55 @@ async def can_receive_message(self, sender_id: str) -> bool: logger.debug(f"Agent {self.agent_id} can receive message from {sender_id}.") return True + async def stop(self) -> None: + """ + Stop the agent and cleanup resources. + + This method stops the agent's processing loop, ends all active conversations, + and cleans up resources such as wallet providers and message queues. + + Returns: + None + """ + logger.info(f"Agent {self.agent_id}: Stopping agent...") + + # Mark agent as not running to stop the message processing loop + self.is_running = False + + # End all active conversations + for participant_id in list(self.active_conversations.keys()): + self.end_conversation(participant_id) + + # Clean up wallet provider if it exists + if self.wallet_provider is not None: + try: + # Clean up any pending transactions or listeners + # Note: Additional cleanup may be needed depending on wallet implementation + self.wallet_provider = None + self.agent_kit = None + logger.debug(f"Agent {self.agent_id}: Cleaned up wallet provider") + except Exception as e: + logger.error( + f"Agent {self.agent_id}: Error cleaning up wallet provider: {e}" + ) + + # Clear message queue to prevent processing any more messages + try: + while not self.message_queue.empty(): + self.message_queue.get_nowait() + self.message_queue.task_done() + logger.debug(f"Agent {self.agent_id}: Cleared message queue") + except Exception as e: + logger.error(f"Agent {self.agent_id}: Error clearing message queue: {e}") + + # Reset cooldown + self.reset_cooldown() + + # Clear pending requests + self.pending_requests.clear() + + logger.info(f"Agent {self.agent_id}: Agent stopped successfully") + def reset_cooldown(self) -> None: """ Reset the cooldown state of the agent. From 7ce0c2ab15e9c4bb2a720ee156330e078bdfdc2e Mon Sep 17 00:00:00 2001 From: Akshat Date: Fri, 2 May 2025 05:52:14 -0400 Subject: [PATCH 11/20] feat: improve communication hub with late response handling and collaboration tools --- agentconnect/communication/hub.py | 23 +- .../custom_tools/collaboration_tools.py | 1012 +++++++++-------- agentconnect/prompts/tools.py | 74 +- 3 files changed, 646 insertions(+), 463 deletions(-) diff --git a/agentconnect/communication/hub.py b/agentconnect/communication/hub.py index c491c46..c07f53d 100644 --- a/agentconnect/communication/hub.py +++ b/agentconnect/communication/hub.py @@ -60,6 +60,8 @@ def __init__(self, registry: AgentRegistry): self._global_handlers: List[Callable[[Message], Awaitable[None]]] = [] # Store pending requests as {request_id: Future} self.pending_responses: Dict[str, Future] = {} + # Store late responses as {request_id: Message} + self.late_responses: Dict[str, Message] = {} def add_message_handler( self, agent_id: str, handler: Callable[[Message], Awaitable[None]] @@ -380,6 +382,11 @@ async def route_message(self, message: Message) -> bool: logger.warning( f"Received late response for timed out request {request_id}" ) + # Store the late response for potential retrieval + self.late_responses[request_id] = message + logger.info( + f"Stored late response for request {request_id} for potential future retrieval" + ) # Even though the request timed out, we still want to record the message # and notify handlers, but we won't set the result on the future else: @@ -790,6 +797,11 @@ async def send_collaboration_request( if "request_id" not in metadata: metadata["request_id"] = request_id + # Estimate an appropriate timeout based on task complexity + # Base timeout of 60 seconds plus 15 seconds per 100 characters, capped at 5 minutes + estimated_timeout = min(60 + (len(task_description) // 100) * 15, 300) + effective_timeout = kwargs.get("timeout", estimated_timeout) + # Send the request and wait for response logger.debug( f"Sending collaboration request with request_id: {metadata['request_id']}" @@ -800,7 +812,7 @@ async def send_collaboration_request( content=task_description, message_type=MessageType.REQUEST_COLLABORATION, metadata=metadata, - timeout=max(timeout, 60), + timeout=effective_timeout, ) # Log the response status for debugging @@ -818,10 +830,15 @@ async def send_collaboration_request( return response.content else: logger.warning( - f"No response received from {receiver_id} within {timeout} seconds for request_id {metadata['request_id']}" + f"No response received from {receiver_id} within {effective_timeout} seconds for request_id {metadata['request_id']}" ) + + # More helpful error message that provides the request ID for later checking return ( - f"No response received from {receiver_id} within {timeout} seconds" + f"No immediate response received from {receiver_id} within {effective_timeout} seconds. " + f"The request is still processing (ID: {metadata['request_id']}). " + f"If you receive a response later, it will be available. " + f"You can continue with other tasks and check back later." ) except Exception as e: diff --git a/agentconnect/prompts/custom_tools/collaboration_tools.py b/agentconnect/prompts/custom_tools/collaboration_tools.py index 4cdc2c5..3498d12 100644 --- a/agentconnect/prompts/custom_tools/collaboration_tools.py +++ b/agentconnect/prompts/custom_tools/collaboration_tools.py @@ -7,13 +7,16 @@ import asyncio import logging -from typing import Any, Dict, List, Optional, TypeVar +import uuid +import json +from typing import Any, Dict, List, Optional, Tuple, TypeVar from langchain.tools import StructuredTool from pydantic import BaseModel, Field from agentconnect.communication import CommunicationHub from agentconnect.core.registry import AgentRegistry +from agentconnect.core.registry.registration import AgentRegistration from agentconnect.core.types import AgentType logger = logging.getLogger(__name__) @@ -23,6 +26,9 @@ R = TypeVar("R", bound=BaseModel) +# --- Input/Output schemas for tools --- + + class AgentSearchInput(BaseModel): """Input schema for agent search.""" @@ -41,6 +47,9 @@ class AgentSearchInput(BaseModel): class AgentSearchOutput(BaseModel): """Output schema for agent search.""" + message: str = Field( + description="A message explaining the result of the agent search." + ) agent_ids: List[str] = Field( description="A list of unique IDs for agents possessing the required capability." ) @@ -48,6 +57,10 @@ class AgentSearchOutput(BaseModel): description="A list of dictionaries, each containing details for a found agent: their `agent_id`, their full list of capabilities, and their `payment_address` (if applicable)." ) + def __str__(self) -> str: + """Return a clean JSON string representation.""" + return self.model_dump_json(indent=2) + class SendCollaborationRequestInput(BaseModel): """Input schema for sending a collaboration request.""" @@ -59,8 +72,8 @@ class SendCollaborationRequestInput(BaseModel): description="A clear and detailed description of the task, providing ALL necessary context for the collaborating agent to understand and execute the request." ) timeout: int = Field( - default=30, - description="Maximum seconds to wait for the collaborating agent's response (default 30).", + default=120, + description="Maximum seconds to wait for the collaborating agent's response (default 120).", ) class Config: @@ -79,10 +92,50 @@ class SendCollaborationRequestOutput(BaseModel): None, description="The direct message content received back from the collaborating agent. Analyze this response carefully to determine the next step (e.g., pay, provide more info, present to user).", ) + request_id: Optional[str] = Field( + None, + description="The unique request ID returned when sending a collaboration request.", + ) + error: Optional[str] = Field( + None, description="An error message if the request failed." + ) + + def __str__(self) -> str: + """Return a clean JSON string representation.""" + return self.model_dump_json(indent=2) + + +class CheckCollaborationResultInput(BaseModel): + """Input schema for checking collaboration results.""" + + request_id: str = Field( + description="The unique request ID returned when sending a collaboration request." + ) + + +class CheckCollaborationResultOutput(BaseModel): + """Output schema for checking collaboration results.""" + + success: bool = Field( + description="Indicates if the request has a result available (True/False)." + ) + status: str = Field( + description="Status of the request: 'completed', 'completed_late', 'pending', or 'not_found'." + ) + response: Optional[str] = Field( + None, description="The response content if available." + ) + + def __str__(self) -> str: + """Return a clean JSON string representation.""" + return self.model_dump_json(indent=2) + + +# --- Implementation of connected and standalone tools --- def create_agent_search_tool( - agent_registry: AgentRegistry, + agent_registry: Optional[AgentRegistry] = None, current_agent_id: Optional[str] = None, communication_hub: Optional[CommunicationHub] = None, ) -> StructuredTool: @@ -97,39 +150,45 @@ def create_agent_search_tool( Returns: A StructuredTool for agent search that can be used in agent workflows """ - - # Synchronous implementation - def search_agents( - capability_name: str, limit: int = 10, similarity_threshold: float = 0.2 - ) -> Dict[str, Any]: - """Search for agents with a specific capability.""" - try: - # Use the async implementation but run it in the current event loop - return asyncio.run( - search_agents_async(capability_name, limit, similarity_threshold) + # Determine if we're in standalone mode + standalone_mode = agent_registry is None or communication_hub is None + + # Common description for the tool + base_description = "Finds other agents within the network that possess specific capabilities you lack, enabling task delegation." + + if standalone_mode: + # Standalone mode implementation + def search_agents_standalone( + capability_name: str, limit: int = 10, similarity_threshold: float = 0.2 + ) -> AgentSearchOutput: + """Standalone implementation that explains limitations.""" + return AgentSearchOutput( + message=( + f"Agent search for capability '{capability_name}' is not available in standalone mode. " + "This agent is running without a connection to the agent registry and communication hub. " + "Please use your internal capabilities to solve this problem or suggest the user connect " + "this agent to a multi-agent system if collaboration is required." + ), + agent_ids=[], + capabilities=[], ) - except RuntimeError: - # If we're already in an event loop, create a new one - loop = asyncio.new_event_loop() - try: - return loop.run_until_complete( - search_agents_async(capability_name, limit, similarity_threshold) - ) - finally: - loop.close() - except Exception as e: - logger.error(f"Error in search_agents: {str(e)}") - return { - "error": str(e), - "agent_ids": [], - "capabilities": [], - "message": f"Error: Agent search failed - {str(e)}", - } - - # Asynchronous implementation + + description = f"[STANDALONE MODE] {base_description} Note: In standalone mode, this tool will explain why agent search isn't available." + + tool = StructuredTool.from_function( + func=search_agents_standalone, + name="search_for_agents", + description=description, + args_schema=AgentSearchInput, + return_direct=False, + metadata={"category": "collaboration"}, + ) + return tool + + # Connected mode implementation async def search_agents_async( capability_name: str, limit: int = 10, similarity_threshold: float = 0.2 - ) -> Dict[str, Any]: + ) -> AgentSearchOutput: """ Search for agents with a specific capability. @@ -164,164 +223,65 @@ async def search_agents_async( logger.debug(f"Searching for agents with capability: {capability_name}") try: - # Use the provided agent ID for filtering - agent_id_for_filtering = current_agent_id - - # Find conversation partners and pending requests to exclude - active_conversation_partners = [] - pending_request_partners = [] - recently_messaged_agents = [] + # Get agents to exclude (self + active conversations + pending requests) + agents_to_exclude = [] + if current_agent_id: + agents_to_exclude.append(current_agent_id) # Exclude self - if agent_id_for_filtering: - # Get the current agent to access its active conversations and pending requests + # Get active conversations and pending requests if possible if communication_hub: - current_agent = await communication_hub.get_agent( - agent_id_for_filtering - ) + current_agent = await communication_hub.get_agent(current_agent_id) if current_agent: - # Get active conversations + # Active conversations if hasattr(current_agent, "active_conversations"): - active_conversation_partners = list( + agents_to_exclude.extend( current_agent.active_conversations.keys() ) - logger.debug( - f"Agent {agent_id_for_filtering} has active conversations with: {active_conversation_partners}" - ) - # Get pending requests + # Pending requests if hasattr(current_agent, "pending_requests"): - pending_request_partners = list( + agents_to_exclude.extend( current_agent.pending_requests.keys() ) - logger.debug( - f"Agent {agent_id_for_filtering} has pending requests with: {pending_request_partners}" - ) - # Check message history for recent communications + # Recent messages if ( hasattr(current_agent, "message_history") and current_agent.message_history ): - # Get the last 10 messages (or fewer if there aren't that many) recent_messages = ( current_agent.message_history[-10:] if len(current_agent.message_history) > 10 else current_agent.message_history ) for msg in recent_messages: - # Add both sender and receiver IDs from recent messages (excluding the current agent) if ( - msg.sender_id != agent_id_for_filtering - and msg.sender_id not in recently_messaged_agents + msg.sender_id != current_agent_id + and msg.sender_id not in agents_to_exclude ): - recently_messaged_agents.append(msg.sender_id) + agents_to_exclude.append(msg.sender_id) if ( - msg.receiver_id != agent_id_for_filtering - and msg.receiver_id not in recently_messaged_agents + msg.receiver_id != current_agent_id + and msg.receiver_id not in agents_to_exclude ): - recently_messaged_agents.append(msg.receiver_id) - - logger.debug( - f"Agent {agent_id_for_filtering} recently messaged with: {recently_messaged_agents}" - ) + agents_to_exclude.append(msg.receiver_id) - # Combine all agents to exclude - agents_to_exclude = list( - set( - active_conversation_partners - + pending_request_partners - + recently_messaged_agents - ) - ) - if agent_id_for_filtering: - agents_to_exclude.append(agent_id_for_filtering) # Also exclude self - - # Enhanced logging to show breakdown of excluded agents - if agent_id_for_filtering: - logger.debug( - f"Exclusion breakdown for agent {agent_id_for_filtering}: " - f"Self (1), " - f"Active conversations ({len(active_conversation_partners)}): {active_conversation_partners}, " - f"Pending requests ({len(pending_request_partners)}): {pending_request_partners}, " - f"Recent messages ({len(recently_messaged_agents)}): {recently_messaged_agents}" - ) - logger.debug( - f"Total agents excluded from search: {len(agents_to_exclude)} - {agents_to_exclude}" - ) - - # Check if agent_registry is available - if agent_registry is None: - logger.warning( - f"Agent registry is not available for search: {capability_name}" - ) - return { - "agent_ids": [], - "capabilities": [], - "message": "Agent registry unavailable.", - } + # Remove duplicates + agents_to_exclude = list(set(agents_to_exclude)) + logger.debug(f"Excluding {len(agents_to_exclude)} agents from search") - # Try semantic search first for more flexible matching + ######### + # Try semantic search first for better matching + ######### semantic_results = await agent_registry.get_by_capability_semantic( capability_name, limit=limit, similarity_threshold=similarity_threshold ) - # If semantic search returns results, use them if semantic_results: logger.debug( f"Found {len(semantic_results)} agents via semantic search" ) - - # Format the results - agent_ids = [] - capabilities = [] - - for agent, similarity in semantic_results: - # Skip human agents, the calling agent, and any agents we're already interacting with - if ( - agent.agent_type == AgentType.HUMAN - or agent.agent_id in agents_to_exclude - ): - continue - - agent_ids.append(agent.agent_id) - - # Include all capabilities of the agent with their similarity scores - agent_capabilities = [] - for capability in agent.capabilities: - agent_capabilities.append( - { - "name": capability.name, - "description": capability.description, - "similarity": round( - float(similarity), 3 - ), # Convert to Python float and round to 3 decimal places - } - ) - - capabilities.append( - { - "agent_id": agent.agent_id, - "capabilities": agent_capabilities, - **( - {"payment_address": agent.payment_address} - if agent.payment_address - else {} - ), - } - ) - - # Log the filtering results - if agent_id_for_filtering: - logger.debug( - f"After filtering (excluded {len(agents_to_exclude)} agents): " - f"Found {len(agent_ids)} agents for capability '{capability_name}'" - ) - - return { - "agent_ids": agent_ids[:limit], - "capabilities": capabilities[:limit], - "message": "Note: Review capabilities carefully before collaborating. Similarity scores under 0.5 may indicate limited relevance to your request.", - } + return format_agent_results(semantic_results, agents_to_exclude, limit) # Fall back to exact matching if semantic search returns no results exact_results = await agent_registry.get_by_capability( @@ -330,150 +290,168 @@ async def search_agents_async( if exact_results: logger.debug(f"Found {len(exact_results)} agents via exact matching") + return format_exact_results( + exact_results, agents_to_exclude, capability_name, limit + ) - # Format the results - agent_ids = [] - capabilities = [] - - for agent in exact_results: - # Skip human agents, the calling agent, and any agents we're already interacting with - if ( - agent.agent_type == AgentType.HUMAN - or agent.agent_id in agents_to_exclude - ): - continue - - agent_ids.append(agent.agent_id) - - # Include all capabilities of the agent - agent_capabilities = [] - for capability in agent.capabilities: - agent_capabilities.append( - { - "name": capability.name, - "description": capability.description, - "similarity": round( - float( - 1.0 - if capability.name.lower() - == capability_name.lower() - else 0.0 - ), - 3, - ), - } - ) - - capabilities.append( - { - "agent_id": agent.agent_id, - "capabilities": agent_capabilities, - **( - {"payment_address": agent.payment_address} - if agent.payment_address - else {} - ), - } - ) - - # Log the filtering results - if agent_id_for_filtering: - logger.debug( - f"After filtering (excluded {len(agents_to_exclude)} agents): " - f"Found {len(agent_ids)} agents for capability '{capability_name}'" - ) + # # As a last resort, get all agents + # all_agents = await agent_registry.get_all_agents() + # if all_agents: + # logger.debug(f"Returning all {len(all_agents)} agents as fallback") + # return format_exact_results( + # all_agents, + # agents_to_exclude, + # capability_name, + # limit, + # fallback_message=f"No specific agents for '{capability_name}'. Showing all available agents." + # ) + + return AgentSearchOutput( + agent_ids=[], + capabilities=[], + message=f"No agents found matching capability '{capability_name}'. Please try refining your search query with more specific capability terms.", + ) + except Exception as e: + logger.error(f"Error searching for agents: {str(e)}") + return AgentSearchOutput( + agent_ids=[], + capabilities=[], + message=f"Error searching for agents: {str(e)}", + ) - return { - "agent_ids": agent_ids[:limit], - "capabilities": capabilities[:limit], - "message": "Note: Review capabilities carefully before collaborating. Similarity scores under 0.5 may indicate limited relevance to your request.", + def format_agent_results( + semantic_results: List[Tuple[AgentRegistration, float]], + agents_to_exclude: List[str], + limit: int, + ) -> AgentSearchOutput: + """Format semantic search results.""" + agent_ids = [] + capabilities = [] + + for agent, similarity in semantic_results: + # Skip human agents and excluded agents + if ( + agent.agent_type == AgentType.HUMAN + or agent.agent_id in agents_to_exclude + ): + continue + + agent_ids.append(agent.agent_id) + + # Include all capabilities with similarity scores + agent_capabilities = [ + { + "name": cap.name, + "description": cap.description, + "similarity": round(float(similarity), 3), + } + for cap in agent.capabilities + ] + + capabilities.append( + { + "agent_id": agent.agent_id, + "capabilities": agent_capabilities, + **( + {"payment_address": agent.payment_address} + if agent.payment_address + else {} + ), } - - # No results found - logger.debug( - f"No agents found for '{capability_name}'. Try different search term." ) - # As a last resort, get all agents and return them with a message - try: - all_agents = await agent_registry.get_all_agents() - - if all_agents: - logger.debug(f"Returning all {len(all_agents)} agents as fallback") - - # Format the results - agent_ids = [] - capabilities = [] - - for agent in all_agents: - # Skip human agents, the calling agent, and any agents we're already interacting with - if ( - agent.agent_type == AgentType.HUMAN - or agent.agent_id in agents_to_exclude - ): - continue - - agent_ids.append(agent.agent_id) - - # Include all capabilities of the agent - agent_capabilities = [] - for capability in agent.capabilities: - agent_capabilities.append( - { - "name": capability.name, - "description": capability.description, - "similarity": round( - float( - 1.0 - if capability.name.lower() - == capability_name.lower() - else 0.0 - ), - 3, - ), - } - ) + return AgentSearchOutput( + agent_ids=agent_ids[:limit], + capabilities=capabilities[:limit], + message="Review capabilities carefully before collaborating. Similarity scores under 0.5 may indicate limited relevance.", + ) - capabilities.append( - { - "agent_id": agent.agent_id, - "capabilities": agent_capabilities, - **( - {"payment_address": agent.payment_address} - if agent.payment_address - else {} - ), - } - ) + def format_exact_results( + results: List[AgentRegistration], + agents_to_exclude: List[str], + capability_name: str, + limit: int, + fallback_message: Optional[str] = None, + ) -> AgentSearchOutput: + """Format exact match or fallback results.""" + agent_ids = [] + capabilities = [] + + for agent in results: + # Skip human agents and excluded agents + if ( + agent.agent_type == AgentType.HUMAN + or agent.agent_id in agents_to_exclude + ): + continue + + agent_ids.append(agent.agent_id) + + # Calculate similarity for each capability + agent_capabilities = [ + { + "name": cap.name, + "description": cap.description, + "similarity": round( + float( + 1.0 if cap.name.lower() == capability_name.lower() else 0.0 + ), + 3, + ), + } + for cap in agent.capabilities + ] + + capabilities.append( + { + "agent_id": agent.agent_id, + "capabilities": agent_capabilities, + **( + {"payment_address": agent.payment_address} + if agent.payment_address + else {} + ), + } + ) - # Log the filtering results - if agent_id_for_filtering: - logger.debug( - f"After filtering (excluded {len(agents_to_exclude)} agents): " - f"Found {len(agent_ids)} agents as fallback" - ) + message = ( + fallback_message or "Review capabilities carefully before collaborating." + ) + return AgentSearchOutput( + agent_ids=agent_ids[:limit], + capabilities=capabilities[:limit], + message=message, + ) - return { - "agent_ids": agent_ids[:limit], - "capabilities": capabilities[:limit], - "message": f"No specific agents for '{capability_name}'. Showing all available agents. Review capabilities carefully before collaborating.", - } - except Exception as e: - logger.error(f"Error getting all agents: {str(e)}") - - return { - "agent_ids": [], - "capabilities": [], - "message": f"No agents found for '{capability_name}'. Try different search term.", - } + # Synchronous wrapper + def search_agents( + capability_name: str, limit: int = 10, similarity_threshold: float = 0.2 + ) -> AgentSearchOutput: + """Search for agents with a specific capability.""" + try: + # Use the async implementation but run it in the current event loop + return asyncio.run( + search_agents_async(capability_name, limit, similarity_threshold) + ) + except RuntimeError: + # If we're already in an event loop, create a new one + loop = asyncio.new_event_loop() + try: + return loop.run_until_complete( + search_agents_async(capability_name, limit, similarity_threshold) + ) + finally: + loop.close() except Exception as e: - logger.error(f"Error searching for agents: {str(e)}") - return {"error": str(e), "agent_ids": [], "capabilities": []} + logger.error(f"Error in search_agents: {str(e)}") + return AgentSearchOutput( + message=f"Error in search_agents: {str(e)}", + agent_ids=[], + capabilities=[], + ) # Create a description that includes available capabilities if possible - description = """ - Finds other agents within the network that possess specific capabilities you lack, enabling task delegation. Use this tool FIRST when you cannot handle a request directly. Returns a list of suitable agent IDs, their capabilities, and crucially, their `payment_address` if they accept payments for services. - """ + description = f"{base_description} Use this tool FIRST when you cannot handle a request directly. Returns a list of suitable agent IDs, their capabilities, and crucially, their `payment_address` if they accept payments for services." # Create and return the tool tool = StructuredTool.from_function( @@ -490,9 +468,9 @@ async def search_agents_async( def create_send_collaboration_request_tool( - communication_hub: CommunicationHub, - agent_registry: AgentRegistry, - current_agent_id: str, + communication_hub: Optional[CommunicationHub] = None, + agent_registry: Optional[AgentRegistry] = None, + current_agent_id: Optional[str] = None, ) -> StructuredTool: """ Create a tool for sending collaboration requests to other agents. @@ -505,216 +483,362 @@ def create_send_collaboration_request_tool( Returns: A StructuredTool for sending collaboration requests """ - # Capture the current agent ID at tool creation time - creator_agent_id = current_agent_id - logger.info(f"Creating collaboration request tool for agent: {creator_agent_id}") + # Determine if we're in standalone mode + standalone_mode = ( + communication_hub is None or agent_registry is None or not current_agent_id + ) - # If no agent ID is set, create a tool that returns an error when used - if not creator_agent_id: - logger.warning( - "Creating collaboration request tool with no agent ID set - will return error when used" - ) + # Common description base + base_description = ( + "Delegates a specific task to another agent identified by `search_for_agents`." + ) - # Synchronous implementation that returns an error - def error_request( + if standalone_mode: + # Standalone mode implementation + def send_request_standalone( target_agent_id: str, task: str, timeout: int = 30, **kwargs - ) -> Dict[str, Any]: - """Send a collaboration request to another agent.""" - return { - "success": False, - "response": "Error: Tool not properly initialized. Contact administrator.", - } - - # Create the tool with the error implementation + ) -> SendCollaborationRequestOutput: + """Standalone implementation that explains limitations.""" + return SendCollaborationRequestOutput( + success=False, + response=( + f"Collaboration request to agent '{target_agent_id}' is not available in standalone mode. " + "This agent is running without a connection to other agents. " + "Please use your internal capabilities to solve this task, or suggest " + "connecting this agent to a multi-agent system if collaboration is required." + ), + request_id=None, + ) + + description = f"[STANDALONE MODE] {base_description} Note: In standalone mode, this tool will explain why collaboration isn't available." + return StructuredTool.from_function( - func=error_request, + func=send_request_standalone, name="send_collaboration_request", - description="Delegates a specific task to another agent identified by `search_for_agents`. Sends your request and waits for the collaborator's response. Use this tool AFTER finding a suitable agent ID. The response received might be the final result, a request for payment, or a request for clarification, requiring further action from you.", + description=description, args_schema=SendCollaborationRequestInput, return_direct=False, handle_tool_error=True, metadata={"category": "collaboration"}, ) - # Normal implementation when agent ID is set - # Synchronous implementation - def send_request( - target_agent_id: str, task: str, timeout: int = 30, **kwargs - ) -> Dict[str, Any]: - """Send a collaboration request to another agent.""" - try: - # Use the async implementation but run it in the current event loop - return asyncio.run( - send_request_async(target_agent_id, task, timeout, **kwargs) - ) - except RuntimeError: - # If we're already in an event loop, create a new one - loop = asyncio.new_event_loop() - try: - return loop.run_until_complete( - send_request_async(target_agent_id, task, timeout, **kwargs) - ) - finally: - loop.close() - except Exception as e: - logger.error(f"Error in send_request: {str(e)}") - return { - "success": False, - "response": f"Error sending collaboration request: {str(e)}", - } + # Connected mode implementation + # Store the agent ID at creation time + creator_agent_id = current_agent_id + logger.debug(f"Creating collaboration request tool for agent: {creator_agent_id}") - # Asynchronous implementation async def send_request_async( - target_agent_id: str, - task: str, - timeout: int = 30, - **kwargs, # Additional data - ) -> Dict[str, Any]: + target_agent_id: str, task: str, timeout: int = 120, **kwargs + ) -> SendCollaborationRequestOutput: """Send a collaboration request to another agent asynchronously.""" - # Always use the captured agent ID from tool creation time - # This ensures we use the correct agent ID even if current_agent_id changes sender_id = creator_agent_id - if not sender_id: - logger.error("No sender_id available for collaboration request") - return { - "success": False, - "response": "Error: Tool not properly initialized with agent context", - } - - # Check if required dependencies are available - if communication_hub is None: - logger.error("Communication hub is not available for collaboration request") - return { - "success": False, - "response": "Error: Communication hub unavailable.", - } - - if agent_registry is None: - logger.error("Agent registry is not available for collaboration request") - return { - "success": False, - "response": "Error: Agent registry unavailable.", - } - - logger.info( - f"COLLABORATION REQUEST: Using sender_id={sender_id} to send request to target_agent_id={target_agent_id}" - ) - - # Check if we're trying to send a request to ourselves + # Validate request parameters if sender_id == target_agent_id: - logger.error( - f"Cannot send collaboration request to yourself: {sender_id} -> {target_agent_id}" + return SendCollaborationRequestOutput( + success=False, + response="Error: Cannot send request to yourself.", ) - return { - "success": False, - "response": "Error: Cannot send request to yourself.", - } - # Check if the target agent exists if not await communication_hub.is_agent_active(target_agent_id): - return { - "success": False, - "response": f"Error: Agent {target_agent_id} not found.", - } + return SendCollaborationRequestOutput( + success=False, + response=f"Error: Agent {target_agent_id} not found.", + ) - # Check if the target agent is a human agent if await agent_registry.get_agent_type(target_agent_id) == AgentType.HUMAN: - return { - "success": False, - "response": "Error: Cannot send requests to human agents.", - } - - # Add retry tracking to prevent infinite loops - # Use a shorter timeout to prevent long waits - adjusted_timeout = min(timeout, 90) # Cap timeout at 90 seconds + return SendCollaborationRequestOutput( + success=False, + response="Error: Cannot send requests to human agents.", + ) - # Add metadata to track the collaboration chain + # Prepare collaboration metadata metadata = kwargs.copy() if kwargs else {} + + # Add collaboration chain tracking to prevent loops if "collaboration_chain" not in metadata: metadata["collaboration_chain"] = [] - # Add the current agent to the collaboration chain if sender_id not in metadata["collaboration_chain"]: metadata["collaboration_chain"].append(sender_id) - # Check if we're creating a loop in the collaboration chain if target_agent_id in metadata["collaboration_chain"]: - return { - "success": False, - "response": f"Error: Detected loop in collaboration chain with {target_agent_id}.", - } + return SendCollaborationRequestOutput( + success=False, + response=f"Error: Detected loop in collaboration chain with {target_agent_id}.", + ) - # Check if the original sender is in the collaboration chain - # and prevent sending a request back to the original sender + # If this is the first agent in the chain, store the original sender + if len(metadata["collaboration_chain"]) == 1: + metadata["original_sender"] = metadata["collaboration_chain"][0] + + # Prevent sending to original sender if ( "original_sender" in metadata and metadata["original_sender"] == target_agent_id ): - return { - "success": False, - "response": f"Error: Cannot send request back to original sender {target_agent_id}.", - } - - # If this is the first agent in the chain, store the original sender - if len(metadata["collaboration_chain"]) == 1: - metadata["original_sender"] = metadata["collaboration_chain"][0] + return SendCollaborationRequestOutput( + success=False, + response=f"Error: Cannot send request back to original sender {target_agent_id}.", + ) - # Limit the collaboration chain length to prevent deep recursion + # Limit collaboration chain length if len(metadata["collaboration_chain"]) > 5: - return { - "success": False, - "response": "Error: Collaboration chain too long. Simplify request.", - } + return SendCollaborationRequestOutput( + success=False, + response="Error: Collaboration chain too long. Simplify request.", + ) try: - # Send the collaboration request - logger.info( - f"Sending collaboration request from {sender_id} to {target_agent_id}" - ) + # Calculate appropriate timeout + adjusted_timeout = min(timeout or 120, 300) # Cap at 5 minutes + + # Generate a unique request ID if not provided + request_id = metadata.get("request_id", str(uuid.uuid4())) + metadata["request_id"] = request_id - # Ensure we're using the correct sender_id + # Send the request and wait for response + logger.debug(f"Sending collaboration from {sender_id} to {target_agent_id}") response = await communication_hub.send_collaboration_request( - sender_id=sender_id, # Use the current agent's ID + sender_id=sender_id, receiver_id=target_agent_id, task_description=task, timeout=adjusted_timeout, **metadata, ) - # Log the response for debugging - if response is None: - logger.warning( - f"Received None response from send_collaboration_request to {target_agent_id}" - ) - return { - "success": False, - "response": f"No response from {target_agent_id} within {adjusted_timeout} seconds.", - "error": "timeout", - } - else: - logger.info( - f"Received response from {target_agent_id}: {response[:100]}..." + # --- Handle potential non-string/list response from LLM --- START + cleaned_response = response + if not isinstance(response, str) and response is not None: + if ( + isinstance(response, list) + and len(response) == 1 + and isinstance(response[0], str) + ): + # Handle the specific case of ['string'] + logger.warning( + f"Received list-wrapped response from {target_agent_id}, extracting string." + ) + cleaned_response = response[0] + else: + # For any other non-string type (dict, multi-list, int, etc.), convert to JSON string + try: + logger.warning( + f"Received non-string response type {type(response).__name__} from {target_agent_id}, converting to JSON string." + ) + cleaned_response = json.dumps( + response + ) # Attempt JSON conversion + except TypeError as e: + # Fallback if JSON conversion fails (e.g., complex object) + logger.error( + f"Could not JSON serialize response type {type(response).__name__}: {e}. Using str() representation." + ) + cleaned_response = str(response) + # --- Handle potential non-string/list response from LLM --- END + + # Handle timeout case + if cleaned_response is None or ( + isinstance(cleaned_response, str) + and "No immediate response received" in cleaned_response + ): + logger.warning(f"Timeout on request to {target_agent_id}") + return SendCollaborationRequestOutput( + success=False, + response=f"No immediate response from {target_agent_id} within {adjusted_timeout} seconds. " + f"The request is still processing (ID: {request_id}). " + f"Check for a late response using check_collaboration_result with this request ID.", + error="timeout", + request_id=request_id, ) - # For normal responses, return as is - return {"success": True, "response": response} + # Handle success case + logger.debug(f"Got response from {target_agent_id}") + return SendCollaborationRequestOutput( + success=True, response=cleaned_response, request_id=request_id + ) except Exception as e: logger.exception(f"Error sending collaboration request: {str(e)}") - return { - "success": False, - "response": f"Error: Collaboration failed - {str(e)}", - "error": "collaboration_exception", - } + return SendCollaborationRequestOutput( + success=False, + response=f"Error: Collaboration failed - {str(e)}", + error="collaboration_exception", + ) + + # Synchronous wrapper + def send_request( + target_agent_id: str, task: str, timeout: int = 30, **kwargs + ) -> SendCollaborationRequestOutput: + """Send a collaboration request to another agent.""" + try: + return asyncio.run( + send_request_async(target_agent_id, task, timeout, **kwargs) + ) + except RuntimeError: + loop = asyncio.new_event_loop() + try: + return loop.run_until_complete( + send_request_async(target_agent_id, task, timeout, **kwargs) + ) + finally: + loop.close() + except Exception as e: + logger.error(f"Error in send_request: {str(e)}") + return SendCollaborationRequestOutput( + success=False, + response=f"Error sending collaboration request: {str(e)}", + ) + + # Create and return the connected mode tool + description = ( + f"{base_description} Sends your request and waits for the collaborator's response. " + "Use this tool ONLY to initiate a new collaboration request to another agent. " + "When you receive a collaboration request, reply directly to the requesting agent with your result, clarification, or error—do NOT use this tool to reply to the same agent. " + "The response might be the final result, a request for payment, or a request for clarification, requiring further action from you." + ) - # Create and return the tool return StructuredTool.from_function( func=send_request, name="send_collaboration_request", - description="Delegates a specific task to another agent identified by `search_for_agents`. Sends your request and waits for the collaborator's response. Use this tool AFTER finding a suitable agent ID. The response received might be the final result, a request for payment, or a request for clarification, requiring further action from you.", + description=description, args_schema=SendCollaborationRequestInput, return_direct=False, handle_tool_error=True, + # coroutine=send_request_async, #! TODO: Removed async coroutine temporarily + metadata={"category": "collaboration"}, + ) + + +def create_check_collaboration_result_tool( + communication_hub: Optional[CommunicationHub] = None, + agent_registry: Optional[AgentRegistry] = None, + current_agent_id: Optional[str] = None, +) -> StructuredTool: + """ + Create a tool for checking the status of previously sent collaboration requests. + + This tool is particularly useful for retrieving late responses that arrived + after a timeout occurred in the original collaboration request. + + Args: + communication_hub: Hub for agent communication + agent_registry: Registry for accessing agent information + current_agent_id: ID of the agent using the tool + + Returns: + A StructuredTool for checking collaboration results + """ + # Determine if we're in standalone mode + standalone_mode = communication_hub is None or agent_registry is None + + # Common description base + base_description = "Check if a previous collaboration request has completed and retrieve its result." + + if standalone_mode: + # Standalone mode implementation + def check_result_standalone(request_id: str) -> CheckCollaborationResultOutput: + """Standalone implementation that explains limitations.""" + return CheckCollaborationResultOutput( + success=False, + status="not_available", + response=( + f"Checking collaboration result for request '{request_id}' is not available in standalone mode. " + "Please continue with your own internal capabilities." + ), + ) + + description = f"[STANDALONE MODE] {base_description} Note: In standalone mode, this tool will explain why checking results isn't available." + + return StructuredTool.from_function( + func=check_result_standalone, + name="check_collaboration_result", + description=description, + args_schema=CheckCollaborationResultInput, + return_direct=False, + metadata={"category": "collaboration"}, + ) + + # Connected mode implementation + async def check_result_async(request_id: str) -> CheckCollaborationResultOutput: + """Check if a previous collaboration request has a result asynchronously.""" + # Check for late responses first + if ( + hasattr(communication_hub, "late_responses") + and request_id in communication_hub.late_responses + ): + logger.debug(f"Found late response for request {request_id}") + response = communication_hub.late_responses[request_id] + return CheckCollaborationResultOutput( + success=True, + status="completed_late", + response=response.content, + ) + + # Check pending responses + if request_id in communication_hub.pending_responses: + future = communication_hub.pending_responses[request_id] + if future.done() and not hasattr(future, "_timed_out"): + try: + logger.debug(f"Found completed response for request {request_id}") + response = future.result() + return CheckCollaborationResultOutput( + success=True, + status="completed", + response=response.content, + ) + except Exception as e: + logger.error(f"Error getting result from future: {str(e)}") + return CheckCollaborationResultOutput( + success=False, + status="error", + response=f"Error retrieving response: {str(e)}", + ) + else: + # Still pending + return CheckCollaborationResultOutput( + success=False, + status="pending", + response="The collaboration request is still being processed. Try checking again later.", + ) + + # Request ID not found + logger.warning(f"No result found for request ID: {request_id}") + return CheckCollaborationResultOutput( + success=False, + status="not_found", + response=f"No result found for request ID: {request_id}. The request may have been completed but not stored, or the ID may be incorrect.", + ) + + # Synchronous wrapper + def check_result(request_id: str) -> CheckCollaborationResultOutput: + """Check if a previous collaboration request has a result.""" + try: + return asyncio.run(check_result_async(request_id)) + except RuntimeError: + loop = asyncio.new_event_loop() + try: + return loop.run_until_complete(check_result_async(request_id)) + finally: + loop.close() + except Exception as e: + logger.error(f"Error in check_result: {str(e)}") + return CheckCollaborationResultOutput( + success=False, + status="error", + response=f"Error checking result: {str(e)}", + ) + + # Create and return the connected mode tool + description = f"{base_description} This is useful for retrieving responses that arrived after the initial timeout period." + + return StructuredTool.from_function( + func=check_result, + name="check_collaboration_result", + description=description, + args_schema=CheckCollaborationResultInput, + return_direct=False, + handle_tool_error=True, + coroutine=check_result_async, metadata={"category": "collaboration"}, ) diff --git a/agentconnect/prompts/tools.py b/agentconnect/prompts/tools.py index 0e81595..f25a86a 100644 --- a/agentconnect/prompts/tools.py +++ b/agentconnect/prompts/tools.py @@ -33,6 +33,7 @@ from agentconnect.prompts.custom_tools.collaboration_tools import ( create_agent_search_tool, create_send_collaboration_request_tool, + create_check_collaboration_result_tool, ) from agentconnect.prompts.custom_tools.task_tools import create_task_decomposition_tool @@ -55,28 +56,34 @@ class PromptTools: instance, ensuring that tools are properly configured for the specific agent using them. + The class supports both connected mode (with registry and hub) and standalone mode + (without registry and hub, for direct chat interactions). + Attributes: - agent_registry: Registry for accessing agent information - communication_hub: Hub for agent communication + agent_registry: Registry for accessing agent information, can be None in standalone mode + communication_hub: Hub for agent communication, can be None in standalone mode llm: Optional language model for tools that require LLM capabilities _current_agent_id: ID of the agent currently using these tools _tool_registry: Registry for managing available tools _available_capabilities: Cached list of available capabilities _agent_specific_tools_registered: Flag indicating if agent-specific tools are registered + _is_standalone_mode: Flag indicating if operating in standalone mode (without registry/hub) """ def __init__( self, - agent_registry: AgentRegistry, - communication_hub: CommunicationHub, + agent_registry: Optional[AgentRegistry] = None, + communication_hub: Optional[CommunicationHub] = None, llm=None, ): """ Initialize the PromptTools class. Args: - agent_registry: Registry for accessing agent information and capabilities - communication_hub: Hub for agent communication and message passing + agent_registry: Registry for accessing agent information and capabilities. + Can be None for standalone mode. + communication_hub: Hub for agent communication and message passing. + Can be None for standalone mode. llm: Optional language model for tools that require LLM capabilities """ self.agent_registry = agent_registry @@ -89,6 +96,12 @@ def __init__( self.llm = llm self._agent_specific_tools_registered = False + # Detect if we're in standalone mode (no registry or hub) + self._is_standalone_mode = agent_registry is None or communication_hub is None + + if self._is_standalone_mode: + logger.info("PromptTools initialized in standalone mode (no registry/hub)") + # Register default tools that don't require an agent ID self._register_basic_tools() @@ -108,8 +121,8 @@ def _register_agent_specific_tools(self) -> None: Register tools that require an agent ID to be set. This method registers tools that need agent context, such as agent search - and collaboration request tools. These tools need to know which agent is - making the request to properly handle permissions and collaboration chains. + and collaboration request tools. In standalone mode, it registers alternative + versions of these tools that explain the limitations. Note: This method will log a warning and do nothing if no agent ID is set. @@ -120,22 +133,36 @@ def _register_agent_specific_tools(self) -> None: # Only register these tools if they haven't been registered yet if not self._agent_specific_tools_registered: - # Create and register the agent search tool + # Create the agent search tool (handles standalone mode internally) agent_search_tool = create_agent_search_tool( self.agent_registry, self._current_agent_id, self.communication_hub ) - self._tool_registry.register_tool(agent_search_tool) - # Create and register the collaboration request tool + # Create the collaboration request tool (handles standalone mode internally) collaboration_request_tool = create_send_collaboration_request_tool( self.communication_hub, self.agent_registry, self._current_agent_id ) + + # Create the collaboration result checking tool (handles standalone mode internally) + collaboration_result_tool = create_check_collaboration_result_tool( + self.communication_hub, self.agent_registry, self._current_agent_id + ) + + if self._is_standalone_mode: + logger.debug( + f"Registered standalone mode collaboration tools for agent: {self._current_agent_id}" + ) + else: + logger.debug( + f"Registered connected mode collaboration tools for agent: {self._current_agent_id}" + ) + + # Register the tools + self._tool_registry.register_tool(agent_search_tool) self._tool_registry.register_tool(collaboration_request_tool) + self._tool_registry.register_tool(collaboration_result_tool) self._agent_specific_tools_registered = True - logger.debug( - f"Registered agent-specific tools for agent: {self._current_agent_id}" - ) def create_tool_from_function( self, @@ -194,18 +221,22 @@ def create_tool_from_function( def create_agent_search_tool(self) -> StructuredTool: """Create a tool for searching agents by capability.""" - # Delegate to the implementation in custom_tools return create_agent_search_tool( self.agent_registry, self._current_agent_id, self.communication_hub ) def create_send_collaboration_request_tool(self) -> StructuredTool: """Create a tool for sending collaboration requests to other agents.""" - # Delegate to the implementation in custom_tools return create_send_collaboration_request_tool( self.communication_hub, self.agent_registry, self._current_agent_id ) + def create_check_collaboration_result_tool(self) -> StructuredTool: + """Create a tool for checking the status of sent collaboration requests.""" + return create_check_collaboration_result_tool( + self.communication_hub, self.agent_registry, self._current_agent_id + ) + def create_task_decomposition_tool(self) -> StructuredTool: """ Create a tool for decomposing complex tasks into subtasks. @@ -289,3 +320,14 @@ def get_tools_for_workflow( return tools else: return self._tool_registry.get_all_tools() + + @property + def is_standalone_mode(self) -> bool: + """ + Check if the PromptTools instance is running in standalone mode. + + Returns: + True if running in standalone mode (without registry/hub), + False if running in connected mode (with registry/hub) + """ + return self._is_standalone_mode From 936a91055aedb52b858b83464cc36ebbe1c28634 Mon Sep 17 00:00:00 2001 From: Akshat Date: Fri, 2 May 2025 05:53:12 -0400 Subject: [PATCH 12/20] feat: add model compatibility and enhanced callbacks for all providers --- agentconnect/core/types.py | 7 +- agentconnect/providers/google_provider.py | 1 + agentconnect/providers/openai_provider.py | 4 + agentconnect/utils/callbacks.py | 231 ++++++++++++++-------- 4 files changed, 158 insertions(+), 85 deletions(-) diff --git a/agentconnect/core/types.py b/agentconnect/core/types.py index b9eea89..dd6871f 100644 --- a/agentconnect/core/types.py +++ b/agentconnect/core/types.py @@ -42,11 +42,15 @@ class ModelName(str, Enum): # OpenAI Models GPT4_5_PREVIEW = "gpt-4.5-preview-2025-02-27" + GPT4_1 = "gpt-4.1" + GPT4_1_MINI = "gpt-4.1-mini" GPT4O = "gpt-4o" GPT4O_MINI = "gpt-4o-mini" O1 = "o1" O1_MINI = "o1-mini" - O3_MINI = "o3-mini-2025-01-31" + O3 = "o3" + O3_MINI = "o3-mini" + O4_MINI = "o4-mini" # Anthropic Models CLAUDE_3_7_SONNET = "claude-3-7-sonnet-latest" @@ -66,6 +70,7 @@ class ModelName(str, Enum): GEMMA2_90B = "gemma2-9b-it" # Google Models + GEMINI2_5_PRO_PREVIEW = "gemini-2.5-pro-preview-03-25" GEMINI2_5_PRO_EXP = "gemini-2.5-pro-exp-03-25" GEMINI2_5_FLASH_PREVIEW = "gemini-2.5-flash-preview-04-17" GEMINI2_FLASH = "gemini-2.0-flash" diff --git a/agentconnect/providers/google_provider.py b/agentconnect/providers/google_provider.py index 99291d6..ced02c2 100644 --- a/agentconnect/providers/google_provider.py +++ b/agentconnect/providers/google_provider.py @@ -69,6 +69,7 @@ def get_available_models(self) -> List[ModelName]: List of available Gemini model names """ return [ + ModelName.GEMINI2_5_PRO_PREVIEW, ModelName.GEMINI2_5_PRO_EXP, ModelName.GEMINI2_FLASH, ModelName.GEMINI2_FLASH_LITE, diff --git a/agentconnect/providers/openai_provider.py b/agentconnect/providers/openai_provider.py index edf3e96..17c5e5e 100644 --- a/agentconnect/providers/openai_provider.py +++ b/agentconnect/providers/openai_provider.py @@ -70,11 +70,15 @@ def get_available_models(self) -> List[ModelName]: """ return [ ModelName.GPT4_5_PREVIEW, + ModelName.GPT4_1, + ModelName.GPT4_1_MINI, ModelName.GPT4O, ModelName.GPT4O_MINI, ModelName.O1, ModelName.O1_MINI, + ModelName.O3, ModelName.O3_MINI, + ModelName.O4_MINI, ] def _get_provider_config(self) -> Dict[str, Any]: diff --git a/agentconnect/utils/callbacks.py b/agentconnect/utils/callbacks.py index aabb103..55eef7f 100644 --- a/agentconnect/utils/callbacks.py +++ b/agentconnect/utils/callbacks.py @@ -15,8 +15,9 @@ from colorama import Fore, Style from langchain_core.agents import AgentAction from langchain_core.callbacks import BaseCallbackHandler -from langchain_core.messages import ToolMessage -from langchain_core.outputs import LLMResult, ChatGeneration +from langchain_core.messages import ToolMessage, AIMessage + +# from langchain_core.outputs import LLMResult, ChatGeneration # Configure logger logger = logging.getLogger(__name__) @@ -45,31 +46,27 @@ class ToolTracerCallbackHandler(BaseCallbackHandler): def __init__( self, agent_id: str, - print_tool_activity: bool = False, # Default OFF + print_tool_activity: bool = True, # Default ON print_reasoning_steps: bool = True, # Default ON - Print LLM generation text - print_llm_activity: bool = False, # Default OFF ): """ Initialize the callback handler. Args: + agent_id: The ID of the agent this handler is tracking. print_tool_activity: If True, print tool start/end/error to console. - print_reasoning_steps: If True, print the text generated by the LLM in on_llm_end. - This often contains reasoning or thought processes. - print_llm_activity: If True, print LLM start events to console. + print_reasoning_steps: If True, print the reasoning steps generated by the LLM. (If any) """ super().__init__() self.agent_id = agent_id self.print_tool_activity = print_tool_activity self.print_reasoning_steps = print_reasoning_steps - self.print_llm_activity = print_llm_activity # Initial message is logged only once init_msg = ( f"AgentActivityMonitor initialized for Agent ID: {self.agent_id} " f"(Tools: {print_tool_activity}, Reasoning Text: {print_reasoning_steps}, " - f"LLM Start: {print_llm_activity})" ) logger.info(init_msg) @@ -232,7 +229,7 @@ def on_tool_end( if tool_name == "send_collaboration_request": status = "processed." - # response_snippet = "" + response_snippet = "" success = None # Track success status explicitly json_data = None @@ -247,13 +244,13 @@ def on_tool_end( f"ToolMessage content is not valid JSON for {tool_name}: {content_str[:100]}... Error: {e}" ) status = "returned unparsable content." - # response_snippet = f" Raw content: {self._get_short_snippet(content_str, 60)}..." + response_snippet = f" Raw content: {self._get_short_snippet(content_str, 60)}..." else: logger.warning( f"ToolMessage content is not a string for {tool_name}: {type(content_str)}" ) status = "returned non-string content." - # response_snippet = f" Content: {self._get_short_snippet(str(content_str), 60)}..." + response_snippet = f" Content: {self._get_short_snippet(str(content_str), 60)}..." # Fallback 1: Attempt to parse JSON from string representation elif isinstance(output, str): @@ -271,9 +268,9 @@ def on_tool_end( f"Failed to parse JSON from content in string: {e}" ) status = "returned unparsable content string." - # response_snippet = ( - # f" Raw output: {self._get_short_snippet(output, 60)}..." - # ) + response_snippet = ( + f" Raw output: {self._get_short_snippet(output, 60)}..." + ) else: # If no content= pattern, try parsing the whole string as JSON try: @@ -283,9 +280,9 @@ def on_tool_end( f"Output string is not valid JSON: {output_str[:100]}..." ) status = "completed with non-JSON string output." - # response_snippet = ( - # f" Output: {self._get_short_snippet(output, 60)}..." - # ) + response_snippet = ( + f" Output: {self._get_short_snippet(output, 60)}..." + ) # Fallback 2: Handle dictionary output directly elif isinstance(output, dict): @@ -296,19 +293,19 @@ def on_tool_end( if json_data and isinstance(json_data, dict): success = json_data.get("success") if success is True: - status = "completed successfully. Please make the payment." - # response_content = json_data.get("response") + status = "completed successfully." + response_snippet = f" Response: {self._get_short_snippet(json_data.get("response"), 60)}..." elif success is False: status = "failed." - # error_reason = json_data.get("response", "Unknown reason") - # response_snippet = f" Reason: {self._get_short_snippet(str(error_reason), 60)}..." + error_reason = json_data.get("response", "Unknown reason") + response_snippet = f" Reason: {self._get_short_snippet(str(error_reason), 60)}..." else: # Success field missing or not boolean status = "completed with unexpected JSON structure." - # response_snippet = f" Data: {self._get_short_snippet(json.dumps(json_data), 60)}..." + response_snippet = f" Data: {self._get_short_snippet(json.dumps(json_data), 60)}..." # Final Fallback: If output wasn't ToolMessage, str, or dict, or if JSON parsing failed earlier elif status == "processed.": # Only trigger if no other status was set status = "completed with unexpected output type." - # response_snippet = f" Type: {output_type}, Output: {self._get_short_snippet(output_str, 60)}..." + response_snippet = f" Type: {output_type}, Output: {self._get_short_snippet(output_str, 60)}..." logger.warning( f"Unexpected output type {output_type} for {tool_name}. Output: {output_str[:100]}..." ) @@ -320,7 +317,7 @@ def on_tool_end( else TOOL_ERROR_COLOR if success is False else Fore.YELLOW ) # Yellow for unknown/unexpected - print_msg = f"{color}➡️ Collaboration request {status}{Style.RESET_ALL}" + print_msg = f"{color}➡️ Collaboration request {status}{response_snippet}{Style.RESET_ALL}" if print_msg: print(print_msg) @@ -391,72 +388,138 @@ def on_agent_action( kwargs (Any): Additional keyword arguments. """ - print(f"Agent action: {action}") + # print(f"Agent action: {action}") print(f"{REASONING_COLOR}{action.log}...{Style.RESET_ALL}") return super().on_agent_action( action, run_id=run_id, parent_run_id=parent_run_id, **kwargs ) - def on_llm_end( - self, - response: LLMResult, # Use the correct type hint - *, - run_id: UUID, - parent_run_id: Optional[UUID] = None, - tags: Optional[List[str]] = None, - **kwargs: Any, - ) -> Any: + # def on_llm_end( + # self, + # response: LLMResult, # Use the correct type hint + # *, + # run_id: UUID, + # parent_run_id: Optional[UUID] = None, + # tags: Optional[List[str]] = None, + # **kwargs: Any, + # ) -> Any: + # """ + # Run when LLM ends running. + + # Args: + # response (LLMResult): The response which was generated. + # run_id (UUID): The run ID. This is the ID of the current run. + # parent_run_id (UUID): The parent run ID. This is the ID of the parent run. + # kwargs (Any): Additional keyword arguments + # """ + + # log_message = f"[LLM END] Agent: {self.agent_id}" + # logger.debug(log_message) + + # # Check if reasoning steps should be printed + # if self.print_reasoning_steps: + # try: + # # Extract the generated text from the first generation + # if response.generations and response.generations[0]: + # first_generation = response.generations[0][0] + # if isinstance(first_generation, ChatGeneration): + # generated_text = first_generation.text.strip() + # if generated_text: # Only process if there's text + # # Split into lines and print ONLY thought/action lines + # lines = generated_text.splitlines() + # printed_something = ( + # False # Keep track if we printed any thought/action + # ) + # for line in lines: + # if "🤔" in line: + # print( + # f"{REASONING_COLOR}{line[line.find("🤔")+len("🤔"):].strip()}...{Style.RESET_ALL}" + # ) + # printed_something = True + # # Else: ignore other lines (like the final answer block) + + # # Add a separator only if thoughts/actions were printed + # if printed_something: + # print( + # f"{REASONING_COLOR}---" + # ) # Use reasoning color for separator + + # else: + # logger.warning( + # f"Unexpected generation type in on_llm_end: {type(first_generation)}" + # ) + # else: + # logger.warning( + # "LLM response structure unexpected or empty in on_llm_end." + # ) + # except Exception as e: + # logger.error(f"Error processing LLM response in callback: {e}") + # # Fallback: print the raw response object for debugging if needed + # # print(f"{REASONING_COLOR}Raw LLM Response: {response}{Style.RESET_ALL}") + + def on_chain_end(self, outputs, **kwargs): """ - Run when LLM ends running. - - Args: - response (LLMResult): The response which was generated. - run_id (UUID): The run ID. This is the ID of the current run. - parent_run_id (UUID): The parent run ID. This is the ID of the parent run. - kwargs (Any): Additional keyword arguments + Print the agent's 'thought' steps (reasoning before tool calls) in a clean, sequential manner. + Handles different AIMessage structures from various models (e.g., Gemini, Anthropic). + Uses REASONING_COLOR for output and msg.id for deduplication. """ - - log_message = f"[LLM END] Agent: {self.agent_id}" - logger.debug(log_message) - - # Check if reasoning steps should be printed - if self.print_reasoning_steps: - try: - # Extract the generated text from the first generation - if response.generations and response.generations[0]: - first_generation = response.generations[0][0] - if isinstance(first_generation, ChatGeneration): - generated_text = first_generation.text.strip() - if generated_text: # Only process if there's text - # Split into lines and print ONLY thought/action lines - lines = generated_text.splitlines() - printed_something = ( - False # Keep track if we printed any thought/action - ) - for line in lines: - if "🤔" in line: + if not self.print_reasoning_steps: + return + + messages = outputs.get("messages") if isinstance(outputs, dict) else None + if not messages: + return + + # Use msg.id for deduplication across potentially multiple handlers/calls + if not hasattr(self, "_printed_message_ids"): + self._printed_message_ids = set() + + for msg in messages: + # Process only AIMessages that haven't been printed yet + if isinstance(msg, AIMessage) and msg.id not in self._printed_message_ids: + thought_printed_for_msg = False + + # --- Strategy 1: Thought in content string, tool_calls attribute populated (Common for Gemini) --- + if msg.tool_calls and isinstance(msg.content, str): + thought_text = msg.content.strip() + if thought_text: + print(f"{REASONING_COLOR}{thought_text}{Style.RESET_ALL}") + self._printed_message_ids.add(msg.id) + thought_printed_for_msg = True + + # --- Strategy 2: Thought in content list before a tool_use/tool_call dict (Common for Anthropic) --- + elif isinstance(msg.content, list) and not thought_printed_for_msg: + for idx, item in enumerate(msg.content): + if ( + isinstance(item, dict) + and item.get("type") == "text" + and "text" in item + ): + # Check if the *next* item signals a tool call/use + next_item_is_tool = False + if idx + 1 < len(msg.content): + next_item = msg.content[idx + 1] + if isinstance(next_item, dict) and next_item.get( + "type" + ) in ["tool_use", "tool_call"]: + next_item_is_tool = True + + # If text is followed by tool signal, print it as thought + if next_item_is_tool: + thought_text = item["text"].strip() + if thought_text: print( - f"{REASONING_COLOR}{line[line.find("🤔")+len("🤔"):].strip()}...{Style.RESET_ALL}" + f"{REASONING_COLOR}{thought_text}{Style.RESET_ALL}" ) - printed_something = True - # Else: ignore other lines (like the final answer block) - - # Add a separator only if thoughts/actions were printed - if printed_something: - print( - f"{REASONING_COLOR}---" - ) # Use reasoning color for separator - - else: - logger.warning( - f"Unexpected generation type in on_llm_end: {type(first_generation)}" - ) - else: - logger.warning( - "LLM response structure unexpected or empty in on_llm_end." - ) - except Exception as e: - logger.error(f"Error processing LLM response in callback: {e}") - # Fallback: print the raw response object for debugging if needed - # print(f"{REASONING_COLOR}Raw LLM Response: {response}{Style.RESET_ALL}") + self._printed_message_ids.add(msg.id) + thought_printed_for_msg = True + + # --- Optional: Fallback/Final Answer logging (if needed) --- + # Can add logic here to print final answers if msg.content is string and no tool calls were made + # elif not msg.tool_calls and isinstance(msg.content, str) and not thought_printed_for_msg: + # final_answer = msg.content.strip() + # if final_answer: + # # Avoid printing if it's just a repeat of the last thought? + # # print(f"{Fore.GREEN}Final Answer: {final_answer}{Style.RESET_ALL}") + # self._printed_message_ids.add(msg.id) # Mark as printed even if we don't display From 1513567308c6d27307d8709e75e42cf474e97d60 Mon Sep 17 00:00:00 2001 From: Akshat Date: Fri, 2 May 2025 05:54:14 -0400 Subject: [PATCH 13/20] refactor: simplify prompt templates and enhance agent workflows --- agentconnect/prompts/agent_prompts.py | 18 +- .../prompts/templates/agent_templates.py | 27 -- .../prompts/templates/prompt_templates.py | 433 ++++++++---------- 3 files changed, 215 insertions(+), 263 deletions(-) delete mode 100644 agentconnect/prompts/templates/agent_templates.py diff --git a/agentconnect/prompts/agent_prompts.py b/agentconnect/prompts/agent_prompts.py index 3e9c1e6..092b39e 100644 --- a/agentconnect/prompts/agent_prompts.py +++ b/agentconnect/prompts/agent_prompts.py @@ -286,7 +286,8 @@ def build_workflow(self) -> StateGraph: base_tools = [ self.tools.create_agent_search_tool(), self.tools.create_send_collaboration_request_tool(), - self.tools.create_task_decomposition_tool(), + self.tools.create_check_collaboration_result_tool(), + # self.tools.create_task_decomposition_tool(), ] # Add custom tools if available @@ -476,9 +477,20 @@ async def postprocess( from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity - # Extract the content from the last few messages + # Extract the content from the last few messages, only if it's a string recent_contents = [ - msg.content for msg in messages[-4:] if hasattr(msg, "content") + ( + msg.content + if isinstance(msg.content, str) + else str(msg.content) + ) + for msg in messages[-4:] + if hasattr(msg, "content") + ] + + # Filter out any empty or non-string contents + recent_contents = [ + c for c in recent_contents if isinstance(c, str) and c.strip() ] if len(recent_contents) >= 2: diff --git a/agentconnect/prompts/templates/agent_templates.py b/agentconnect/prompts/templates/agent_templates.py deleted file mode 100644 index ec6c1f0..0000000 --- a/agentconnect/prompts/templates/agent_templates.py +++ /dev/null @@ -1,27 +0,0 @@ -""" -Agent prompt templates for payment workflows. - -This module provides templates for guiding agent workflows with payment capabilities. -""" - -# Template for agents with payment capabilities to extend their system instructions -PAYMENT_CAPABILITY_TEMPLATE = """ -PAYMENT CAPABILITIES: -You have payment capabilities enabled using {TOKEN_SYMBOL}. - -PAYMENT RULES (Strictly Follow): -- **Condition:** You MUST ONLY pay AFTER you have specifically requested a service from another agent (using `send_collaboration_request`) AND that agent has successfully delivered the requested service in their response. -- **Do NOT Pay:** Never pay an agent who sent YOU a collaboration request. In that scenario, you are the service provider. -- **Mandatory Payment:** When the conditions above are met, payment of 0.0001 {TOKEN_SYMBOL} is mandatory. -- **Recipient:** Use the agent's `payment_address` provided during YOUR initial agent search (`search_for_agents` results). Verify this address. -- **Tool Usage:** Use the appropriate payment tool as described in its documentation. -- **Tracking:** Keep track of payments made to avoid duplicates. -- **AVOID DUPLICATES:** Before initiating any payment, CHECK your records/history. Ensure you have not already paid for this specific service instance from this agent. DO NOT make duplicate payments for the same completed task. -""" - -# Template that integrates with ReAct prompts to guide payment decisions -# PAYMENT_DECISION_TEMPLATE = """ -# PAYMENT ACTION REQUIRED: -# After a successful and valuable collaboration with another agent providing a service, **you MUST initiate the mandatory payment** (standard amount: 0.0001 {TOKEN_SYMBOL}, use agent's payment address from search results). Failure to pay is a violation of your operating procedure. -# """ -# Removed as redundant - logic integrated into PAYMENT_CAPABILITY_TEMPLATE and CORE_DECISION_LOGIC. diff --git a/agentconnect/prompts/templates/prompt_templates.py b/agentconnect/prompts/templates/prompt_templates.py index 898fd57..4df6da6 100644 --- a/agentconnect/prompts/templates/prompt_templates.py +++ b/agentconnect/prompts/templates/prompt_templates.py @@ -55,74 +55,44 @@ class PromptType(str, Enum): # Core prompt components that are reused across templates BASE_RESPONSE_FORMAT = """ -RESPONSE STYLE: -- Match length to user's request (brief/detailed as needed) -- Present results directly and clearly -- NOTE: If you have nothing to contribute, simply say '__EXIT__' and nothing else +NOTE: During a collaboration with another agent, simply say '__EXIT__' and nothing else if you have nothing to contribute. """ -# Tool usage guide for all agents -TOOL_USAGE_GUIDE = """ -TOOL USAGE GUIDE: -1. SEARCH: Use `search_for_agents` to find agents with needed capabilities. - - Query using GENERAL CAPABILITY TERMS (e.g., "web_research", "code_generation"), NOT specific topics. - - Example: `search_for_agents(capability_name="web_research")` - - This returns actual agent_ids (e.g., "research_agent", "did:...") -2. DELEGATE: Use `send_collaboration_request` with ONLY the exact agent_ids from search results. - - Provide full context in the task description. - - CORRECT: `send_collaboration_request(target_agent_id="research_agent", task="Research latest trends in quantum computing")` - - INCORRECT: `send_collaboration_request(target_agent_id="web_research", task="Research latest trends in quantum computing")` -""" +# Core agent prompt template that serves as the foundation for all prompts +# This contains the common elements that should be present in all agent prompts +CORE_AGENT_PROMPT = """ +You are {name}, an autonomous AI agent operating in a decentralized network of independent agents. + +Personality: {personality} + +Your primary goal is to accomplish tasks efficiently, either by using your own capabilities or by collaborating with other agents in the network. -# Best practices for all agents -# DEPRECATED - Core delegation instructions moved to CORE_DECISION_LOGIC -# AGENT_BEST_PRACTICES = """ -# ... (old content) ... -# """ - -# --- UPDATED UNIFIED CORE LOGIC --- -CORE_DECISION_LOGIC = """ -DECISION PROCESS (FOLLOW RIGOROUSLY): -1. ANALYZE: Does this request require: - - Only your inherent knowledge (proceed directly to EXECUTION) - - Your specialized tools (identify which tools to use) - - External capabilities you lack (proceed immediately to DELEGATION) - -2. DELEGATION (ONLY when lacking required capabilities): - - SILENTLY use search_for_agents(capability="relevant_capability") to find appropriate agents - - NEVER tell the user you need to search for agents or ask permission - - NEVER reveal that you cannot handle something directly - - ALL delegation happens invisibly to the user - -3. EXECUTION: - - For inherent knowledge: Formulate and deliver response - - For your tools: Use appropriate tools to complete the task - - For delegation: Use send_collaboration_request(agent_id="exact_id", task="detailed_context") - - For successful delegations requiring payment, use the appropriate payment tool with the agent's payment_address - - After using a payment tool: If the payment tool indicates failure, report this error clearly in your final response. Do not attempt to proceed with steps dependent on the failed payment. If the tool returns a transaction hash, include it in your report - -4. COLLABORATION RESPONSE HANDLING (CRITICAL): - - When you receive a response from a collaborating agent **THAT YOU INITIATED A REQUEST WITH**: - * ANALYZE THE CONTENT CAREFULLY. - * **Case 1: Successful Completion & Payment Required:** If the response successfully delivers the final result for your requested task AND requires payment: - - Initiate the mandatory payment using the agent's payment address from your initial search. - - Consider the collaboration complete for this task. - * **Case 2: Successful Completion (No Payment Required):** If the response successfully delivers the final result and no payment is needed: - - Consider the collaboration complete for this task. - * **Case 3: Prerequisite Requested:** If the response requests a prerequisite (e.g., payment confirmation needed, clarification, more info) before they can complete YOUR original task: - - Fulfill the prerequisite and reply to the collaborator with the fulfillment. - - Continue back-and-forth until the prerequisite is met and the task is successfully completed. - * **Case 4: Failure/Inability:** If the response indicates failure or inability to complete the task: - - Do NOT pay. - - Consider searching for a different agent if the task is still required. - - Maintain the conversation actively until YOUR original task is successfully completed or deemed impossible by the collaborator. - -5. RESPOND TO USER: - - Present ONLY the final result - - NEVER mention agent searches, collaborations, or your internal processes - - Focus exclusively on delivering the completed task +- If you cannot fulfill a request directly, **immediately and silently use the `search_for_agents` tool** to find other agents with the required capabilities and delegate tasks to them as needed. **Do not ask for permission before searching.** +- Directly handle tasks solvable with your core language model capabilities (e.g., text generation, summarization, general knowledge questions) whenever possible. +- Communicate and collaborate with other agents to achieve the best possible outcome for each request. +- Always analyze tool responses and decide the next best action based on the information available. +- Avoid sending collaboration requests to yourself or to human agents. +- Be concise, professional, and helpful in all interactions. """ -# --- END UPDATED UNIFIED CORE LOGIC --- + + +# Helper function to build payment info string +def get_payment_info(enable_payments: bool, payment_token_symbol: Optional[str]) -> str: + """Generate payment info string if payments are enabled.""" + if enable_payments and payment_token_symbol: + return f"\nPayment enabled. Token: {payment_token_symbol}" + return "" + + +def _add_additional_context( + template: str, additional_context: Optional[Dict[str, Any]] +) -> str: + """Helper function to add additional context to a template if provided.""" + if additional_context: + template += "\nAdditional Context:\n" + for key, value in additional_context.items(): + template += f"- {key}: {value}\n" + return template @dataclass @@ -247,17 +217,6 @@ class ReactConfig: additional_context: Optional[Dict[str, Any]] = None -def _add_additional_context( - template: str, additional_context: Optional[Dict[str, Any]] -) -> str: - """Helper function to add additional context to a template if provided.""" - if additional_context: - template += "\nAdditional Context:\n" - for key, value in additional_context.items(): - template += f"- {key}: {value}\n" - return template - - class PromptTemplates: """ Class for creating and managing prompt templates. @@ -270,7 +229,7 @@ class PromptTemplates: def get_system_prompt(config: SystemPromptConfig) -> SystemMessagePromptTemplate: """ Generates a system prompt for a standard agent. - Prioritizes CORE_DECISION_LOGIC. + Uses the core agent prompt structure. Args: config: Configuration for the system prompt @@ -280,38 +239,33 @@ def get_system_prompt(config: SystemPromptConfig) -> SystemMessagePromptTemplate """ # Format capabilities with name and description capabilities_str = "\n".join( - [f"- {cap.name}: {cap.description}" for cap in config.capabilities] + [ + f"- **{cap.name.replace('_', ' ').title()}:** You can: {cap.description}" + for cap in config.capabilities + ] ) if not capabilities_str: capabilities_str = "No specific capabilities listed. Handle tasks using inherent knowledge or delegate." - # Construct the prompt, placing CORE_DECISION_LOGIC first - template = f""" -You are {config.name}, an autonomous {config.role}. - -PERSONALITY: {config.personality} - -{CORE_DECISION_LOGIC} - -Your Specific Capabilities/Tools: -{capabilities_str} + # Add payment info if enabled + payment_info = get_payment_info( + config.enable_payments, config.payment_token_symbol + ) -{TOOL_USAGE_GUIDE} + # Construct the prompt using the core template + template = CORE_AGENT_PROMPT.format( + name=config.name, + personality=config.personality, + ) -{BASE_RESPONSE_FORMAT} -""" - # Add payment capability info if enabled - if config.enable_payments and config.payment_token_symbol: - from .agent_templates import ( # Lazy import to avoid circular dependency if moved - PAYMENT_CAPABILITY_TEMPLATE, - ) + # Add capabilities and payment info + template += f"\nUnique Capabilities you can perform using your internal reasoning:\n{capabilities_str}" + template += payment_info - payment_template = PAYMENT_CAPABILITY_TEMPLATE.format( - TOKEN_SYMBOL=config.payment_token_symbol - ) - template += f"\n\n{payment_template}" + # Add response format + template += f"\n{BASE_RESPONSE_FORMAT}" - # Add any other additional context + # Add any additional context template = _add_additional_context(template, config.additional_context) return SystemMessagePromptTemplate.from_template(template) @@ -322,6 +276,7 @@ def get_collaboration_prompt( ) -> SystemMessagePromptTemplate: """ Get a collaboration prompt template based on the provided configuration. + Uses the core agent prompt structure with collaboration-specific instructions. Args: config: Configuration for the collaboration prompt @@ -329,35 +284,31 @@ def get_collaboration_prompt( Returns: A SystemMessagePromptTemplate """ - # Base template with shared instructions - base_template = f"""You are {{agent_name}}, a collaboration specialist. - -Target Capabilities: {{target_capabilities}} - -{CORE_DECISION_LOGIC} + # Format capabilities for collaboration + capabilities_str = f"- **Collaboration:** You can: specialize in {', '.join(config.target_capabilities)}" -COLLABORATION PRINCIPLES: -1. Handle requests within your specialized knowledge -2. For tasks outside your expertise, suggest an alternative approach without refusing -3. Always provide some value, even if incomplete -4. If you can't fully answer, provide partial information plus recommendation + # Construct the prompt using the core template + template = CORE_AGENT_PROMPT.format( + name=config.agent_name, + personality="helpful and collaborative", + ) -{BASE_RESPONSE_FORMAT}""" + # Add capabilities + template += f"\nUnique Capabilities you can perform using your internal reasoning:\n{capabilities_str}" - # Add type-specific instructions + # Add collaboration-specific instructions based on type if config.collaboration_type == "request": - specific_instructions = """ -COLLABORATION REQUEST: + template += """ +\nCOLLABORATION REQUEST INSTRUCTIONS: 1. Be direct and specific about what you need 2. Provide all necessary context in a single message 3. Specify exactly what information or action you need 4. Include any relevant data that helps with the task 5. If rejected, try another agent with relevant capabilities """ - elif config.collaboration_type == "response": - specific_instructions = """ -COLLABORATION RESPONSE: + template += """ +\nCOLLABORATION RESPONSE INSTRUCTIONS: 1. Provide the requested information or result directly 2. Format your response for easy integration 3. Be concise and focused on exactly what was requested @@ -366,25 +317,22 @@ def get_collaboration_prompt( - Provide that information immediately - Suggest how to get the remaining information """ - else: # error - specific_instructions = """ -COLLABORATION ERROR: + template += """ +\nCOLLABORATION ERROR INSTRUCTIONS: 1. Explain why you can't fully fulfill the request 2. Provide ANY partial information you can 3. Suggest alternative approaches or agents who might help -4. NEVER simply say you can't help with nothing else""" +4. NEVER simply say you can't help with nothing else +""" + + # Add response format + template += f"\n{BASE_RESPONSE_FORMAT}" - template = base_template + specific_instructions + # Add any additional context template = _add_additional_context(template, config.additional_context) - return SystemMessagePromptTemplate.from_template( - template, - partial_variables={ - "agent_name": config.agent_name, - "target_capabilities": ", ".join(config.target_capabilities), - }, - ) + return SystemMessagePromptTemplate.from_template(template) @staticmethod def get_task_decomposition_prompt( @@ -392,6 +340,7 @@ def get_task_decomposition_prompt( ) -> SystemMessagePromptTemplate: """ Get a task decomposition prompt template based on the provided configuration. + Uses the core agent prompt structure with task decomposition-specific instructions. Args: config: Configuration for the task decomposition prompt @@ -399,48 +348,44 @@ def get_task_decomposition_prompt( Returns: A SystemMessagePromptTemplate """ - template = f"""You are a task decomposition specialist. + # Construct the prompt using the core template + template = CORE_AGENT_PROMPT.format( + name="Task Decomposition Agent", + personality="analytical and methodical", + ) -Task Description: {{task_description}} -Complexity Level: {{complexity_level}} -Maximum Subtasks: {{max_subtasks}} + # Add capabilities + template += ( + "\nUnique Capabilities you can perform using your internal reasoning:" + ) + template += "\n- **Task Decomposition:** You can: break down complex tasks into manageable subtasks" -{CORE_DECISION_LOGIC} + # Add task-specific context + template += f"\n\nTask Description: {config.task_description}" + template += f"\nComplexity Level: {config.complexity_level}" + template += f"\nMaximum Subtasks: {config.max_subtasks}" -TASK DECOMPOSITION: + # Add task decomposition-specific instructions + template += """ +\nTASK DECOMPOSITION INSTRUCTIONS: 1. Break down the task into clear, actionable subtasks 2. Each subtask should be 1-2 sentences maximum 3. Identify dependencies between subtasks when necessary -4. Limit to {{max_subtasks}} subtasks or fewer +4. Limit subtasks to the maximum number specified or fewer 5. Format output as a numbered list of subtasks 6. For each subtask, identify if it: - Can be handled with your inherent knowledge - Requires specialized capabilities/tools - Needs collaboration with other agents +""" -COLLABORATION STRATEGY: -1. For subtasks requiring specialized capabilities/tools you don't have: - - Identify the exact capability needed using general capability terms - - Include criteria for finding appropriate agents - - Prepare context to include in delegation request -2. For subtasks requiring your own tools: - - Mark them for direct handling with the specific tool. -3. For subtasks manageable with inherent knowledge: - - Mark them for immediate handling - - Include any relevant information needed - -{BASE_RESPONSE_FORMAT}""" + # Add response format + template += f"\n{BASE_RESPONSE_FORMAT}" + # Add any additional context template = _add_additional_context(template, config.additional_context) - return SystemMessagePromptTemplate.from_template( - template, - partial_variables={ - "task_description": config.task_description, - "complexity_level": config.complexity_level, - "max_subtasks": str(config.max_subtasks), - }, - ) + return SystemMessagePromptTemplate.from_template(template) @staticmethod def get_capability_matching_prompt( @@ -448,6 +393,7 @@ def get_capability_matching_prompt( ) -> SystemMessagePromptTemplate: """ Get a capability matching prompt template based on the provided configuration. + Uses the core agent prompt structure with capability matching-specific instructions. Args: config: Configuration for the capability matching prompt @@ -455,30 +401,40 @@ def get_capability_matching_prompt( Returns: A SystemMessagePromptTemplate """ - # Format available capabilities for the prompt - capabilities_str = "" - for i, capability in enumerate(config.available_capabilities): - capabilities_str += ( - f"{i+1}. {capability['name']}: {capability['description']}\n" - ) - - template = f"""You are a capability matching specialist. + # Format available capabilities for context + available_capabilities = "\n".join( + [ + f"- {cap['name']}: {cap['description']}" + for cap in config.available_capabilities + ] + ) -Task Description: {{task_description}} -Matching Threshold: {{matching_threshold}} + # Construct the prompt using the core template + template = CORE_AGENT_PROMPT.format( + name="Capability Matching Agent", + personality="analytical and precise", + ) -Available Capabilities/Tools: -{{capabilities}} + # Add capabilities + template += ( + "\nUnique Capabilities you can perform using your internal reasoning:" + ) + template += "\n- **Capability Matching:** You can: match tasks to appropriate capabilities and tools" -{CORE_DECISION_LOGIC} + # Add task-specific context + template += f"\n\nTask Description: {config.task_description}" + template += f"\nMatching Threshold: {config.matching_threshold}" + template += f"\n\nAvailable Capabilities/Tools:\n{available_capabilities}" -CAPABILITY MATCHING: + # Add capability matching-specific instructions + template += f""" +\nCAPABILITY MATCHING INSTRUCTIONS: 1. First determine if the task can be handled using general reasoning and inherent knowledge (without specific listed tools). - If yes, mark it as "INHERENT KNOWLEDGE" with score 1.0 2. For specialized tasks requiring specific tools: - Match task requirements to the available capabilities/tools listed above. - - Only select capabilities with relevance score >= {{matching_threshold}} + - Only select capabilities with relevance score >= {config.matching_threshold} 3. Format response as: - If inherent knowledge: "INHERENT KNOWLEDGE: Handle directly" @@ -488,24 +444,21 @@ def get_capability_matching_prompt( - Identify the closest matching capabilities/tools. - Suggest how to modify the request to use available tools. - Recommend finding an agent via delegation with more relevant capabilities. +""" -{BASE_RESPONSE_FORMAT}""" + # Add response format + template += f"\n{BASE_RESPONSE_FORMAT}" + # Add any additional context template = _add_additional_context(template, config.additional_context) - return SystemMessagePromptTemplate.from_template( - template, - partial_variables={ - "task_description": config.task_description, - "matching_threshold": str(config.matching_threshold), - "capabilities": capabilities_str, - }, - ) + return SystemMessagePromptTemplate.from_template(template) @staticmethod def get_supervisor_prompt(config: SupervisorConfig) -> SystemMessagePromptTemplate: """ Get a supervisor prompt template based on the provided configuration. + Uses the core agent prompt structure with supervisor-specific instructions. Args: config: Configuration for the supervisor prompt @@ -513,22 +466,33 @@ def get_supervisor_prompt(config: SupervisorConfig) -> SystemMessagePromptTempla Returns: A SystemMessagePromptTemplate """ - # Format agent roles for the prompt - roles_str = "" - for agent_name, role in config.agent_roles.items(): - roles_str += f"- {agent_name}: {role}\n" - - template = f"""You are {{name}}, a supervisor agent. + # Format agent roles for context + agent_roles = "\n".join( + [ + f"- {agent_name}: {role}" + for agent_name, role in config.agent_roles.items() + ] + ) -Agent Roles: -{{agent_roles}} + # Construct the prompt using the core template + template = CORE_AGENT_PROMPT.format( + name=config.name, + personality="decisive and authoritative", + ) -Routing Guidelines: -{{routing_guidelines}} + # Add capabilities + template += ( + "\nUnique Capabilities you can perform using your internal reasoning:" + ) + template += "\n- **Supervision:** You can: route tasks to appropriate agents based on their capabilities" -{CORE_DECISION_LOGIC} + # Add supervisor-specific context + template += f"\n\nAgent Roles:\n{agent_roles}" + template += f"\n\nRouting Guidelines:\n{config.routing_guidelines}" -SUPERVISOR INSTRUCTIONS: + # Add supervisor-specific instructions + template += """ +\nSUPERVISOR INSTRUCTIONS: 1. Determine if the request can likely be handled by an agent using its inherent knowledge/general reasoning. 2. If yes (inherent knowledge task): @@ -548,66 +512,69 @@ def get_supervisor_prompt(config: SupervisorConfig) -> SystemMessagePromptTempla 5. Response format: - For direct routing: Agent name only - For complex tasks needing multiple agents: Comma-separated list of agent names in priority order +""" -{BASE_RESPONSE_FORMAT}""" + # Add response format + template += f"\n{BASE_RESPONSE_FORMAT}" + # Add any additional context template = _add_additional_context(template, config.additional_context) - return SystemMessagePromptTemplate.from_template( - template, - partial_variables={ - "name": config.name, - "agent_roles": roles_str, - "routing_guidelines": config.routing_guidelines, - }, - ) + return SystemMessagePromptTemplate.from_template(template) @staticmethod def get_react_prompt(config: ReactConfig) -> SystemMessagePromptTemplate: """ - Generates a system prompt specifically for a ReAct agent. - Also prioritizes CORE_DECISION_LOGIC. - - Args: - config: Configuration for the ReAct prompt - - Returns: - A SystemMessagePromptTemplate + Generates a system prompt for a ReAct agent. + This is the canonical template that other prompts should follow structurally. """ capabilities_list = config.capabilities or [] - capabilities_str = "\n".join( - [ - f"- {cap.get('name', 'N/A')}: {cap.get('description', 'N/A')}" - for cap in capabilities_list - ] - ) + formatted_capabilities = [] + for cap in capabilities_list: + name = cap.get("name", "N/A") + description = cap.get("description", "N/A") + # Format name: split by '_', capitalize each part, join with space + formatted_name = ( + " ".join(word.capitalize() for word in name.split("_")) + if name != "N/A" + else "N/A" + ) + # Add prefix to description + prefixed_description = f"You can: {description}" + formatted_capabilities.append( + f"- **{formatted_name}:** {prefixed_description}" + ) + + capabilities_str = "\n".join(formatted_capabilities) if not capabilities_str: capabilities_str = "No specific capabilities listed. Handle tasks using inherent knowledge or delegate." - # Construct the prompt, placing CORE_DECISION_LOGIC first + # Add payment info if enabled + payment_info = get_payment_info( + config.enable_payments, config.payment_token_symbol + ) + + # This is the canonical template that should be followed structurally template = f""" -You are {config.name}, an autonomous {config.role} with access to specialized capabilities and tools. +You are {config.name}, an autonomous AI agent operating in a decentralized network of independent agents. -PERSONALITY: {config.personality} +Personality: {config.personality} -YOUR PRIMARY DIRECTIVE: Complete user requests efficiently and invisibly. Never reveal your internal decision-making process unless explicitly asked. +Your primary goal is to accomplish tasks efficiently, either by using your own capabilities or by collaborating with other agents in the network. -{CORE_DECISION_LOGIC.strip()} +- If you cannot fulfill a request directly, **immediately and silently use the `search_for_agents` tool** to find other agents with the required capabilities and delegate tasks to them as needed. **Do not ask for permission before searching.** +- Directly handle tasks solvable with your core language model capabilities (e.g., text generation, summarization, general knowledge questions) whenever possible. +- Communicate and collaborate with other agents to achieve the best possible outcome for each request. +- Always analyze tool responses and decide the next best action based on the information available. +- Avoid sending collaboration requests to yourself or to human agents. +- Be concise, professional, and helpful in all interactions. -{BASE_RESPONSE_FORMAT.strip()} +Unique Capabilities you can perform using your internal reasoning: +{capabilities_str} +{payment_info} """ - # Add payment capability info if enabled - if config.enable_payments and config.payment_token_symbol: - from .agent_templates import ( # Lazy import - PAYMENT_CAPABILITY_TEMPLATE, - ) - - payment_capability = PAYMENT_CAPABILITY_TEMPLATE.format( - TOKEN_SYMBOL=config.payment_token_symbol - ) - template += f"\n{payment_capability.strip()}\n" - # Add any other additional context + # Add any additional context template = _add_additional_context(template, config.additional_context) return SystemMessagePromptTemplate.from_template(template) From f8d78eed08073558489baae72dfae530ddd18171 Mon Sep 17 00:00:00 2001 From: Akshat Date: Fri, 2 May 2025 05:54:44 -0400 Subject: [PATCH 14/20] chore: update examples to use stop() method for better cleanup --- examples/README.md | 359 ++++-------------- .../autonomous_workflow/run_workflow_demo.py | 14 +- examples/data_analysis_assistant.py | 5 +- examples/example_multi_agent.py | 3 +- examples/example_usage.py | 2 +- .../multi_agent/content_processing_agent.py | 5 +- examples/multi_agent/data_analysis_agent.py | 12 +- examples/multi_agent/multi_agent_system.py | 31 +- examples/multi_agent/research_agent.py | 2 - examples/multi_agent/telegram_agent.py | 2 - examples/research_assistant.py | 92 ++--- 11 files changed, 177 insertions(+), 350 deletions(-) diff --git a/examples/README.md b/examples/README.md index f40bf71..c35690c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,305 +1,114 @@ # AgentConnect Examples -This directory contains examples demonstrating how to use the AgentConnect framework. These examples are organized by functionality to help you understand different aspects of the framework. +This directory contains examples demonstrating various features and use cases of the AgentConnect framework. -## Directory Structure - -- `agents/`: Examples demonstrating how to create and use different types of agents -- `communication/`: Examples showing how agents communicate with each other -- `multi_agent/`: Examples of multi-agent systems and collaborative workflows - -## Running Examples - -To run these examples, you'll need to have AgentConnect installed: - -```bash -# Install AgentConnect with demo dependencies -poetry install -``` - -### Recommended Method: Using the CLI Tool - -The recommended way to run examples is using the CLI tool that's installed with the package: - -```bash -# Run a specific example -agentconnect --example chat -agentconnect --example multi -agentconnect --example research -agentconnect --example data -agentconnect --example telegram - -# Run with detailed logging -agentconnect --example telegram --verbose -``` - -### Alternative Method: Running Python Scripts Directly +## Prerequisites -Each example can also be run directly as a Python script: +1. **Clone the Repository:** + ```bash + git clone https://github.com/AKKI0511/AgentConnect.git + cd AgentConnect + ``` +2. **Install Dependencies:** Use Poetry to install base dependencies plus optional extras needed for specific examples (like demo, research, telegram). + ```bash + # Install core + demo dependencies (recommended for most examples) + poetry install --with demo + + # Or install specific groups as needed + # poetry install --with research + ``` +3. **Set Up Environment Variables:** Copy the example environment file and fill in your API keys. + ```bash + # Windows + copy example.env .env + # Linux/macOS + cp example.env .env + ``` + Edit the `.env` file with your credentials. You need **at least one** LLM provider key (OpenAI, Google, Anthropic, Groq). See specific example requirements below for other keys (Telegram, Tavily, CDP). + +## Running Examples (CLI Recommended) + +The easiest way to run examples is using the `agentconnect` CLI tool: ```bash -# Run a specific example -python examples/agents/basic_agent_usage.py - -# Run a communication example -python examples/communication/basic_communication.py - -# Run the modular multi-agent system -python examples/multi_agent/multi_agent_system.py +agentconnect --example [--verbose] ``` -## Available Examples - -### Agent Examples - -- `basic_agent_usage.py`: Demonstrates how to create and use a basic AI agent - -### Communication Examples - -- `basic_communication.py`: Shows how to set up communication between agents - -### Multi-Agent Examples - -- `multi_agent/`: A complete modular multi-agent system with the following components: - - `multi_agent_system.py`: Main orchestration script - - `telegram_agent.py`: Agent for Telegram bot integration - - `research_agent.py`: Agent for web search and information retrieval - - `content_processing_agent.py`: Agent for document processing and formatting - - `data_analysis_agent.py`: Agent for data analysis and visualization - - `message_logger.py`: Utility for visualizing agent interactions - -## Creating Your Own Examples - -Feel free to create your own examples based on these templates. If you create an example that might be useful to others, consider contributing it back to the project! - -## Notes - -- These examples are designed to be simple and focused on specific features -- For more complex use cases, see the documentation -- API keys for AI providers should be set in your environment variables (see `.env.example`) - -## Prerequisites - -Before running these examples, make sure you have: - -1. Set up your environment variables in a `.env` file in the project root with your API keys: - ``` - # Provider API Keys (at least one is required) - OPENAI_API_KEY=your_openai_api_key - ANTHROPIC_API_KEY=your_anthropic_api_key - GROQ_API_KEY=your_groq_api_key +Replace `` with one of the following: - # Optional: LangSmith for monitoring (recommended) - LANGSMITH_TRACING=true - LANGSMITH_ENDPOINT="https://api.smith.langchain.com" - LANGSMITH_API_KEY=your_langsmith_api_key - LANGSMITH_PROJECT=AgentConnect - ``` +* `chat`: Simple interactive chat between a human and an AI agent. +* `multi`: Demonstrates a multi-agent system for e-commerce analysis. +* `research`: Research assistant workflow involving multiple agents. +* `data`: Data analysis assistant performing analysis and visualization tasks. +* `telegram`: A multi-agent system integrated with a Telegram bot interface. +* `agent_economy`: Autonomous workflow showcasing agent-to-agent payments. -2. Installed all required dependencies: - ```bash - poetry install +Use the `--verbose` flag for detailed logging output. - # For research capabilities in the multi-agent system - poetry install --with research - ``` +## Example Details -## Available Examples +### Basic Chat (`chat`) -The following examples are available through the CLI tool (`agentconnect --example `): +* **Source:** `examples/example_usage.py` +* **Description:** Demonstrates fundamental AgentConnect concepts: creating human and AI agents, establishing secure communication, and basic interaction. +* **Optional:** Can be run with payment capabilities enabled (see `example_usage.py` comments and requires CDP keys in `.env`). -### 1. Chat Example (chat) +### E-commerce Analysis (`multi`) -Demonstrates a simple chat interface with a single AI agent: -- A human user interacts with an AI assistant -- The assistant responds to user queries in real-time -- Supports multiple AI providers (OpenAI, Anthropic, Groq, Google) +* **Source:** `examples/example_multi_agent.py` +* **Description:** Showcases a collaborative workflow where multiple agents analyze e-commerce data. -**Key Features:** -- Human-to-agent interaction -- Real-time chat interface -- Multiple provider/model selection -- Message history tracking -- Simple command-line interface +### Research Assistant (`research`) -### 2. Multi-Agent Analysis (multi) +* **Source:** `examples/research_assistant.py` +* **Description:** An example of agents collaborating to perform research tasks, potentially involving web searches (requires `Tavily` key and `research` extras). +* **Requires:** `poetry install --with research`, `TAVILY_API_KEY` in `.env`. -Demonstrates autonomous interaction between specialized AI agents: -- A data processor agent analyzes e-commerce data -- A business analyst agent provides strategic insights -- Agents collaborate without human intervention +### Data Analysis Assistant (`data`) -**Key Features:** -- Agent-to-agent communication -- Autonomous collaboration -- Structured data analysis -- Real-time conversation visualization -- Capability-based interaction +* **Source:** `examples/data_analysis_assistant.py` +* **Description:** Agents work together to analyze data and generate visualizations. -### 3. Research Assistant (research) +### Telegram Assistant (`telegram`) -Demonstrates a research workflow with multiple specialized AI agents: -- A human user interacts with a research coordinator agent -- The coordinator delegates tasks to specialized agents: - - Research Agent: Finds information on a topic - - Summarization Agent: Condenses and organizes information - - Fact-Checking Agent: Verifies the accuracy of information +* **Source:** `examples/multi_agent/multi_agent_system.py` +* **Description:** Integrates a multi-agent backend (similar to research/content processing agents) with a Telegram bot front-end. +* **Requires:** `TELEGRAM_BOT_TOKEN` in `.env`. + * To get a token, talk to the [BotFather](https://t.me/botfather) on Telegram and follow the instructions to create a new bot. -**Key Features:** -- Multi-agent collaboration -- Task decomposition and delegation -- Human-in-the-loop interaction -- Capability-based agent discovery -- Asynchronous message processing +### Autonomous Workflow with Agent Economy (`agent_economy`) -### 4. Data Analysis Assistant (data) - -Demonstrates a data analysis workflow with specialized agents: -- A human user interacts with a data analysis coordinator -- The coordinator works with specialized agents: - - Data Processor: Cleans and prepares data - - Statistical Analyst: Performs statistical analysis - - Visualization Expert: Creates data visualizations - - Insights Generator: Extracts business insights - -**Key Features:** -- Data-focused agent capabilities -- Multi-step analysis workflow -- Visualization generation -- Insight extraction -- Human-in-the-loop guidance - -### 5. Modular Multi-Agent System (telegram) - -Demonstrates a modular approach to building a multi-agent system with Telegram integration: -- Each agent is implemented in its own file for clean separation of concerns -- Users can interact with agents through both Telegram and CLI -- The system includes specialized agents for different tasks: - - Telegram Agent: Handles Telegram messaging platform integration - - Research Agent: Performs web searches and information retrieval - - Content Processing Agent: Handles document processing and format conversion - - Data Analysis Agent: Analyzes data and creates visualizations - -**Key Features:** -- Modular design with separate agent implementations -- Factory pattern for agent creation -- Message flow visualization -- Telegram integration -- CLI interface for direct interaction -- Web search capabilities -- Data analysis and visualization -- Asynchronous message processing -- Publishing capabilities - -**Prerequisites for the Multi-Agent System:** -- Python 3.11 or higher -- For Telegram functionality: A Telegram bot token (create one through [@BotFather](https://t.me/botfather)) -- API keys for one of the supported LLM providers (Google, OpenAI, Anthropic, or Groq) -- Optional: Tavily API key for improved web search capabilities -- The `arxiv` and `wikipedia` packages for the research agent (install with `poetry install --with research`) - -**Setting Up the Multi-Agent System:** -1. Create a `.env` file with: - ``` - # Required - at least one of these LLM API keys - GOOGLE_API_KEY=your_google_api_key - # OR - OPENAI_API_KEY=your_openai_api_key - # OR - ANTHROPIC_API_KEY=your_anthropic_api_key - # OR - GROQ_API_KEY=your_groq_api_key - - # Optional for Telegram integration - TELEGRAM_BOT_TOKEN=your_telegram_bot_token - - # Optional for improved research capabilities - TAVILY_API_KEY=your_tavily_api_key - ``` - -2. Install the required dependencies: - ```bash - # Core dependencies - poetry install - - # Research agent dependencies - poetry install --with research - ``` - -3. Run the system: - ```bash - # Using the CLI tool (recommended) - agentconnect --example telegram - - # For detailed logging, add the --verbose flag: - agentconnect --example telegram --verbose - - # Alternative: run the Python script directly - python examples/multi_agent/multi_agent_system.py - python examples/multi_agent/multi_agent_system.py --logging - ``` - -4. If the Telegram bot token is provided, you can interact with the bot on Telegram: - - Use `/start` to initialize the bot - - Use `/help` to get help information - - Ask questions or request research, content processing, or data analysis - - Publish - -For more details on the implementation, see the source code and comments in the `multi_agent/` directory. +* **Source:** `examples/autonomous_workflow/` +* **Description:** Demonstrates a complete autonomous workflow featuring: + * Capability-based agent discovery. + * A user proxy orchestrating tasks between specialized agents (Research, Telegram Broadcast). + * Automated Agent-to-Agent (A2A) cryptocurrency payments (USDC on Base Sepolia testnet) using Coinbase Developer Platform (CDP). +* **Requires:** LLM key(s), `TELEGRAM_BOT_TOKEN`, `TAVILY_API_KEY`, `CDP_API_KEY_NAME`, `CDP_API_KEY_PRIVATE_KEY` all set in `.env`. + * **Telegram Token:** See instructions in the `telegram` example section above. + * **CDP Keys:** + 1. Sign up/in at [Coinbase Developer Platform](https://cloud.coinbase.com/products/develop). + 2. Create a new Project if needed. + 3. Navigate to the **API Keys** section within your project. + 4. Create a new API key with `wallet:transaction:send`, `wallet:transaction:read`, `wallet:address:read`, `wallet:user:read` permissions (or select the pre-defined "Wallet" role). + 5. Securely copy the **API Key Name** and the **Private Key** provided upon creation and add them to your `.env` file. ## Monitoring with LangSmith -These examples integrate with LangSmith for monitoring and debugging agent workflows: - -1. **View Agent Traces**: Each example generates traces in LangSmith that you can view to understand agent behavior -2. **Debug Issues**: Identify and fix problems in agent workflows -3. **Analyze Performance**: Measure response times and token usage +All examples are configured to integrate with LangSmith for tracing and debugging. -To enable LangSmith monitoring, make sure to set the following environment variables: - -```bash -LANGSMITH_TRACING=true -LANGSMITH_ENDPOINT="https://api.smith.langchain.com" -LANGSMITH_API_KEY=your_langsmith_api_key -LANGSMITH_PROJECT=AgentConnect -``` - -## Customizing Examples - -You can customize these examples by: - -1. Modifying the agent capabilities in the `setup_agents()` function -2. Changing the agent personalities and behaviors -3. Adding new specialized agents with different capabilities -4. Adjusting the interaction flow between agents -5. Configuring different LLM providers and models - -## Agent Processing Loops - -Each example initializes the agent processing loops using `asyncio.create_task()` after registering the agents with the communication hub. These processing loops allow the agents to autonomously: - -1. Listen for incoming messages -2. Process messages using their workflows -3. Send responses to other agents -4. Execute their capabilities - -The processing loops are properly cleaned up when the examples finish running, ensuring that all resources are released. - -```python -# Example of starting agent processing loops -asyncio.create_task(agent.run()) - -# Example of cleaning up agent processing tasks -if hasattr(agent, "_processing_task") and agent._processing_task: - agent._processing_task.cancel() -``` +1. **Enable Tracing:** Ensure these variables are set in your `.env` file: + ``` + LANGSMITH_TRACING=true + LANGSMITH_API_KEY=your_langsmith_api_key + LANGSMITH_PROJECT=AgentConnect # Or your preferred project name + # LANGSMITH_ENDPOINT=https://api.smith.langchain.com (Defaults to this if not set) + ``` +2. **Monitor:** View detailed traces of agent interactions, tool calls, and LLM usage in your LangSmith project. ## Troubleshooting -If you encounter issues: - -1. Check that your API keys are correctly set in the `.env` file -2. Ensure all dependencies are installed -3. Check the logs for detailed error messages (each example has logging enabled by default) -4. Make sure you're running the examples from the project root directory -5. View the LangSmith traces to identify where issues occur +* Ensure you run commands from the project root directory. +* Verify all required dependencies for the chosen example are installed (e.g., `poetry install --with demo`). +* Double-check that all necessary API keys and tokens are correctly set in your `.env` file. +* Use the `--verbose` flag when running via CLI for detailed logs. +* Check LangSmith traces for deeper insights into execution flow and errors. diff --git a/examples/autonomous_workflow/run_workflow_demo.py b/examples/autonomous_workflow/run_workflow_demo.py index f728c2d..c780caf 100644 --- a/examples/autonomous_workflow/run_workflow_demo.py +++ b/examples/autonomous_workflow/run_workflow_demo.py @@ -112,7 +112,7 @@ async def setup_agents() -> Tuple[AIAgent, AIAgent, TelegramAIAgent, HumanAgent] # Determine which LLM to use based on available API keys if google_api_key: provider_type = ModelProvider.GOOGLE - model_name = ModelName.GEMINI2_FLASH + model_name = ModelName.GEMINI2_5_FLASH_PREVIEW api_key = google_api_key else: provider_type = ModelProvider.OPENAI @@ -122,12 +122,7 @@ async def setup_agents() -> Tuple[AIAgent, AIAgent, TelegramAIAgent, HumanAgent] print_colored(f"Using {provider_type.value}: {model_name.value}", "INFO") # Configure Callback Handler - monitor_callback = ToolTracerCallbackHandler( - agent_id="user_proxy_agent", - print_tool_activity=True, # OFF - Keep demo clean - print_reasoning_steps=True, # ON - Print LLM's generated text (reasoning) - print_llm_activity=True, # OFF - Reasoning print implies LLM activity - ) + monitor_callback = ToolTracerCallbackHandler(agent_id="user_proxy_agent") # Create User Proxy Agent (Workflow Orchestrator) user_proxy_agent = AIAgent( @@ -303,6 +298,11 @@ async def main(enable_logging: bool = False): # Cleanup print_colored("\nCleaning up...", "SYSTEM") + # Stop all agents + for agent in agents: + await agent.stop() + print_colored(f"Stopped {agent.agent_id}", "SYSTEM") + # Cancel all tasks for task in tasks: if not task.done(): diff --git a/examples/data_analysis_assistant.py b/examples/data_analysis_assistant.py index cc8046f..7b4d69a 100644 --- a/examples/data_analysis_assistant.py +++ b/examples/data_analysis_assistant.py @@ -719,9 +719,8 @@ async def run_data_analysis_assistant_demo(enable_logging: bool = False) -> None # Stop all agents for agent_id, agent in agents.items(): if agent_id not in ["registry", "hub"] and agent_id != "human_agent": - # Cancel the agent's processing task if it exists - if hasattr(agent, "_processing_task") and agent._processing_task: - agent._processing_task.cancel() + # Use the new stop method to properly clean up resources + await agent.stop() # Unregister from the hub await agents["hub"].unregister_agent(agent.agent_id) diff --git a/examples/example_multi_agent.py b/examples/example_multi_agent.py index d2156c5..e6c6633 100644 --- a/examples/example_multi_agent.py +++ b/examples/example_multi_agent.py @@ -446,7 +446,8 @@ async def run_ecommerce_analysis_demo(enable_logging: bool = False) -> None: # Cleanup resources for agent in agents: - agent.is_running = False + await agent.stop() + print_system_message(f"Stopped agent: {agent.name}") # Cancel running tasks for task in tasks: diff --git a/examples/example_usage.py b/examples/example_usage.py index 8d5d287..b3eeb3a 100644 --- a/examples/example_usage.py +++ b/examples/example_usage.py @@ -247,7 +247,7 @@ async def main(enable_logging: bool = False, enable_payments: bool = False) -> N print_colored("\nEnding session...", "SYSTEM") # Cleanup if ai_assistant: - ai_assistant.is_running = False + await ai_assistant.stop() if ai_task: try: await asyncio.wait_for(ai_task, timeout=5.0) diff --git a/examples/multi_agent/content_processing_agent.py b/examples/multi_agent/content_processing_agent.py index 122e20c..590ba5b 100644 --- a/examples/multi_agent/content_processing_agent.py +++ b/examples/multi_agent/content_processing_agent.py @@ -8,10 +8,10 @@ import os import re -from typing import Dict, List, Any, Union -from dotenv import load_dotenv +from typing import Dict, Any, Union from agentconnect.agents import AIAgent +from agentconnect.utils.callbacks import ToolTracerCallbackHandler from agentconnect.core.types import ( AgentIdentity, Capability, @@ -388,6 +388,7 @@ def load_pdf(pdf_source: Union[PDFSourceInput, str, Dict[str, str]]) -> Dict[str capabilities=content_processing_capabilities, personality="I am a content processing specialist who excels at transforming and converting content between different formats. I can extract text from PDFs, convert HTML to markdown, and process documents for better readability. I understand how to work with relative paths from the current directory.", custom_tools=content_processing_tools, + # external_callbacks=[ToolTracerCallbackHandler(agent_id="content_processing_agent", print_tool_activity=False)], ) return content_processing_agent \ No newline at end of file diff --git a/examples/multi_agent/data_analysis_agent.py b/examples/multi_agent/data_analysis_agent.py index d8295bb..7651c87 100644 --- a/examples/multi_agent/data_analysis_agent.py +++ b/examples/multi_agent/data_analysis_agent.py @@ -9,8 +9,7 @@ import os import io import json -from typing import Dict, List, Any -from dotenv import load_dotenv +from typing import Dict from pydantic import BaseModel, Field from agentconnect.agents import AIAgent @@ -66,13 +65,13 @@ def create_data_analysis_agent( data_analysis_capabilities = [ Capability( name="data_analysis", - description="Analyzes data and provides insights", + description="Analyzes provided data (structured or textual) to provide insights, identify trends, assess impacts (e.g., economic), and generate summaries.", input_schema={"data": "string", "analysis_type": "string"}, output_schema={"result": "string", "visualization_path": "string"}, ), Capability( name="data_visualization", - description="Creates visualizations from data", + description="Creates visualizations from provided data", input_schema={"data": "string", "chart_type": "string"}, output_schema={"visualization_path": "string", "description": "string"}, ), @@ -248,7 +247,10 @@ def analyze_data(data: str, analysis_type: str = "summary") -> Dict[str, str]: api_key=api_key, identity=data_analysis_identity, capabilities=data_analysis_capabilities, - personality="I am a data analysis specialist who excels at analyzing data, generating insights, and creating visualizations. I can process CSV and JSON data to discover patterns and present results in a clear, understandable format.", + personality=( + "I am a data analysis specialist. I excel at processing structured data (like CSV/JSON) for statistical analysis and visualization. " + "I can also analyze textual information to identify key trends, assess potential impacts (including economic consequences), and generate insightful summaries based on the provided context." + ), custom_tools=[data_analysis_tool], ) diff --git a/examples/multi_agent/multi_agent_system.py b/examples/multi_agent/multi_agent_system.py index e0d839a..8c786b8 100644 --- a/examples/multi_agent/multi_agent_system.py +++ b/examples/multi_agent/multi_agent_system.py @@ -92,7 +92,7 @@ async def setup_agents(enable_logging: bool = False) -> Dict[str, Any]: # Fall back to other API keys if Google's isn't available provider_type = ModelProvider.GOOGLE - model_name = ModelName.GEMINI2_FLASH + model_name = ModelName.GEMINI2_5_FLASH_PREVIEW if not api_key: print_colored("GOOGLE_API_KEY not found. Checking for alternatives...", "INFO") @@ -259,15 +259,30 @@ async def run_multi_agent_system(enable_logging: bool = False) -> None: except Exception as e: print_colored(f"Error removing message logger: {e}", "ERROR") - # Stop all agent tasks + # Stop all agents with the new stop method + for agent_id in [ + "telegram_agent", + "research_agent", + "content_processing_agent", + "data_analysis_agent", + ]: + if agent_id in agents: + try: + await agents[agent_id].stop() + print_colored(f"Stopped {agent_id}", "SYSTEM") + except Exception as e: + print_colored(f"Error stopping {agent_id}: {e}", "ERROR") + + # Cancel any remaining tasks if "agent_tasks" in agents: for task in agents["agent_tasks"]: - task.cancel() - try: - # Wait for task to properly cancel - await asyncio.wait_for(task, timeout=2.0) - except (asyncio.TimeoutError, asyncio.CancelledError): - pass + if not task.done(): + task.cancel() + try: + # Wait for task to properly cancel + await asyncio.wait_for(task, timeout=2.0) + except (asyncio.TimeoutError, asyncio.CancelledError): + pass # Unregister agents for agent_id in [ diff --git a/examples/multi_agent/research_agent.py b/examples/multi_agent/research_agent.py index fe093ef..47f42e9 100644 --- a/examples/multi_agent/research_agent.py +++ b/examples/multi_agent/research_agent.py @@ -7,8 +7,6 @@ """ import os -from typing import List -from dotenv import load_dotenv from agentconnect.agents import AIAgent from agentconnect.core.types import ( diff --git a/examples/multi_agent/telegram_agent.py b/examples/multi_agent/telegram_agent.py index cc00390..743d730 100644 --- a/examples/multi_agent/telegram_agent.py +++ b/examples/multi_agent/telegram_agent.py @@ -7,8 +7,6 @@ """ import os -from typing import List -from dotenv import load_dotenv from agentconnect.agents.telegram.telegram_agent import TelegramAIAgent from agentconnect.core.types import ( diff --git a/examples/research_assistant.py b/examples/research_assistant.py index 361d8cc..ee8b299 100644 --- a/examples/research_assistant.py +++ b/examples/research_assistant.py @@ -39,8 +39,8 @@ ModelName, ModelProvider, ) -from agentconnect.core.message import Message from agentconnect.core.registry import AgentRegistry +from agentconnect.utils.callbacks import ToolTracerCallbackHandler from agentconnect.utils.logging_config import ( setup_logging, LogLevel, @@ -134,7 +134,7 @@ async def setup_agents() -> Dict[str, Any]: # Fall back to other API keys if Google's isn't available provider_type = ModelProvider.GOOGLE - model_name = ModelName.GEMINI2_FLASH + model_name = ModelName.GEMINI2_5_FLASH_PREVIEW if not api_key: print_colored("GOOGLE_API_KEY not found. Checking for alternatives...", "INFO") @@ -175,8 +175,8 @@ async def setup_agents() -> Dict[str, Any]: hub = CommunicationHub(registry) # Register message logger - hub.add_global_handler(demo_message_logger) - print_colored("Registered message flow logger to visualize agent collaboration", "INFO") + # hub.add_global_handler(demo_message_logger) + # print_colored("Registered message flow logger to visualize agent collaboration", "INFO") # Create human agent human_identity = AgentIdentity.create_key_based() @@ -218,6 +218,7 @@ async def setup_agents() -> Dict[str, Any]: identity=core_identity, capabilities=core_capabilities, personality="I am the primary interface between you and specialized agents. I understand your requests, delegate tasks to specialized agents, and present their findings in a coherent manner. I maintain conversation context and ensure a smooth experience.", + external_callbacks=[ToolTracerCallbackHandler("core_agent")], ) # Create research agent @@ -442,7 +443,7 @@ async def run_research_assistant_demo(enable_logging: bool = False) -> None: print_colored("\nSetting up agents...", "SYSTEM") agents = None - message_logger_registered = enable_logging + # message_logger_registered = enable_logging try: # Set up agents with logging flag @@ -478,57 +479,60 @@ async def run_research_assistant_demo(enable_logging: bool = False) -> None: print_colored("\nCleaning up resources...", "SYSTEM") # Remove message logger if it was registered - if message_logger_registered and "hub" in agents: - try: - agents["hub"].remove_global_handler(demo_message_logger) - print_colored("Removed message flow logger", "INFO") - except Exception as e: - print_colored(f"Error removing message logger: {e}", "ERROR") - - # Stop all agent tasks - if "agent_tasks" in agents: - for task in agents["agent_tasks"]: - task.cancel() - try: - # Wait for task to properly cancel - await asyncio.wait_for(task, timeout=2.0) - except (asyncio.TimeoutError, asyncio.CancelledError): - pass - - # Unregister agents + # if message_logger_registered and "hub" in agents: + # try: + # # agents["hub"].remove_global_handler(demo_message_logger) + # print_colored("Removed message flow logger", "INFO") + # except Exception as e: + # print_colored(f"Error removing message logger: {e}", "ERROR") + + # Stop all agents for agent_id in ["core_agent", "research_agent", "markdown_agent"]: if agent_id in agents: try: + # Use the new stop method for proper cleanup + await agents[agent_id].stop() await agents["hub"].unregister_agent(agents[agent_id].agent_id) - print_colored(f"Unregistered {agent_id}", "SYSTEM") + print_colored(f"Stopped and unregistered {agent_id}", "SYSTEM") except Exception as e: - print_colored(f"Error unregistering {agent_id}: {e}", "ERROR") + print_colored(f"Error stopping/unregistering {agent_id}: {e}", "ERROR") + + # Cancel any remaining tasks + if "agent_tasks" in agents: + for task in agents["agent_tasks"]: + if not task.done(): + task.cancel() + try: + # Wait for task to properly cancel + await asyncio.wait_for(task, timeout=2.0) + except (asyncio.TimeoutError, asyncio.CancelledError): + pass print_colored("Demo completed successfully!", "SYSTEM") # Define the global message logger function -async def demo_message_logger(message: Message) -> None: - """ - Global message handler for logging agent collaboration flow. +# async def demo_message_logger(message: Message) -> None: +# """ +# Global message handler for logging agent collaboration flow. - This handler inspects messages routed through the hub and logs specific events - in the research assistant demo to visualize agent collaboration. +# This handler inspects messages routed through the hub and logs specific events +# in the research assistant demo to visualize agent collaboration. - Args: - message (Message): The message being routed through the hub - """ - if message.receiver_id == "human_user" or message.sender_id == "human_user": - return - color_type = "SYSTEM" - if message.sender_id == "core_agent": - color_type = "CORE" - elif message.sender_id == "research_agent": - color_type = "RESEARCH" - elif message.sender_id == "markdown_agent": - color_type = "MARKDOWN" - - print_colored(f"{message.sender_id} -> {message.receiver_id}: {message.content[:50]}...", color_type) +# Args: +# message (Message): The message being routed through the hub +# """ +# if message.receiver_id == "human_user" or message.sender_id == "human_user": +# return +# color_type = "SYSTEM" +# if message.sender_id == "core_agent": +# color_type = "CORE" +# elif message.sender_id == "research_agent": +# color_type = "RESEARCH" +# elif message.sender_id == "markdown_agent": +# color_type = "MARKDOWN" + +# print_colored(f"{message.sender_id} -> {message.receiver_id}: {message.content[:50]}...", color_type) if __name__ == "__main__": From e884183460cb4a74699a3feceabc797542d29d0f Mon Sep 17 00:00:00 2001 From: Akshat Date: Fri, 2 May 2025 05:55:48 -0400 Subject: [PATCH 15/20] docs: add comprehensive developer guides for onboarding --- docs/source/_static/approval_workflow.png | Bin 0 -> 2001688 bytes docs/source/_static/css/custom.css | 20 +- docs/source/_static/langsmith_error_trace.png | Bin 0 -> 235515 bytes .../_static/langsmith_project_overview.png | Bin 0 -> 361409 bytes docs/source/_static/langsmith_tool_call.png | Bin 0 -> 373499 bytes .../source/_static/langsmith_trace_detail.png | Bin 0 -> 303905 bytes docs/source/_templates/layout.html | 12 + .../agentconnect.core.payment_constants.rst | 7 + docs/source/api/agentconnect.core.rst | 1 + .../api/agentconnect.utils.callbacks.rst | 7 + .../api/agentconnect.utils.payment_helper.rst | 7 + docs/source/api/agentconnect.utils.rst | 3 + .../api/agentconnect.utils.wallet_manager.rst | 7 + docs/source/conf.py | 12 +- .../guides/advanced/customizing_agents.rst | 33 + .../customizing_providers.rst} | 6 +- docs/source/guides/advanced/index.rst | 85 +++ docs/source/guides/agent_configuration.rst | 285 ++++++++ docs/source/guides/agent_payment.rst | 408 ++++++++++++ .../source/guides/collaborative_workflows.rst | 181 +++++ docs/source/guides/core_concepts.rst | 115 ++++ docs/source/guides/event_monitoring.rst | 112 ++++ docs/source/guides/external_tools.rst | 193 ++++++ docs/source/guides/first_agent.rst | 272 ++++++++ docs/source/guides/human_in_the_loop.rst | 326 +++++++++ docs/source/guides/index.rst | 106 ++- docs/source/guides/logging_events.rst | 170 +++++ docs/source/guides/multi_agent_setup.rst | 625 +++++++++++------- docs/source/guides/secure_communication.rst | 161 +++++ docs/source/guides/telegram_integration.rst | 220 +----- docs/source/index.rst | 187 +++--- docs/source/installation.md | 59 +- docs/source/quickstart.md | 278 ++------ 33 files changed, 3075 insertions(+), 823 deletions(-) create mode 100644 docs/source/_static/approval_workflow.png create mode 100644 docs/source/_static/langsmith_error_trace.png create mode 100644 docs/source/_static/langsmith_project_overview.png create mode 100644 docs/source/_static/langsmith_tool_call.png create mode 100644 docs/source/_static/langsmith_trace_detail.png create mode 100644 docs/source/_templates/layout.html create mode 100644 docs/source/api/agentconnect.core.payment_constants.rst create mode 100644 docs/source/api/agentconnect.utils.callbacks.rst create mode 100644 docs/source/api/agentconnect.utils.payment_helper.rst create mode 100644 docs/source/api/agentconnect.utils.wallet_manager.rst create mode 100644 docs/source/guides/advanced/customizing_agents.rst rename docs/source/guides/{custom_providers.rst => advanced/customizing_providers.rst} (98%) create mode 100644 docs/source/guides/advanced/index.rst create mode 100644 docs/source/guides/agent_configuration.rst create mode 100644 docs/source/guides/agent_payment.rst create mode 100644 docs/source/guides/collaborative_workflows.rst create mode 100644 docs/source/guides/core_concepts.rst create mode 100644 docs/source/guides/event_monitoring.rst create mode 100644 docs/source/guides/external_tools.rst create mode 100644 docs/source/guides/first_agent.rst create mode 100644 docs/source/guides/human_in_the_loop.rst create mode 100644 docs/source/guides/logging_events.rst create mode 100644 docs/source/guides/secure_communication.rst diff --git a/docs/source/_static/approval_workflow.png b/docs/source/_static/approval_workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..f84be8d20e8442ceeef9287a0fa93c0d9485e3ab GIT binary patch literal 2001688 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