From a659a45864696691bbcd55262f008d30fa5df5bd Mon Sep 17 00:00:00 2001 From: cthomas Date: Mon, 17 Mar 2025 18:20:15 -0700 Subject: [PATCH] chore: bump version 0.6.43 (#2500) Co-authored-by: Matthew Zhou --- .../1e553a664210_add_metadata_to_tools.py | 31 + letta/__init__.py | 2 +- letta/agent.py | 14 +- letta/helpers/converters.py | 22 +- letta/helpers/tool_rule_solver.py | 124 ++-- letta/orm/sqlalchemy_base.py | 11 +- letta/orm/tool.py | 1 + letta/schemas/enums.py | 6 +- letta/schemas/tool.py | 5 +- letta/schemas/tool_rule.py | 75 ++- .../pydantic_agent_schema.py | 5 +- letta/server/rest_api/app.py | 2 +- letta/server/rest_api/routers/v1/tools.py | 2 +- letta/server/server.py | 13 +- letta/services/tool_manager.py | 10 +- letta/settings.py | 1 + letta/tracing.py | 10 + otel-collector-config-clickhouse-dev.yaml | 43 ++ otel-collector-config-clickhouse-prod.yaml | 83 +++ otel-collector-config-clickhouse.yaml | 33 - otel-collector-config-file-dev.yaml | 30 + poetry.lock | 462 +++++++------ project.json | 3 +- pyproject.toml | 2 +- start-otel-collector.sh | 26 + tests/integration_test_agent_tool_graph.py | 615 ++++++++---------- tests/test_managers.py | 39 +- tests/test_tool_rule_solver.py | 140 ++-- 28 files changed, 1045 insertions(+), 765 deletions(-) create mode 100644 alembic/versions/1e553a664210_add_metadata_to_tools.py create mode 100644 otel-collector-config-clickhouse-dev.yaml create mode 100644 otel-collector-config-clickhouse-prod.yaml create mode 100644 otel-collector-config-file-dev.yaml create mode 100755 start-otel-collector.sh diff --git a/alembic/versions/1e553a664210_add_metadata_to_tools.py b/alembic/versions/1e553a664210_add_metadata_to_tools.py new file mode 100644 index 00000000..51b6da20 --- /dev/null +++ b/alembic/versions/1e553a664210_add_metadata_to_tools.py @@ -0,0 +1,31 @@ +"""Add metadata to Tools + +Revision ID: 1e553a664210 +Revises: 2cceb07c2384 +Create Date: 2025-03-17 15:50:05.562302 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "1e553a664210" +down_revision: Union[str, None] = "2cceb07c2384" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("tools", sa.Column("metadata_", sa.JSON(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("tools", "metadata_") + # ### end Alembic commands ### diff --git a/letta/__init__.py b/letta/__init__.py index 285a2eba..1982a4d5 100644 --- a/letta/__init__.py +++ b/letta/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.6.41" +__version__ = "0.6.43" # import clients from letta.client.client import LocalClient, RESTClient, create_client diff --git a/letta/agent.py b/letta/agent.py index 5286f9cb..55dc838d 100644 --- a/letta/agent.py +++ b/letta/agent.py @@ -367,7 +367,10 @@ class Agent(BaseAgent): ) -> ChatCompletionResponse: """Get response from LLM API with robust retry mechanism.""" log_telemetry(self.logger, "_get_ai_reply start") - allowed_tool_names = self.tool_rules_solver.get_allowed_tool_names(last_function_response=self.last_function_response) + available_tools = set([t.name for t in self.agent_state.tools]) + allowed_tool_names = self.tool_rules_solver.get_allowed_tool_names( + available_tools=available_tools, last_function_response=self.last_function_response + ) agent_state_tool_jsons = [t.json_schema for t in self.agent_state.tools] allowed_functions = ( @@ -377,8 +380,8 @@ class Agent(BaseAgent): ) # Don't allow a tool to be called if it failed last time - if last_function_failed and self.tool_rules_solver.last_tool_name: - allowed_functions = [f for f in allowed_functions if f["name"] != self.tool_rules_solver.last_tool_name] + if last_function_failed and self.tool_rules_solver.tool_call_history: + allowed_functions = [f for f in allowed_functions if f["name"] != self.tool_rules_solver.tool_call_history[-1]] if not allowed_functions: return None @@ -773,6 +776,11 @@ class Agent(BaseAgent): **kwargs, ) -> LettaUsageStatistics: """Run Agent.step in a loop, handling chaining via heartbeat requests and function failures""" + # Defensively clear the tool rules solver history + # Usually this would be extraneous as Agent loop is re-loaded on every message send + # But just to be safe + self.tool_rules_solver.clear_tool_history() + next_input_message = messages if isinstance(messages, list) else [messages] counter = 0 total_usage = UsageStatistics() diff --git a/letta/helpers/converters.py b/letta/helpers/converters.py index 73d1196f..4f0510de 100644 --- a/letta/helpers/converters.py +++ b/letta/helpers/converters.py @@ -20,7 +20,15 @@ from letta.schemas.letta_message_content import ( ) from letta.schemas.llm_config import LLMConfig from letta.schemas.message import ToolReturn -from letta.schemas.tool_rule import ChildToolRule, ConditionalToolRule, ContinueToolRule, InitToolRule, TerminalToolRule, ToolRule +from letta.schemas.tool_rule import ( + ChildToolRule, + ConditionalToolRule, + ContinueToolRule, + InitToolRule, + MaxCountPerStepToolRule, + TerminalToolRule, + ToolRule, +) # -------------------------- # LLMConfig Serialization @@ -85,23 +93,27 @@ def deserialize_tool_rules(data: Optional[List[Dict]]) -> List[Union[ChildToolRu return [deserialize_tool_rule(rule_data) for rule_data in data] -def deserialize_tool_rule(data: Dict) -> Union[ChildToolRule, InitToolRule, TerminalToolRule, ConditionalToolRule, ContinueToolRule]: +def deserialize_tool_rule( + data: Dict, +) -> Union[ChildToolRule, InitToolRule, TerminalToolRule, ConditionalToolRule, ContinueToolRule, MaxCountPerStepToolRule]: """Deserialize a dictionary to the appropriate ToolRule subclass based on 'type'.""" rule_type = ToolRuleType(data.get("type")) - if rule_type == ToolRuleType.run_first or rule_type == ToolRuleType.InitToolRule: + if rule_type == ToolRuleType.run_first: data["type"] = ToolRuleType.run_first return InitToolRule(**data) - elif rule_type == ToolRuleType.exit_loop or rule_type == ToolRuleType.TerminalToolRule: + elif rule_type == ToolRuleType.exit_loop: data["type"] = ToolRuleType.exit_loop return TerminalToolRule(**data) - elif rule_type == ToolRuleType.constrain_child_tools or rule_type == ToolRuleType.ToolRule: + elif rule_type == ToolRuleType.constrain_child_tools: data["type"] = ToolRuleType.constrain_child_tools return ChildToolRule(**data) elif rule_type == ToolRuleType.conditional: return ConditionalToolRule(**data) elif rule_type == ToolRuleType.continue_loop: return ContinueToolRule(**data) + elif rule_type == ToolRuleType.max_count_per_step: + return MaxCountPerStepToolRule(**data) raise ValueError(f"Unknown ToolRule type: {rule_type}") diff --git a/letta/helpers/tool_rule_solver.py b/letta/helpers/tool_rule_solver.py index ca885616..4572bc90 100644 --- a/letta/helpers/tool_rule_solver.py +++ b/letta/helpers/tool_rule_solver.py @@ -1,10 +1,17 @@ -import json -from typing import List, Optional, Union +from typing import List, Optional, Set, Union from pydantic import BaseModel, Field from letta.schemas.enums import ToolRuleType -from letta.schemas.tool_rule import BaseToolRule, ChildToolRule, ConditionalToolRule, ContinueToolRule, InitToolRule, TerminalToolRule +from letta.schemas.tool_rule import ( + BaseToolRule, + ChildToolRule, + ConditionalToolRule, + ContinueToolRule, + InitToolRule, + MaxCountPerStepToolRule, + TerminalToolRule, +) class ToolRuleValidationError(Exception): @@ -21,13 +28,15 @@ class ToolRulesSolver(BaseModel): continue_tool_rules: List[ContinueToolRule] = Field( default_factory=list, description="Continue tool rules to be used to continue tool execution." ) - tool_rules: List[Union[ChildToolRule, ConditionalToolRule]] = Field( + # TODO: This should be renamed? + # TODO: These are tools that control the set of allowed functions in the next turn + child_based_tool_rules: List[Union[ChildToolRule, ConditionalToolRule, MaxCountPerStepToolRule]] = Field( default_factory=list, description="Standard tool rules for controlling execution sequence and allowed transitions." ) terminal_tool_rules: List[TerminalToolRule] = Field( default_factory=list, description="Terminal tool rules that end the agent loop if called." ) - last_tool_name: Optional[str] = Field(None, description="The most recent tool used, updated with each tool call.") + tool_call_history: List[str] = Field(default_factory=list, description="History of tool calls, updated with each tool call.") def __init__(self, tool_rules: List[BaseToolRule], **kwargs): super().__init__(**kwargs) @@ -38,45 +47,60 @@ class ToolRulesSolver(BaseModel): self.init_tool_rules.append(rule) elif rule.type == ToolRuleType.constrain_child_tools: assert isinstance(rule, ChildToolRule) - self.tool_rules.append(rule) + self.child_based_tool_rules.append(rule) elif rule.type == ToolRuleType.conditional: assert isinstance(rule, ConditionalToolRule) self.validate_conditional_tool(rule) - self.tool_rules.append(rule) + self.child_based_tool_rules.append(rule) elif rule.type == ToolRuleType.exit_loop: assert isinstance(rule, TerminalToolRule) self.terminal_tool_rules.append(rule) elif rule.type == ToolRuleType.continue_loop: assert isinstance(rule, ContinueToolRule) self.continue_tool_rules.append(rule) + elif rule.type == ToolRuleType.max_count_per_step: + assert isinstance(rule, MaxCountPerStepToolRule) + self.child_based_tool_rules.append(rule) def update_tool_usage(self, tool_name: str): - """Update the internal state to track the last tool called.""" - self.last_tool_name = tool_name + """Update the internal state to track tool call history.""" + self.tool_call_history.append(tool_name) - def get_allowed_tool_names(self, error_on_empty: bool = False, last_function_response: Optional[str] = None) -> List[str]: + def clear_tool_history(self): + """Clear the history of tool calls.""" + self.tool_call_history.clear() + + def get_allowed_tool_names( + self, available_tools: Set[str], error_on_empty: bool = False, last_function_response: Optional[str] = None + ) -> List[str]: """Get a list of tool names allowed based on the last tool called.""" - if self.last_tool_name is None: - # Use initial tool rules if no tool has been called yet - return [rule.tool_name for rule in self.init_tool_rules] + # TODO: This piece of code here is quite ugly and deserves a refactor + # TODO: There's some weird logic encoded here: + # TODO: -> This only takes into consideration Init, and a set of Child/Conditional/MaxSteps tool rules + # TODO: -> Init tool rules outputs are treated additively, Child/Conditional/MaxSteps are intersection based + # TODO: -> Tool rules should probably be refactored to take in a set of tool names? + # If no tool has been called yet, return InitToolRules additively + if not self.tool_call_history: + if self.init_tool_rules: + # If there are init tool rules, only return those defined in the init tool rules + return [rule.tool_name for rule in self.init_tool_rules] + else: + # Otherwise, return all the available tools + return list(available_tools) else: - # Find a matching ToolRule for the last tool used - current_rule = next((rule for rule in self.tool_rules if rule.tool_name == self.last_tool_name), None) + # Collect valid tools from all child-based rules + valid_tool_sets = [ + rule.get_valid_tools(self.tool_call_history, available_tools, last_function_response) + for rule in self.child_based_tool_rules + ] - if current_rule is None: - if error_on_empty: - raise ValueError(f"No tool rule found for {self.last_tool_name}") - return [] + # Compute intersection of all valid tool sets + final_allowed_tools = set.intersection(*valid_tool_sets) if valid_tool_sets else available_tools - # If the current rule is a conditional tool rule, use the LLM response to - # determine which child tool to use - if isinstance(current_rule, ConditionalToolRule): - if not last_function_response: - raise ValueError("Conditional tool rule requires an LLM response to determine which child tool to use") - next_tool = self.evaluate_conditional_tool(current_rule, last_function_response) - return [next_tool] if next_tool else [] + if error_on_empty and not final_allowed_tools: + raise ValueError("No valid tools found based on tool rules.") - return current_rule.children if current_rule.children else [] + return list(final_allowed_tools) def is_terminal_tool(self, tool_name: str) -> bool: """Check if the tool is defined as a terminal tool in the terminal tool rules.""" @@ -84,7 +108,7 @@ class ToolRulesSolver(BaseModel): def has_children_tools(self, tool_name): """Check if the tool has children tools""" - return any(rule.tool_name == tool_name for rule in self.tool_rules) + return any(rule.tool_name == tool_name for rule in self.child_based_tool_rules) def is_continue_tool(self, tool_name): """Check if the tool is defined as a continue tool in the tool rules.""" @@ -103,47 +127,3 @@ class ToolRulesSolver(BaseModel): if len(rule.child_output_mapping) == 0: raise ToolRuleValidationError("Conditional tool rule must have at least one child tool.") return True - - def evaluate_conditional_tool(self, tool: ConditionalToolRule, last_function_response: str) -> str: - """ - Parse function response to determine which child tool to use based on the mapping - - Args: - tool (ConditionalToolRule): The conditional tool rule - last_function_response (str): The function response in JSON format - - Returns: - str: The name of the child tool to use next - """ - json_response = json.loads(last_function_response) - function_output = json_response["message"] - - # Try to match the function output with a mapping key - for key in tool.child_output_mapping: - - # Convert function output to match key type for comparison - if isinstance(key, bool): - typed_output = function_output.lower() == "true" - elif isinstance(key, int): - try: - typed_output = int(function_output) - except (ValueError, TypeError): - continue - elif isinstance(key, float): - try: - typed_output = float(function_output) - except (ValueError, TypeError): - continue - else: # string - if function_output == "True" or function_output == "False": - typed_output = function_output.lower() - elif function_output == "None": - typed_output = None - else: - typed_output = function_output - - if typed_output == key: - return tool.child_output_mapping[key] - - # If no match found, use default - return tool.default_child diff --git a/letta/orm/sqlalchemy_base.py b/letta/orm/sqlalchemy_base.py index fd211b86..3b45c6ee 100644 --- a/letta/orm/sqlalchemy_base.py +++ b/letta/orm/sqlalchemy_base.py @@ -508,10 +508,13 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base): raise NotImplementedError("Sqlalchemy models must declare a __pydantic_model__ property to be convertable.") def to_pydantic(self) -> "BaseModel": - """converts to the basic pydantic model counterpart""" - model = self.__pydantic_model__.model_validate(self) - if hasattr(self, "metadata_"): - model.metadata = self.metadata_ + """Converts the SQLAlchemy model to its corresponding Pydantic model.""" + model = self.__pydantic_model__.model_validate(self, from_attributes=True) + + # Explicitly map metadata_ to metadata in Pydantic model + if hasattr(self, "metadata_") and hasattr(model, "metadata_"): + setattr(model, "metadata_", self.metadata_) # Ensures correct assignment + return model def pretty_print_columns(self) -> str: diff --git a/letta/orm/tool.py b/letta/orm/tool.py index c43a0a88..7a7c3199 100644 --- a/letta/orm/tool.py +++ b/letta/orm/tool.py @@ -44,5 +44,6 @@ class Tool(SqlalchemyBase, OrganizationMixin): source_code: Mapped[Optional[str]] = mapped_column(String, doc="The source code of the function.") json_schema: Mapped[Optional[dict]] = mapped_column(JSON, default=lambda: {}, doc="The OAI compatable JSON schema of the function.") args_json_schema: Mapped[Optional[dict]] = mapped_column(JSON, default=lambda: {}, doc="The JSON schema of the function arguments.") + metadata_: Mapped[Optional[dict]] = mapped_column(JSON, default=lambda: {}, doc="A dictionary of additional metadata for the tool.") # relationships organization: Mapped["Organization"] = relationship("Organization", back_populates="tools", lazy="selectin") diff --git a/letta/schemas/enums.py b/letta/schemas/enums.py index 1852aa5d..9fde25cd 100644 --- a/letta/schemas/enums.py +++ b/letta/schemas/enums.py @@ -47,8 +47,4 @@ class ToolRuleType(str, Enum): continue_loop = "continue_loop" conditional = "conditional" constrain_child_tools = "constrain_child_tools" - require_parent_tools = "require_parent_tools" - # Deprecated - InitToolRule = "InitToolRule" - TerminalToolRule = "TerminalToolRule" - ToolRule = "ToolRule" + max_count_per_step = "max_count_per_step" diff --git a/letta/schemas/tool.py b/letta/schemas/tool.py index 55fac00c..b23d9ac2 100644 --- a/letta/schemas/tool.py +++ b/letta/schemas/tool.py @@ -66,6 +66,7 @@ class Tool(BaseTool): # metadata fields created_by_id: Optional[str] = Field(None, description="The id of the user that made this Tool.") last_updated_by_id: Optional[str] = Field(None, description="The id of the user that made this Tool.") + metadata_: Optional[Dict[str, Any]] = Field(default_factory=dict, description="A dictionary of additional metadata for the tool.") @model_validator(mode="after") def refresh_source_code_and_json_schema(self): @@ -137,10 +138,6 @@ class ToolCreate(LettaBase): @classmethod def from_mcp(cls, mcp_server_name: str, mcp_tool: MCPTool) -> "ToolCreate": - - # Get the MCP tool from the MCP server - # NVM - # Pass the MCP tool to the schema generator json_schema = generate_tool_schema_for_mcp(mcp_tool=mcp_tool) diff --git a/letta/schemas/tool_rule.py b/letta/schemas/tool_rule.py index e0065e68..37158063 100644 --- a/letta/schemas/tool_rule.py +++ b/letta/schemas/tool_rule.py @@ -1,4 +1,5 @@ -from typing import Annotated, Any, Dict, List, Literal, Optional, Union +import json +from typing import Annotated, Any, Dict, List, Literal, Optional, Set, Union from pydantic import Field @@ -11,6 +12,9 @@ class BaseToolRule(LettaBase): tool_name: str = Field(..., description="The name of the tool. Must exist in the database for the user's organization.") type: ToolRuleType = Field(..., description="The type of the message.") + def get_valid_tools(self, tool_call_history: List[str], available_tools: Set[str], last_function_response: Optional[str]) -> set[str]: + raise NotImplementedError + class ChildToolRule(BaseToolRule): """ @@ -20,6 +24,10 @@ class ChildToolRule(BaseToolRule): type: Literal[ToolRuleType.constrain_child_tools] = ToolRuleType.constrain_child_tools children: List[str] = Field(..., description="The children tools that can be invoked.") + def get_valid_tools(self, tool_call_history: List[str], available_tools: Set[str], last_function_response: Optional[str]) -> Set[str]: + last_tool = tool_call_history[-1] if tool_call_history else None + return set(self.children) if last_tool == self.tool_name else available_tools + class ConditionalToolRule(BaseToolRule): """ @@ -31,6 +39,50 @@ class ConditionalToolRule(BaseToolRule): child_output_mapping: Dict[Any, str] = Field(..., description="The output case to check for mapping") require_output_mapping: bool = Field(default=False, description="Whether to throw an error when output doesn't match any case") + def get_valid_tools(self, tool_call_history: List[str], available_tools: Set[str], last_function_response: Optional[str]) -> Set[str]: + """Determine valid tools based on function output mapping.""" + if not tool_call_history or tool_call_history[-1] != self.tool_name: + return available_tools # No constraints if this rule doesn't apply + + if not last_function_response: + raise ValueError("Conditional tool rule requires an LLM response to determine which child tool to use") + + try: + json_response = json.loads(last_function_response) + function_output = json_response.get("message", "") + except json.JSONDecodeError: + if self.require_output_mapping: + return set() # Strict mode: Invalid response means no allowed tools + return {self.default_child} if self.default_child else available_tools + + # Match function output to a mapped child tool + for key, tool in self.child_output_mapping.items(): + if self._matches_key(function_output, key): + return {tool} + + # If no match found, use default or allow all tools if no default is set + if self.require_output_mapping: + return set() # Strict mode: No match means no valid tools + + return {self.default_child} if self.default_child else available_tools + + def _matches_key(self, function_output: str, key: Any) -> bool: + """Helper function to determine if function output matches a mapping key.""" + if isinstance(key, bool): + return function_output.lower() == "true" if key else function_output.lower() == "false" + elif isinstance(key, int): + try: + return int(function_output) == key + except ValueError: + return False + elif isinstance(key, float): + try: + return float(function_output) == key + except ValueError: + return False + else: # Assume string + return str(function_output) == str(key) + class InitToolRule(BaseToolRule): """ @@ -56,7 +108,26 @@ class ContinueToolRule(BaseToolRule): type: Literal[ToolRuleType.continue_loop] = ToolRuleType.continue_loop +class MaxCountPerStepToolRule(BaseToolRule): + """ + Represents a tool rule configuration which constrains the total number of times this tool can be invoked in a single step. + """ + + type: Literal[ToolRuleType.max_count_per_step] = ToolRuleType.max_count_per_step + max_count_limit: int = Field(..., description="The max limit for the total number of times this tool can be invoked in a single step.") + + def get_valid_tools(self, tool_call_history: List[str], available_tools: Set[str], last_function_response: Optional[str]) -> Set[str]: + """Restricts the tool if it has been called max_count_limit times in the current step.""" + count = tool_call_history.count(self.tool_name) + + # If the tool has been used max_count_limit times, it is no longer allowed + if count >= self.max_count_limit: + return available_tools - {self.tool_name} + + return available_tools + + ToolRule = Annotated[ - Union[ChildToolRule, InitToolRule, TerminalToolRule, ConditionalToolRule, ContinueToolRule], + Union[ChildToolRule, InitToolRule, TerminalToolRule, ConditionalToolRule, ContinueToolRule, MaxCountPerStepToolRule], Field(discriminator="type"), ] diff --git a/letta/serialize_schemas/pydantic_agent_schema.py b/letta/serialize_schemas/pydantic_agent_schema.py index ce1d65ce..593c0377 100644 --- a/letta/serialize_schemas/pydantic_agent_schema.py +++ b/letta/serialize_schemas/pydantic_agent_schema.py @@ -15,7 +15,7 @@ class CoreMemoryBlockSchema(BaseModel): is_template: bool label: str limit: int - metadata_: Dict[str, Any] = Field(default_factory=dict) + metadata_: Optional[Dict] = None template_name: Optional[str] updated_at: str value: str @@ -85,6 +85,7 @@ class ToolSchema(BaseModel): tags: List[str] tool_type: str updated_at: str + metadata_: Optional[Dict] = None class AgentSchema(BaseModel): @@ -99,7 +100,7 @@ class AgentSchema(BaseModel): llm_config: LLMConfig message_buffer_autoclear: bool messages: List[MessageSchema] - metadata_: Dict + metadata_: Optional[Dict] = None multi_agent_group: Optional[Any] name: str system: str diff --git a/letta/server/rest_api/app.py b/letta/server/rest_api/app.py index 6212e584..c2b1c137 100644 --- a/letta/server/rest_api/app.py +++ b/letta/server/rest_api/app.py @@ -257,7 +257,7 @@ def create_application() -> "FastAPI": # Set up OpenTelemetry tracing otlp_endpoint = settings.otel_exporter_otlp_endpoint - if otlp_endpoint: + if otlp_endpoint and not settings.disable_tracing: print(f"▶ Using OTLP tracing with endpoint: {otlp_endpoint}") env_name_suffix = os.getenv("ENV_NAME") service_name = f"letta-server-{env_name_suffix.lower()}" if env_name_suffix else "letta-server" diff --git a/letta/server/rest_api/routers/v1/tools.py b/letta/server/rest_api/routers/v1/tools.py index b4423027..b302a569 100644 --- a/letta/server/rest_api/routers/v1/tools.py +++ b/letta/server/rest_api/routers/v1/tools.py @@ -398,7 +398,7 @@ def add_mcp_tool( ) tool_create = ToolCreate.from_mcp(mcp_server_name=mcp_server_name, mcp_tool=mcp_tool) - return server.tool_manager.create_or_update_mcp_tool(tool_create=tool_create, actor=actor) + return server.tool_manager.create_or_update_mcp_tool(tool_create=tool_create, mcp_server_name=mcp_server_name, actor=actor) @router.put("/mcp/servers", response_model=List[Union[StdioServerConfig, SSEServerConfig]], operation_id="add_mcp_server") diff --git a/letta/server/server.py b/letta/server/server.py index 4faabd67..fd417f19 100644 --- a/letta/server/server.py +++ b/letta/server/server.py @@ -723,10 +723,17 @@ class SyncServer(Server): assert isinstance(message, MessageCreate) # If wrapping is enabled, wrap with metadata before placing content inside the Message object + if isinstance(message.content, str): + message_content = message.content + elif message.content and len(message.content) > 0 and isinstance(message.content[0], TextContent): + message_content = message.content[0].text + else: + assert message_content is not None, "Message content is empty" + if message.role == MessageRole.user and wrap_user_message: - message.content = system.package_user_message(user_message=message.content) + message_content = system.package_user_message(user_message=message_content) elif message.role == MessageRole.system and wrap_system_message: - message.content = system.package_system_message(system_message=message.content) + message_content = system.package_system_message(system_message=message_content) else: raise ValueError(f"Invalid message role: {message.role}") @@ -735,7 +742,7 @@ class SyncServer(Server): Message( agent_id=agent_id, role=message.role, - content=[TextContent(text=message.content)] if message.content else [], + content=[TextContent(text=message_content)] if message_content else [], name=message.name, # assigned later? model=None, diff --git a/letta/services/tool_manager.py b/letta/services/tool_manager.py index e5eb1d2f..2b6e5a28 100644 --- a/letta/services/tool_manager.py +++ b/letta/services/tool_manager.py @@ -2,7 +2,7 @@ import importlib import warnings from typing import List, Optional -from letta.constants import BASE_FUNCTION_RETURN_CHAR_LIMIT, BASE_MEMORY_TOOLS, BASE_TOOLS, MULTI_AGENT_TOOLS +from letta.constants import BASE_FUNCTION_RETURN_CHAR_LIMIT, BASE_MEMORY_TOOLS, BASE_TOOLS, MCP_TOOL_TAG_NAME_PREFIX, MULTI_AGENT_TOOLS from letta.functions.functions import derive_openai_json_schema, load_function_set from letta.log import get_logger from letta.orm.enums import ToolType @@ -57,9 +57,13 @@ class ToolManager: return tool @enforce_types - def create_or_update_mcp_tool(self, tool_create: ToolCreate, actor: PydanticUser) -> PydanticTool: + def create_or_update_mcp_tool(self, tool_create: ToolCreate, mcp_server_name: str, actor: PydanticUser) -> PydanticTool: + metadata = {MCP_TOOL_TAG_NAME_PREFIX: {"server_name": mcp_server_name}} return self.create_or_update_tool( - PydanticTool(tool_type=ToolType.EXTERNAL_MCP, name=tool_create.json_schema["name"], **tool_create.model_dump()), actor + PydanticTool( + tool_type=ToolType.EXTERNAL_MCP, name=tool_create.json_schema["name"], metadata_=metadata, **tool_create.model_dump() + ), + actor, ) @enforce_types diff --git a/letta/settings.py b/letta/settings.py index 5acfd532..f6ade17e 100644 --- a/letta/settings.py +++ b/letta/settings.py @@ -180,6 +180,7 @@ class Settings(BaseSettings): # telemetry logging verbose_telemetry_logging: bool = False otel_exporter_otlp_endpoint: str = "http://localhost:4317" + disable_tracing: bool = False # uvicorn settings uvicorn_workers: int = 1 diff --git a/letta/tracing.py b/letta/tracing.py index e0dda3d5..0144f1f5 100644 --- a/letta/tracing.py +++ b/letta/tracing.py @@ -112,6 +112,16 @@ def setup_tracing( global _is_tracing_initialized provider = TracerProvider(resource=Resource.create({"service.name": service_name})) + import uuid + + provider = TracerProvider( + resource=Resource.create( + { + "service.name": service_name, + "device.id": uuid.getnode(), # MAC address as unique device identifier + } + ) + ) if endpoint: provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint=endpoint))) _is_tracing_initialized = True diff --git a/otel-collector-config-clickhouse-dev.yaml b/otel-collector-config-clickhouse-dev.yaml new file mode 100644 index 00000000..1c9c0cca --- /dev/null +++ b/otel-collector-config-clickhouse-dev.yaml @@ -0,0 +1,43 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +processors: + batch: + timeout: 1s + send_batch_size: 1024 + +exporters: + file: + path: ${HOME}/.letta/logs/traces.json + rotation: + max_megabytes: 100 + max_days: 7 + max_backups: 5 + clickhouse: + endpoint: ${CLICKHOUSE_ENDPOINT} + database: ${CLICKHOUSE_DATABASE} + username: ${CLICKHOUSE_USERNAME} + password: ${CLICKHOUSE_PASSWORD} + timeout: 5s + sending_queue: + queue_size: 100 + retry_on_failure: + enabled: true + initial_interval: 5s + max_interval: 30s + max_elapsed_time: 300s + +service: + telemetry: + logs: + level: error + pipelines: + traces: + receivers: [otlp] + processors: [batch] + exporters: [file, clickhouse] diff --git a/otel-collector-config-clickhouse-prod.yaml b/otel-collector-config-clickhouse-prod.yaml new file mode 100644 index 00000000..cb35ecc5 --- /dev/null +++ b/otel-collector-config-clickhouse-prod.yaml @@ -0,0 +1,83 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + filelog: + include: + - /root/.letta/logs/Letta.log + multiline: + line_start_pattern: ^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3} + operators: + # Extract timestamp and other fields + - type: regex_parser + regex: '^(?P\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3})\s+-\s+(?P[\w\.-]+)\s+-\s+(?P\w+)\s+-\s+(?P.*)$' + # Parse the timestamp + - type: time_parser + parse_from: attributes.timestamp + layout: '%Y-%m-%d %H:%M:%S,%L' + # Set severity + - type: severity_parser + parse_from: attributes.severity + mapping: + debug: DEBUG + info: INFO + warning: WARN + error: ERROR + critical: FATAL + # Add resource attributes + - type: add + field: resource.service_name + value: letta-server + - type: add + field: resource.environment + value: ${ENV_NAME} + +processors: + batch: + timeout: 1s + send_batch_size: 1024 + +exporters: + clickhouse: + endpoint: ${CLICKHOUSE_ENDPOINT} + database: ${CLICKHOUSE_DATABASE} + username: ${CLICKHOUSE_USERNAME} + password: ${CLICKHOUSE_PASSWORD} + timeout: 5s + sending_queue: + queue_size: 100 + retry_on_failure: + enabled: true + initial_interval: 5s + max_interval: 30s + max_elapsed_time: 300s + +extensions: + health_check: + pprof: + zpages: + +service: + telemetry: + logs: + level: debug + development: true + metrics: + address: 0.0.0.0:8888 + extensions: [health_check, pprof, zpages] + pipelines: + traces: + receivers: [otlp] + processors: [batch] + exporters: [clickhouse] + logs: + receivers: [filelog] + processors: [batch] + exporters: [clickhouse] + metrics: + receivers: [otlp] + processors: [batch] + exporters: [clickhouse] diff --git a/otel-collector-config-clickhouse.yaml b/otel-collector-config-clickhouse.yaml index 6840610f..aa00ce0d 100644 --- a/otel-collector-config-clickhouse.yaml +++ b/otel-collector-config-clickhouse.yaml @@ -5,35 +5,6 @@ receivers: endpoint: 0.0.0.0:4317 http: endpoint: 0.0.0.0:4318 - filelog: - include: - - /root/.letta/logs/Letta.log - multiline: - line_start_pattern: ^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3} - operators: - # Extract timestamp and other fields - - type: regex_parser - regex: '^(?P\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3})\s+-\s+(?P[\w\.-]+)\s+-\s+(?P\w+)\s+-\s+(?P.*)$' - # Parse the timestamp - - type: time_parser - parse_from: attributes.timestamp - layout: '%Y-%m-%d %H:%M:%S,%L' - # Set severity - - type: severity_parser - parse_from: attributes.severity - mapping: - debug: DEBUG - info: INFO - warning: WARN - error: ERROR - critical: FATAL - # Add resource attributes - - type: add - field: resource.service_name - value: letta-server - - type: add - field: resource.environment - value: ${ENV_NAME} processors: batch: @@ -70,7 +41,3 @@ service: receivers: [otlp] processors: [batch] exporters: [file, clickhouse] - logs: - receivers: [filelog] - processors: [batch] - exporters: [clickhouse] diff --git a/otel-collector-config-file-dev.yaml b/otel-collector-config-file-dev.yaml new file mode 100644 index 00000000..dbb21454 --- /dev/null +++ b/otel-collector-config-file-dev.yaml @@ -0,0 +1,30 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: localhost:4317 + http: + endpoint: localhost:4318 + +processors: + batch: + timeout: 1s + send_batch_size: 1024 + +exporters: + file: + path: ${HOME}/.letta/logs/traces.json + rotation: + max_megabytes: 100 + max_days: 7 + max_backups: 5 + +service: + telemetry: + logs: + level: error + pipelines: + traces: + receivers: [otlp] + processors: [batch] + exporters: [file] diff --git a/poetry.lock b/poetry.lock index f9a8b5cd..17d82df6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -13,92 +13,92 @@ files = [ [[package]] name = "aiohttp" -version = "3.11.13" +version = "3.11.14" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.9" files = [ - {file = "aiohttp-3.11.13-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a4fe27dbbeec445e6e1291e61d61eb212ee9fed6e47998b27de71d70d3e8777d"}, - {file = "aiohttp-3.11.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9e64ca2dbea28807f8484c13f684a2f761e69ba2640ec49dacd342763cc265ef"}, - {file = "aiohttp-3.11.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9840be675de208d1f68f84d578eaa4d1a36eee70b16ae31ab933520c49ba1325"}, - {file = "aiohttp-3.11.13-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28a772757c9067e2aee8a6b2b425d0efaa628c264d6416d283694c3d86da7689"}, - {file = "aiohttp-3.11.13-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b88aca5adbf4625e11118df45acac29616b425833c3be7a05ef63a6a4017bfdb"}, - {file = "aiohttp-3.11.13-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce10ddfbe26ed5856d6902162f71b8fe08545380570a885b4ab56aecfdcb07f4"}, - {file = "aiohttp-3.11.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa48dac27f41b36735c807d1ab093a8386701bbf00eb6b89a0f69d9fa26b3671"}, - {file = "aiohttp-3.11.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89ce611b1eac93ce2ade68f1470889e0173d606de20c85a012bfa24be96cf867"}, - {file = "aiohttp-3.11.13-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:78e4dd9c34ec7b8b121854eb5342bac8b02aa03075ae8618b6210a06bbb8a115"}, - {file = "aiohttp-3.11.13-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:66047eacbc73e6fe2462b77ce39fc170ab51235caf331e735eae91c95e6a11e4"}, - {file = "aiohttp-3.11.13-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5ad8f1c19fe277eeb8bc45741c6d60ddd11d705c12a4d8ee17546acff98e0802"}, - {file = "aiohttp-3.11.13-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64815c6f02e8506b10113ddbc6b196f58dbef135751cc7c32136df27b736db09"}, - {file = "aiohttp-3.11.13-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:967b93f21b426f23ca37329230d5bd122f25516ae2f24a9cea95a30023ff8283"}, - {file = "aiohttp-3.11.13-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf1f31f83d16ec344136359001c5e871915c6ab685a3d8dee38e2961b4c81730"}, - {file = "aiohttp-3.11.13-cp310-cp310-win32.whl", hash = "sha256:00c8ac69e259c60976aa2edae3f13d9991cf079aaa4d3cd5a49168ae3748dee3"}, - {file = "aiohttp-3.11.13-cp310-cp310-win_amd64.whl", hash = "sha256:90d571c98d19a8b6e793b34aa4df4cee1e8fe2862d65cc49185a3a3d0a1a3996"}, - {file = "aiohttp-3.11.13-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6b35aab22419ba45f8fc290d0010898de7a6ad131e468ffa3922b1b0b24e9d2e"}, - {file = "aiohttp-3.11.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f81cba651db8795f688c589dd11a4fbb834f2e59bbf9bb50908be36e416dc760"}, - {file = "aiohttp-3.11.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f55d0f242c2d1fcdf802c8fabcff25a9d85550a4cf3a9cf5f2a6b5742c992839"}, - {file = "aiohttp-3.11.13-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4bea08a6aad9195ac9b1be6b0c7e8a702a9cec57ce6b713698b4a5afa9c2e33"}, - {file = "aiohttp-3.11.13-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6070bcf2173a7146bb9e4735b3c62b2accba459a6eae44deea0eb23e0035a23"}, - {file = "aiohttp-3.11.13-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:718d5deb678bc4b9d575bfe83a59270861417da071ab44542d0fcb6faa686636"}, - {file = "aiohttp-3.11.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f6b2c5b4a4d22b8fb2c92ac98e0747f5f195e8e9448bfb7404cd77e7bfa243f"}, - {file = "aiohttp-3.11.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:747ec46290107a490d21fe1ff4183bef8022b848cf9516970cb31de6d9460088"}, - {file = "aiohttp-3.11.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:01816f07c9cc9d80f858615b1365f8319d6a5fd079cd668cc58e15aafbc76a54"}, - {file = "aiohttp-3.11.13-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:a08ad95fcbd595803e0c4280671d808eb170a64ca3f2980dd38e7a72ed8d1fea"}, - {file = "aiohttp-3.11.13-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c97be90d70f7db3aa041d720bfb95f4869d6063fcdf2bb8333764d97e319b7d0"}, - {file = "aiohttp-3.11.13-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ab915a57c65f7a29353c8014ac4be685c8e4a19e792a79fe133a8e101111438e"}, - {file = "aiohttp-3.11.13-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:35cda4e07f5e058a723436c4d2b7ba2124ab4e0aa49e6325aed5896507a8a42e"}, - {file = "aiohttp-3.11.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:af55314407714fe77a68a9ccaab90fdb5deb57342585fd4a3a8102b6d4370080"}, - {file = "aiohttp-3.11.13-cp311-cp311-win32.whl", hash = "sha256:42d689a5c0a0c357018993e471893e939f555e302313d5c61dfc566c2cad6185"}, - {file = "aiohttp-3.11.13-cp311-cp311-win_amd64.whl", hash = "sha256:b73a2b139782a07658fbf170fe4bcdf70fc597fae5ffe75e5b67674c27434a9f"}, - {file = "aiohttp-3.11.13-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2eabb269dc3852537d57589b36d7f7362e57d1ece308842ef44d9830d2dc3c90"}, - {file = "aiohttp-3.11.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b77ee42addbb1c36d35aca55e8cc6d0958f8419e458bb70888d8c69a4ca833d"}, - {file = "aiohttp-3.11.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55789e93c5ed71832e7fac868167276beadf9877b85697020c46e9a75471f55f"}, - {file = "aiohttp-3.11.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c929f9a7249a11e4aa5c157091cfad7f49cc6b13f4eecf9b747104befd9f56f2"}, - {file = "aiohttp-3.11.13-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d33851d85537bbf0f6291ddc97926a754c8f041af759e0aa0230fe939168852b"}, - {file = "aiohttp-3.11.13-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9229d8613bd8401182868fe95688f7581673e1c18ff78855671a4b8284f47bcb"}, - {file = "aiohttp-3.11.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:669dd33f028e54fe4c96576f406ebb242ba534dd3a981ce009961bf49960f117"}, - {file = "aiohttp-3.11.13-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c1b20a1ace54af7db1f95af85da530fe97407d9063b7aaf9ce6a32f44730778"}, - {file = "aiohttp-3.11.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5724cc77f4e648362ebbb49bdecb9e2b86d9b172c68a295263fa072e679ee69d"}, - {file = "aiohttp-3.11.13-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:aa36c35e94ecdb478246dd60db12aba57cfcd0abcad43c927a8876f25734d496"}, - {file = "aiohttp-3.11.13-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9b5b37c863ad5b0892cc7a4ceb1e435e5e6acd3f2f8d3e11fa56f08d3c67b820"}, - {file = "aiohttp-3.11.13-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e06cf4852ce8c4442a59bae5a3ea01162b8fcb49ab438d8548b8dc79375dad8a"}, - {file = "aiohttp-3.11.13-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5194143927e494616e335d074e77a5dac7cd353a04755330c9adc984ac5a628e"}, - {file = "aiohttp-3.11.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:afcb6b275c2d2ba5d8418bf30a9654fa978b4f819c2e8db6311b3525c86fe637"}, - {file = "aiohttp-3.11.13-cp312-cp312-win32.whl", hash = "sha256:7104d5b3943c6351d1ad7027d90bdd0ea002903e9f610735ac99df3b81f102ee"}, - {file = "aiohttp-3.11.13-cp312-cp312-win_amd64.whl", hash = "sha256:47dc018b1b220c48089b5b9382fbab94db35bef2fa192995be22cbad3c5730c8"}, - {file = "aiohttp-3.11.13-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9862d077b9ffa015dbe3ce6c081bdf35135948cb89116e26667dd183550833d1"}, - {file = "aiohttp-3.11.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fbfef0666ae9e07abfa2c54c212ac18a1f63e13e0760a769f70b5717742f3ece"}, - {file = "aiohttp-3.11.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a1f7d857c4fcf7cabb1178058182c789b30d85de379e04f64c15b7e88d66fb"}, - {file = "aiohttp-3.11.13-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba40b7ae0f81c7029583a338853f6607b6d83a341a3dcde8bed1ea58a3af1df9"}, - {file = "aiohttp-3.11.13-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5b95787335c483cd5f29577f42bbe027a412c5431f2f80a749c80d040f7ca9f"}, - {file = "aiohttp-3.11.13-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7d474c5c1f0b9405c1565fafdc4429fa7d986ccbec7ce55bc6a330f36409cad"}, - {file = "aiohttp-3.11.13-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e83fb1991e9d8982b3b36aea1e7ad27ea0ce18c14d054c7a404d68b0319eebb"}, - {file = "aiohttp-3.11.13-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4586a68730bd2f2b04a83e83f79d271d8ed13763f64b75920f18a3a677b9a7f0"}, - {file = "aiohttp-3.11.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fe4eb0e7f50cdb99b26250d9328faef30b1175a5dbcfd6d0578d18456bac567"}, - {file = "aiohttp-3.11.13-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2a8a6bc19818ac3e5596310ace5aa50d918e1ebdcc204dc96e2f4d505d51740c"}, - {file = "aiohttp-3.11.13-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f27eec42f6c3c1df09cfc1f6786308f8b525b8efaaf6d6bd76c1f52c6511f6a"}, - {file = "aiohttp-3.11.13-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2a4a13dfbb23977a51853b419141cd0a9b9573ab8d3a1455c6e63561387b52ff"}, - {file = "aiohttp-3.11.13-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:02876bf2f69b062584965507b07bc06903c2dc93c57a554b64e012d636952654"}, - {file = "aiohttp-3.11.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b992778d95b60a21c4d8d4a5f15aaab2bd3c3e16466a72d7f9bfd86e8cea0d4b"}, - {file = "aiohttp-3.11.13-cp313-cp313-win32.whl", hash = "sha256:507ab05d90586dacb4f26a001c3abf912eb719d05635cbfad930bdbeb469b36c"}, - {file = "aiohttp-3.11.13-cp313-cp313-win_amd64.whl", hash = "sha256:5ceb81a4db2decdfa087381b5fc5847aa448244f973e5da232610304e199e7b2"}, - {file = "aiohttp-3.11.13-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:51c3ff9c7a25f3cad5c09d9aacbc5aefb9267167c4652c1eb737989b554fe278"}, - {file = "aiohttp-3.11.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e271beb2b1dabec5cd84eb488bdabf9758d22ad13471e9c356be07ad139b3012"}, - {file = "aiohttp-3.11.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0e9eb7e5764abcb49f0e2bd8f5731849b8728efbf26d0cac8e81384c95acec3f"}, - {file = "aiohttp-3.11.13-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baae005092e3f200de02699314ac8933ec20abf998ec0be39448f6605bce93df"}, - {file = "aiohttp-3.11.13-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1982c98ac62c132d2b773d50e2fcc941eb0b8bad3ec078ce7e7877c4d5a2dce7"}, - {file = "aiohttp-3.11.13-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2b25b2eeb35707113b2d570cadc7c612a57f1c5d3e7bb2b13870fe284e08fc0"}, - {file = "aiohttp-3.11.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b27961d65639128336b7a7c3f0046dcc62a9443d5ef962e3c84170ac620cec47"}, - {file = "aiohttp-3.11.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a01fe9f1e05025eacdd97590895e2737b9f851d0eb2e017ae9574d9a4f0b6252"}, - {file = "aiohttp-3.11.13-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa1fb1b61881c8405829c50e9cc5c875bfdbf685edf57a76817dfb50643e4a1a"}, - {file = "aiohttp-3.11.13-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:25de43bb3cf83ad83efc8295af7310219af6dbe4c543c2e74988d8e9c8a2a917"}, - {file = "aiohttp-3.11.13-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fe7065e2215e4bba63dc00db9ae654c1ba3950a5fff691475a32f511142fcddb"}, - {file = "aiohttp-3.11.13-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:7836587eef675a17d835ec3d98a8c9acdbeb2c1d72b0556f0edf4e855a25e9c1"}, - {file = "aiohttp-3.11.13-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:85fa0b18558eb1427090912bd456a01f71edab0872f4e0f9e4285571941e4090"}, - {file = "aiohttp-3.11.13-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a86dc177eb4c286c19d1823ac296299f59ed8106c9536d2b559f65836e0fb2c6"}, - {file = "aiohttp-3.11.13-cp39-cp39-win32.whl", hash = "sha256:684eea71ab6e8ade86b9021bb62af4bf0881f6be4e926b6b5455de74e420783a"}, - {file = "aiohttp-3.11.13-cp39-cp39-win_amd64.whl", hash = "sha256:82c249f2bfa5ecbe4a1a7902c81c0fba52ed9ebd0176ab3047395d02ad96cfcb"}, - {file = "aiohttp-3.11.13.tar.gz", hash = "sha256:8ce789231404ca8fff7f693cdce398abf6d90fd5dae2b1847477196c243b1fbb"}, + {file = "aiohttp-3.11.14-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e2bc827c01f75803de77b134afdbf74fa74b62970eafdf190f3244931d7a5c0d"}, + {file = "aiohttp-3.11.14-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e365034c5cf6cf74f57420b57682ea79e19eb29033399dd3f40de4d0171998fa"}, + {file = "aiohttp-3.11.14-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c32593ead1a8c6aabd58f9d7ee706e48beac796bb0cb71d6b60f2c1056f0a65f"}, + {file = "aiohttp-3.11.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4e7c7ec4146a94a307ca4f112802a8e26d969018fabed526efc340d21d3e7d0"}, + {file = "aiohttp-3.11.14-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8b2df9feac55043759aa89f722a967d977d80f8b5865a4153fc41c93b957efc"}, + {file = "aiohttp-3.11.14-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c7571f99525c76a6280f5fe8e194eeb8cb4da55586c3c61c59c33a33f10cfce7"}, + {file = "aiohttp-3.11.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b59d096b5537ec7c85954cb97d821aae35cfccce3357a2cafe85660cc6295628"}, + {file = "aiohttp-3.11.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b42dbd097abb44b3f1156b4bf978ec5853840802d6eee2784857be11ee82c6a0"}, + {file = "aiohttp-3.11.14-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b05774864c87210c531b48dfeb2f7659407c2dda8643104fb4ae5e2c311d12d9"}, + {file = "aiohttp-3.11.14-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4e2e8ef37d4bc110917d038807ee3af82700a93ab2ba5687afae5271b8bc50ff"}, + {file = "aiohttp-3.11.14-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e9faafa74dbb906b2b6f3eb9942352e9e9db8d583ffed4be618a89bd71a4e914"}, + {file = "aiohttp-3.11.14-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:7e7abe865504f41b10777ac162c727af14e9f4db9262e3ed8254179053f63e6d"}, + {file = "aiohttp-3.11.14-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:4848ae31ad44330b30f16c71e4f586cd5402a846b11264c412de99fa768f00f3"}, + {file = "aiohttp-3.11.14-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2d0b46abee5b5737cb479cc9139b29f010a37b1875ee56d142aefc10686a390b"}, + {file = "aiohttp-3.11.14-cp310-cp310-win32.whl", hash = "sha256:a0d2c04a623ab83963576548ce098baf711a18e2c32c542b62322a0b4584b990"}, + {file = "aiohttp-3.11.14-cp310-cp310-win_amd64.whl", hash = "sha256:5409a59d5057f2386bb8b8f8bbcfb6e15505cedd8b2445db510563b5d7ea1186"}, + {file = "aiohttp-3.11.14-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f296d637a50bb15fb6a229fbb0eb053080e703b53dbfe55b1e4bb1c5ed25d325"}, + {file = "aiohttp-3.11.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ec6cd1954ca2bbf0970f531a628da1b1338f594bf5da7e361e19ba163ecc4f3b"}, + {file = "aiohttp-3.11.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:572def4aad0a4775af66d5a2b5923c7de0820ecaeeb7987dcbccda2a735a993f"}, + {file = "aiohttp-3.11.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c68e41c4d576cd6aa6c6d2eddfb32b2acfb07ebfbb4f9da991da26633a3db1a"}, + {file = "aiohttp-3.11.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99b8bbfc8111826aa8363442c0fc1f5751456b008737ff053570f06a151650b3"}, + {file = "aiohttp-3.11.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b0a200e85da5c966277a402736a96457b882360aa15416bf104ca81e6f5807b"}, + {file = "aiohttp-3.11.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d173c0ac508a2175f7c9a115a50db5fd3e35190d96fdd1a17f9cb10a6ab09aa1"}, + {file = "aiohttp-3.11.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:413fe39fd929329f697f41ad67936f379cba06fcd4c462b62e5b0f8061ee4a77"}, + {file = "aiohttp-3.11.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:65c75b14ee74e8eeff2886321e76188cbe938d18c85cff349d948430179ad02c"}, + {file = "aiohttp-3.11.14-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:321238a42ed463848f06e291c4bbfb3d15ba5a79221a82c502da3e23d7525d06"}, + {file = "aiohttp-3.11.14-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:59a05cdc636431f7ce843c7c2f04772437dd816a5289f16440b19441be6511f1"}, + {file = "aiohttp-3.11.14-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:daf20d9c3b12ae0fdf15ed92235e190f8284945563c4b8ad95b2d7a31f331cd3"}, + {file = "aiohttp-3.11.14-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:05582cb2d156ac7506e68b5eac83179faedad74522ed88f88e5861b78740dc0e"}, + {file = "aiohttp-3.11.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:12c5869e7ddf6b4b1f2109702b3cd7515667b437da90a5a4a50ba1354fe41881"}, + {file = "aiohttp-3.11.14-cp311-cp311-win32.whl", hash = "sha256:92868f6512714efd4a6d6cb2bfc4903b997b36b97baea85f744229f18d12755e"}, + {file = "aiohttp-3.11.14-cp311-cp311-win_amd64.whl", hash = "sha256:bccd2cb7aa5a3bfada72681bdb91637094d81639e116eac368f8b3874620a654"}, + {file = "aiohttp-3.11.14-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:70ab0f61c1a73d3e0342cedd9a7321425c27a7067bebeeacd509f96695b875fc"}, + {file = "aiohttp-3.11.14-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:602d4db80daf4497de93cb1ce00b8fc79969c0a7cf5b67bec96fa939268d806a"}, + {file = "aiohttp-3.11.14-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a8a0d127c10b8d89e69bbd3430da0f73946d839e65fec00ae48ca7916a31948"}, + {file = "aiohttp-3.11.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9f835cdfedcb3f5947304e85b8ca3ace31eef6346d8027a97f4de5fb687534"}, + {file = "aiohttp-3.11.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8aa5c68e1e68fff7cd3142288101deb4316b51f03d50c92de6ea5ce646e6c71f"}, + {file = "aiohttp-3.11.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b512f1de1c688f88dbe1b8bb1283f7fbeb7a2b2b26e743bb2193cbadfa6f307"}, + {file = "aiohttp-3.11.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc9253069158d57e27d47a8453d8a2c5a370dc461374111b5184cf2f147a3cc3"}, + {file = "aiohttp-3.11.14-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b2501f1b981e70932b4a552fc9b3c942991c7ae429ea117e8fba57718cdeed0"}, + {file = "aiohttp-3.11.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:28a3d083819741592685762d51d789e6155411277050d08066537c5edc4066e6"}, + {file = "aiohttp-3.11.14-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0df3788187559c262922846087e36228b75987f3ae31dd0a1e5ee1034090d42f"}, + {file = "aiohttp-3.11.14-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e73fa341d8b308bb799cf0ab6f55fc0461d27a9fa3e4582755a3d81a6af8c09"}, + {file = "aiohttp-3.11.14-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:51ba80d473eb780a329d73ac8afa44aa71dfb521693ccea1dea8b9b5c4df45ce"}, + {file = "aiohttp-3.11.14-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8d1dd75aa4d855c7debaf1ef830ff2dfcc33f893c7db0af2423ee761ebffd22b"}, + {file = "aiohttp-3.11.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41cf0cefd9e7b5c646c2ef529c8335e7eafd326f444cc1cdb0c47b6bc836f9be"}, + {file = "aiohttp-3.11.14-cp312-cp312-win32.whl", hash = "sha256:948abc8952aff63de7b2c83bfe3f211c727da3a33c3a5866a0e2cf1ee1aa950f"}, + {file = "aiohttp-3.11.14-cp312-cp312-win_amd64.whl", hash = "sha256:3b420d076a46f41ea48e5fcccb996f517af0d406267e31e6716f480a3d50d65c"}, + {file = "aiohttp-3.11.14-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d14e274828561db91e4178f0057a915f3af1757b94c2ca283cb34cbb6e00b50"}, + {file = "aiohttp-3.11.14-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f30fc72daf85486cdcdfc3f5e0aea9255493ef499e31582b34abadbfaafb0965"}, + {file = "aiohttp-3.11.14-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4edcbe34e6dba0136e4cabf7568f5a434d89cc9de5d5155371acda275353d228"}, + {file = "aiohttp-3.11.14-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a7169ded15505f55a87f8f0812c94c9412623c744227b9e51083a72a48b68a5"}, + {file = "aiohttp-3.11.14-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad1f2fb9fe9b585ea4b436d6e998e71b50d2b087b694ab277b30e060c434e5db"}, + {file = "aiohttp-3.11.14-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:20412c7cc3720e47a47e63c0005f78c0c2370020f9f4770d7fc0075f397a9fb0"}, + {file = "aiohttp-3.11.14-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dd9766da617855f7e85f27d2bf9a565ace04ba7c387323cd3e651ac4329db91"}, + {file = "aiohttp-3.11.14-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:599b66582f7276ebefbaa38adf37585e636b6a7a73382eb412f7bc0fc55fb73d"}, + {file = "aiohttp-3.11.14-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b41693b7388324b80f9acfabd479bd1c84f0bc7e8f17bab4ecd9675e9ff9c734"}, + {file = "aiohttp-3.11.14-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:86135c32d06927339c8c5e64f96e4eee8825d928374b9b71a3c42379d7437058"}, + {file = "aiohttp-3.11.14-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:04eb541ce1e03edc1e3be1917a0f45ac703e913c21a940111df73a2c2db11d73"}, + {file = "aiohttp-3.11.14-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dc311634f6f28661a76cbc1c28ecf3b3a70a8edd67b69288ab7ca91058eb5a33"}, + {file = "aiohttp-3.11.14-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:69bb252bfdca385ccabfd55f4cd740d421dd8c8ad438ded9637d81c228d0da49"}, + {file = "aiohttp-3.11.14-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2b86efe23684b58a88e530c4ab5b20145f102916bbb2d82942cafec7bd36a647"}, + {file = "aiohttp-3.11.14-cp313-cp313-win32.whl", hash = "sha256:b9c60d1de973ca94af02053d9b5111c4fbf97158e139b14f1be68337be267be6"}, + {file = "aiohttp-3.11.14-cp313-cp313-win_amd64.whl", hash = "sha256:0a29be28e60e5610d2437b5b2fed61d6f3dcde898b57fb048aa5079271e7f6f3"}, + {file = "aiohttp-3.11.14-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:14fc03508359334edc76d35b2821832f092c8f092e4b356e74e38419dfe7b6de"}, + {file = "aiohttp-3.11.14-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:92007c89a8cb7be35befa2732b0b32bf3a394c1b22ef2dff0ef12537d98a7bda"}, + {file = "aiohttp-3.11.14-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6d3986112e34eaa36e280dc8286b9dd4cc1a5bcf328a7f147453e188f6fe148f"}, + {file = "aiohttp-3.11.14-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:749f1eb10e51dbbcdba9df2ef457ec060554842eea4d23874a3e26495f9e87b1"}, + {file = "aiohttp-3.11.14-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:781c8bd423dcc4641298c8c5a2a125c8b1c31e11f828e8d35c1d3a722af4c15a"}, + {file = "aiohttp-3.11.14-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:997b57e38aa7dc6caab843c5e042ab557bc83a2f91b7bd302e3c3aebbb9042a1"}, + {file = "aiohttp-3.11.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a8b0321e40a833e381d127be993b7349d1564b756910b28b5f6588a159afef3"}, + {file = "aiohttp-3.11.14-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8778620396e554b758b59773ab29c03b55047841d8894c5e335f12bfc45ebd28"}, + {file = "aiohttp-3.11.14-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e906da0f2bcbf9b26cc2b144929e88cb3bf943dd1942b4e5af066056875c7618"}, + {file = "aiohttp-3.11.14-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:87f0e003fb4dd5810c7fbf47a1239eaa34cd929ef160e0a54c570883125c4831"}, + {file = "aiohttp-3.11.14-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:7f2dadece8b85596ac3ab1ec04b00694bdd62abc31e5618f524648d18d9dd7fa"}, + {file = "aiohttp-3.11.14-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:fe846f0a98aa9913c2852b630cd39b4098f296e0907dd05f6c7b30d911afa4c3"}, + {file = "aiohttp-3.11.14-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ced66c5c6ad5bcaf9be54560398654779ec1c3695f1a9cf0ae5e3606694a000a"}, + {file = "aiohttp-3.11.14-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a40087b82f83bd671cbeb5f582c233d196e9653220404a798798bfc0ee189fff"}, + {file = "aiohttp-3.11.14-cp39-cp39-win32.whl", hash = "sha256:95d7787f2bcbf7cb46823036a8d64ccfbc2ffc7d52016b4044d901abceeba3db"}, + {file = "aiohttp-3.11.14-cp39-cp39-win_amd64.whl", hash = "sha256:22a8107896877212130c58f74e64b77f7007cb03cea8698be317272643602d45"}, + {file = "aiohttp-3.11.14.tar.gz", hash = "sha256:d6edc538c7480fa0a3b2bdd705f8010062d74700198da55d16498e1b49549b9c"}, ] [package.dependencies] @@ -184,13 +184,13 @@ vertex = ["google-auth (>=2,<3)"] [[package]] name = "anyio" -version = "4.8.0" +version = "4.9.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" files = [ - {file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"}, - {file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"}, + {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, + {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, ] [package.dependencies] @@ -200,8 +200,8 @@ sniffio = ">=1.1" typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] -doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] +doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] +test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] trio = ["trio (>=0.26.1)"] [[package]] @@ -447,17 +447,17 @@ files = [ [[package]] name = "boto3" -version = "1.37.13" +version = "1.37.14" description = "The AWS SDK for Python" optional = true python-versions = ">=3.8" files = [ - {file = "boto3-1.37.13-py3-none-any.whl", hash = "sha256:90fa5a91d7d7456219f0b7c4a93b38335dc5cf4613d885da4d4c1d099e04c6b7"}, - {file = "boto3-1.37.13.tar.gz", hash = "sha256:295648f887464ab74c5c301a44982df76f9ba39ebfc16be5b8f071ad1a81fe95"}, + {file = "boto3-1.37.14-py3-none-any.whl", hash = "sha256:56b4d1e084dbca43d5fdd070f633a84de61a6ce592655b4d239d263d1a0097fc"}, + {file = "boto3-1.37.14.tar.gz", hash = "sha256:cf2e5e6d56efd5850db8ce3d9094132e4759cf2d4b5fd8200d69456bf61a20f3"}, ] [package.dependencies] -botocore = ">=1.37.13,<1.38.0" +botocore = ">=1.37.14,<1.38.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.11.0,<0.12.0" @@ -466,13 +466,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.37.13" +version = "1.37.14" description = "Low-level, data-driven core of boto 3." optional = true python-versions = ">=3.8" files = [ - {file = "botocore-1.37.13-py3-none-any.whl", hash = "sha256:aa417bac0f4d79533080e6e17c0509e149353aec83cfe7879597a7942f7f08d0"}, - {file = "botocore-1.37.13.tar.gz", hash = "sha256:60dfb831c54eb466db9b91891a6c8a0c223626caa049969d5d42858ad1e7f8c7"}, + {file = "botocore-1.37.14-py3-none-any.whl", hash = "sha256:709a1796f436f8e378e52170e58501c1f3b5f2d1308238cf1d6a3bdba2e32851"}, + {file = "botocore-1.37.14.tar.gz", hash = "sha256:b0adce3f0fb42b914eb05079f50cf368cb9cf9745fdd206bd91fe6ac67b29aca"}, ] [package.dependencies] @@ -874,13 +874,13 @@ test = ["pytest"] [[package]] name = "composio-core" -version = "0.7.8" +version = "0.7.9" description = "Core package to act as a bridge between composio platform and other services." optional = false python-versions = "<4,>=3.9" files = [ - {file = "composio_core-0.7.8-py3-none-any.whl", hash = "sha256:c481f02d64e1b7f5a7907bde626c36271b116cc6c7d82439ce37f7f7bbeea583"}, - {file = "composio_core-0.7.8.tar.gz", hash = "sha256:7bf5fde0889c353fd79654e90f216f60cc8c36b190b1b406bfa95d6fcfcdc73f"}, + {file = "composio_core-0.7.9-py3-none-any.whl", hash = "sha256:0712330111eb05b58bf97131ea04597882326326928d055c88cb34c6c0a15241"}, + {file = "composio_core-0.7.9.tar.gz", hash = "sha256:70afebfbcf0c89cbfa4c1f0ec24413753298004c5202e4860c83aa6a5688f537"}, ] [package.dependencies] @@ -911,13 +911,13 @@ tools = ["diskcache", "flake8", "networkx", "pathspec", "pygments", "ruff", "tra [[package]] name = "composio-langchain" -version = "0.7.8" +version = "0.7.9" description = "Use Composio to get an array of tools with your LangChain agent." optional = false python-versions = "<4,>=3.9" files = [ - {file = "composio_langchain-0.7.8-py3-none-any.whl", hash = "sha256:78c49c8387d83e573b3d4837325c9f44ff4ca0adc0a9aadbaf31d6953ed01ef3"}, - {file = "composio_langchain-0.7.8.tar.gz", hash = "sha256:b6dd2f9ff0bdd50e01200d837e1d00806590591da4abd3bf1067e17b3efbbd62"}, + {file = "composio_langchain-0.7.9-py3-none-any.whl", hash = "sha256:6536fd728b716716bd2ec2c7c3a85b2366b1739fe4b0bddd620e5d21f85d37ca"}, + {file = "composio_langchain-0.7.9.tar.gz", hash = "sha256:9dbfa3f77f862a8daddf413d8c552219bbe963cfa0441f6b5153aaba3b75602e"}, ] [package.dependencies] @@ -1764,20 +1764,20 @@ websockets = ">=13.0,<15.0dev" [[package]] name = "googleapis-common-protos" -version = "1.69.1" +version = "1.69.2" description = "Common protobufs used in Google APIs" optional = false python-versions = ">=3.7" files = [ - {file = "googleapis_common_protos-1.69.1-py2.py3-none-any.whl", hash = "sha256:4077f27a6900d5946ee5a369fab9c8ded4c0ef1c6e880458ea2f70c14f7b70d5"}, - {file = "googleapis_common_protos-1.69.1.tar.gz", hash = "sha256:e20d2d8dda87da6fe7340afbbdf4f0bcb4c8fae7e6cadf55926c31f946b0b9b1"}, + {file = "googleapis_common_protos-1.69.2-py3-none-any.whl", hash = "sha256:0b30452ff9c7a27d80bfc5718954063e8ab53dd3697093d3bc99581f5fd24212"}, + {file = "googleapis_common_protos-1.69.2.tar.gz", hash = "sha256:3e1b904a27a33c821b4b749fd31d334c0c9c30e6113023d495e48979a3dc9c5f"}, ] [package.dependencies] -protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" +protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" [package.extras] -grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] +grpc = ["grpcio (>=1.44.0,<2.0.0)"] [[package]] name = "greenlet" @@ -2653,18 +2653,18 @@ typing-extensions = ">=4.7" [[package]] name = "langchain-openai" -version = "0.3.8" +version = "0.3.9" description = "An integration package connecting OpenAI and LangChain" optional = false python-versions = "<4.0,>=3.9" files = [ - {file = "langchain_openai-0.3.8-py3-none-any.whl", hash = "sha256:9004dc8ef853aece0d8f0feca7753dc97f710fa3e53874c8db66466520436dbb"}, - {file = "langchain_openai-0.3.8.tar.gz", hash = "sha256:4d73727eda8102d1d07a2ca036278fccab0bb5e0abf353cec9c3973eb72550ec"}, + {file = "langchain_openai-0.3.9-py3-none-any.whl", hash = "sha256:1ad95c09a620910c39a8eb826eb146bd96bfbc55e4fca78b1e28ffd5e4f5b261"}, + {file = "langchain_openai-0.3.9.tar.gz", hash = "sha256:a2897d15765a435eff3fed7043235c25ec1e192e6c45a81e9e4fae2951335fb3"}, ] [package.dependencies] -langchain-core = ">=0.3.42,<1.0.0" -openai = ">=1.58.1,<2.0.0" +langchain-core = ">=0.3.45,<1.0.0" +openai = ">=1.66.3,<2.0.0" tiktoken = ">=0.7,<1" [[package]] @@ -2727,13 +2727,13 @@ pytest = ["pytest (>=7.0.0)", "rich (>=13.9.4,<14.0.0)"] [[package]] name = "letta-client" -version = "0.1.71" +version = "0.1.75" description = "" optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "letta_client-0.1.71-py3-none-any.whl", hash = "sha256:b18831ae94c2e5685a95e0cec2f7530cebe1d26377a6e3aee6c193518cd855f6"}, - {file = "letta_client-0.1.71.tar.gz", hash = "sha256:4c5a865cfef82091f005dbe1f3280bcd44bcc37bebd472f8145c881e9dd4d074"}, + {file = "letta_client-0.1.75-py3-none-any.whl", hash = "sha256:9bb94357d19997a8c5116aacb5c5c803ab1ba74f45959a79c57a0c41a4a7e740"}, + {file = "letta_client-0.1.75.tar.gz", hash = "sha256:959f805ef4ab16a8fd719715dba8e336d69ed01b3990dd561da589eb4a650760"}, ] [package.dependencies] @@ -3013,13 +3013,13 @@ llama-cloud-services = ">=0.6.4" [[package]] name = "locust" -version = "2.33.1" +version = "2.33.2" description = "Developer-friendly load testing framework" optional = true python-versions = ">=3.9" files = [ - {file = "locust-2.33.1-py3-none-any.whl", hash = "sha256:5a658fa65e37ea5cc0b4fb8c57055d30d86e734a4af9a00b6db7c746222896f2"}, - {file = "locust-2.33.1.tar.gz", hash = "sha256:610da1600c56a15edb11bc77370c26ba6d29f54624426c4004ca9a58c2ae38a4"}, + {file = "locust-2.33.2-py3-none-any.whl", hash = "sha256:a2f3b53dcd5ed22cecee874cd989912749663d82ec9b030637d3e43044e5878e"}, + {file = "locust-2.33.2.tar.gz", hash = "sha256:e626ed0156f36cec94c3c6b030fc91046469e7e2f5c2e91a99aab0f28b84977e"}, ] [package.dependencies] @@ -3320,103 +3320,103 @@ files = [ [[package]] name = "multidict" -version = "6.1.0" +version = "6.2.0" description = "multidict implementation" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"}, - {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"}, - {file = "multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53"}, - {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5"}, - {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581"}, - {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56"}, - {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429"}, - {file = "multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748"}, - {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db"}, - {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056"}, - {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76"}, - {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160"}, - {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7"}, - {file = "multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0"}, - {file = "multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d"}, - {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6"}, - {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156"}, - {file = "multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb"}, - {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b"}, - {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72"}, - {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304"}, - {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351"}, - {file = "multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb"}, - {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3"}, - {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399"}, - {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423"}, - {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3"}, - {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753"}, - {file = "multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80"}, - {file = "multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926"}, - {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa"}, - {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436"}, - {file = "multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761"}, - {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e"}, - {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef"}, - {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95"}, - {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925"}, - {file = "multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966"}, - {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305"}, - {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2"}, - {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2"}, - {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6"}, - {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3"}, - {file = "multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133"}, - {file = "multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1"}, - {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008"}, - {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f"}, - {file = "multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28"}, - {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b"}, - {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c"}, - {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3"}, - {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44"}, - {file = "multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2"}, - {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3"}, - {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa"}, - {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa"}, - {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4"}, - {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6"}, - {file = "multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81"}, - {file = "multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774"}, - {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392"}, - {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a"}, - {file = "multidict-6.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2"}, - {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc"}, - {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478"}, - {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4"}, - {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d"}, - {file = "multidict-6.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6"}, - {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2"}, - {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd"}, - {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6"}, - {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492"}, - {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd"}, - {file = "multidict-6.1.0-cp38-cp38-win32.whl", hash = "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167"}, - {file = "multidict-6.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef"}, - {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c"}, - {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1"}, - {file = "multidict-6.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c"}, - {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c"}, - {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f"}, - {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875"}, - {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255"}, - {file = "multidict-6.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30"}, - {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057"}, - {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657"}, - {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28"}, - {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972"}, - {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43"}, - {file = "multidict-6.1.0-cp39-cp39-win32.whl", hash = "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada"}, - {file = "multidict-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a"}, - {file = "multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506"}, - {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"}, + {file = "multidict-6.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b9f6392d98c0bd70676ae41474e2eecf4c7150cb419237a41f8f96043fcb81d1"}, + {file = "multidict-6.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3501621d5e86f1a88521ea65d5cad0a0834c77b26f193747615b7c911e5422d2"}, + {file = "multidict-6.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:32ed748ff9ac682eae7859790d3044b50e3076c7d80e17a44239683769ff485e"}, + {file = "multidict-6.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc826b9a8176e686b67aa60fd6c6a7047b0461cae5591ea1dc73d28f72332a8a"}, + {file = "multidict-6.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:214207dcc7a6221d9942f23797fe89144128a71c03632bf713d918db99bd36de"}, + {file = "multidict-6.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:05fefbc3cddc4e36da209a5e49f1094bbece9a581faa7f3589201fd95df40e5d"}, + {file = "multidict-6.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e851e6363d0dbe515d8de81fd544a2c956fdec6f8a049739562286727d4a00c3"}, + {file = "multidict-6.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32c9b4878f48be3e75808ea7e499d6223b1eea6d54c487a66bc10a1871e3dc6a"}, + {file = "multidict-6.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7243c5a6523c5cfeca76e063efa5f6a656d1d74c8b1fc64b2cd1e84e507f7e2a"}, + {file = "multidict-6.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0e5a644e50ef9fb87878d4d57907f03a12410d2aa3b93b3acdf90a741df52c49"}, + {file = "multidict-6.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0dc25a3293c50744796e87048de5e68996104d86d940bb24bc3ec31df281b191"}, + {file = "multidict-6.2.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a49994481b99cd7dedde07f2e7e93b1d86c01c0fca1c32aded18f10695ae17eb"}, + {file = "multidict-6.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:641cf2e3447c9ecff2f7aa6e9eee9eaa286ea65d57b014543a4911ff2799d08a"}, + {file = "multidict-6.2.0-cp310-cp310-win32.whl", hash = "sha256:0c383d28857f66f5aebe3e91d6cf498da73af75fbd51cedbe1adfb85e90c0460"}, + {file = "multidict-6.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:a33273a541f1e1a8219b2a4ed2de355848ecc0254264915b9290c8d2de1c74e1"}, + {file = "multidict-6.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:84e87a7d75fa36839a3a432286d719975362d230c70ebfa0948549cc38bd5b46"}, + {file = "multidict-6.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8de4d42dffd5ced9117af2ce66ba8722402541a3aa98ffdf78dde92badb68932"}, + {file = "multidict-6.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d91a230c7f8af86c904a5a992b8c064b66330544693fd6759c3d6162382ecf"}, + {file = "multidict-6.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f6cad071960ba1914fa231677d21b1b4a3acdcce463cee41ea30bc82e6040cf"}, + {file = "multidict-6.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f74f2fc51555f4b037ef278efc29a870d327053aba5cb7d86ae572426c7cccc"}, + {file = "multidict-6.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:14ed9ed1bfedd72a877807c71113deac292bf485159a29025dfdc524c326f3e1"}, + {file = "multidict-6.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ac3fcf9a2d369bd075b2c2965544036a27ccd277fc3c04f708338cc57533081"}, + {file = "multidict-6.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fc6af8e39f7496047c7876314f4317736eac82bf85b54c7c76cf1a6f8e35d98"}, + {file = "multidict-6.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5f8cb1329f42fadfb40d6211e5ff568d71ab49be36e759345f91c69d1033d633"}, + {file = "multidict-6.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5389445f0173c197f4a3613713b5fb3f3879df1ded2a1a2e4bc4b5b9c5441b7e"}, + {file = "multidict-6.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:94a7bb972178a8bfc4055db80c51efd24baefaced5e51c59b0d598a004e8305d"}, + {file = "multidict-6.2.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da51d8928ad8b4244926fe862ba1795f0b6e68ed8c42cd2f822d435db9c2a8f4"}, + {file = "multidict-6.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:063be88bd684782a0715641de853e1e58a2f25b76388538bd62d974777ce9bc2"}, + {file = "multidict-6.2.0-cp311-cp311-win32.whl", hash = "sha256:52b05e21ff05729fbea9bc20b3a791c3c11da61649ff64cce8257c82a020466d"}, + {file = "multidict-6.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1e2a2193d3aa5cbf5758f6d5680a52aa848e0cf611da324f71e5e48a9695cc86"}, + {file = "multidict-6.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:437c33561edb6eb504b5a30203daf81d4a9b727e167e78b0854d9a4e18e8950b"}, + {file = "multidict-6.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9f49585f4abadd2283034fc605961f40c638635bc60f5162276fec075f2e37a4"}, + {file = "multidict-6.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5dd7106d064d05896ce28c97da3f46caa442fe5a43bc26dfb258e90853b39b44"}, + {file = "multidict-6.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e25b11a0417475f093d0f0809a149aff3943c2c56da50fdf2c3c88d57fe3dfbd"}, + {file = "multidict-6.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac380cacdd3b183338ba63a144a34e9044520a6fb30c58aa14077157a033c13e"}, + {file = "multidict-6.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61d5541f27533f803a941d3a3f8a3d10ed48c12cf918f557efcbf3cd04ef265c"}, + {file = "multidict-6.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:facaf11f21f3a4c51b62931feb13310e6fe3475f85e20d9c9fdce0d2ea561b87"}, + {file = "multidict-6.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:095a2eabe8c43041d3e6c2cb8287a257b5f1801c2d6ebd1dd877424f1e89cf29"}, + {file = "multidict-6.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0cc398350ef31167e03f3ca7c19313d4e40a662adcb98a88755e4e861170bdd"}, + {file = "multidict-6.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7c611345bbe7cb44aabb877cb94b63e86f2d0db03e382667dbd037866d44b4f8"}, + {file = "multidict-6.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8cd1a0644ccaf27e9d2f6d9c9474faabee21f0578fe85225cc5af9a61e1653df"}, + {file = "multidict-6.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:89b3857652183b8206a891168af47bac10b970d275bba1f6ee46565a758c078d"}, + {file = "multidict-6.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:125dd82b40f8c06d08d87b3510beaccb88afac94e9ed4a6f6c71362dc7dbb04b"}, + {file = "multidict-6.2.0-cp312-cp312-win32.whl", hash = "sha256:76b34c12b013d813e6cb325e6bd4f9c984db27758b16085926bbe7ceeaace626"}, + {file = "multidict-6.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:0b183a959fb88ad1be201de2c4bdf52fa8e46e6c185d76201286a97b6f5ee65c"}, + {file = "multidict-6.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5c5e7d2e300d5cb3b2693b6d60d3e8c8e7dd4ebe27cd17c9cb57020cac0acb80"}, + {file = "multidict-6.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:256d431fe4583c5f1e0f2e9c4d9c22f3a04ae96009b8cfa096da3a8723db0a16"}, + {file = "multidict-6.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a3c0ff89fe40a152e77b191b83282c9664357dce3004032d42e68c514ceff27e"}, + {file = "multidict-6.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef7d48207926edbf8b16b336f779c557dd8f5a33035a85db9c4b0febb0706817"}, + {file = "multidict-6.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3c099d3899b14e1ce52262eb82a5f5cb92157bb5106bf627b618c090a0eadc"}, + {file = "multidict-6.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e16e7297f29a544f49340012d6fc08cf14de0ab361c9eb7529f6a57a30cbfda1"}, + {file = "multidict-6.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:042028348dc5a1f2be6c666437042a98a5d24cee50380f4c0902215e5ec41844"}, + {file = "multidict-6.2.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:08549895e6a799bd551cf276f6e59820aa084f0f90665c0f03dd3a50db5d3c48"}, + {file = "multidict-6.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ccfd74957ef53fa7380aaa1c961f523d582cd5e85a620880ffabd407f8202c0"}, + {file = "multidict-6.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83b78c680d4b15d33042d330c2fa31813ca3974197bddb3836a5c635a5fd013f"}, + {file = "multidict-6.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b4c153863dd6569f6511845922c53e39c8d61f6e81f228ad5443e690fca403de"}, + {file = "multidict-6.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:98aa8325c7f47183b45588af9c434533196e241be0a4e4ae2190b06d17675c02"}, + {file = "multidict-6.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9e658d1373c424457ddf6d55ec1db93c280b8579276bebd1f72f113072df8a5d"}, + {file = "multidict-6.2.0-cp313-cp313-win32.whl", hash = "sha256:3157126b028c074951839233647bd0e30df77ef1fedd801b48bdcad242a60f4e"}, + {file = "multidict-6.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:2e87f1926e91855ae61769ba3e3f7315120788c099677e0842e697b0bfb659f2"}, + {file = "multidict-6.2.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:2529ddbdaa424b2c6c2eb668ea684dd6b75b839d0ad4b21aad60c168269478d7"}, + {file = "multidict-6.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:13551d0e2d7201f0959725a6a769b6f7b9019a168ed96006479c9ac33fe4096b"}, + {file = "multidict-6.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d1996ee1330e245cd3aeda0887b4409e3930524c27642b046e4fae88ffa66c5e"}, + {file = "multidict-6.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c537da54ce4ff7c15e78ab1292e5799d0d43a2108e006578a57f531866f64025"}, + {file = "multidict-6.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f249badb360b0b4d694307ad40f811f83df4da8cef7b68e429e4eea939e49dd"}, + {file = "multidict-6.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48d39b1824b8d6ea7de878ef6226efbe0773f9c64333e1125e0efcfdd18a24c7"}, + {file = "multidict-6.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b99aac6bb2c37db336fa03a39b40ed4ef2818bf2dfb9441458165ebe88b793af"}, + {file = "multidict-6.2.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07bfa8bc649783e703263f783f73e27fef8cd37baaad4389816cf6a133141331"}, + {file = "multidict-6.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2c00ad31fbc2cbac85d7d0fcf90853b2ca2e69d825a2d3f3edb842ef1544a2c"}, + {file = "multidict-6.2.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d57a01a2a9fa00234aace434d8c131f0ac6e0ac6ef131eda5962d7e79edfb5b"}, + {file = "multidict-6.2.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:abf5b17bc0cf626a8a497d89ac691308dbd825d2ac372aa990b1ca114e470151"}, + {file = "multidict-6.2.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:f7716f7e7138252d88607228ce40be22660d6608d20fd365d596e7ca0738e019"}, + {file = "multidict-6.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d5a36953389f35f0a4e88dc796048829a2f467c9197265504593f0e420571547"}, + {file = "multidict-6.2.0-cp313-cp313t-win32.whl", hash = "sha256:e653d36b1bf48fa78c7fcebb5fa679342e025121ace8c87ab05c1cefd33b34fc"}, + {file = "multidict-6.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ca23db5fb195b5ef4fd1f77ce26cadefdf13dba71dab14dadd29b34d457d7c44"}, + {file = "multidict-6.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b4f3d66dd0354b79761481fc15bdafaba0b9d9076f1f42cc9ce10d7fcbda205a"}, + {file = "multidict-6.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e2a2d6749e1ff2c9c76a72c6530d5baa601205b14e441e6d98011000f47a7ac"}, + {file = "multidict-6.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cca83a629f77402cfadd58352e394d79a61c8015f1694b83ab72237ec3941f88"}, + {file = "multidict-6.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:781b5dd1db18c9e9eacc419027b0acb5073bdec9de1675c0be25ceb10e2ad133"}, + {file = "multidict-6.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf8d370b2fea27fb300825ec3984334f7dd54a581bde6456799ba3776915a656"}, + {file = "multidict-6.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25bb96338512e2f46f615a2bb7c6012fe92a4a5ebd353e5020836a7e33120349"}, + {file = "multidict-6.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19e2819b0b468174de25c0ceed766606a07cedeab132383f1e83b9a4e96ccb4f"}, + {file = "multidict-6.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6aed763b6a1b28c46c055692836879328f0b334a6d61572ee4113a5d0c859872"}, + {file = "multidict-6.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a1133414b771619aa3c3000701c11b2e4624a7f492f12f256aedde97c28331a2"}, + {file = "multidict-6.2.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:639556758c36093b35e2e368ca485dada6afc2bd6a1b1207d85ea6dfc3deab27"}, + {file = "multidict-6.2.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:163f4604e76639f728d127293d24c3e208b445b463168af3d031b92b0998bb90"}, + {file = "multidict-6.2.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2325105e16d434749e1be8022f942876a936f9bece4ec41ae244e3d7fae42aaf"}, + {file = "multidict-6.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e4371591e621579cb6da8401e4ea405b33ff25a755874a3567c4075ca63d56e2"}, + {file = "multidict-6.2.0-cp39-cp39-win32.whl", hash = "sha256:d1175b0e0d6037fab207f05774a176d71210ebd40b1c51f480a04b65ec5c786d"}, + {file = "multidict-6.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:ad81012b24b88aad4c70b2cbc2dad84018783221b7f923e926f4690ff8569da3"}, + {file = "multidict-6.2.0-py3-none-any.whl", hash = "sha256:5d26547423e5e71dcc562c4acdc134b900640a39abd9066d7326a7cc2324c530"}, + {file = "multidict-6.2.0.tar.gz", hash = "sha256:0085b0afb2446e57050140240a8595846ed64d1cbd26cef936bfab3192c673b8"}, ] [package.dependencies] @@ -4386,7 +4386,6 @@ files = [ {file = "psycopg2-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:0435034157049f6846e95103bd8f5a668788dd913a7c30162ca9503fdf542cb4"}, {file = "psycopg2-2.9.10-cp312-cp312-win32.whl", hash = "sha256:65a63d7ab0e067e2cdb3cf266de39663203d38d6a8ed97f5ca0cb315c73fe067"}, {file = "psycopg2-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:4a579d6243da40a7b3182e0430493dbd55950c493d8c68f4eec0b302f6bbf20e"}, - {file = "psycopg2-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:91fd603a2155da8d0cfcdbf8ab24a2d54bca72795b90d2a3ed2b6da8d979dee2"}, {file = "psycopg2-2.9.10-cp39-cp39-win32.whl", hash = "sha256:9d5b3b94b79a844a986d029eee38998232451119ad653aea42bb9220a8c5066b"}, {file = "psycopg2-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:88138c8dedcbfa96408023ea2b0c369eda40fe5d75002c0964c78f46f11fa442"}, {file = "psycopg2-2.9.10.tar.gz", hash = "sha256:12ec0b40b0273f95296233e8750441339298e6a572f7039da5b260e3c8b60e11"}, @@ -4446,7 +4445,6 @@ files = [ {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567"}, - {file = "psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864"}, @@ -4739,13 +4737,13 @@ tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] [[package]] name = "pypdf" -version = "5.3.1" +version = "5.4.0" description = "A pure-python PDF library capable of splitting, merging, cropping, and transforming PDF files" optional = false python-versions = ">=3.8" files = [ - {file = "pypdf-5.3.1-py3-none-any.whl", hash = "sha256:20ea5b8686faad1b695fda054462b667d5e5f51e25fbbc092f12c5e0bb20d738"}, - {file = "pypdf-5.3.1.tar.gz", hash = "sha256:0b9b715252b3c60bacc052e6a780e8b742cee9b9a2135f6007bb018e22a5adad"}, + {file = "pypdf-5.4.0-py3-none-any.whl", hash = "sha256:db994ab47cadc81057ea1591b90e5b543e2b7ef2d0e31ef41a9bfe763c119dab"}, + {file = "pypdf-5.4.0.tar.gz", hash = "sha256:9af476a9dc30fcb137659b0dec747ea94aa954933c52cf02ee33e39a16fe9175"}, ] [package.dependencies] @@ -4961,27 +4959,27 @@ files = [ [[package]] name = "pywin32" -version = "309" +version = "310" description = "Python for Window Extensions" optional = false python-versions = "*" files = [ - {file = "pywin32-309-cp310-cp310-win32.whl", hash = "sha256:5b78d98550ca093a6fe7ab6d71733fbc886e2af9d4876d935e7f6e1cd6577ac9"}, - {file = "pywin32-309-cp310-cp310-win_amd64.whl", hash = "sha256:728d08046f3d65b90d4c77f71b6fbb551699e2005cc31bbffd1febd6a08aa698"}, - {file = "pywin32-309-cp310-cp310-win_arm64.whl", hash = "sha256:c667bcc0a1e6acaca8984eb3e2b6e42696fc035015f99ff8bc6c3db4c09a466a"}, - {file = "pywin32-309-cp311-cp311-win32.whl", hash = "sha256:d5df6faa32b868baf9ade7c9b25337fa5eced28eb1ab89082c8dae9c48e4cd51"}, - {file = "pywin32-309-cp311-cp311-win_amd64.whl", hash = "sha256:e7ec2cef6df0926f8a89fd64959eba591a1eeaf0258082065f7bdbe2121228db"}, - {file = "pywin32-309-cp311-cp311-win_arm64.whl", hash = "sha256:54ee296f6d11db1627216e9b4d4c3231856ed2d9f194c82f26c6cb5650163f4c"}, - {file = "pywin32-309-cp312-cp312-win32.whl", hash = "sha256:de9acacced5fa82f557298b1fed5fef7bd49beee04190f68e1e4783fbdc19926"}, - {file = "pywin32-309-cp312-cp312-win_amd64.whl", hash = "sha256:6ff9eebb77ffc3d59812c68db33c0a7817e1337e3537859499bd27586330fc9e"}, - {file = "pywin32-309-cp312-cp312-win_arm64.whl", hash = "sha256:619f3e0a327b5418d833f44dc87859523635cf339f86071cc65a13c07be3110f"}, - {file = "pywin32-309-cp313-cp313-win32.whl", hash = "sha256:008bffd4afd6de8ca46c6486085414cc898263a21a63c7f860d54c9d02b45c8d"}, - {file = "pywin32-309-cp313-cp313-win_amd64.whl", hash = "sha256:bd0724f58492db4cbfbeb1fcd606495205aa119370c0ddc4f70e5771a3ab768d"}, - {file = "pywin32-309-cp313-cp313-win_arm64.whl", hash = "sha256:8fd9669cfd41863b688a1bc9b1d4d2d76fd4ba2128be50a70b0ea66b8d37953b"}, - {file = "pywin32-309-cp38-cp38-win32.whl", hash = "sha256:617b837dc5d9dfa7e156dbfa7d3906c009a2881849a80a9ae7519f3dd8c6cb86"}, - {file = "pywin32-309-cp38-cp38-win_amd64.whl", hash = "sha256:0be3071f555480fbfd86a816a1a773880ee655bf186aa2931860dbb44e8424f8"}, - {file = "pywin32-309-cp39-cp39-win32.whl", hash = "sha256:72ae9ae3a7a6473223589a1621f9001fe802d59ed227fd6a8503c9af67c1d5f4"}, - {file = "pywin32-309-cp39-cp39-win_amd64.whl", hash = "sha256:88bc06d6a9feac70783de64089324568ecbc65866e2ab318eab35da3811fd7ef"}, + {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]] diff --git a/project.json b/project.json index 18b70617..88637f0f 100644 --- a/project.json +++ b/project.json @@ -26,7 +26,8 @@ "dev": { "executor": "@nxlv/python:run-commands", "options": { - "command": "poetry run letta server", + "commands": ["./start-otel-collector.sh", "poetry run letta server"], + "parallel": true, "cwd": "apps/core" } }, diff --git a/pyproject.toml b/pyproject.toml index 7557e221..bcd66f10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "letta" -version = "0.6.42" +version = "0.6.43" packages = [ {include = "letta"}, ] diff --git a/start-otel-collector.sh b/start-otel-collector.sh new file mode 100755 index 00000000..58d5225d --- /dev/null +++ b/start-otel-collector.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -e # Exit on any error + +# Create bin directory if it doesn't exist +mkdir -p bin + +# Download and extract collector if not already present +if [ ! -f "bin/otelcol-contrib" ]; then + echo "Downloading OpenTelemetry Collector..." + curl -L https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v0.96.0/otelcol-contrib_0.96.0_darwin_amd64.tar.gz -o otelcol.tar.gz + tar xzf otelcol.tar.gz -C bin/ + rm otelcol.tar.gz + chmod +x bin/otelcol-contrib +fi + +# Start OpenTelemetry Collector +if [ -n "$CLICKHOUSE_ENDPOINT" ] && [ -n "$CLICKHOUSE_PASSWORD" ]; then + echo "Starting OpenTelemetry Collector with Clickhouse export..." + CONFIG_FILE="otel-collector-config-clickhouse-dev.yaml" +else + echo "Starting OpenTelemetry Collector with file export only..." + CONFIG_FILE="otel-collector-config-file-dev.yaml" +fi + +# Run collector +exec ./bin/otelcol-contrib --config "$CONFIG_FILE" diff --git a/tests/integration_test_agent_tool_graph.py b/tests/integration_test_agent_tool_graph.py index 9c931ee2..6e17bd92 100644 --- a/tests/integration_test_agent_tool_graph.py +++ b/tests/integration_test_agent_tool_graph.py @@ -5,14 +5,14 @@ import pytest from letta import create_client from letta.schemas.letta_message import ToolCallMessage -from letta.schemas.tool_rule import ChildToolRule, ConditionalToolRule, ContinueToolRule, InitToolRule, TerminalToolRule +from letta.schemas.tool_rule import ChildToolRule, ContinueToolRule, InitToolRule, MaxCountPerStepToolRule, TerminalToolRule from tests.helpers.endpoints_helper import ( assert_invoked_function_call, assert_invoked_send_message_with_keyword, assert_sanity_checks, setup_agent, ) -from tests.helpers.utils import cleanup +from tests.helpers.utils import cleanup, retry_until_success # Generate uuid for agent name for this example namespace = uuid.NAMESPACE_DNS @@ -85,25 +85,6 @@ def flip_coin(): return "hj2hwibbqm" -def flip_coin_hard(): - """ - Call this to retrieve the password to the secret word, which you will need to output in a send_message later. - If it returns an empty string, try flipping again! - - Returns: - str: The password or an empty string - """ - import random - - # Flip a coin with 50% chance - result = random.random() - if result < 0.5: - return "" - if result < 0.75: - return "START_OVER" - return "hj2hwibbqm" - - def can_play_game(): """ Call this to start the tool chain. @@ -345,320 +326,243 @@ def test_agent_no_structured_output_with_one_child_tool(mock_e2b_api_key_none): cleanup(client=client, agent_uuid=agent_uuid) -@pytest.mark.timeout(60) # Sets a 60-second timeout for the test since this could loop infinitely -def test_agent_conditional_tool_easy(mock_e2b_api_key_none): - """ - Test the agent with a conditional tool that has a child tool. - - Tool Flow: - - ------- - | | - | v - -- flip_coin - | - v - reveal_secret_word - """ - - client = create_client() - cleanup(client=client, agent_uuid=agent_uuid) - - coin_flip_name = "flip_coin" - secret_word_tool = "fourth_secret_word" - flip_coin_tool = client.create_or_update_tool(flip_coin) - reveal_secret = client.create_or_update_tool(fourth_secret_word) - - # Make tool rules - tool_rules = [ - InitToolRule(tool_name=coin_flip_name), - ConditionalToolRule( - tool_name=coin_flip_name, - default_child=coin_flip_name, - child_output_mapping={ - "hj2hwibbqm": secret_word_tool, - }, - ), - TerminalToolRule(tool_name=secret_word_tool), - ] - tools = [flip_coin_tool, reveal_secret] - - config_file = "tests/configs/llm_model_configs/claude-3-5-sonnet.json" - agent_state = setup_agent(client, config_file, agent_uuid=agent_uuid, tool_ids=[t.id for t in tools], tool_rules=tool_rules) - response = client.user_message(agent_id=agent_state.id, message="flip a coin until you get the secret word") - - # Make checks - assert_sanity_checks(response) - - # Assert the tools were called - assert_invoked_function_call(response.messages, "flip_coin") - assert_invoked_function_call(response.messages, "fourth_secret_word") - - # Check ordering of tool calls - found_secret_word = False - for m in response.messages: - if isinstance(m, ToolCallMessage): - if m.tool_call.name == secret_word_tool: - # Should be the last tool call - found_secret_word = True - else: - # Before finding secret_word, only flip_coin should be called - assert m.tool_call.name == coin_flip_name - assert not found_secret_word - - # Ensure we found the secret word exactly once - assert found_secret_word - - print(f"Got successful response from client: \n\n{response}") - cleanup(client=client, agent_uuid=agent_uuid) +# @pytest.mark.timeout(60) # Sets a 60-second timeout for the test since this could loop infinitely +# def test_agent_conditional_tool_easy(mock_e2b_api_key_none): +# """ +# Test the agent with a conditional tool that has a child tool. +# +# Tool Flow: +# +# ------- +# | | +# | v +# -- flip_coin +# | +# v +# reveal_secret_word +# """ +# +# client = create_client() +# cleanup(client=client, agent_uuid=agent_uuid) +# +# coin_flip_name = "flip_coin" +# secret_word_tool = "fourth_secret_word" +# flip_coin_tool = client.create_or_update_tool(flip_coin) +# reveal_secret = client.create_or_update_tool(fourth_secret_word) +# +# # Make tool rules +# tool_rules = [ +# InitToolRule(tool_name=coin_flip_name), +# ConditionalToolRule( +# tool_name=coin_flip_name, +# default_child=coin_flip_name, +# child_output_mapping={ +# "hj2hwibbqm": secret_word_tool, +# }, +# ), +# TerminalToolRule(tool_name=secret_word_tool), +# ] +# tools = [flip_coin_tool, reveal_secret] +# +# config_file = "tests/configs/llm_model_configs/claude-3-5-sonnet.json" +# agent_state = setup_agent(client, config_file, agent_uuid=agent_uuid, tool_ids=[t.id for t in tools], tool_rules=tool_rules) +# response = client.user_message(agent_id=agent_state.id, message="flip a coin until you get the secret word") +# +# # Make checks +# assert_sanity_checks(response) +# +# # Assert the tools were called +# assert_invoked_function_call(response.messages, "flip_coin") +# assert_invoked_function_call(response.messages, "fourth_secret_word") +# +# # Check ordering of tool calls +# found_secret_word = False +# for m in response.messages: +# if isinstance(m, ToolCallMessage): +# if m.tool_call.name == secret_word_tool: +# # Should be the last tool call +# found_secret_word = True +# else: +# # Before finding secret_word, only flip_coin should be called +# assert m.tool_call.name == coin_flip_name +# assert not found_secret_word +# +# # Ensure we found the secret word exactly once +# assert found_secret_word +# +# print(f"Got successful response from client: \n\n{response}") +# cleanup(client=client, agent_uuid=agent_uuid) -@pytest.mark.timeout(90) # Longer timeout since this test has more steps -def test_agent_conditional_tool_hard(mock_e2b_api_key_none): - """ - Test the agent with a complex conditional tool graph - - Tool Flow: - - can_play_game <---+ - | | - v | - flip_coin -----+ - | - v - fourth_secret_word - """ - client = create_client() - cleanup(client=client, agent_uuid=agent_uuid) - - # Create tools - play_game = "can_play_game" - coin_flip_name = "flip_coin_hard" - final_tool = "fourth_secret_word" - play_game_tool = client.create_or_update_tool(can_play_game) - flip_coin_tool = client.create_or_update_tool(flip_coin_hard) - reveal_secret = client.create_or_update_tool(fourth_secret_word) - - # Make tool rules - chain them together with conditional rules - tool_rules = [ - InitToolRule(tool_name=play_game), - ConditionalToolRule( - tool_name=play_game, - default_child=play_game, # Keep trying if we can't play - child_output_mapping={True: coin_flip_name}, # Only allow access when can_play_game returns True - ), - ConditionalToolRule( - tool_name=coin_flip_name, default_child=coin_flip_name, child_output_mapping={"hj2hwibbqm": final_tool, "START_OVER": play_game} - ), - TerminalToolRule(tool_name=final_tool), - ] - - # Setup agent with all tools - tools = [play_game_tool, flip_coin_tool, reveal_secret] - config_file = "tests/configs/llm_model_configs/claude-3-5-sonnet.json" - agent_state = setup_agent(client, config_file, agent_uuid=agent_uuid, tool_ids=[t.id for t in tools], tool_rules=tool_rules) - - # Ask agent to try to get all secret words - response = client.user_message(agent_id=agent_state.id, message="hi") - - # Make checks - assert_sanity_checks(response) - - # Assert all tools were called - assert_invoked_function_call(response.messages, play_game) - assert_invoked_function_call(response.messages, final_tool) - - # Check ordering of tool calls - found_words = [] - for m in response.messages: - if isinstance(m, ToolCallMessage): - name = m.tool_call.name - if name in [play_game, coin_flip_name]: - # Before finding secret_word, only can_play_game and flip_coin should be called - assert name in [play_game, coin_flip_name] - else: - # Should find secret words in order - expected_word = final_tool - assert name == expected_word, f"Found {name} but expected {expected_word}" - found_words.append(name) - - # Ensure we found all secret words in order - assert found_words == [final_tool] - - print(f"Got successful response from client: \n\n{response}") - cleanup(client=client, agent_uuid=agent_uuid) +# @pytest.mark.timeout(60) +# def test_agent_conditional_tool_without_default_child(mock_e2b_api_key_none): +# """ +# Test the agent with a conditional tool that allows any child tool to be called if a function returns None. +# +# Tool Flow: +# +# return_none +# | +# v +# any tool... <-- When output doesn't match mapping, agent can call any tool +# """ +# client = create_client() +# cleanup(client=client, agent_uuid=agent_uuid) +# +# # Create tools - we'll make several available to the agent +# tool_name = "return_none" +# +# tool = client.create_or_update_tool(return_none) +# secret_word = client.create_or_update_tool(first_secret_word) +# +# # Make tool rules - only map one output, let others be free choice +# tool_rules = [ +# InitToolRule(tool_name=tool_name), +# ConditionalToolRule( +# tool_name=tool_name, +# default_child=None, # Allow any tool to be called if output doesn't match +# child_output_mapping={"anything but none": "first_secret_word"}, +# ), +# ] +# tools = [tool, secret_word] +# +# # Setup agent with all tools +# agent_state = setup_agent(client, config_file, agent_uuid=agent_uuid, tool_ids=[t.id for t in tools], tool_rules=tool_rules) +# +# # Ask agent to try different tools based on the game output +# response = client.user_message(agent_id=agent_state.id, message="call a function, any function. then call send_message") +# +# # Make checks +# assert_sanity_checks(response) +# +# # Assert return_none was called +# assert_invoked_function_call(response.messages, tool_name) +# +# # Assert any base function called afterward +# found_any_tool = False +# found_return_none = False +# for m in response.messages: +# if isinstance(m, ToolCallMessage): +# if m.tool_call.name == tool_name: +# found_return_none = True +# elif found_return_none and m.tool_call.name: +# found_any_tool = True +# break +# +# assert found_any_tool, "Should have called any tool after return_none" +# +# print(f"Got successful response from client: \n\n{response}") +# cleanup(client=client, agent_uuid=agent_uuid) -@pytest.mark.timeout(60) -def test_agent_conditional_tool_without_default_child(mock_e2b_api_key_none): - """ - Test the agent with a conditional tool that allows any child tool to be called if a function returns None. - - Tool Flow: - - return_none - | - v - any tool... <-- When output doesn't match mapping, agent can call any tool - """ - client = create_client() - cleanup(client=client, agent_uuid=agent_uuid) - - # Create tools - we'll make several available to the agent - tool_name = "return_none" - - tool = client.create_or_update_tool(return_none) - secret_word = client.create_or_update_tool(first_secret_word) - - # Make tool rules - only map one output, let others be free choice - tool_rules = [ - InitToolRule(tool_name=tool_name), - ConditionalToolRule( - tool_name=tool_name, - default_child=None, # Allow any tool to be called if output doesn't match - child_output_mapping={"anything but none": "first_secret_word"}, - ), - ] - tools = [tool, secret_word] - - # Setup agent with all tools - agent_state = setup_agent(client, config_file, agent_uuid=agent_uuid, tool_ids=[t.id for t in tools], tool_rules=tool_rules) - - # Ask agent to try different tools based on the game output - response = client.user_message(agent_id=agent_state.id, message="call a function, any function. then call send_message") - - # Make checks - assert_sanity_checks(response) - - # Assert return_none was called - assert_invoked_function_call(response.messages, tool_name) - - # Assert any base function called afterward - found_any_tool = False - found_return_none = False - for m in response.messages: - if isinstance(m, ToolCallMessage): - if m.tool_call.name == tool_name: - found_return_none = True - elif found_return_none and m.tool_call.name: - found_any_tool = True - break - - assert found_any_tool, "Should have called any tool after return_none" - - print(f"Got successful response from client: \n\n{response}") - cleanup(client=client, agent_uuid=agent_uuid) +# @pytest.mark.timeout(60) +# def test_agent_reload_remembers_function_response(mock_e2b_api_key_none): +# """ +# Test that when an agent is reloaded, it remembers the last function response for conditional tool chaining. +# +# Tool Flow: +# +# flip_coin +# | +# v +# fourth_secret_word <-- Should remember coin flip result after reload +# """ +# client = create_client() +# cleanup(client=client, agent_uuid=agent_uuid) +# +# # Create tools +# flip_coin_name = "flip_coin" +# secret_word = "fourth_secret_word" +# flip_coin_tool = client.create_or_update_tool(flip_coin) +# secret_word_tool = client.create_or_update_tool(fourth_secret_word) +# +# # Make tool rules - map coin flip to fourth_secret_word +# tool_rules = [ +# InitToolRule(tool_name=flip_coin_name), +# ConditionalToolRule( +# tool_name=flip_coin_name, +# default_child=flip_coin_name, # Allow any tool to be called if output doesn't match +# child_output_mapping={"hj2hwibbqm": secret_word}, +# ), +# TerminalToolRule(tool_name=secret_word), +# ] +# tools = [flip_coin_tool, secret_word_tool] +# +# # Setup initial agent +# agent_state = setup_agent(client, config_file, agent_uuid=agent_uuid, tool_ids=[t.id for t in tools], tool_rules=tool_rules) +# +# # Call flip_coin first +# response = client.user_message(agent_id=agent_state.id, message="flip a coin") +# assert_invoked_function_call(response.messages, flip_coin_name) +# assert_invoked_function_call(response.messages, secret_word) +# found_fourth_secret = False +# for m in response.messages: +# if isinstance(m, ToolCallMessage) and m.tool_call.name == secret_word: +# found_fourth_secret = True +# break +# +# assert found_fourth_secret, "Reloaded agent should remember coin flip result and call fourth_secret_word if True" +# +# # Reload the agent +# reloaded_agent = client.server.load_agent(agent_id=agent_state.id, actor=client.user) +# assert reloaded_agent.last_function_response is not None +# +# print(f"Got successful response from client: \n\n{response}") +# cleanup(client=client, agent_uuid=agent_uuid) -@pytest.mark.timeout(60) -def test_agent_reload_remembers_function_response(mock_e2b_api_key_none): - """ - Test that when an agent is reloaded, it remembers the last function response for conditional tool chaining. - - Tool Flow: - - flip_coin - | - v - fourth_secret_word <-- Should remember coin flip result after reload - """ - client = create_client() - cleanup(client=client, agent_uuid=agent_uuid) - - # Create tools - flip_coin_name = "flip_coin" - secret_word = "fourth_secret_word" - flip_coin_tool = client.create_or_update_tool(flip_coin) - secret_word_tool = client.create_or_update_tool(fourth_secret_word) - - # Make tool rules - map coin flip to fourth_secret_word - tool_rules = [ - InitToolRule(tool_name=flip_coin_name), - ConditionalToolRule( - tool_name=flip_coin_name, - default_child=flip_coin_name, # Allow any tool to be called if output doesn't match - child_output_mapping={"hj2hwibbqm": secret_word}, - ), - TerminalToolRule(tool_name=secret_word), - ] - tools = [flip_coin_tool, secret_word_tool] - - # Setup initial agent - agent_state = setup_agent(client, config_file, agent_uuid=agent_uuid, tool_ids=[t.id for t in tools], tool_rules=tool_rules) - - # Call flip_coin first - response = client.user_message(agent_id=agent_state.id, message="flip a coin") - assert_invoked_function_call(response.messages, flip_coin_name) - assert_invoked_function_call(response.messages, secret_word) - found_fourth_secret = False - for m in response.messages: - if isinstance(m, ToolCallMessage) and m.tool_call.name == secret_word: - found_fourth_secret = True - break - - assert found_fourth_secret, "Reloaded agent should remember coin flip result and call fourth_secret_word if True" - - # Reload the agent - reloaded_agent = client.server.load_agent(agent_id=agent_state.id, actor=client.user) - assert reloaded_agent.last_function_response is not None - - print(f"Got successful response from client: \n\n{response}") - cleanup(client=client, agent_uuid=agent_uuid) - - -@pytest.mark.timeout(60) # Sets a 60-second timeout for the test since this could loop infinitely -def test_simple_tool_rule(mock_e2b_api_key_none): - """ - Test a simple tool rule where fourth_secret_word must be called after flip_coin. - - Tool Flow: - flip_coin - | - v - fourth_secret_word - """ - client = create_client() - cleanup(client=client, agent_uuid=agent_uuid) - - # Create tools - flip_coin_name = "flip_coin" - secret_word = "fourth_secret_word" - random_tool = "can_play_game" - flip_coin_tool = client.create_or_update_tool(flip_coin) - secret_word_tool = client.create_or_update_tool(fourth_secret_word) - another_secret_word_tool = client.create_or_update_tool(first_secret_word) - random_tool = client.create_or_update_tool(can_play_game) - tools = [flip_coin_tool, secret_word_tool, another_secret_word_tool, random_tool] - - # Create tool rule: after flip_coin, must call fourth_secret_word - tool_rule = ConditionalToolRule( - tool_name=flip_coin_name, - default_child=secret_word, - child_output_mapping={"*": secret_word}, - ) - - # Set up agent with the tool rule - agent_state = setup_agent( - client, config_file, agent_uuid, tool_rules=[tool_rule], tool_ids=[t.id for t in tools], include_base_tools=False - ) - - # Start conversation - response = client.user_message(agent_id=agent_state.id, message="Help me test the tools.") - - # Verify the tool calls - tool_calls = [msg for msg in response.messages if isinstance(msg, ToolCallMessage)] - assert len(tool_calls) >= 2 # Should have at least flip_coin and fourth_secret_word calls - assert_invoked_function_call(response.messages, flip_coin_name) - assert_invoked_function_call(response.messages, secret_word) - - # Find the flip_coin call - flip_coin_call = next((call for call in tool_calls if call.tool_call.name == "flip_coin"), None) - - # Verify that fourth_secret_word was called after flip_coin - flip_coin_call_index = tool_calls.index(flip_coin_call) - assert tool_calls[flip_coin_call_index + 1].tool_call.name == secret_word, "Fourth secret word should be called after flip_coin" - - cleanup(client, agent_uuid=agent_state.id) +# @pytest.mark.timeout(60) # Sets a 60-second timeout for the test since this could loop infinitely +# def test_simple_tool_rule(mock_e2b_api_key_none): +# """ +# Test a simple tool rule where fourth_secret_word must be called after flip_coin. +# +# Tool Flow: +# flip_coin +# | +# v +# fourth_secret_word +# """ +# client = create_client() +# cleanup(client=client, agent_uuid=agent_uuid) +# +# # Create tools +# flip_coin_name = "flip_coin" +# secret_word = "fourth_secret_word" +# flip_coin_tool = client.create_or_update_tool(flip_coin) +# secret_word_tool = client.create_or_update_tool(fourth_secret_word) +# another_secret_word_tool = client.create_or_update_tool(first_secret_word) +# random_tool = client.create_or_update_tool(can_play_game) +# tools = [flip_coin_tool, secret_word_tool, another_secret_word_tool, random_tool] +# +# # Create tool rule: after flip_coin, must call fourth_secret_word +# tool_rule = ConditionalToolRule( +# tool_name=flip_coin_name, +# default_child=secret_word, +# child_output_mapping={"*": secret_word}, +# ) +# +# # Set up agent with the tool rule +# agent_state = setup_agent( +# client, config_file, agent_uuid, tool_rules=[tool_rule], tool_ids=[t.id for t in tools], include_base_tools=False +# ) +# +# # Start conversation +# response = client.user_message(agent_id=agent_state.id, message="Help me test the tools.") +# +# # Verify the tool calls +# tool_calls = [msg for msg in response.messages if isinstance(msg, ToolCallMessage)] +# assert len(tool_calls) >= 2 # Should have at least flip_coin and fourth_secret_word calls +# assert_invoked_function_call(response.messages, flip_coin_name) +# assert_invoked_function_call(response.messages, secret_word) +# +# # Find the flip_coin call +# flip_coin_call = next((call for call in tool_calls if call.tool_call.name == "flip_coin"), None) +# +# # Verify that fourth_secret_word was called after flip_coin +# flip_coin_call_index = tool_calls.index(flip_coin_call) +# assert tool_calls[flip_coin_call_index + 1].tool_call.name == secret_word, "Fourth secret word should be called after flip_coin" +# +# cleanup(client, agent_uuid=agent_state.id) def test_init_tool_rule_always_fails_one_tool(): @@ -768,3 +672,56 @@ def test_continue_tool_rule(): if call.tool_call.name == "core_memory_append": core_memory_append_call_index = i assert send_message_call_index < core_memory_append_call_index, "send_message should have been called before core_memory_append" + + +@pytest.mark.timeout(60) +@retry_until_success(max_attempts=3, sleep_time_seconds=2) +def test_max_count_per_step_tool_rule_integration(mock_e2b_api_key_none): + """ + Test an agent with MaxCountPerStepToolRule to ensure a tool can only be called a limited number of times. + + Tool Flow: + repeatable_tool (max 2 times) + | + v + send_message + """ + client = create_client() + cleanup(client=client, agent_uuid=agent_uuid) + + # Create tools + repeatable_tool_name = "first_secret_word" + final_tool_name = "send_message" + + repeatable_tool = client.create_or_update_tool(first_secret_word) + send_message_tool = client.get_tool(client.get_tool_id(final_tool_name)) # Assume send_message is a default tool + + # Define tool rules + tool_rules = [ + InitToolRule(tool_name=repeatable_tool_name), + MaxCountPerStepToolRule(tool_name=repeatable_tool_name, max_count_limit=2), + TerminalToolRule(tool_name=final_tool_name), + ] + + tools = [repeatable_tool, send_message_tool] + + # Setup agent + agent_state = setup_agent(client, config_file, agent_uuid=agent_uuid, tool_ids=[t.id for t in tools], tool_rules=tool_rules) + + # Start conversation + response = client.user_message( + agent_id=agent_state.id, message=f"Keep calling {repeatable_tool_name} nonstop without calling ANY other tool." + ) + + # Make checks + assert_sanity_checks(response) + + # Ensure the repeatable tool is only called twice + count = sum(1 for m in response.messages if isinstance(m, ToolCallMessage) and m.tool_call.name == repeatable_tool_name) + assert count == 2, f"Expected 'first_secret_word' to be called exactly 2 times, but got {count}" + + # Ensure send_message was eventually called + assert_invoked_function_call(response.messages, final_tool_name) + + print(f"Got successful response from client: \n\n{response}") + cleanup(client=client, agent_uuid=agent_uuid) diff --git a/tests/test_managers.py b/tests/test_managers.py index 1d1dc240..6bd11b18 100644 --- a/tests/test_managers.py +++ b/tests/test_managers.py @@ -8,9 +8,10 @@ from openai.types.chat.chat_completion_message_tool_call import Function as Open from sqlalchemy.exc import IntegrityError from letta.config import LettaConfig -from letta.constants import BASE_MEMORY_TOOLS, BASE_TOOLS, LETTA_TOOL_EXECUTION_DIR, MULTI_AGENT_TOOLS +from letta.constants import BASE_MEMORY_TOOLS, BASE_TOOLS, LETTA_TOOL_EXECUTION_DIR, MCP_TOOL_TAG_NAME_PREFIX, MULTI_AGENT_TOOLS from letta.embeddings import embedding_model from letta.functions.functions import derive_openai_json_schema, parse_source_code +from letta.functions.mcp_client.types import MCPTool from letta.orm import Base from letta.orm.enums import JobType, ToolType from letta.orm.errors import NoResultFound, UniqueConstraintViolationError @@ -145,8 +146,9 @@ def print_tool(server: SyncServer, default_user, default_organization): source_type = "python" description = "test_description" tags = ["test"] + metadata = {"a": "b"} - tool = PydanticTool(description=description, tags=tags, source_code=source_code, source_type=source_type) + tool = PydanticTool(description=description, tags=tags, source_code=source_code, source_type=source_type, metadata_=metadata) derived_json_schema = derive_openai_json_schema(source_code=tool.source_code, name=tool.name) derived_name = derived_json_schema["name"] @@ -166,6 +168,30 @@ def composio_github_star_tool(server, default_user): yield tool +@pytest.fixture +def mcp_tool(server, default_user): + mcp_tool = MCPTool( + name="weather_lookup", + description="Fetches the current weather for a given location.", + inputSchema={ + "type": "object", + "properties": { + "location": {"type": "string", "description": "The name of the city or location."}, + "units": { + "type": "string", + "enum": ["metric", "imperial"], + "description": "The unit system for temperature (metric or imperial).", + }, + }, + "required": ["location"], + }, + ) + mcp_server_name = "test" + tool_create = ToolCreate.from_mcp(mcp_server_name=mcp_server_name, mcp_tool=mcp_tool) + tool = server.tool_manager.create_or_update_mcp_tool(tool_create=tool_create, mcp_server_name=mcp_server_name, actor=default_user) + yield tool + + @pytest.fixture def default_job(server: SyncServer, default_user): """Fixture to create and return a default job.""" @@ -1816,6 +1842,14 @@ def test_create_composio_tool(server: SyncServer, composio_github_star_tool, def assert composio_github_star_tool.tool_type == ToolType.EXTERNAL_COMPOSIO +def test_create_mcp_tool(server: SyncServer, mcp_tool, default_user, default_organization): + # Assertions to ensure the created tool matches the expected values + assert mcp_tool.created_by_id == default_user.id + assert mcp_tool.organization_id == default_organization.id + assert mcp_tool.tool_type == ToolType.EXTERNAL_MCP + assert mcp_tool.metadata_[MCP_TOOL_TAG_NAME_PREFIX]["server_name"] == "test" + + @pytest.mark.skipif(USING_SQLITE, reason="Test not applicable when using SQLite.") def test_create_tool_duplicate_name(server: SyncServer, print_tool, default_user, default_organization): data = print_tool.model_dump(exclude=["id"]) @@ -1834,6 +1868,7 @@ def test_get_tool_by_id(server: SyncServer, print_tool, default_user): assert fetched_tool.name == print_tool.name assert fetched_tool.description == print_tool.description assert fetched_tool.tags == print_tool.tags + assert fetched_tool.metadata_ == print_tool.metadata_ assert fetched_tool.source_code == print_tool.source_code assert fetched_tool.source_type == print_tool.source_type assert fetched_tool.tool_type == ToolType.CUSTOM diff --git a/tests/test_tool_rule_solver.py b/tests/test_tool_rule_solver.py index dcb66e1b..a8a86011 100644 --- a/tests/test_tool_rule_solver.py +++ b/tests/test_tool_rule_solver.py @@ -2,7 +2,7 @@ import pytest from letta.helpers import ToolRulesSolver from letta.helpers.tool_rule_solver import ToolRuleValidationError -from letta.schemas.tool_rule import ChildToolRule, ConditionalToolRule, InitToolRule, TerminalToolRule +from letta.schemas.tool_rule import ChildToolRule, ConditionalToolRule, InitToolRule, MaxCountPerStepToolRule, TerminalToolRule # Constants for tool names used in the tests START_TOOL = "start_tool" @@ -15,145 +15,163 @@ UNRECOGNIZED_TOOL = "unrecognized_tool" def test_get_allowed_tool_names_with_init_rules(): - # Setup: Initial tool rule configuration init_rule_1 = InitToolRule(tool_name=START_TOOL) init_rule_2 = InitToolRule(tool_name=PREP_TOOL) - solver = ToolRulesSolver(init_tool_rules=[init_rule_1, init_rule_2], tool_rules=[], terminal_tool_rules=[]) + solver = ToolRulesSolver(tool_rules=[init_rule_1, init_rule_2]) - # Action: Get allowed tool names when no tool has been called - allowed_tools = solver.get_allowed_tool_names() + allowed_tools = solver.get_allowed_tool_names(set()) - # Assert: Both init tools should be allowed initially assert allowed_tools == [START_TOOL, PREP_TOOL], "Should allow only InitToolRule tools at the start" def test_get_allowed_tool_names_with_subsequent_rule(): - # Setup: Tool rule sequence init_rule = InitToolRule(tool_name=START_TOOL) rule_1 = ChildToolRule(tool_name=START_TOOL, children=[NEXT_TOOL, HELPER_TOOL]) - solver = ToolRulesSolver(init_tool_rules=[init_rule], tool_rules=[rule_1], terminal_tool_rules=[]) + solver = ToolRulesSolver(tool_rules=[init_rule, rule_1]) - # Action: Update usage and get allowed tools solver.update_tool_usage(START_TOOL) - allowed_tools = solver.get_allowed_tool_names() + allowed_tools = solver.get_allowed_tool_names({START_TOOL, NEXT_TOOL, HELPER_TOOL}) - # Assert: Only children of "start_tool" should be allowed - assert allowed_tools == [NEXT_TOOL, HELPER_TOOL], "Should allow only children of the last tool used" + assert sorted(allowed_tools) == sorted([NEXT_TOOL, HELPER_TOOL]), "Should allow only children of the last tool used" def test_is_terminal_tool(): - # Setup: Terminal tool rule configuration init_rule = InitToolRule(tool_name=START_TOOL) terminal_rule = TerminalToolRule(tool_name=END_TOOL) - solver = ToolRulesSolver(init_tool_rules=[init_rule], tool_rules=[], terminal_tool_rules=[terminal_rule]) + solver = ToolRulesSolver(tool_rules=[init_rule, terminal_rule]) - # Action & Assert: Verify terminal and non-terminal tools assert solver.is_terminal_tool(END_TOOL) is True, "Should recognize 'end_tool' as a terminal tool" assert solver.is_terminal_tool(START_TOOL) is False, "Should not recognize 'start_tool' as a terminal tool" -def test_get_allowed_tool_names_no_matching_rule_warning(): - # Setup: Tool rules with no matching rule for the last tool - init_rule = InitToolRule(tool_name=START_TOOL) - solver = ToolRulesSolver(init_tool_rules=[init_rule], tool_rules=[], terminal_tool_rules=[]) - - # Action: Set last tool to an unrecognized tool and check warnings - solver.update_tool_usage(UNRECOGNIZED_TOOL) - - # # NOTE: removed for now since this warning is getting triggered on every LLM call - # with warnings.catch_warnings(record=True) as w: - # allowed_tools = solver.get_allowed_tool_names() - - # # Assert: Expecting a warning and an empty list of allowed tools - # assert len(w) == 1, "Expected a warning for no matching rule" - # assert "resolved to no more possible tool calls" in str(w[-1].message) - # assert allowed_tools == [], "Should return an empty list if no matching rule" - - def test_get_allowed_tool_names_no_matching_rule_error(): - # Setup: Tool rules with no matching rule for the last tool init_rule = InitToolRule(tool_name=START_TOOL) - solver = ToolRulesSolver(init_tool_rules=[init_rule], tool_rules=[], terminal_tool_rules=[]) + solver = ToolRulesSolver(tool_rules=[init_rule]) - # Action & Assert: Set last tool to an unrecognized tool and expect ValueError solver.update_tool_usage(UNRECOGNIZED_TOOL) - with pytest.raises(ValueError, match=f"No tool rule found for {UNRECOGNIZED_TOOL}"): - solver.get_allowed_tool_names(error_on_empty=True) + with pytest.raises(ValueError, match=f"No valid tools found based on tool rules."): + solver.get_allowed_tool_names(set(), error_on_empty=True) def test_update_tool_usage_and_get_allowed_tool_names_combined(): - # Setup: More complex rule chaining init_rule = InitToolRule(tool_name=START_TOOL) rule_1 = ChildToolRule(tool_name=START_TOOL, children=[NEXT_TOOL]) rule_2 = ChildToolRule(tool_name=NEXT_TOOL, children=[FINAL_TOOL]) terminal_rule = TerminalToolRule(tool_name=FINAL_TOOL) - solver = ToolRulesSolver(init_tool_rules=[init_rule], tool_rules=[rule_1, rule_2], terminal_tool_rules=[terminal_rule]) + solver = ToolRulesSolver(tool_rules=[init_rule, rule_1, rule_2, terminal_rule]) - # Step 1: Initially allowed tools - assert solver.get_allowed_tool_names() == [START_TOOL], "Initial allowed tool should be 'start_tool'" + assert solver.get_allowed_tool_names({START_TOOL}) == [START_TOOL], "Initial allowed tool should be 'start_tool'" - # Step 2: After using 'start_tool' solver.update_tool_usage(START_TOOL) - assert solver.get_allowed_tool_names() == [NEXT_TOOL], "After 'start_tool', should allow 'next_tool'" + assert solver.get_allowed_tool_names({NEXT_TOOL}) == [NEXT_TOOL], "After 'start_tool', should allow 'next_tool'" - # Step 3: After using 'next_tool' solver.update_tool_usage(NEXT_TOOL) - assert solver.get_allowed_tool_names() == [FINAL_TOOL], "After 'next_tool', should allow 'final_tool'" + assert solver.get_allowed_tool_names({FINAL_TOOL}) == [FINAL_TOOL], "After 'next_tool', should allow 'final_tool'" - # Step 4: 'final_tool' should be terminal assert solver.is_terminal_tool(FINAL_TOOL) is True, "Should recognize 'final_tool' as terminal" def test_conditional_tool_rule(): - # Setup: Define a conditional tool rule init_rule = InitToolRule(tool_name=START_TOOL) terminal_rule = TerminalToolRule(tool_name=END_TOOL) rule = ConditionalToolRule(tool_name=START_TOOL, default_child=None, child_output_mapping={True: END_TOOL, False: START_TOOL}) solver = ToolRulesSolver(tool_rules=[init_rule, rule, terminal_rule]) - # Action & Assert: Verify the rule properties - # Step 1: Initially allowed tools - assert solver.get_allowed_tool_names() == [START_TOOL], "Initial allowed tool should be 'start_tool'" + assert solver.get_allowed_tool_names({START_TOOL}) == [START_TOOL], "Initial allowed tool should be 'start_tool'" - # Step 2: After using 'start_tool' solver.update_tool_usage(START_TOOL) - assert solver.get_allowed_tool_names(last_function_response='{"message": "true"}') == [ + assert solver.get_allowed_tool_names({END_TOOL}, last_function_response='{"message": "true"}') == [ END_TOOL ], "After 'start_tool' returns true, should allow 'end_tool'" - assert solver.get_allowed_tool_names(last_function_response='{"message": "false"}') == [ + assert solver.get_allowed_tool_names({START_TOOL}, last_function_response='{"message": "false"}') == [ START_TOOL ], "After 'start_tool' returns false, should allow 'start_tool'" - # Step 3: After using 'end_tool' assert solver.is_terminal_tool(END_TOOL) is True, "Should recognize 'end_tool' as terminal" def test_invalid_conditional_tool_rule(): - # Setup: Define an invalid conditional tool rule init_rule = InitToolRule(tool_name=START_TOOL) terminal_rule = TerminalToolRule(tool_name=END_TOOL) invalid_rule_1 = ConditionalToolRule(tool_name=START_TOOL, default_child=END_TOOL, child_output_mapping={}) - # Test 1: Missing child output mapping with pytest.raises(ToolRuleValidationError, match="Conditional tool rule must have at least one child tool."): ToolRulesSolver(tool_rules=[init_rule, invalid_rule_1, terminal_rule]) def test_tool_rules_with_invalid_path(): - # Setup: Define tool rules with both connected, disconnected nodes and a cycle init_rule = InitToolRule(tool_name=START_TOOL) rule_1 = ChildToolRule(tool_name=START_TOOL, children=[NEXT_TOOL]) rule_2 = ChildToolRule(tool_name=NEXT_TOOL, children=[HELPER_TOOL]) - rule_3 = ChildToolRule(tool_name=HELPER_TOOL, children=[START_TOOL]) # This creates a cycle: start -> next -> helper -> start - rule_4 = ChildToolRule(tool_name=FINAL_TOOL, children=[END_TOOL]) # Disconnected rule, no cycle here + rule_3 = ChildToolRule(tool_name=HELPER_TOOL, children=[START_TOOL]) + rule_4 = ChildToolRule(tool_name=FINAL_TOOL, children=[END_TOOL]) terminal_rule = TerminalToolRule(tool_name=END_TOOL) ToolRulesSolver(tool_rules=[init_rule, rule_1, rule_2, rule_3, rule_4, terminal_rule]) - # Now: add a path from the start tool to the final tool rule_5 = ConditionalToolRule( tool_name=HELPER_TOOL, default_child=FINAL_TOOL, child_output_mapping={True: START_TOOL, False: FINAL_TOOL}, ) ToolRulesSolver(tool_rules=[init_rule, rule_1, rule_2, rule_3, rule_4, rule_5, terminal_rule]) + + +def test_max_count_per_step_tool_rule(): + init_rule = InitToolRule(tool_name=START_TOOL) + rule_1 = MaxCountPerStepToolRule(tool_name=START_TOOL, max_count_limit=2) + solver = ToolRulesSolver(tool_rules=[init_rule, rule_1]) + + assert solver.get_allowed_tool_names({START_TOOL}) == [START_TOOL], "Initially should allow 'start_tool'" + + solver.update_tool_usage(START_TOOL) + assert solver.get_allowed_tool_names({START_TOOL}) == [START_TOOL], "After first use, should still allow 'start_tool'" + + solver.update_tool_usage(START_TOOL) + assert solver.get_allowed_tool_names({START_TOOL}) == [], "After reaching max count, 'start_tool' should no longer be allowed" + + +def test_max_count_per_step_tool_rule_allows_usage_up_to_limit(): + """Ensure the tool is allowed exactly max_count_limit times.""" + rule = MaxCountPerStepToolRule(tool_name=START_TOOL, max_count_limit=3) + solver = ToolRulesSolver(tool_rules=[rule]) + + assert solver.get_allowed_tool_names({START_TOOL}) == [START_TOOL], "Initially should allow 'start_tool'" + + solver.update_tool_usage(START_TOOL) + assert solver.get_allowed_tool_names({START_TOOL}) == [START_TOOL], "Should still allow 'start_tool' after 1 use" + + solver.update_tool_usage(START_TOOL) + assert solver.get_allowed_tool_names({START_TOOL}) == [START_TOOL], "Should still allow 'start_tool' after 2 uses" + + solver.update_tool_usage(START_TOOL) + assert solver.get_allowed_tool_names({START_TOOL}) == [], "Should no longer allow 'start_tool' after 3 uses" + + +def test_max_count_per_step_tool_rule_does_not_affect_other_tools(): + """Ensure exceeding max count for one tool does not impact others.""" + rule = MaxCountPerStepToolRule(tool_name=START_TOOL, max_count_limit=2) + another_tool_rules = ChildToolRule(tool_name=NEXT_TOOL, children=[HELPER_TOOL]) + solver = ToolRulesSolver(tool_rules=[rule, another_tool_rules]) + + solver.update_tool_usage(START_TOOL) + solver.update_tool_usage(START_TOOL) + + assert sorted(solver.get_allowed_tool_names({START_TOOL, NEXT_TOOL, HELPER_TOOL})) == sorted( + [NEXT_TOOL, HELPER_TOOL] + ), "Other tools should still be allowed even if 'start_tool' is over limit" + + +def test_max_count_per_step_tool_rule_resets_on_clear(): + """Ensure clearing tool history resets the rule's limit.""" + rule = MaxCountPerStepToolRule(tool_name=START_TOOL, max_count_limit=2) + solver = ToolRulesSolver(tool_rules=[rule]) + + solver.update_tool_usage(START_TOOL) + solver.update_tool_usage(START_TOOL) + + assert solver.get_allowed_tool_names({START_TOOL}) == [], "Should not allow 'start_tool' after reaching limit" + + solver.clear_tool_history() + + assert solver.get_allowed_tool_names({START_TOOL}) == [START_TOOL], "Should allow 'start_tool' again after clearing history"