chore: rm composio (#5151)
This commit is contained in:
committed by
Caren Thomas
parent
07a687880f
commit
e07a589796
7
.github/scripts/model-sweep/conftest.py
vendored
7
.github/scripts/model-sweep/conftest.py
vendored
@@ -62,13 +62,6 @@ def default_user(default_organization):
|
||||
yield user
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def check_composio_key_set():
|
||||
original_api_key = tool_settings.composio_api_key
|
||||
assert original_api_key is not None, "Missing composio key! Cannot execute this test."
|
||||
yield
|
||||
|
||||
|
||||
# --- Tool Fixtures ---
|
||||
@pytest.fixture
|
||||
def weather_tool_func():
|
||||
|
||||
1
.github/workflows/model-sweep.yaml
vendored
1
.github/workflows/model-sweep.yaml
vendored
@@ -88,7 +88,6 @@ jobs:
|
||||
AZURE_API_KEY: ${{ env.AZURE_API_KEY }}
|
||||
AZURE_BASE_URL: ${{ secrets.AZURE_BASE_URL }}
|
||||
GEMINI_API_KEY: ${{ env.GEMINI_API_KEY }}
|
||||
COMPOSIO_API_KEY: ${{ env.COMPOSIO_API_KEY }}
|
||||
GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT}}
|
||||
GOOGLE_CLOUD_LOCATION: ${{ secrets.GOOGLE_CLOUD_LOCATION}}
|
||||
DEEPSEEK_API_KEY: ${{ env.DEEPSEEK_API_KEY}}
|
||||
|
||||
1
.github/workflows/reusable-test-workflow.yml
vendored
1
.github/workflows/reusable-test-workflow.yml
vendored
@@ -367,7 +367,6 @@ jobs:
|
||||
LETTA_MISTRAL_API_KEY: ${{ secrets.LETTA_MISTRAL_API_KEY }}
|
||||
|
||||
# External service API Keys (shared across all test types)
|
||||
COMPOSIO_API_KEY: ${{ env.COMPOSIO_API_KEY }}
|
||||
E2B_API_KEY: ${{ env.E2B_API_KEY }}
|
||||
E2B_SANDBOX_TEMPLATE_ID: ${{ env.E2B_SANDBOX_TEMPLATE_ID }}
|
||||
|
||||
|
||||
@@ -69,8 +69,7 @@ ENV LETTA_ENVIRONMENT=${LETTA_ENVIRONMENT} \
|
||||
PATH="/app/.venv/bin:$PATH" \
|
||||
POSTGRES_USER=letta \
|
||||
POSTGRES_PASSWORD=letta \
|
||||
POSTGRES_DB=letta \
|
||||
COMPOSIO_DISABLE_VERSION_CHECK=true
|
||||
POSTGRES_DB=letta
|
||||
|
||||
ARG LETTA_VERSION
|
||||
ENV LETTA_VERSION=${LETTA_VERSION}
|
||||
|
||||
@@ -9,7 +9,6 @@ Create Date: 2025-01-16 14:21:33.764332
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
from letta.schemas.enums import ToolType
|
||||
from letta.settings import settings
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
@@ -25,8 +24,8 @@ def upgrade() -> None:
|
||||
return
|
||||
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
# Define the value for EXTERNAL_COMPOSIO
|
||||
external_composio_value = ToolType.EXTERNAL_COMPOSIO.value
|
||||
# Define the value for EXTERNAL_COMPOSIO (using string literal since enum was removed)
|
||||
external_composio_value = "external_composio"
|
||||
|
||||
# Update tool_type to EXTERNAL_COMPOSIO if the tags field includes "composio"
|
||||
# This is super brittle and awful but no other way to do this
|
||||
@@ -46,7 +45,8 @@ def downgrade() -> None:
|
||||
return
|
||||
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
custom_value = ToolType.CUSTOM.value
|
||||
# Use string literal for CUSTOM value
|
||||
custom_value = "custom"
|
||||
|
||||
# Update tool_type to CUSTOM if the tags field includes "composio"
|
||||
# This is super brittle and awful but no other way to do this
|
||||
|
||||
@@ -168,9 +168,6 @@ navigation:
|
||||
path: pages/agents/tool_rules.mdx
|
||||
- page: Tool Variables
|
||||
path: pages/agents/tool_variables.mdx
|
||||
- page: Composio Integration
|
||||
path: pages/agents/composio.mdx
|
||||
hidden: true
|
||||
- section: Model Context Protocol
|
||||
path: pages/mcp/overview.mdx
|
||||
contents:
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
"""
|
||||
Example of using composio tools in Letta
|
||||
|
||||
Make sure you set `COMPOSIO_API_KEY` environment variable or run `composio login` to authenticate with Composio.
|
||||
"""
|
||||
|
||||
from composio import Action
|
||||
from letta_client import Letta
|
||||
|
||||
client = Letta(base_url="http://localhost:8283")
|
||||
|
||||
# add a composio tool
|
||||
tool = client.tools.add_composio_tool(composio_action_name=Action.GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER.name)
|
||||
|
||||
# create an agent with the tool
|
||||
agent = client.agents.create(
|
||||
name="file_editing_agent",
|
||||
memory_blocks=[{"label": "persona", "value": "I am a helpful assistant"}],
|
||||
model="anthropic/claude-3-5-sonnet-20241022",
|
||||
embedding="openai/text-embedding-3-small",
|
||||
tool_ids=[tool.id],
|
||||
)
|
||||
print("Agent tools", [tool.name for tool in agent.tools])
|
||||
|
||||
# message the agent
|
||||
response = client.agents.messages.create(
|
||||
agent_id=agent.id, messages=[{"role": "user", "content": "Star the github repo `letta` by `letta-ai`"}]
|
||||
)
|
||||
for message in response.messages:
|
||||
print(message)
|
||||
@@ -976,127 +976,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/tools/composio/apps": {
|
||||
"get": {
|
||||
"tags": ["tools"],
|
||||
"summary": "List Composio Apps",
|
||||
"description": "Get a list of all Composio apps",
|
||||
"operationId": "list_composio_apps",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AppModel"
|
||||
},
|
||||
"title": "Response List Composio Apps"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/tools/composio/apps/{composio_app_name}/actions": {
|
||||
"get": {
|
||||
"tags": ["tools"],
|
||||
"summary": "List Composio Actions By App",
|
||||
"description": "Get a list of all Composio actions for a specific app",
|
||||
"operationId": "list_composio_actions_by_app",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "composio_app_name",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Composio App Name"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ActionModel"
|
||||
},
|
||||
"title": "Response List Composio Actions By App"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/tools/composio/{composio_action_name}": {
|
||||
"post": {
|
||||
"tags": ["tools"],
|
||||
"summary": "Add Composio Tool",
|
||||
"description": "Add a new Composio tool by action name (Composio refers to each tool as an `Action`)",
|
||||
"operationId": "add_composio_tool",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "composio_action_name",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Composio Action Name"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Tool"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/tools/mcp/servers": {
|
||||
"get": {
|
||||
"tags": ["tools"],
|
||||
@@ -15796,8 +15675,8 @@
|
||||
"letta_voice_sleeptime_core",
|
||||
"letta_builtin",
|
||||
"letta_files_core",
|
||||
"external_composio",
|
||||
"external_langchain",
|
||||
"external_composio",
|
||||
"external_mcp"
|
||||
]
|
||||
},
|
||||
@@ -21269,185 +21148,6 @@
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"ActionModel": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Name"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"title": "Description"
|
||||
},
|
||||
"parameters": {
|
||||
"$ref": "#/components/schemas/ActionParametersModel"
|
||||
},
|
||||
"response": {
|
||||
"$ref": "#/components/schemas/ActionResponseModel"
|
||||
},
|
||||
"appName": {
|
||||
"type": "string",
|
||||
"title": "Appname"
|
||||
},
|
||||
"appId": {
|
||||
"type": "string",
|
||||
"title": "Appid"
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"title": "Version"
|
||||
},
|
||||
"available_versions": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Available Versions"
|
||||
},
|
||||
"tags": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Tags"
|
||||
},
|
||||
"logo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Logo"
|
||||
},
|
||||
"display_name": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Display Name"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"title": "Enabled",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"description",
|
||||
"parameters",
|
||||
"response",
|
||||
"appName",
|
||||
"appId",
|
||||
"version",
|
||||
"available_versions",
|
||||
"tags"
|
||||
],
|
||||
"title": "ActionModel",
|
||||
"description": "Action data model."
|
||||
},
|
||||
"ActionParametersModel": {
|
||||
"properties": {
|
||||
"properties": {
|
||||
"additionalProperties": true,
|
||||
"type": "object",
|
||||
"title": "Properties"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"title": "Title"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"title": "Type"
|
||||
},
|
||||
"required": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Required"
|
||||
},
|
||||
"examples": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Examples"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["properties", "title", "type"],
|
||||
"title": "ActionParametersModel",
|
||||
"description": "Action parameter data models."
|
||||
},
|
||||
"ActionResponseModel": {
|
||||
"properties": {
|
||||
"properties": {
|
||||
"additionalProperties": true,
|
||||
"type": "object",
|
||||
"title": "Properties"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"title": "Title"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"title": "Type"
|
||||
},
|
||||
"required": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Required"
|
||||
},
|
||||
"examples": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Examples"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["properties", "title", "type"],
|
||||
"title": "ActionResponseModel",
|
||||
"description": "Action response data model."
|
||||
},
|
||||
"AgentEnvironmentVariable": {
|
||||
"properties": {
|
||||
"created_by_id": {
|
||||
@@ -22184,269 +21884,6 @@
|
||||
"title": "AgentType",
|
||||
"description": "Enum to represent the type of agent."
|
||||
},
|
||||
"AppAuthScheme": {
|
||||
"properties": {
|
||||
"scheme_name": {
|
||||
"type": "string",
|
||||
"title": "Scheme Name"
|
||||
},
|
||||
"auth_mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"OAUTH2",
|
||||
"OAUTH1",
|
||||
"API_KEY",
|
||||
"BASIC",
|
||||
"BEARER_TOKEN",
|
||||
"BASIC_WITH_JWT",
|
||||
"GOOGLE_SERVICE_ACCOUNT",
|
||||
"GOOGLEADS_AUTH",
|
||||
"NO_AUTH",
|
||||
"CALCOM_AUTH"
|
||||
],
|
||||
"title": "Auth Mode"
|
||||
},
|
||||
"fields": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AuthSchemeField"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Fields"
|
||||
},
|
||||
"proxy": {
|
||||
"anyOf": [
|
||||
{
|
||||
"additionalProperties": true,
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Proxy"
|
||||
},
|
||||
"authorization_url": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Authorization Url"
|
||||
},
|
||||
"token_url": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Token Url"
|
||||
},
|
||||
"default_scopes": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Default Scopes"
|
||||
},
|
||||
"token_response_metadata": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Token Response Metadata"
|
||||
},
|
||||
"client_id": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Client Id"
|
||||
},
|
||||
"client_secret": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Client Secret"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["scheme_name", "auth_mode", "fields"],
|
||||
"title": "AppAuthScheme",
|
||||
"description": "App authenticatio scheme."
|
||||
},
|
||||
"AppModel": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Name"
|
||||
},
|
||||
"key": {
|
||||
"type": "string",
|
||||
"title": "Key"
|
||||
},
|
||||
"appId": {
|
||||
"type": "string",
|
||||
"title": "Appid"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"title": "Description"
|
||||
},
|
||||
"categories": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Categories"
|
||||
},
|
||||
"meta": {
|
||||
"additionalProperties": true,
|
||||
"type": "object",
|
||||
"title": "Meta"
|
||||
},
|
||||
"logo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Logo"
|
||||
},
|
||||
"docs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Docs"
|
||||
},
|
||||
"group": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Group"
|
||||
},
|
||||
"status": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Status"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"title": "Enabled",
|
||||
"default": false
|
||||
},
|
||||
"no_auth": {
|
||||
"type": "boolean",
|
||||
"title": "No Auth",
|
||||
"default": false
|
||||
},
|
||||
"auth_schemes": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AppAuthScheme"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Auth Schemes"
|
||||
},
|
||||
"testConnectors": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"additionalProperties": true,
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Testconnectors"
|
||||
},
|
||||
"documentation_doc_text": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Documentation Doc Text"
|
||||
},
|
||||
"configuration_docs_text": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Configuration Docs Text"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"key",
|
||||
"appId",
|
||||
"description",
|
||||
"categories",
|
||||
"meta"
|
||||
],
|
||||
"title": "AppModel",
|
||||
"description": "App data model."
|
||||
},
|
||||
"ApprovalCreate": {
|
||||
"properties": {
|
||||
"type": {
|
||||
@@ -23063,69 +22500,6 @@
|
||||
"required": ["uuid"],
|
||||
"title": "AuthResponse"
|
||||
},
|
||||
"AuthSchemeField": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Name"
|
||||
},
|
||||
"display_name": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Display Name"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"title": "Description"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"title": "Type"
|
||||
},
|
||||
"default": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Default"
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean",
|
||||
"title": "Required",
|
||||
"default": false
|
||||
},
|
||||
"expected_from_customer": {
|
||||
"type": "boolean",
|
||||
"title": "Expected From Customer",
|
||||
"default": true
|
||||
},
|
||||
"get_current_user_endpoint": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Get Current User Endpoint"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["name", "description", "type"],
|
||||
"title": "AuthSchemeField",
|
||||
"description": "Auth scheme field."
|
||||
},
|
||||
"Base64Image": {
|
||||
"properties": {
|
||||
"type": {
|
||||
@@ -34641,8 +34015,8 @@
|
||||
"letta_voice_sleeptime_core",
|
||||
"letta_builtin",
|
||||
"letta_files_core",
|
||||
"external_composio",
|
||||
"external_langchain",
|
||||
"external_composio",
|
||||
"external_mcp"
|
||||
],
|
||||
"title": "ToolType"
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
---
|
||||
title: Connecting Letta to Composio
|
||||
slug: guides/agents/composio
|
||||
---
|
||||
|
||||
<Warning>
|
||||
The Letta Composio integration (via the Composio API endpoints) is deprecated and will be removed in a future release. If you would like to use Composio tools, we recommend using them via our native [MCP integration](/guides/mcp/overview) instead.
|
||||
</Warning>
|
||||
|
||||
## Composio integration (deprecated)
|
||||
|
||||
<Tip>
|
||||
If you're getting an error when calling Composio tools that says "*Could not find connection... entity=default*",
|
||||
go to [Composio's website](https://app.composio.dev/connections) to check your `ENTITY ID`.
|
||||
If it's not `default`, then you need to set a tool variable `COMPOSIO_ENTITY` to your `ENTITY ID` value (see [here](#using-entities-in-composio-tools)).
|
||||
</Tip>
|
||||
|
||||
[Composio](https://docs.composio.dev) is an external tool service that makes it easy to connect Letta agents to popular services via custom tools.
|
||||
For example, you can use Composio tools to connect Letta agents to Google, GitHub, Slack, Cal.com, and [many more services](https://composio.dev/tools).
|
||||
|
||||
Composio makes agent authentication to third party platforms easy.
|
||||
To use Composio, you need to create an account at [composio.dev](https://composio.dev) and create a Composio API key.
|
||||
|
||||
Once you have a Composio API key, you can connect it to Letta to allow your Letta agents to use Composio tools.
|
||||
Composio's free tier gives you 2000 API calls per month.
|
||||
|
||||
## Connecting Composio Tools to Letta Agents
|
||||
Once you have a Composio API key, you can register it with the Letta server using the environment variable `COMPOSIO_API_KEY`.
|
||||
|
||||
If you're self-hosting a Letta server ([instructions](guides/server/docker)), you would pass this environment variable to `docker run`:
|
||||
```bash
|
||||
docker run \
|
||||
-v ~/.letta/.persist/pgdata:/var/lib/postgresql/data \
|
||||
-p 8283:8283 \
|
||||
-e OPENAI_API_KEY="your_openai_api_key" \
|
||||
-e COMPOSIO_API_KEY="your_composio_api_key" \
|
||||
letta/letta:latest
|
||||
```
|
||||
|
||||
In Letta Cloud, you can set your `COMPOSIO_API_KEY` under **Settings** > **Integrations** > **Composio**.
|
||||
|
||||
## Adding Composio tools via the ADE
|
||||
Once you've connected your `COMPOSIO_API_KEY` to the Letta server (or Letta Cloud), you will be able to view Composio tools when you click the **Add Tool** button (the + button in the bottom left tools panel).
|
||||
<img src="../../images/tavily.png" />
|
||||
|
||||
<Warning>
|
||||
If you did not successfully pass your `COMPOSIO_API_KEY` to the Letta server, you'll see the following message when you browse Composio tools:
|
||||
"To attach this tool and 4000+ other tools to your agent, connect to Composio"
|
||||
</Warning>
|
||||
|
||||
### Authenticating a Tool in Composio
|
||||
In order for the tool to function properly, you must have first authenticated the tool on Composio's website. For example, for Tavily, we need to provide Composio our Tavily API key.
|
||||
|
||||
To do this, you can click the **View on Composio** button and follow the instructions on Composio's website to authenticate the tool.
|
||||
<img src="../../images/tavily_connect.png" />
|
||||
|
||||
### Attaching a Tool to a Letta Agent
|
||||
To give your agent access to the tool, you need to click **Attach Tool**. Once the tool is successfully attached (you will see it in the tools panel in the main ADE view), your agent will be able to use the tool.
|
||||
Let's try getting the example agent to use the Tavily search tool:
|
||||
<img src="../../images/tavily_call.png" />
|
||||
|
||||
If we click on the tool execution button in the chat, we can see the exact inputs to the Composio tool, and the exact outputs from the tool:
|
||||
<img src="../../images/tavily_call_expanded.png" />
|
||||
|
||||
## Using entities in Composio tools
|
||||
<Tip>
|
||||
To set a tool variable, click "**Variables**" in the Agent Simulator (center column, top), then click "**Add new tool variable**". Once you've added the variable, click "**Update tool variables**" to save.
|
||||
</Tip>
|
||||
In Composio tool execution is associated with an `ENTITY ID`.
|
||||
By default, this is `default` - you can check what your `ENTITY ID` is by going to [the connections page on Composio's website](https://app.composio.dev/connections).
|
||||
In Letta, you can set the `ENTITY ID` in Composio through the use of tool variables - specifically, the variable `COMPOSIO_ENTITY`.
|
||||
|
||||
If your `ENTITY ID` is not `default`, then in order for your Composio tools to work in Letta, you need to create a **[tool variable](/guides/agents/tool-variables)** called `COMPOSIO_ENTITY` and set it to be your Composio `ENTITY ID`. If you don't set `COMPOSIO_ENTITY`, Letta will default to assuming it is `default`.
|
||||
<img src="../../images/tool_variables.png" />
|
||||
|
||||
You can also assign tool variables on agent creation in the API with the `tool_exec_environment_variables` parameter (see [examples here](/guides/agents/tool-variables)).
|
||||
|
||||
## Entities in Composio tools for multi-user
|
||||
In multi-user settings (where you have many users all using different agents), you may want to use the concept of [entities](https://docs.composio.dev/patterns/Auth/connected_account#entities) in Composio, which allow you to scope Composio tool execution to specific users.
|
||||
|
||||
For example, let's say you're using Letta to create an application where users each get their own personal secretary that can schedule their calendar. As a developer, you only have one `COMPOSIO_API_KEY` to manage the connection between Letta and Composio, but you want to make associate each Composio tool call from a specific agent with a specific user.
|
||||
|
||||
Composio allows you to do this through **entities**: each **user** on your Composio account will have a unique Composio entity ID, and in Letta each **agent** will be associated with a specific Composio entity ID.
|
||||
|
||||
## Adding Composio tools to agents in the Python SDK
|
||||
<Note>
|
||||
Adding Composio tools to agents is supported in the Python SDK, but not the TypeScript SDK.
|
||||
</Note>
|
||||
|
||||
To use Letta with [Composio](https://docs.composio.dev) tools, make sure you install dependencies with `pip install 'letta[external-tools]`. Then, make sure you log in to Composio:
|
||||
```bash title="shell"
|
||||
composio login
|
||||
```
|
||||
|
||||
Next, depending on your desired Composio tool, you need to add the necessary authentication via `composio add` (for example, to connect GitHub tools):
|
||||
```bash title="shell"
|
||||
composio add github
|
||||
```
|
||||
To attach a Composio tool to an agent, you must first create a Letta tool from composio by specifying the action name:
|
||||
```python title="python"
|
||||
from composio import Action
|
||||
|
||||
# create a Letta tool object
|
||||
tool = client.tools.add_composio_tool(
|
||||
composio_action_name=Action.GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER.name
|
||||
)
|
||||
```
|
||||
Below is a full example of creating a Letta agent that can start a Github repository.
|
||||
```python title="python" maxLines=50
|
||||
from letta_client import Letta
|
||||
from composio import Action
|
||||
|
||||
client = Letta(base_url="http://localhost:8283")
|
||||
|
||||
# add a composio tool
|
||||
tool = client.tools.add_composio_tool(composio_action_name=Action.GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER.name)
|
||||
|
||||
# create an agent with the tool
|
||||
agent = client.agents.create(
|
||||
name="file_editing_agent",
|
||||
memory_blocks=[
|
||||
{"label": "persona", "value": "I am a helpful assistant"}
|
||||
],
|
||||
model="anthropic/claude-3-5-sonnet-20241022",
|
||||
embedding="openai/text-embedding-3-small",
|
||||
tool_ids=[tool.id]
|
||||
)
|
||||
print("Agent tools", [tool.name for tool in agent.tools])
|
||||
|
||||
# message the agent
|
||||
response = client.agents.messages.create(
|
||||
agent_id=agent.id,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Star the github repo `letta` by `letta-ai`"
|
||||
}
|
||||
]
|
||||
)
|
||||
for message in response.messages:
|
||||
print(message)
|
||||
```
|
||||
@@ -28,7 +28,7 @@ curl -X POST http://localhost:8283/v1/agents/ \
|
||||
"llm":"openai/gpt-4o-mini",
|
||||
"embedding":"openai/text-embedding-3-small",
|
||||
"tool_exec_environment_variables": {
|
||||
"COMPOSIO_ENTITY": "banana"
|
||||
"API_KEY": "your-api-key-here"
|
||||
}
|
||||
}'
|
||||
```
|
||||
@@ -38,7 +38,7 @@ agent_state = client.agents.create(
|
||||
model="openai/gpt-4o-mini",
|
||||
embedding="openai/text-embedding-3-small",
|
||||
tool_exec_environment_variables={
|
||||
"COMPOSIO_ENTITY": "banana"
|
||||
"API_KEY": "your-api-key-here"
|
||||
}
|
||||
)
|
||||
```
|
||||
@@ -48,7 +48,7 @@ const agentState = await client.agents.create({
|
||||
model: "openai/gpt-4o-mini",
|
||||
embedding: "openai/text-embedding-3-small",
|
||||
toolExecEnvironmentVariables: {
|
||||
"COMPOSIO_ENTITY": "banana"
|
||||
"API_KEY": "your-api-key-here"
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -86,7 +86,7 @@ If the Letta server is not password protected, we can omit the `X-BARE-PASSWORD`
|
||||
|
||||
### Adding additional environment variables
|
||||
|
||||
To help you get started, when you deploy the template you have the option to fill in the example environment variables `OPENAI_API_KEY` (to connect your Letta agents to GPT models), `ANTHROPIC_API_KEY` (to connect your Letta agents to Claude models), and `COMPOSIO_API_KEY` (to connect your Letta agents to [Composio's library of over 7k pre-made tools](/guides/agents/composio)).
|
||||
To help you get started, when you deploy the template you have the option to fill in the example environment variables `OPENAI_API_KEY` (to connect your Letta agents to GPT models) and `ANTHROPIC_API_KEY` (to connect your Letta agents to Claude models).
|
||||
|
||||
There are many more providers you can enable on the Letta server via additional environment variables (for example vLLM, Ollama, etc). For more information on available providers, see [our documentation](/guides/server/docker).
|
||||
|
||||
|
||||
@@ -1595,7 +1595,6 @@ class LettaAgent(BaseAgent):
|
||||
ToolType.LETTA_VOICE_SLEEPTIME_CORE,
|
||||
ToolType.LETTA_BUILTIN,
|
||||
ToolType.LETTA_FILES_CORE,
|
||||
ToolType.EXTERNAL_COMPOSIO,
|
||||
ToolType.EXTERNAL_MCP,
|
||||
}
|
||||
]
|
||||
|
||||
@@ -343,8 +343,7 @@ class VoiceAgent(BaseAgent):
|
||||
tools = [
|
||||
t
|
||||
for t in agent_state.tools
|
||||
if t.tool_type
|
||||
in {ToolType.EXTERNAL_COMPOSIO, ToolType.CUSTOM, ToolType.LETTA_FILES_CORE, ToolType.LETTA_BUILTIN, ToolType.EXTERNAL_MCP}
|
||||
if t.tool_type in {ToolType.CUSTOM, ToolType.LETTA_FILES_CORE, ToolType.LETTA_BUILTIN, ToolType.EXTERNAL_MCP}
|
||||
]
|
||||
else:
|
||||
tools = agent_state.tools
|
||||
|
||||
@@ -13,9 +13,6 @@ API_PREFIX = "/v1"
|
||||
OLLAMA_API_PREFIX = "/v1"
|
||||
OPENAI_API_PREFIX = "/openai"
|
||||
|
||||
COMPOSIO_ENTITY_ENV_VAR_KEY = "COMPOSIO_ENTITY"
|
||||
COMPOSIO_TOOL_TAG_NAME = "composio"
|
||||
|
||||
MCP_CONFIG_NAME = "mcp_config.json"
|
||||
MCP_TOOL_TAG_NAME_PREFIX = "mcp" # full format, mcp:server_name
|
||||
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
from composio import ComposioToolSet as BaseComposioToolSet
|
||||
from composio.exceptions import (
|
||||
ApiKeyNotProvidedError,
|
||||
ComposioSDKError,
|
||||
ConnectedAccountNotFoundError,
|
||||
EnumMetadataNotFound,
|
||||
EnumStringNotFound,
|
||||
)
|
||||
|
||||
|
||||
class AsyncComposioToolSet(BaseComposioToolSet, runtime="letta", description_char_limit=1024):
|
||||
"""
|
||||
Async version of ComposioToolSet client for interacting with Composio API
|
||||
Used to asynchronously hit the execute action endpoint
|
||||
|
||||
https://docs.composio.dev/api-reference/api-reference/v3/tools/post-api-v-3-tools-execute-action
|
||||
"""
|
||||
|
||||
def __init__(self, api_key: str, entity_id: str, lock: bool = True):
|
||||
"""
|
||||
Initialize the AsyncComposioToolSet client
|
||||
|
||||
Args:
|
||||
api_key (str): Your Composio API key
|
||||
entity_id (str): Your Composio entity ID
|
||||
lock (bool): Whether to use locking (default: True)
|
||||
"""
|
||||
super().__init__(api_key=api_key, entity_id=entity_id, lock=lock)
|
||||
|
||||
self.headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-API-Key": self._api_key,
|
||||
}
|
||||
|
||||
async def execute_action(
|
||||
self,
|
||||
action: str,
|
||||
params: dict[str, Any] = {},
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Execute an action asynchronously using the Composio API
|
||||
|
||||
Args:
|
||||
action (str): The name of the action to execute
|
||||
params (dict[str, Any], optional): Parameters for the action
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: The API response
|
||||
|
||||
Raises:
|
||||
ApiKeyNotProvidedError: if the API key is not provided
|
||||
ComposioSDKError: if a general Composio SDK error occurs
|
||||
ConnectedAccountNotFoundError: if the connected account is not found
|
||||
EnumMetadataNotFound: if enum metadata is not found
|
||||
EnumStringNotFound: if enum string is not found
|
||||
aiohttp.ClientError: if a network-related error occurs
|
||||
ValueError: if an error with the parameters or response occurs
|
||||
"""
|
||||
API_VERSION = "v3"
|
||||
endpoint = f"{self._base_url}/{API_VERSION}/tools/execute/{action}"
|
||||
|
||||
json_payload = {
|
||||
"entity_id": self.entity_id,
|
||||
"arguments": params or {},
|
||||
}
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(endpoint, headers=self.headers, json=json_payload) as response:
|
||||
print(response, response.status, response.reason, response.content)
|
||||
if response.status == 200:
|
||||
return await response.json()
|
||||
else:
|
||||
error_text = await response.text()
|
||||
try:
|
||||
error_json = json.loads(error_text)
|
||||
error_message = error_json.get("message", error_text)
|
||||
error_code = error_json.get("code")
|
||||
|
||||
# Handle specific error codes from Composio API
|
||||
if error_code == 10401 or "API_KEY_NOT_FOUND" in error_message:
|
||||
raise ApiKeyNotProvidedError()
|
||||
if (
|
||||
"connected account not found" in error_message.lower()
|
||||
or "no connected account found" in error_message.lower()
|
||||
):
|
||||
raise ConnectedAccountNotFoundError(f"Connected account not found: {error_message}")
|
||||
if "enum metadata not found" in error_message.lower():
|
||||
raise EnumMetadataNotFound(f"Enum metadata not found: {error_message}")
|
||||
if "enum string not found" in error_message.lower():
|
||||
raise EnumStringNotFound(f"Enum string not found: {error_message}")
|
||||
except json.JSONDecodeError:
|
||||
error_message = error_text
|
||||
|
||||
# If no specific error was identified, raise a general error
|
||||
raise ValueError(f"API request failed with status {response.status}: {error_message}")
|
||||
except aiohttp.ClientError as e:
|
||||
# Wrap network errors in ComposioSDKError
|
||||
raise ComposioSDKError(f"Network error when calling Composio API: {str(e)}")
|
||||
except ValueError:
|
||||
# Re-raise ValueError (which could be our custom error message or a JSON parsing error)
|
||||
raise
|
||||
except Exception as e:
|
||||
# Catch any other exceptions and wrap them in ComposioSDKError
|
||||
raise ComposioSDKError(f"Unexpected error when calling Composio API: {str(e)}")
|
||||
@@ -1,96 +0,0 @@
|
||||
import os
|
||||
from typing import Any, Optional
|
||||
|
||||
from composio.constants import DEFAULT_ENTITY_ID
|
||||
from composio.exceptions import (
|
||||
ApiKeyNotProvidedError,
|
||||
ComposioSDKError,
|
||||
ConnectedAccountNotFoundError,
|
||||
EnumMetadataNotFound,
|
||||
EnumStringNotFound,
|
||||
)
|
||||
|
||||
from letta.constants import COMPOSIO_ENTITY_ENV_VAR_KEY
|
||||
from letta.functions.async_composio_toolset import AsyncComposioToolSet
|
||||
from letta.utils import run_async_task
|
||||
|
||||
|
||||
# TODO: This is kind of hacky, as this is used to search up the action later on composio's side
|
||||
# TODO: So be very careful changing/removing these pair of functions
|
||||
def _generate_func_name_from_composio_action(action_name: str) -> str:
|
||||
"""
|
||||
Generates the composio function name from the composio action.
|
||||
|
||||
Args:
|
||||
action_name: The composio action name
|
||||
|
||||
Returns:
|
||||
function name
|
||||
"""
|
||||
return action_name.lower()
|
||||
|
||||
|
||||
def generate_composio_action_from_func_name(func_name: str) -> str:
|
||||
"""
|
||||
Generates the composio action from the composio function name.
|
||||
|
||||
Args:
|
||||
func_name: The composio function name
|
||||
|
||||
Returns:
|
||||
composio action name
|
||||
"""
|
||||
return func_name.upper()
|
||||
|
||||
|
||||
def generate_composio_tool_wrapper(action_name: str) -> tuple[str, str]:
|
||||
# Generate func name
|
||||
func_name = _generate_func_name_from_composio_action(action_name)
|
||||
|
||||
wrapper_function_str = f"""\
|
||||
def {func_name}(**kwargs):
|
||||
raise RuntimeError("Something went wrong - we should never be using the persisted source code for Composio. Please reach out to Letta team")
|
||||
"""
|
||||
|
||||
# Compile safety check
|
||||
_assert_code_gen_compilable(wrapper_function_str.strip())
|
||||
|
||||
return func_name, wrapper_function_str.strip()
|
||||
|
||||
|
||||
async def execute_composio_action_async(
|
||||
action_name: str, args: dict, api_key: Optional[str] = None, entity_id: Optional[str] = None
|
||||
) -> tuple[str, str]:
|
||||
entity_id = entity_id or os.getenv(COMPOSIO_ENTITY_ENV_VAR_KEY, DEFAULT_ENTITY_ID)
|
||||
composio_toolset = AsyncComposioToolSet(api_key=api_key, entity_id=entity_id, lock=False)
|
||||
try:
|
||||
response = await composio_toolset.execute_action(action=action_name, params=args)
|
||||
except ApiKeyNotProvidedError as e:
|
||||
raise RuntimeError(f"API key not provided or invalid for Composio action '{action_name}': {str(e)}")
|
||||
except ConnectedAccountNotFoundError as e:
|
||||
raise RuntimeError(f"Connected account not found for Composio action '{action_name}': {str(e)}")
|
||||
except EnumMetadataNotFound as e:
|
||||
raise RuntimeError(f"Enum metadata not found for Composio action '{action_name}': {str(e)}")
|
||||
except EnumStringNotFound as e:
|
||||
raise RuntimeError(f"Enum string not found for Composio action '{action_name}': {str(e)}")
|
||||
except ComposioSDKError as e:
|
||||
raise RuntimeError(f"Composio SDK error while executing action '{action_name}': {str(e)}")
|
||||
except Exception as e:
|
||||
print(type(e))
|
||||
raise RuntimeError(f"An unexpected error occurred in Composio SDK while executing action '{action_name}': {str(e)}")
|
||||
|
||||
if "error" in response and response["error"]:
|
||||
raise RuntimeError(f"Error while executing action '{action_name}': {str(response['error'])}")
|
||||
|
||||
return response.get("data")
|
||||
|
||||
|
||||
def execute_composio_action(action_name: str, args: dict, api_key: Optional[str] = None, entity_id: Optional[str] = None) -> Any:
|
||||
return run_async_task(execute_composio_action_async(action_name, args, api_key, entity_id))
|
||||
|
||||
|
||||
def _assert_code_gen_compilable(code_str):
|
||||
try:
|
||||
compile(code_str, "<string>", "exec")
|
||||
except SyntaxError as e:
|
||||
print(f"Syntax error in code: {e}")
|
||||
@@ -2,7 +2,6 @@ import inspect
|
||||
import warnings
|
||||
from typing import Any, Dict, List, Optional, Tuple, Type, Union, get_args, get_origin
|
||||
|
||||
from composio.client.collections import ActionParametersModel
|
||||
from docstring_parser import parse
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import Literal
|
||||
@@ -792,73 +791,3 @@ def generate_tool_schema_for_mcp(
|
||||
"description": description,
|
||||
"parameters": parameters_schema,
|
||||
}
|
||||
|
||||
|
||||
def generate_tool_schema_for_composio(
|
||||
parameters_model: ActionParametersModel,
|
||||
name: str,
|
||||
description: str,
|
||||
append_heartbeat: bool = True,
|
||||
strict: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
properties_json = {}
|
||||
required_fields = parameters_model.required or []
|
||||
|
||||
# Extract properties from the ActionParametersModel
|
||||
for field_name, field_props in parameters_model.properties.items():
|
||||
# Initialize the property structure
|
||||
property_schema = {
|
||||
"type": field_props["type"],
|
||||
"description": field_props.get("description", ""),
|
||||
}
|
||||
|
||||
# Handle optional default values
|
||||
if "default" in field_props:
|
||||
property_schema["default"] = field_props["default"]
|
||||
|
||||
# Handle enumerations
|
||||
if "enum" in field_props:
|
||||
property_schema["enum"] = field_props["enum"]
|
||||
|
||||
# Handle array item types
|
||||
if field_props["type"] == "array":
|
||||
if "items" in field_props:
|
||||
property_schema["items"] = field_props["items"]
|
||||
elif "anyOf" in field_props:
|
||||
property_schema["items"] = [t for t in field_props["anyOf"] if "items" in t][0]["items"]
|
||||
|
||||
# Add the property to the schema
|
||||
properties_json[field_name] = property_schema
|
||||
|
||||
# Add the optional heartbeat parameter
|
||||
if append_heartbeat:
|
||||
properties_json[REQUEST_HEARTBEAT_PARAM] = {
|
||||
"type": "boolean",
|
||||
"description": REQUEST_HEARTBEAT_DESCRIPTION,
|
||||
}
|
||||
required_fields.append(REQUEST_HEARTBEAT_PARAM)
|
||||
|
||||
# Return the final schema
|
||||
if strict:
|
||||
# https://platform.openai.com/docs/guides/function-calling#strict-mode
|
||||
return {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"strict": True, # NOTE
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": properties_json,
|
||||
"additionalProperties": False, # NOTE
|
||||
"required": required_fields,
|
||||
},
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": properties_json,
|
||||
"required": required_fields,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
from logging import Logger
|
||||
from typing import Optional
|
||||
|
||||
from letta.schemas.user import User
|
||||
from letta.services.sandbox_config_manager import SandboxConfigManager
|
||||
from letta.settings import tool_settings
|
||||
|
||||
|
||||
def get_composio_api_key(actor: User, logger: Optional[Logger] = None) -> Optional[str]:
|
||||
api_keys = SandboxConfigManager().list_sandbox_env_vars_by_key(key="COMPOSIO_API_KEY", actor=actor)
|
||||
if not api_keys:
|
||||
if logger:
|
||||
logger.debug("No API keys found for Composio. Defaulting to the environment variable...")
|
||||
if tool_settings.composio_api_key:
|
||||
return tool_settings.composio_api_key
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
# TODO: Add more protections around this
|
||||
# Ideally, not tied to a specific sandbox, but for now we just get the first one
|
||||
# Theoretically possible for someone to have different composio api keys per sandbox
|
||||
return api_keys[0].value
|
||||
|
||||
|
||||
async def get_composio_api_key_async(actor: User, logger: Optional[Logger] = None) -> Optional[str]:
|
||||
api_keys = await SandboxConfigManager().list_sandbox_env_vars_by_key_async(key="COMPOSIO_API_KEY", actor=actor)
|
||||
if not api_keys:
|
||||
if logger:
|
||||
logger.debug("No API keys found for Composio. Defaulting to the environment variable...")
|
||||
if tool_settings.composio_api_key:
|
||||
return tool_settings.composio_api_key
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
# TODO: Add more protections around this
|
||||
# Ideally, not tied to a specific sandbox, but for now we just get the first one
|
||||
# Theoretically possible for someone to have different composio api keys per sandbox
|
||||
return api_keys[0].value
|
||||
@@ -4,9 +4,6 @@ import typer
|
||||
|
||||
from letta.cli.cli import server
|
||||
|
||||
# disable composio print on exit
|
||||
os.environ["COMPOSIO_DISABLE_VERSION_CHECK"] = "true"
|
||||
|
||||
app = typer.Typer(pretty_exceptions_enable=False)
|
||||
|
||||
# Register server as both the default command and as a subcommand
|
||||
|
||||
@@ -152,12 +152,13 @@ class ToolType(str, Enum):
|
||||
LETTA_VOICE_SLEEPTIME_CORE = "letta_voice_sleeptime_core"
|
||||
LETTA_BUILTIN = "letta_builtin"
|
||||
LETTA_FILES_CORE = "letta_files_core"
|
||||
EXTERNAL_COMPOSIO = "external_composio"
|
||||
EXTERNAL_LANGCHAIN = "external_langchain"
|
||||
EXTERNAL_LANGCHAIN = "external_langchain" # DEPRECATED
|
||||
EXTERNAL_COMPOSIO = "external_composio" # DEPRECATED
|
||||
# TODO is "external" the right name here? Since as of now, MCP is local / doesn't support remote?
|
||||
EXTERNAL_MCP = "external_mcp"
|
||||
|
||||
|
||||
|
||||
class JobType(str, Enum):
|
||||
JOB = "job"
|
||||
RUN = "run"
|
||||
|
||||
@@ -3,7 +3,6 @@ from typing import Any, Dict, List, Optional
|
||||
from pydantic import ConfigDict, Field, model_validator
|
||||
|
||||
from letta.constants import (
|
||||
COMPOSIO_TOOL_TAG_NAME,
|
||||
FUNCTION_RETURN_CHAR_LIMIT,
|
||||
LETTA_BUILTIN_TOOL_MODULE_NAME,
|
||||
LETTA_CORE_TOOL_MODULE_NAME,
|
||||
@@ -16,13 +15,9 @@ from letta.constants import (
|
||||
# MCP Tool metadata constants for schema health status
|
||||
MCP_TOOL_METADATA_SCHEMA_STATUS = f"{MCP_TOOL_TAG_NAME_PREFIX}:SCHEMA_STATUS"
|
||||
MCP_TOOL_METADATA_SCHEMA_WARNINGS = f"{MCP_TOOL_TAG_NAME_PREFIX}:SCHEMA_WARNINGS"
|
||||
from letta.functions.composio_helpers import generate_composio_tool_wrapper
|
||||
from letta.functions.functions import get_json_schema_from_module
|
||||
from letta.functions.mcp_client.types import MCPTool
|
||||
from letta.functions.schema_generator import (
|
||||
generate_tool_schema_for_composio,
|
||||
generate_tool_schema_for_mcp,
|
||||
)
|
||||
from letta.functions.schema_generator import generate_tool_schema_for_mcp
|
||||
from letta.log import get_logger
|
||||
from letta.schemas.enums import ToolSourceType, ToolType
|
||||
from letta.schemas.letta_base import LettaBase
|
||||
@@ -106,9 +101,6 @@ class Tool(BaseTool):
|
||||
elif self.tool_type in {ToolType.LETTA_FILES_CORE}:
|
||||
# If it's letta files tool, we generate the json_schema on the fly here
|
||||
self.json_schema = get_json_schema_from_module(module_name=LETTA_FILES_TOOL_MODULE_NAME, function_name=self.name)
|
||||
elif self.tool_type in {ToolType.EXTERNAL_COMPOSIO}:
|
||||
# Composio schemas handled separately
|
||||
pass
|
||||
|
||||
return self
|
||||
|
||||
@@ -153,45 +145,6 @@ class ToolCreate(LettaBase):
|
||||
json_schema=json_schema,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_composio(cls, action_name: str) -> "ToolCreate":
|
||||
"""
|
||||
Class method to create an instance of Letta-compatible Composio Tool.
|
||||
Check https://docs.composio.dev/introduction/intro/overview to look at options for from_composio
|
||||
|
||||
This function will error if we find more than one tool, or 0 tools.
|
||||
|
||||
Args:
|
||||
action_name str: A action name to filter tools by.
|
||||
Returns:
|
||||
Tool: A Letta Tool initialized with attributes derived from the Composio tool.
|
||||
"""
|
||||
from composio import ComposioToolSet, LogLevel
|
||||
|
||||
composio_toolset = ComposioToolSet(logging_level=LogLevel.ERROR, lock=False)
|
||||
composio_action_schemas = composio_toolset.get_action_schemas(actions=[action_name], check_connected_accounts=False)
|
||||
|
||||
assert len(composio_action_schemas) > 0, "User supplied parameters do not match any Composio tools"
|
||||
assert len(composio_action_schemas) == 1, (
|
||||
f"User supplied parameters match too many Composio tools; {len(composio_action_schemas)} > 1"
|
||||
)
|
||||
|
||||
composio_action_schema = composio_action_schemas[0]
|
||||
|
||||
description = composio_action_schema.description
|
||||
source_type = "python"
|
||||
tags = [COMPOSIO_TOOL_TAG_NAME]
|
||||
wrapper_func_name, wrapper_function_str = generate_composio_tool_wrapper(action_name)
|
||||
json_schema = generate_tool_schema_for_composio(composio_action_schema.parameters, name=wrapper_func_name, description=description)
|
||||
|
||||
return cls(
|
||||
description=description,
|
||||
source_type=source_type,
|
||||
tags=tags,
|
||||
source_code=wrapper_function_str,
|
||||
json_schema=json_schema,
|
||||
)
|
||||
|
||||
|
||||
class ToolUpdate(LettaBase):
|
||||
description: Optional[str] = Field(None, description="The description of the tool.")
|
||||
|
||||
@@ -2,15 +2,6 @@ import json
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any, Dict, List, Literal, Optional, Union
|
||||
|
||||
from composio.client import ComposioClientError, HTTPError, NoItemsFound
|
||||
from composio.client.collections import ActionModel, AppModel
|
||||
from composio.exceptions import (
|
||||
ApiKeyNotProvidedError,
|
||||
ComposioSDKError,
|
||||
ConnectedAccountNotFoundError,
|
||||
EnumMetadataNotFound,
|
||||
EnumStringNotFound,
|
||||
)
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, Query, Request
|
||||
from httpx import ConnectError, HTTPStatusError
|
||||
from pydantic import BaseModel, Field
|
||||
@@ -20,7 +11,6 @@ from letta.errors import LettaToolCreateError, LettaToolNameConflictError
|
||||
from letta.functions.functions import derive_openai_json_schema
|
||||
from letta.functions.mcp_client.exceptions import MCPTimeoutError
|
||||
from letta.functions.mcp_client.types import MCPTool, SSEServerConfig, StdioServerConfig, StreamableHTTPServerConfig
|
||||
from letta.helpers.composio_helpers import get_composio_api_key
|
||||
from letta.helpers.decorators import deprecated
|
||||
from letta.llm_api.llm_client import LLMClient
|
||||
from letta.log import get_logger
|
||||
@@ -357,132 +347,6 @@ async def run_tool_from_source(
|
||||
)
|
||||
|
||||
|
||||
# Specific routes for Composio
|
||||
@router.get("/composio/apps", response_model=List[AppModel], operation_id="list_composio_apps")
|
||||
async def list_composio_apps(
|
||||
server: SyncServer = Depends(get_letta_server),
|
||||
headers: HeaderParams = Depends(get_headers),
|
||||
):
|
||||
"""
|
||||
Get a list of all Composio apps
|
||||
"""
|
||||
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
||||
composio_api_key = get_composio_api_key(actor=actor, logger=logger)
|
||||
if not composio_api_key:
|
||||
raise HTTPException(
|
||||
status_code=400, # Bad Request
|
||||
detail="No API keys found for Composio. Please add your Composio API Key as an environment variable for your sandbox configuration, or set it as environment variable COMPOSIO_API_KEY.",
|
||||
)
|
||||
return server.get_composio_apps(api_key=composio_api_key)
|
||||
|
||||
|
||||
@router.get("/composio/apps/{composio_app_name}/actions", response_model=List[ActionModel], operation_id="list_composio_actions_by_app")
|
||||
async def list_composio_actions_by_app(
|
||||
composio_app_name: str,
|
||||
server: SyncServer = Depends(get_letta_server),
|
||||
headers: HeaderParams = Depends(get_headers),
|
||||
):
|
||||
"""
|
||||
Get a list of all Composio actions for a specific app
|
||||
"""
|
||||
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
||||
composio_api_key = get_composio_api_key(actor=actor, logger=logger)
|
||||
if not composio_api_key:
|
||||
raise HTTPException(
|
||||
status_code=400, # Bad Request
|
||||
detail="No API keys found for Composio. Please add your Composio API Key as an environment variable for your sandbox configuration, or set it as environment variable COMPOSIO_API_KEY.",
|
||||
)
|
||||
return server.get_composio_actions_from_app_name(composio_app_name=composio_app_name, api_key=composio_api_key)
|
||||
|
||||
|
||||
@router.post("/composio/{composio_action_name}", response_model=Tool, operation_id="add_composio_tool")
|
||||
async def add_composio_tool(
|
||||
composio_action_name: str,
|
||||
server: SyncServer = Depends(get_letta_server),
|
||||
headers: HeaderParams = Depends(get_headers),
|
||||
):
|
||||
"""
|
||||
Add a new Composio tool by action name (Composio refers to each tool as an `Action`)
|
||||
"""
|
||||
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
||||
|
||||
try:
|
||||
tool_create = ToolCreate.from_composio(action_name=composio_action_name)
|
||||
return await server.tool_manager.create_or_update_composio_tool_async(tool_create=tool_create, actor=actor)
|
||||
except ConnectedAccountNotFoundError as e:
|
||||
raise HTTPException(
|
||||
status_code=400, # Bad Request
|
||||
detail={
|
||||
"code": "ConnectedAccountNotFoundError",
|
||||
"message": str(e),
|
||||
"composio_action_name": composio_action_name,
|
||||
},
|
||||
)
|
||||
except EnumStringNotFound as e:
|
||||
raise HTTPException(
|
||||
status_code=400, # Bad Request
|
||||
detail={
|
||||
"code": "EnumStringNotFound",
|
||||
"message": str(e),
|
||||
"composio_action_name": composio_action_name,
|
||||
},
|
||||
)
|
||||
except EnumMetadataNotFound as e:
|
||||
raise HTTPException(
|
||||
status_code=400, # Bad Request
|
||||
detail={
|
||||
"code": "EnumMetadataNotFound",
|
||||
"message": str(e),
|
||||
"composio_action_name": composio_action_name,
|
||||
},
|
||||
)
|
||||
except HTTPError as e:
|
||||
raise HTTPException(
|
||||
status_code=400, # Bad Request
|
||||
detail={
|
||||
"code": "HTTPError",
|
||||
"message": str(e),
|
||||
"composio_action_name": composio_action_name,
|
||||
},
|
||||
)
|
||||
except NoItemsFound as e:
|
||||
raise HTTPException(
|
||||
status_code=400, # Bad Request
|
||||
detail={
|
||||
"code": "NoItemsFound",
|
||||
"message": str(e),
|
||||
"composio_action_name": composio_action_name,
|
||||
},
|
||||
)
|
||||
except ApiKeyNotProvidedError as e:
|
||||
raise HTTPException(
|
||||
status_code=400, # Bad Request
|
||||
detail={
|
||||
"code": "ApiKeyNotProvidedError",
|
||||
"message": str(e),
|
||||
"composio_action_name": composio_action_name,
|
||||
},
|
||||
)
|
||||
except ComposioClientError as e:
|
||||
raise HTTPException(
|
||||
status_code=400, # Bad Request
|
||||
detail={
|
||||
"code": "ComposioClientError",
|
||||
"message": str(e),
|
||||
"composio_action_name": composio_action_name,
|
||||
},
|
||||
)
|
||||
except ComposioSDKError as e:
|
||||
raise HTTPException(
|
||||
status_code=400, # Bad Request
|
||||
detail={
|
||||
"code": "ComposioSDKError",
|
||||
"message": str(e),
|
||||
"composio_action_name": composio_action_name,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# Specific routes for MCP
|
||||
@router.get(
|
||||
"/mcp/servers",
|
||||
|
||||
@@ -10,8 +10,6 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
||||
|
||||
import httpx
|
||||
from anthropic import AsyncAnthropic
|
||||
from composio.client import Composio
|
||||
from composio.client.collections import ActionModel, AppModel
|
||||
from fastapi import HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
@@ -319,19 +317,6 @@ class SyncServer(object):
|
||||
print(f"Default user: {self.default_user} and org: {self.default_org}")
|
||||
await self.tool_manager.upsert_base_tools_async(actor=self.default_user)
|
||||
|
||||
# Add composio keys to the tool sandbox env vars of the org
|
||||
if tool_settings.composio_api_key:
|
||||
manager = SandboxConfigManager()
|
||||
sandbox_config = await manager.get_or_create_default_sandbox_config_async(
|
||||
sandbox_type=SandboxType.LOCAL, actor=self.default_user
|
||||
)
|
||||
|
||||
await manager.create_sandbox_env_var_async(
|
||||
SandboxEnvironmentVariableCreate(key="COMPOSIO_API_KEY", value=tool_settings.composio_api_key),
|
||||
sandbox_config_id=sandbox_config.id,
|
||||
actor=self.default_user,
|
||||
)
|
||||
|
||||
# For OSS users, create a local sandbox config
|
||||
oss_default_user = await self.user_manager.get_default_actor_async()
|
||||
use_venv = False if not tool_settings.tool_exec_venv_name else True
|
||||
@@ -1259,33 +1244,6 @@ class SyncServer(object):
|
||||
stderr=[traceback.format_exc()],
|
||||
)
|
||||
|
||||
# Composio wrappers
|
||||
@staticmethod
|
||||
def get_composio_client(api_key: Optional[str] = None):
|
||||
if api_key:
|
||||
return Composio(api_key=api_key)
|
||||
elif tool_settings.composio_api_key:
|
||||
return Composio(api_key=tool_settings.composio_api_key)
|
||||
else:
|
||||
return Composio()
|
||||
|
||||
@staticmethod
|
||||
def get_composio_apps(api_key: Optional[str] = None) -> List["AppModel"]:
|
||||
"""Get a list of all Composio apps with actions"""
|
||||
apps = SyncServer.get_composio_client(api_key=api_key).apps.get()
|
||||
apps_with_actions = []
|
||||
for app in apps:
|
||||
# A bit of hacky logic until composio patches this
|
||||
if app.meta["actionsCount"] > 0 and not app.name.lower().endswith("_beta"):
|
||||
apps_with_actions.append(app)
|
||||
|
||||
return apps_with_actions
|
||||
|
||||
def get_composio_actions_from_app_name(self, composio_app_name: str, api_key: Optional[str] = None) -> List["ActionModel"]:
|
||||
actions = self.get_composio_client(api_key=api_key).actions.get(apps=[composio_app_name])
|
||||
# Filter out deprecated composio actions
|
||||
return [action for action in actions if "deprecated" not in action.description.lower()]
|
||||
|
||||
# MCP wrappers
|
||||
# TODO support both command + SSE servers (via config)
|
||||
def get_mcp_servers(self) -> dict[str, Union[SSEServerConfig, StdioServerConfig]]:
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from letta.constants import COMPOSIO_ENTITY_ENV_VAR_KEY
|
||||
from letta.functions.composio_helpers import execute_composio_action_async, generate_composio_action_from_func_name
|
||||
from letta.helpers.composio_helpers import get_composio_api_key_async
|
||||
from letta.otel.tracing import trace_method
|
||||
from letta.schemas.agent import AgentState
|
||||
from letta.schemas.sandbox_config import SandboxConfig
|
||||
from letta.schemas.tool import Tool
|
||||
from letta.schemas.tool_execution_result import ToolExecutionResult
|
||||
from letta.schemas.user import User
|
||||
from letta.services.tool_executor.tool_executor_base import ToolExecutor
|
||||
|
||||
|
||||
class ExternalComposioToolExecutor(ToolExecutor):
|
||||
"""Executor for external Composio tools."""
|
||||
|
||||
@trace_method
|
||||
async def execute(
|
||||
self,
|
||||
function_name: str,
|
||||
function_args: dict,
|
||||
tool: Tool,
|
||||
actor: User,
|
||||
agent_state: Optional[AgentState] = None,
|
||||
sandbox_config: Optional[SandboxConfig] = None,
|
||||
sandbox_env_vars: Optional[Dict[str, Any]] = None,
|
||||
) -> ToolExecutionResult:
|
||||
if agent_state is None:
|
||||
return ToolExecutionResult(
|
||||
status="error",
|
||||
func_return="Agent state is required for external Composio tools. Please contact Letta support if you see this error.",
|
||||
)
|
||||
action_name = generate_composio_action_from_func_name(tool.name)
|
||||
|
||||
# Get entity ID from the agent_state
|
||||
entity_id = self._get_entity_id(agent_state)
|
||||
|
||||
# Get composio_api_key
|
||||
composio_api_key = await get_composio_api_key_async(actor=actor)
|
||||
|
||||
# TODO (matt): Roll in execute_composio_action into this class
|
||||
function_response = await execute_composio_action_async(
|
||||
action_name=action_name, args=function_args, api_key=composio_api_key, entity_id=entity_id
|
||||
)
|
||||
|
||||
return ToolExecutionResult(
|
||||
status="success",
|
||||
func_return=function_response,
|
||||
)
|
||||
|
||||
def _get_entity_id(self, agent_state: AgentState) -> Optional[str]:
|
||||
"""Extract the entity ID from environment variables."""
|
||||
for env_var in agent_state.secrets:
|
||||
if env_var.key == COMPOSIO_ENTITY_ENV_VAR_KEY:
|
||||
return env_var.value
|
||||
return None
|
||||
@@ -20,7 +20,6 @@ from letta.services.message_manager import MessageManager
|
||||
from letta.services.passage_manager import PassageManager
|
||||
from letta.services.run_manager import RunManager
|
||||
from letta.services.tool_executor.builtin_tool_executor import LettaBuiltinToolExecutor
|
||||
from letta.services.tool_executor.composio_tool_executor import ExternalComposioToolExecutor
|
||||
from letta.services.tool_executor.core_tool_executor import LettaCoreToolExecutor
|
||||
from letta.services.tool_executor.files_tool_executor import LettaFileToolExecutor
|
||||
from letta.services.tool_executor.mcp_tool_executor import ExternalMCPToolExecutor
|
||||
@@ -40,7 +39,6 @@ class ToolExecutorFactory:
|
||||
ToolType.LETTA_MULTI_AGENT_CORE: LettaMultiAgentToolExecutor,
|
||||
ToolType.LETTA_BUILTIN: LettaBuiltinToolExecutor,
|
||||
ToolType.LETTA_FILES_CORE: LettaFileToolExecutor,
|
||||
ToolType.EXTERNAL_COMPOSIO: ExternalComposioToolExecutor,
|
||||
ToolType.EXTERNAL_MCP: ExternalMCPToolExecutor,
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ class ToolManager:
|
||||
generated_schema = generate_schema_for_tool_creation(pydantic_tool)
|
||||
if generated_schema:
|
||||
pydantic_tool.json_schema = generated_schema
|
||||
else:
|
||||
else:
|
||||
raise ValueError("Failed to generate schema for tool", pydantic_tool.source_code)
|
||||
|
||||
print("SCHEMA", pydantic_tool.json_schema)
|
||||
@@ -91,9 +91,8 @@ class ToolManager:
|
||||
)
|
||||
tool = await self.get_tool_by_id_async(current_tool.id, actor=actor)
|
||||
return tool
|
||||
|
||||
return await self.create_tool_async(pydantic_tool, actor=actor)
|
||||
|
||||
return await self.create_tool_async(pydantic_tool, actor=actor)
|
||||
|
||||
@enforce_types
|
||||
async def create_mcp_server(
|
||||
@@ -126,13 +125,6 @@ class ToolManager:
|
||||
actor,
|
||||
)
|
||||
|
||||
@enforce_types
|
||||
@trace_method
|
||||
async def create_or_update_composio_tool_async(self, tool_create: ToolCreate, actor: PydanticUser) -> PydanticTool:
|
||||
return await self.create_or_update_tool_async(
|
||||
PydanticTool(tool_type=ToolType.EXTERNAL_COMPOSIO, name=tool_create.json_schema["name"], **tool_create.model_dump()), actor
|
||||
)
|
||||
|
||||
@enforce_types
|
||||
@trace_method
|
||||
async def create_tool_async(self, pydantic_tool: PydanticTool, actor: PydanticUser) -> PydanticTool:
|
||||
@@ -570,9 +562,9 @@ class ToolManager:
|
||||
elif new_schema and new_name != current_tool.name:
|
||||
# Schema provides a different name but name wasn't explicitly changed
|
||||
update_data["name"] = new_name
|
||||
#raise ValueError(
|
||||
# raise ValueError(
|
||||
# f"JSON schema name '{new_name}' conflicts with current tool name '{current_tool.name}'. Update the name field explicitly if you want to rename the tool."
|
||||
#)
|
||||
# )
|
||||
|
||||
# If name changes, enforce uniqueness
|
||||
if new_name != current_tool.name:
|
||||
|
||||
@@ -12,8 +12,6 @@ from letta.services.summarizer.enums import SummarizationMode
|
||||
|
||||
|
||||
class ToolSettings(BaseSettings):
|
||||
composio_api_key: str | None = Field(default=None, description="API key for Composio")
|
||||
|
||||
# Sandbox Configurations
|
||||
e2b_api_key: str | None = Field(default=None, description="API key for using E2B as a tool sandbox")
|
||||
e2b_sandbox_template_id: str | None = Field(default=None, description="Template ID for E2B Sandbox. Updated Manually.")
|
||||
|
||||
@@ -32,7 +32,6 @@ dependencies = [
|
||||
"pydantic-settings>=2.2.1",
|
||||
"httpx-sse>=0.4.0",
|
||||
"nltk>=3.8.1",
|
||||
"composio-core>=0.7.7",
|
||||
"alembic>=1.13.3",
|
||||
"pyhumps>=3.8.0",
|
||||
"pathvalidate>=3.2.1",
|
||||
|
||||
@@ -185,13 +185,6 @@ async def default_user(default_organization):
|
||||
yield user
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def check_composio_key_set():
|
||||
original_api_key = tool_settings.composio_api_key
|
||||
assert original_api_key is not None, "Missing composio key! Cannot execute this test."
|
||||
yield
|
||||
|
||||
|
||||
# --- Tool Fixtures ---
|
||||
@pytest.fixture
|
||||
def weather_tool_func():
|
||||
|
||||
@@ -12,9 +12,7 @@ from letta.schemas.embedding_config import EmbeddingConfig
|
||||
from letta.schemas.enums import MessageStreamStatus
|
||||
from letta.schemas.llm_config import LLMConfig
|
||||
from letta.schemas.openai.chat_completion_request import ChatCompletionRequest, UserMessage as OpenAIUserMessage
|
||||
from letta.schemas.tool import ToolCreate
|
||||
from letta.schemas.usage import LettaUsageStatistics
|
||||
from letta.services.tool_manager import ToolManager
|
||||
from tests.utils import wait_for_server
|
||||
|
||||
# --- Server Management --- #
|
||||
@@ -98,13 +96,6 @@ def weather_tool(client):
|
||||
yield tool
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def composio_gmail_get_profile_tool(default_user):
|
||||
tool_create = ToolCreate.from_composio(action_name="GMAIL_GET_PROFILE")
|
||||
tool = ToolManager().create_or_update_composio_tool(tool_create=tool_create, actor=default_user)
|
||||
yield tool
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def agent(client, roll_dice_tool, weather_tool):
|
||||
"""Creates an agent and ensures cleanup after tests."""
|
||||
|
||||
@@ -75,26 +75,6 @@ async def test_mcp_tools_get_health_status():
|
||||
assert any("type" in reason for reason in invalid_tool.health.reasons)
|
||||
|
||||
|
||||
def test_composio_like_schema_marked_non_strict():
|
||||
"""Test that Composio-like schemas are correctly marked as NON_STRICT_ONLY."""
|
||||
|
||||
# Example schema from Composio with free-form message object
|
||||
composio_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {"type": "object", "additionalProperties": {}, "description": "Message to send"} # Free-form, missing "type"
|
||||
},
|
||||
"required": ["message"],
|
||||
"additionalProperties": False,
|
||||
}
|
||||
|
||||
status, reasons = validate_complete_json_schema(composio_schema)
|
||||
|
||||
assert status == SchemaHealth.NON_STRICT_ONLY
|
||||
assert len(reasons) > 0
|
||||
assert any("additionalProperties" in reason for reason in reasons)
|
||||
|
||||
|
||||
def test_empty_object_in_required_marked_invalid():
|
||||
"""Test that required properties allowing empty objects are marked INVALID."""
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class TestSchemaValidator:
|
||||
assert reasons == []
|
||||
|
||||
def test_free_form_object_non_strict(self):
|
||||
"""Test that free-form objects (like Composio message) are marked as NON_STRICT_ONLY."""
|
||||
"""Test that free-form objects are marked as NON_STRICT_ONLY."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -207,26 +207,6 @@ class TestSchemaValidator:
|
||||
assert status == SchemaHealth.INVALID
|
||||
assert any("items" in reason for reason in reasons)
|
||||
|
||||
def test_composio_like_schema(self):
|
||||
"""Test a schema similar to Composio's free-form message structure."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "object",
|
||||
"description": "Message to send",
|
||||
# No properties defined, no additionalProperties: false
|
||||
# This is a free-form object
|
||||
}
|
||||
},
|
||||
"required": ["message"],
|
||||
"additionalProperties": False,
|
||||
}
|
||||
|
||||
status, reasons = validate_complete_json_schema(schema)
|
||||
assert status == SchemaHealth.NON_STRICT_ONLY
|
||||
assert any("additionalProperties" in reason for reason in reasons)
|
||||
|
||||
def test_non_dict_schema(self):
|
||||
"""Test that non-dict schemas are marked INVALID."""
|
||||
schema = "not a dict"
|
||||
@@ -249,23 +229,6 @@ class TestSchemaValidator:
|
||||
assert status == SchemaHealth.STRICT_COMPLIANT
|
||||
assert reasons == []
|
||||
|
||||
def test_composio_schema_with_optional_root_properties_non_strict(self):
|
||||
"""Test that Composio-like schemas with optional root properties are STRICT_COMPLIANT (validator is relaxed)."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"thinking": {"type": "string", "description": "Deep inner monologue"},
|
||||
"connected_account_id": {"type": "string", "description": "Specific connected account ID"},
|
||||
"toolkit": {"type": "string", "description": "Name of the toolkit"},
|
||||
"request_heartbeat": {"type": "boolean", "description": "Request immediate heartbeat"},
|
||||
},
|
||||
"required": ["thinking", "request_heartbeat"], # Not all properties are required
|
||||
"additionalProperties": False,
|
||||
}
|
||||
|
||||
status, reasons = validate_complete_json_schema(schema)
|
||||
assert status == SchemaHealth.STRICT_COMPLIANT
|
||||
assert reasons == []
|
||||
|
||||
def test_root_level_without_required_non_strict(self):
|
||||
"""Test that root-level objects without 'required' field are STRICT_COMPLIANT (validator is relaxed)."""
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -685,7 +685,6 @@ def test_agent_download_upload_flow(server, server_url, serialize_test_agent, de
|
||||
@pytest.mark.parametrize(
|
||||
"filename",
|
||||
[
|
||||
"composio_github_star_agent.af",
|
||||
"outreach_workflow_agent.af",
|
||||
"customer_service.af",
|
||||
"deep_research_agent.af",
|
||||
|
||||
@@ -236,47 +236,6 @@ def test_valid_schemas_via_openai(openai_model: str, structured_output: bool):
|
||||
print(f"Total execution time: {end_time - start_time:.2f} seconds")
|
||||
|
||||
|
||||
# Parallel implementation for Composio test
|
||||
def _run_composio_test(action_name, openai_model, structured_output):
|
||||
"""Run a single Composio test case in parallel"""
|
||||
try:
|
||||
tool_create = ToolCreate.from_composio(action_name=action_name)
|
||||
assert tool_create.json_schema
|
||||
schema = tool_create.json_schema
|
||||
|
||||
if structured_output:
|
||||
tool_schema = convert_to_structured_output(schema)
|
||||
else:
|
||||
tool_schema = schema
|
||||
|
||||
api_key = os.getenv("OPENAI_API_KEY")
|
||||
assert api_key is not None, "OPENAI_API_KEY must be set"
|
||||
|
||||
system_prompt = "You job is to test the tool that you've been provided. Don't ask for any clarification on the args, just come up with some dummy data and try executing the tool."
|
||||
|
||||
url = "https://api.openai.com/v1/chat/completions"
|
||||
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}
|
||||
data = {
|
||||
"model": openai_model,
|
||||
"messages": [
|
||||
{"role": "system", "content": system_prompt},
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": tool_schema,
|
||||
}
|
||||
],
|
||||
"tool_choice": "auto",
|
||||
"parallel_tool_calls": False,
|
||||
}
|
||||
|
||||
make_post_request(url, headers, data)
|
||||
return (action_name, True, None) # Success
|
||||
except Exception as e:
|
||||
return (action_name, False, str(e)) # Failure with error message
|
||||
|
||||
|
||||
# Helper function for pydantic args schema test
|
||||
def _run_pydantic_args_test(filename, openai_model, structured_output):
|
||||
"""Run a single pydantic args schema test case"""
|
||||
|
||||
165
uv.lock
generated
165
uv.lock
generated
@@ -1,5 +1,5 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
revision = 2
|
||||
requires-python = ">=3.11, <3.14"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.13'",
|
||||
@@ -371,60 +371,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/d6/f9168956276934162ec8d48232f9920f2985ee45aa7602e3c6b4bc203613/banks-2.2.0-py3-none-any.whl", hash = "sha256:963cd5c85a587b122abde4f4064078def35c50c688c1b9d36f43c92503854e7d", size = 29244, upload-time = "2025-07-18T16:28:27.835Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bcrypt"
|
||||
version = "4.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bb/5d/6d7433e0f3cd46ce0b43cd65e1db465ea024dbb8216fb2404e919c2ad77b/bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18", size = 25697, upload-time = "2025-02-28T01:24:09.174Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/2c/3d44e853d1fe969d229bd58d39ae6902b3d924af0e2b5a60d17d4b809ded/bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281", size = 483719, upload-time = "2025-02-28T01:22:34.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/e2/58ff6e2a22eca2e2cff5370ae56dba29d70b1ea6fc08ee9115c3ae367795/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb", size = 272001, upload-time = "2025-02-28T01:22:38.078Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/1f/c55ed8dbe994b1d088309e366749633c9eb90d139af3c0a50c102ba68a1a/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180", size = 277451, upload-time = "2025-02-28T01:22:40.787Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/1c/794feb2ecf22fe73dcfb697ea7057f632061faceb7dcf0f155f3443b4d79/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f", size = 272792, upload-time = "2025-02-28T01:22:43.144Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/b7/0b289506a3f3598c2ae2bdfa0ea66969812ed200264e3f61df77753eee6d/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09", size = 289752, upload-time = "2025-02-28T01:22:45.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/24/d0fb023788afe9e83cc118895a9f6c57e1044e7e1672f045e46733421fe6/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d", size = 277762, upload-time = "2025-02-28T01:22:47.023Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/38/cde58089492e55ac4ef6c49fea7027600c84fd23f7520c62118c03b4625e/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd", size = 272384, upload-time = "2025-02-28T01:22:49.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/6a/d5026520843490cfc8135d03012a413e4532a400e471e6188b01b2de853f/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af", size = 277329, upload-time = "2025-02-28T01:22:51.603Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/a3/4fc5255e60486466c389e28c12579d2829b28a527360e9430b4041df4cf9/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231", size = 305241, upload-time = "2025-02-28T01:22:53.283Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/15/2b37bc07d6ce27cc94e5b10fd5058900eb8fb11642300e932c8c82e25c4a/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c", size = 309617, upload-time = "2025-02-28T01:22:55.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/1f/99f65edb09e6c935232ba0430c8c13bb98cb3194b6d636e61d93fe60ac59/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f", size = 335751, upload-time = "2025-02-28T01:22:57.81Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/1b/b324030c706711c99769988fcb694b3cb23f247ad39a7823a78e361bdbb8/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d", size = 355965, upload-time = "2025-02-28T01:22:59.181Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/dd/20372a0579dd915dfc3b1cd4943b3bca431866fcb1dfdfd7518c3caddea6/bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4", size = 155316, upload-time = "2025-02-28T01:23:00.763Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/52/45d969fcff6b5577c2bf17098dc36269b4c02197d551371c023130c0f890/bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669", size = 147752, upload-time = "2025-02-28T01:23:02.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/22/5ada0b9af72b60cbc4c9a399fdde4af0feaa609d27eb0adc61607997a3fa/bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d", size = 498019, upload-time = "2025-02-28T01:23:05.838Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/8c/252a1edc598dc1ce57905be173328eda073083826955ee3c97c7ff5ba584/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b", size = 279174, upload-time = "2025-02-28T01:23:07.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/5b/4547d5c49b85f0337c13929f2ccbe08b7283069eea3550a457914fc078aa/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e", size = 283870, upload-time = "2025-02-28T01:23:09.151Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/21/7dbaf3fa1745cb63f776bb046e481fbababd7d344c5324eab47f5ca92dd2/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59", size = 279601, upload-time = "2025-02-28T01:23:11.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/64/e042fc8262e971347d9230d9abbe70d68b0a549acd8611c83cebd3eaec67/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753", size = 297660, upload-time = "2025-02-28T01:23:12.989Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/b8/6294eb84a3fef3b67c69b4470fcdd5326676806bf2519cda79331ab3c3a9/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761", size = 284083, upload-time = "2025-02-28T01:23:14.5Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/e6/baff635a4f2c42e8788fe1b1633911c38551ecca9a749d1052d296329da6/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb", size = 279237, upload-time = "2025-02-28T01:23:16.686Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/48/46f623f1b0c7dc2e5de0b8af5e6f5ac4cc26408ac33f3d424e5ad8da4a90/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d", size = 283737, upload-time = "2025-02-28T01:23:18.897Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/8b/70671c3ce9c0fca4a6cc3cc6ccbaa7e948875a2e62cbd146e04a4011899c/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f", size = 312741, upload-time = "2025-02-28T01:23:21.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/fb/910d3a1caa2d249b6040a5caf9f9866c52114d51523ac2fb47578a27faee/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732", size = 316472, upload-time = "2025-02-28T01:23:23.183Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/cf/7cf3a05b66ce466cfb575dbbda39718d45a609daa78500f57fa9f36fa3c0/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef", size = 343606, upload-time = "2025-02-28T01:23:25.361Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/b8/e970ecc6d7e355c0d892b7f733480f4aa8509f99b33e71550242cf0b7e63/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304", size = 362867, upload-time = "2025-02-28T01:23:26.875Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/97/8d3118efd8354c555a3422d544163f40d9f236be5b96c714086463f11699/bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51", size = 160589, upload-time = "2025-02-28T01:23:28.381Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/07/416f0b99f7f3997c69815365babbc2e8754181a4b1899d921b3c7d5b6f12/bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62", size = 152794, upload-time = "2025-02-28T01:23:30.187Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/c1/3fa0e9e4e0bfd3fd77eb8b52ec198fd6e1fd7e9402052e43f23483f956dd/bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3", size = 498969, upload-time = "2025-02-28T01:23:31.945Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/d4/755ce19b6743394787fbd7dff6bf271b27ee9b5912a97242e3caf125885b/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24", size = 279158, upload-time = "2025-02-28T01:23:34.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/5d/805ef1a749c965c46b28285dfb5cd272a7ed9fa971f970435a5133250182/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef", size = 284285, upload-time = "2025-02-28T01:23:35.765Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/2b/698580547a4a4988e415721b71eb45e80c879f0fb04a62da131f45987b96/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b", size = 279583, upload-time = "2025-02-28T01:23:38.021Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/87/62e1e426418204db520f955ffd06f1efd389feca893dad7095bf35612eec/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676", size = 297896, upload-time = "2025-02-28T01:23:39.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/c6/8fedca4c2ada1b6e889c52d2943b2f968d3427e5d65f595620ec4c06fa2f/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1", size = 284492, upload-time = "2025-02-28T01:23:40.901Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/4d/c43332dcaaddb7710a8ff5269fcccba97ed3c85987ddaa808db084267b9a/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe", size = 279213, upload-time = "2025-02-28T01:23:42.653Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/7f/1e36379e169a7df3a14a1c160a49b7b918600a6008de43ff20d479e6f4b5/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0", size = 284162, upload-time = "2025-02-28T01:23:43.964Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/0a/644b2731194b0d7646f3210dc4d80c7fee3ecb3a1f791a6e0ae6bb8684e3/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f", size = 312856, upload-time = "2025-02-28T01:23:46.011Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/62/2a871837c0bb6ab0c9a88bf54de0fc021a6a08832d4ea313ed92a669d437/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23", size = 316726, upload-time = "2025-02-28T01:23:47.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/a1/9898ea3faac0b156d457fd73a3cb9c2855c6fd063e44b8522925cdd8ce46/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe", size = 343664, upload-time = "2025-02-28T01:23:49.059Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/f2/71b4ed65ce38982ecdda0ff20c3ad1b15e71949c78b2c053df53629ce940/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505", size = 363128, upload-time = "2025-02-28T01:23:50.399Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/99/12f6a58eca6dea4be992d6c681b7ec9410a1d9f5cf368c61437e31daa879/bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a", size = 160598, upload-time = "2025-02-28T01:23:51.775Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799, upload-time = "2025-02-28T01:23:53.139Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/b1/1289e21d710496b88340369137cc4c5f6ee036401190ea116a7b4ae6d32a/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a839320bf27d474e52ef8cb16449bb2ce0ba03ca9f44daba6d93fa1d8828e48a", size = 275103, upload-time = "2025-02-28T01:24:00.764Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/41/19be9fe17e4ffc5d10b7b67f10e459fc4eee6ffe9056a88de511920cfd8d/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:bdc6a24e754a555d7316fa4774e64c6c3997d27ed2d1964d55920c7c227bc4ce", size = 280513, upload-time = "2025-02-28T01:24:02.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/73/05687a9ef89edebdd8ad7474c16d8af685eb4591c3c38300bb6aad4f0076/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:55a935b8e9a1d2def0626c4269db3fcd26728cbff1e84f0341465c31c4ee56d8", size = 274685, upload-time = "2025-02-28T01:24:04.512Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/13/47bba97924ebe86a62ef83dc75b7c8a881d53c535f83e2c54c4bd701e05c/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57967b7a28d855313a963aaea51bf6df89f833db4320da458e5b3c5ab6d4c938", size = 280110, upload-time = "2025-02-28T01:24:05.896Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "beautifulsoup4"
|
||||
version = "4.13.5"
|
||||
@@ -746,35 +692,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "composio-core"
|
||||
version = "0.7.20"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
{ name = "click" },
|
||||
{ name = "fastapi" },
|
||||
{ name = "importlib-metadata" },
|
||||
{ name = "inflection" },
|
||||
{ name = "jsonref" },
|
||||
{ name = "jsonschema" },
|
||||
{ name = "paramiko" },
|
||||
{ name = "pillow" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pyperclip" },
|
||||
{ name = "pysher" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "requests" },
|
||||
{ name = "rich" },
|
||||
{ name = "semver" },
|
||||
{ name = "sentry-sdk" },
|
||||
{ name = "uvicorn" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bc/d3/00262ca35be5e6a171104a917cd962179c62d709f0a419030899534be689/composio_core-0.7.20.tar.gz", hash = "sha256:1dc29dbf73eb72d2df1c5b0d4d2f21459d15029322cf74df8fdecc44dcaeb1f4", size = 334637, upload-time = "2025-07-03T08:48:54.157Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/56/b4e2ccdda8bc7732c5616bdb3bb4cea6019fdbdbbb2ee435ca784055cb8e/composio_core-0.7.20-py3-none-any.whl", hash = "sha256:e1cfb9cfc68a4622bc15827143ddf726f429d281e8f9de5d4c0965e75d039f14", size = 501152, upload-time = "2025-07-03T08:48:52.058Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "configargparse"
|
||||
version = "1.7.1"
|
||||
@@ -1952,15 +1869,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/eb/427ed2b20a38a4ee29f24dbe4ae2dafab198674fe9a85e3d6adf9e5f5f41/inflect-7.5.0-py3-none-any.whl", hash = "sha256:2aea70e5e70c35d8350b8097396ec155ffd68def678c7ff97f51aa69c1d92344", size = 35197, upload-time = "2024-12-28T17:11:15.931Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inflection"
|
||||
version = "0.5.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e1/7e/691d061b7329bc8d54edbf0ec22fbfb2afe61facb681f9aaa9bff7a27d04/inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", size = 15091, upload-time = "2020-08-22T08:16:29.139Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2", size = 9454, upload-time = "2020-08-22T08:16:27.816Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.1.0"
|
||||
@@ -2178,15 +2086,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonref"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/aa/0d/c1f3277e90ccdb50d33ed5ba1ec5b3f0a242ed8c1b1a85d3afeb68464dca/jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", size = 8814, upload-time = "2023-01-16T16:10:04.455Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9", size = 9425, upload-time = "2023-01-16T16:10:02.255Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema"
|
||||
version = "4.25.1"
|
||||
@@ -2422,7 +2321,6 @@ dependencies = [
|
||||
{ name = "brotli" },
|
||||
{ name = "certifi" },
|
||||
{ name = "colorama" },
|
||||
{ name = "composio-core" },
|
||||
{ name = "datamodel-code-generator", extra = ["http"] },
|
||||
{ name = "demjson3" },
|
||||
{ name = "docstring-parser" },
|
||||
@@ -2569,7 +2467,6 @@ requires-dist = [
|
||||
{ name = "brotli", specifier = ">=1.1.0" },
|
||||
{ name = "certifi", specifier = ">=2025.6.15" },
|
||||
{ name = "colorama", specifier = ">=0.4.6" },
|
||||
{ name = "composio-core", specifier = ">=0.7.7" },
|
||||
{ name = "datamodel-code-generator", extras = ["http"], specifier = ">=0.25.0" },
|
||||
{ name = "demjson3", specifier = ">=3.0.6" },
|
||||
{ name = "docker", marker = "extra == 'desktop'", specifier = ">=7.1.0" },
|
||||
@@ -3903,21 +3800,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436, upload-time = "2024-09-20T13:09:48.112Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paramiko"
|
||||
version = "4.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "bcrypt" },
|
||||
{ name = "cryptography" },
|
||||
{ name = "invoke" },
|
||||
{ name = "pynacl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/e7/81fdcbc7f190cdb058cffc9431587eb289833bdd633e2002455ca9bb13d4/paramiko-4.0.0.tar.gz", hash = "sha256:6a25f07b380cc9c9a88d2b920ad37167ac4667f8d9886ccebd8f90f654b5d69f", size = 1630743, upload-time = "2025-08-04T01:02:03.711Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/90/a744336f5af32c433bd09af7854599682a383b37cfd78f7de263de6ad6cb/paramiko-4.0.0-py3-none-any.whl", hash = "sha256:0e20e00ac666503bf0b4eda3b6d833465a2b7aff2e2b3d79a8bba5ef144ee3b9", size = 223932, upload-time = "2025-08-04T01:02:02.029Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parso"
|
||||
version = "0.8.5"
|
||||
@@ -4569,26 +4451,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/11/a1938340ecb32d71e47ad4914843775011e6e9da59ba1229f181fef3119e/pyhumps-3.8.0-py3-none-any.whl", hash = "sha256:060e1954d9069f428232a1adda165db0b9d8dfdce1d265d36df7fbff540acfd6", size = 6095, upload-time = "2022-10-21T10:38:58.231Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pynacl"
|
||||
version = "1.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a7/22/27582568be639dfe22ddb3902225f91f2f17ceff88ce80e4db396c8986da/PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba", size = 3392854, upload-time = "2022-01-07T22:05:41.134Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/75/0b8ede18506041c0bf23ac4d8e2971b4161cd6ce630b177d0a08eb0d8857/PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1", size = 349920, upload-time = "2022-01-07T22:05:49.156Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/bb/fddf10acd09637327a97ef89d2a9d621328850a72f1fdc8c08bdf72e385f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92", size = 601722, upload-time = "2022-01-07T22:05:50.989Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/70/87a065c37cca41a75f2ce113a5a2c2aa7533be648b184ade58971b5f7ccc/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394", size = 680087, upload-time = "2022-01-07T22:05:52.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/87/f1bb6a595f14a327e8285b9eb54d41fef76c585a0edef0a45f6fc95de125/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d", size = 856678, upload-time = "2022-01-07T22:05:54.251Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/28/ca86676b69bf9f90e710571b67450508484388bfce09acf8a46f0b8c785f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858", size = 1133660, upload-time = "2022-01-07T22:05:56.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/85/c262db650e86812585e2bc59e497a8f59948a005325a11bbbc9ecd3fe26b/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b", size = 663824, upload-time = "2022-01-07T22:05:57.434Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/1a/cc308a884bd299b651f1633acb978e8596c71c33ca85e9dc9fa33a5399b9/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff", size = 1117912, upload-time = "2022-01-07T22:05:58.665Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/2d/b7df6ddb0c2a33afdb358f8af6ea3b8c4d1196ca45497dd37a56f0c122be/PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543", size = 204624, upload-time = "2022-01-07T22:06:00.085Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/22/d3db169895faaf3e2eda892f005f433a62db2decbcfbc2f61e6517adfa87/PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", size = 212141, upload-time = "2022-01-07T22:06:01.861Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
version = "3.2.3"
|
||||
@@ -4607,12 +4469,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/83/2cacc506eb322bb31b747bc06ccb82cc9aa03e19ee9c1245e538e49d52be/pypdf-6.0.0-py3-none-any.whl", hash = "sha256:56ea60100ce9f11fc3eec4f359da15e9aec3821b036c1f06d2b660d35683abb8", size = 310465, upload-time = "2025-08-11T14:22:00.481Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyperclip"
|
||||
version = "1.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/30/23/2f0a3efc4d6a32f3b63cdff36cd398d9701d26cda58e3ab97ac79fb5e60d/pyperclip-1.9.0.tar.gz", hash = "sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310", size = 20961, upload-time = "2024-06-18T20:38:48.401Z" }
|
||||
|
||||
[[package]]
|
||||
name = "pyreadline3"
|
||||
version = "3.5.4"
|
||||
@@ -4635,16 +4491,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/84/30/89aa7f7d7a875bbb9a577d4b1dc5a3e404e3d2ae2657354808e905e358e0/pyright-1.1.404-py3-none-any.whl", hash = "sha256:c7b7ff1fdb7219c643079e4c3e7d4125f0dafcc19d253b47e898d130ea426419", size = 5902951, upload-time = "2025-08-20T18:46:12.096Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pysher"
|
||||
version = "1.0.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "requests" },
|
||||
{ name = "websocket-client" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/12/a0/d0638470df605ce266991fb04f74c69ab1bed3b90ac3838e9c3c8b69b66a/Pysher-1.0.8.tar.gz", hash = "sha256:7849c56032b208e49df67d7bd8d49029a69042ab0bb45b2ed59fa08f11ac5988", size = 9071, upload-time = "2022-10-10T13:41:09.936Z" }
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.4.1"
|
||||
@@ -5207,15 +5053,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/69/bf/54b5d40bea1c1805175ead2d496c267f05eec87561687dd73ab76869d8d9/scramp-1.4.6-py3-none-any.whl", hash = "sha256:a0cf9d2b4624b69bac5432dd69fecfc55a542384fe73c3a23ed9b138cda484e1", size = 12812, upload-time = "2025-07-05T14:44:02.345Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "3.0.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/d1/d3159231aec234a59dd7d601e9dd9fe96f3afff15efd33c1070019b26132/semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602", size = 269730, upload-time = "2025-01-24T13:19:27.617Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912, upload-time = "2025-01-24T13:19:24.949Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-sdk"
|
||||
version = "2.19.1"
|
||||
|
||||
Reference in New Issue
Block a user