feat: composio async execution (#1941)

This commit is contained in:
Andy Li 2025-04-30 16:05:03 -07:00 committed by GitHub
parent 60fc1fb8cc
commit 12ff04f194
15 changed files with 161 additions and 200 deletions

View File

@ -60,7 +60,7 @@ Last updated Oct 2, 2024. Please check `composio` documentation for any composio
def main(): def main():
from composio_langchain import Action from composio import Action
# Add the composio tool # Add the composio tool
tool = client.load_composio_tool(action=Action.GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER) tool = client.load_composio_tool(action=Action.GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER)

View File

@ -21,8 +21,8 @@ from letta.constants import (
) )
from letta.errors import ContextWindowExceededError from letta.errors import ContextWindowExceededError
from letta.functions.ast_parsers import coerce_dict_args_by_annotations, get_function_annotations_from_source from letta.functions.ast_parsers import coerce_dict_args_by_annotations, get_function_annotations_from_source
from letta.functions.composio_helpers import execute_composio_action, generate_composio_action_from_func_name
from letta.functions.functions import get_function_from_module from letta.functions.functions import get_function_from_module
from letta.functions.helpers import execute_composio_action, generate_composio_action_from_func_name
from letta.functions.mcp_client.base_client import BaseMCPClient from letta.functions.mcp_client.base_client import BaseMCPClient
from letta.helpers import ToolRulesSolver from letta.helpers import ToolRulesSolver
from letta.helpers.composio_helpers import get_composio_api_key from letta.helpers.composio_helpers import get_composio_api_key

View File

@ -179,6 +179,7 @@ class LettaAgent(BaseAgent):
ToolType.LETTA_SLEEPTIME_CORE, ToolType.LETTA_SLEEPTIME_CORE,
} }
or (t.tool_type == ToolType.LETTA_MULTI_AGENT_CORE and t.name == "send_message_to_agents_matching_tags") or (t.tool_type == ToolType.LETTA_MULTI_AGENT_CORE and t.name == "send_message_to_agents_matching_tags")
or (t.tool_type == ToolType.EXTERNAL_COMPOSIO)
] ]
valid_tool_names = tool_rules_solver.get_allowed_tool_names(available_tools=set([t.name for t in tools])) valid_tool_names = tool_rules_solver.get_allowed_tool_names(available_tools=set([t.name for t in tools]))
@ -331,6 +332,10 @@ class LettaAgent(BaseAgent):
results = await self._send_message_to_agents_matching_tags(**tool_args) results = await self._send_message_to_agents_matching_tags(**tool_args)
log_event(name="finish_send_message_to_agents_matching_tags", attributes=tool_args) log_event(name="finish_send_message_to_agents_matching_tags", attributes=tool_args)
return json.dumps(results), True return json.dumps(results), True
elif target_tool.type == ToolType.EXTERNAL_COMPOSIO:
log_event(name=f"start_composio_{tool_name}_execution", attributes=tool_args)
log_event(name=f"finish_compsio_{tool_name}_execution", attributes=tool_args)
return tool_execution_result.func_return, True
else: else:
tool_execution_manager = ToolExecutionManager(agent_state=agent_state, actor=self.actor) tool_execution_manager = ToolExecutionManager(agent_state=agent_state, actor=self.actor)
# TODO: Integrate sandbox result # TODO: Integrate sandbox result

View File

@ -0,0 +1,100 @@
import asyncio
import os
from typing import Any, Optional
from composio import ComposioToolSet
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
# 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]:
try:
loop = asyncio.get_running_loop()
return await loop.run_in_executor(None, execute_composio_action, action_name, args, api_key, entity_id)
except Exception as e:
raise RuntimeError(f"Error in execute_composio_action_async: {e}") from e
def execute_composio_action(action_name: str, args: dict, api_key: Optional[str] = None, entity_id: Optional[str] = None) -> Any:
entity_id = entity_id or os.getenv(COMPOSIO_ENTITY_ENV_VAR_KEY, DEFAULT_ENTITY_ID)
try:
composio_toolset = ComposioToolSet(api_key=api_key, entity_id=entity_id, lock=False)
response = composio_toolset.execute_action(action=action_name, params=args)
except ApiKeyNotProvidedError:
raise RuntimeError(
f"Composio API key is missing for action '{action_name}'. "
"Please set the sandbox environment variables either through the ADE or the API."
)
except ConnectedAccountNotFoundError:
raise RuntimeError(f"No connected account was found for action '{action_name}'. " "Please link an account and try again.")
except EnumStringNotFound as e:
raise RuntimeError(f"Invalid value provided for action '{action_name}': " + str(e) + ". Please check the action parameters.")
except EnumMetadataNotFound as e:
raise RuntimeError(f"Invalid value provided for action '{action_name}': " + str(e) + ". Please check the action parameters.")
except ComposioSDKError as 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 _assert_code_gen_compilable(code_str):
try:
compile(code_str, "<string>", "exec")
except SyntaxError as e:
print(f"Syntax error in code: {e}")

View File

@ -1,8 +1,9 @@
import importlib import importlib
import inspect import inspect
from collections.abc import Callable
from textwrap import dedent # remove indentation from textwrap import dedent # remove indentation
from types import ModuleType from types import ModuleType
from typing import Dict, List, Literal, Optional from typing import Any, Dict, List, Literal, Optional
from letta.errors import LettaToolCreateError from letta.errors import LettaToolCreateError
from letta.functions.schema_generator import generate_schema from letta.functions.schema_generator import generate_schema
@ -66,7 +67,8 @@ def parse_source_code(func) -> str:
return source_code return source_code
def get_function_from_module(module_name: str, function_name: str): # TODO (cliandy) refactor below two funcs
def get_function_from_module(module_name: str, function_name: str) -> Callable[..., Any]:
""" """
Dynamically imports a function from a specified module. Dynamically imports a function from a specified module.

View File

@ -6,10 +6,9 @@ from random import uniform
from typing import Any, Dict, List, Optional, Type, Union from typing import Any, Dict, List, Optional, Type, Union
import humps import humps
from composio.constants import DEFAULT_ENTITY_ID
from pydantic import BaseModel, Field, create_model from pydantic import BaseModel, Field, create_model
from letta.constants import COMPOSIO_ENTITY_ENV_VAR_KEY, DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
from letta.functions.interface import MultiAgentMessagingInterface from letta.functions.interface import MultiAgentMessagingInterface
from letta.orm.errors import NoResultFound from letta.orm.errors import NoResultFound
from letta.schemas.enums import MessageRole from letta.schemas.enums import MessageRole
@ -21,34 +20,6 @@ from letta.server.rest_api.utils import get_letta_server
from letta.settings import settings from letta.settings import settings
# 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()
# TODO needed? # TODO needed?
def generate_mcp_tool_wrapper(mcp_tool_name: str) -> tuple[str, str]: def generate_mcp_tool_wrapper(mcp_tool_name: str) -> tuple[str, str]:
@ -58,62 +29,11 @@ def {mcp_tool_name}(**kwargs):
""" """
# Compile safety check # Compile safety check
assert_code_gen_compilable(wrapper_function_str.strip()) _assert_code_gen_compilable(wrapper_function_str.strip())
return mcp_tool_name, wrapper_function_str.strip() return mcp_tool_name, wrapper_function_str.strip()
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()
def execute_composio_action(action_name: str, args: dict, api_key: Optional[str] = None, entity_id: Optional[str] = None) -> Any:
import os
from composio.exceptions import (
ApiKeyNotProvidedError,
ComposioSDKError,
ConnectedAccountNotFoundError,
EnumMetadataNotFound,
EnumStringNotFound,
)
from composio_langchain import ComposioToolSet
entity_id = entity_id or os.getenv(COMPOSIO_ENTITY_ENV_VAR_KEY, DEFAULT_ENTITY_ID)
try:
composio_toolset = ComposioToolSet(api_key=api_key, entity_id=entity_id, lock=False)
response = composio_toolset.execute_action(action=action_name, params=args)
except ApiKeyNotProvidedError:
raise RuntimeError(
f"Composio API key is missing for action '{action_name}'. "
"Please set the sandbox environment variables either through the ADE or the API."
)
except ConnectedAccountNotFoundError:
raise RuntimeError(f"No connected account was found for action '{action_name}'. " "Please link an account and try again.")
except EnumStringNotFound as e:
raise RuntimeError(f"Invalid value provided for action '{action_name}': " + str(e) + ". Please check the action parameters.")
except EnumMetadataNotFound as e:
raise RuntimeError(f"Invalid value provided for action '{action_name}': " + str(e) + ". Please check the action parameters.")
except ComposioSDKError as e:
raise RuntimeError(f"An unexpected error occurred in Composio SDK while executing action '{action_name}': " + str(e))
if "error" in response:
raise RuntimeError(f"Error while executing action '{action_name}': " + str(response["error"]))
return response.get("data")
def generate_langchain_tool_wrapper( def generate_langchain_tool_wrapper(
tool: "LangChainBaseTool", additional_imports_module_attr_map: dict[str, str] = None tool: "LangChainBaseTool", additional_imports_module_attr_map: dict[str, str] = None
) -> tuple[str, str]: ) -> tuple[str, str]:
@ -139,12 +59,12 @@ def {func_name}(**kwargs):
""" """
# Compile safety check # Compile safety check
assert_code_gen_compilable(wrapper_function_str) _assert_code_gen_compilable(wrapper_function_str)
return func_name, wrapper_function_str return func_name, wrapper_function_str
def assert_code_gen_compilable(code_str): def _assert_code_gen_compilable(code_str):
try: try:
compile(code_str, "<string>", "exec") compile(code_str, "<string>", "exec")
except SyntaxError as e: except SyntaxError as e:
@ -157,7 +77,7 @@ def assert_all_classes_are_imported(tool: Union["LangChainBaseTool"], additional
current_class_imports = {tool_name} current_class_imports = {tool_name}
if additional_imports_module_attr_map: if additional_imports_module_attr_map:
current_class_imports.update(set(additional_imports_module_attr_map.values())) current_class_imports.update(set(additional_imports_module_attr_map.values()))
required_class_imports = set(find_required_class_names_for_import(tool)) required_class_imports = set(_find_required_class_names_for_import(tool))
if not current_class_imports.issuperset(required_class_imports): if not current_class_imports.issuperset(required_class_imports):
err_msg = f"[ERROR] You are missing module_attr pairs in `additional_imports_module_attr_map`. Currently, you have imports for {current_class_imports}, but the required classes for import are {required_class_imports}" err_msg = f"[ERROR] You are missing module_attr pairs in `additional_imports_module_attr_map`. Currently, you have imports for {current_class_imports}, but the required classes for import are {required_class_imports}"
@ -165,7 +85,7 @@ def assert_all_classes_are_imported(tool: Union["LangChainBaseTool"], additional
raise RuntimeError(err_msg) raise RuntimeError(err_msg)
def find_required_class_names_for_import(obj: Union["LangChainBaseTool", BaseModel]) -> list[str]: def _find_required_class_names_for_import(obj: Union["LangChainBaseTool", BaseModel]) -> list[str]:
""" """
Finds all the class names for required imports when instantiating the `obj`. Finds all the class names for required imports when instantiating the `obj`.
NOTE: This does not return the full import path, only the class name. NOTE: This does not return the full import path, only the class name.
@ -181,7 +101,7 @@ def find_required_class_names_for_import(obj: Union["LangChainBaseTool", BaseMod
# Collect all possible candidates for BaseModel objects # Collect all possible candidates for BaseModel objects
candidates = [] candidates = []
if is_base_model(curr_obj): if _is_base_model(curr_obj):
# If it is a base model, we get all the values of the object parameters # If it is a base model, we get all the values of the object parameters
# i.e., if obj('b' = <class A>), we would want to inspect <class A> # i.e., if obj('b' = <class A>), we would want to inspect <class A>
fields = dict(curr_obj) fields = dict(curr_obj)
@ -198,7 +118,7 @@ def find_required_class_names_for_import(obj: Union["LangChainBaseTool", BaseMod
# Filter out all candidates that are not BaseModels # Filter out all candidates that are not BaseModels
# In the list example above, ['a', 3, None, <class A>], we want to filter out 'a', 3, and None # In the list example above, ['a', 3, None, <class A>], we want to filter out 'a', 3, and None
candidates = filter(lambda x: is_base_model(x), candidates) candidates = filter(lambda x: _is_base_model(x), candidates)
# Classic BFS here # Classic BFS here
for c in candidates: for c in candidates:
@ -216,7 +136,7 @@ def generate_imported_tool_instantiation_call_str(obj: Any) -> Optional[str]:
# If it is a basic Python type, we trivially return the string version of that value # If it is a basic Python type, we trivially return the string version of that value
# Handle basic types # Handle basic types
return repr(obj) return repr(obj)
elif is_base_model(obj): elif _is_base_model(obj):
# Otherwise, if it is a BaseModel # Otherwise, if it is a BaseModel
# We want to pull out all the parameters, and reformat them into strings # We want to pull out all the parameters, and reformat them into strings
# e.g. {arg}={value} # e.g. {arg}={value}
@ -269,7 +189,7 @@ def generate_imported_tool_instantiation_call_str(obj: Any) -> Optional[str]:
return None return None
def is_base_model(obj: Any): def _is_base_model(obj: Any):
return isinstance(obj, BaseModel) return isinstance(obj, BaseModel)
@ -286,7 +206,7 @@ def generate_import_code(module_attr_map: Optional[dict]):
return "\n".join(code_lines) return "\n".join(code_lines)
def parse_letta_response_for_assistant_message( def _parse_letta_response_for_assistant_message(
target_agent_id: str, target_agent_id: str,
letta_response: LettaResponse, letta_response: LettaResponse,
) -> Optional[str]: ) -> Optional[str]:
@ -346,7 +266,7 @@ def execute_send_message_to_agent(
return asyncio.run(async_execute_send_message_to_agent(sender_agent, messages, other_agent_id, log_prefix)) return asyncio.run(async_execute_send_message_to_agent(sender_agent, messages, other_agent_id, log_prefix))
async def send_message_to_agent_no_stream( async def _send_message_to_agent_no_stream(
server: "SyncServer", server: "SyncServer",
agent_id: str, agent_id: str,
actor: User, actor: User,
@ -389,7 +309,7 @@ async def async_send_message_with_retries(
for attempt in range(1, max_retries + 1): for attempt in range(1, max_retries + 1):
try: try:
response = await asyncio.wait_for( response = await asyncio.wait_for(
send_message_to_agent_no_stream( _send_message_to_agent_no_stream(
server=server, server=server,
agent_id=target_agent_id, agent_id=target_agent_id,
actor=sender_agent.user, actor=sender_agent.user,
@ -399,7 +319,7 @@ async def async_send_message_with_retries(
) )
# Then parse out the assistant message # Then parse out the assistant message
assistant_message = parse_letta_response_for_assistant_message(target_agent_id, response) assistant_message = _parse_letta_response_for_assistant_message(target_agent_id, response)
if assistant_message: if assistant_message:
sender_agent.logger.info(f"{logging_prefix} - {assistant_message}") sender_agent.logger.info(f"{logging_prefix} - {assistant_message}")
return assistant_message return assistant_message

View File

@ -3,7 +3,7 @@ from typing import Any, Dict, Optional
from letta.constants import COMPOSIO_ENTITY_ENV_VAR_KEY, PRE_EXECUTION_MESSAGE_ARG from letta.constants import COMPOSIO_ENTITY_ENV_VAR_KEY, PRE_EXECUTION_MESSAGE_ARG
from letta.functions.ast_parsers import coerce_dict_args_by_annotations, get_function_annotations_from_source from letta.functions.ast_parsers import coerce_dict_args_by_annotations, get_function_annotations_from_source
from letta.functions.helpers import execute_composio_action, generate_composio_action_from_func_name from letta.functions.composio_helpers import execute_composio_action, generate_composio_action_from_func_name
from letta.helpers.composio_helpers import get_composio_api_key from letta.helpers.composio_helpers import get_composio_api_key
from letta.orm.enums import ToolType from letta.orm.enums import ToolType
from letta.schemas.agent import AgentState from letta.schemas.agent import AgentState

View File

@ -11,13 +11,9 @@ from letta.constants import (
MCP_TOOL_TAG_NAME_PREFIX, MCP_TOOL_TAG_NAME_PREFIX,
) )
from letta.functions.ast_parsers import get_function_name_and_description from letta.functions.ast_parsers import get_function_name_and_description
from letta.functions.composio_helpers import generate_composio_tool_wrapper
from letta.functions.functions import derive_openai_json_schema, get_json_schema_from_module from letta.functions.functions import derive_openai_json_schema, get_json_schema_from_module
from letta.functions.helpers import ( from letta.functions.helpers import generate_langchain_tool_wrapper, generate_mcp_tool_wrapper, generate_model_from_args_json_schema
generate_composio_tool_wrapper,
generate_langchain_tool_wrapper,
generate_mcp_tool_wrapper,
generate_model_from_args_json_schema,
)
from letta.functions.mcp_client.types import MCPTool from letta.functions.mcp_client.types import MCPTool
from letta.functions.schema_generator import ( from letta.functions.schema_generator import (
generate_schema_from_args_schema_v2, generate_schema_from_args_schema_v2,
@ -176,8 +172,7 @@ class ToolCreate(LettaBase):
Returns: Returns:
Tool: A Letta Tool initialized with attributes derived from the Composio tool. Tool: A Letta Tool initialized with attributes derived from the Composio tool.
""" """
from composio import LogLevel from composio import ComposioToolSet, LogLevel
from composio_langchain import ComposioToolSet
composio_toolset = ComposioToolSet(logging_level=LogLevel.ERROR, lock=False) composio_toolset = ComposioToolSet(logging_level=LogLevel.ERROR, lock=False)
composio_action_schemas = composio_toolset.get_action_schemas(actions=[action_name], check_connected_accounts=False) composio_action_schemas = composio_toolset.get_action_schemas(actions=[action_name], check_connected_accounts=False)

View File

@ -100,7 +100,7 @@ class ToolExecutionManager:
try: try:
executor = ToolExecutorFactory.get_executor(tool.tool_type) executor = ToolExecutorFactory.get_executor(tool.tool_type)
# TODO: Extend this async model to composio # TODO: Extend this async model to composio
if isinstance(executor, SandboxToolExecutor): if isinstance(executor, (SandboxToolExecutor, ExternalComposioToolExecutor)):
result = await executor.execute(function_name, function_args, self.agent_state, tool, self.actor) result = await executor.execute(function_name, function_args, self.agent_state, tool, self.actor)
else: else:
result = executor.execute(function_name, function_args, self.agent_state, tool, self.actor) result = executor.execute(function_name, function_args, self.agent_state, tool, self.actor)

View File

@ -5,7 +5,7 @@ from typing import Any, Dict, Optional
from letta.constants import COMPOSIO_ENTITY_ENV_VAR_KEY, CORE_MEMORY_LINE_NUMBER_WARNING, RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE from letta.constants import COMPOSIO_ENTITY_ENV_VAR_KEY, CORE_MEMORY_LINE_NUMBER_WARNING, RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE
from letta.functions.ast_parsers import coerce_dict_args_by_annotations, get_function_annotations_from_source from letta.functions.ast_parsers import coerce_dict_args_by_annotations, get_function_annotations_from_source
from letta.functions.helpers import execute_composio_action, generate_composio_action_from_func_name 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 from letta.helpers.composio_helpers import get_composio_api_key
from letta.helpers.json_helpers import json_dumps from letta.helpers.json_helpers import json_dumps
from letta.schemas.agent import AgentState from letta.schemas.agent import AgentState
@ -486,7 +486,7 @@ class LettaMultiAgentToolExecutor(ToolExecutor):
class ExternalComposioToolExecutor(ToolExecutor): class ExternalComposioToolExecutor(ToolExecutor):
"""Executor for external Composio tools.""" """Executor for external Composio tools."""
def execute( async def execute(
self, self,
function_name: str, function_name: str,
function_args: dict, function_args: dict,
@ -505,7 +505,7 @@ class ExternalComposioToolExecutor(ToolExecutor):
composio_api_key = get_composio_api_key(actor=actor) composio_api_key = get_composio_api_key(actor=actor)
# TODO (matt): Roll in execute_composio_action into this class # TODO (matt): Roll in execute_composio_action into this class
function_response = execute_composio_action( function_response = await execute_composio_action_async(
action_name=action_name, args=function_args, api_key=composio_api_key, entity_id=entity_id action_name=action_name, args=function_args, api_key=composio_api_key, entity_id=entity_id
) )

104
poetry.lock generated
View File

@ -1016,25 +1016,6 @@ e2b = ["e2b (>=0.17.2a37,<1.1.0)", "e2b-code-interpreter"]
flyio = ["gql", "requests_toolbelt"] flyio = ["gql", "requests_toolbelt"]
tools = ["diskcache", "flake8", "networkx", "pathspec", "pygments", "ruff", "transformers"] tools = ["diskcache", "flake8", "networkx", "pathspec", "pygments", "ruff", "transformers"]
[[package]]
name = "composio-langchain"
version = "0.7.15"
description = "Use Composio to get an array of tools with your LangChain agent."
optional = false
python-versions = "<4,>=3.9"
groups = ["main"]
files = [
{file = "composio_langchain-0.7.15-py3-none-any.whl", hash = "sha256:a71b5371ad6c3ee4d4289c7a994fad1424e24c29a38e820b6b2ed259056abb65"},
{file = "composio_langchain-0.7.15.tar.gz", hash = "sha256:cb75c460289ecdf9590caf7ddc0d7888b0a6622ca4f800c9358abe90c25d055e"},
]
[package.dependencies]
composio_core = ">=0.7.0,<0.8.0"
langchain = ">=0.1.0"
langchain-openai = ">=0.0.2.post1"
langchainhub = ">=0.1.15"
pydantic = ">=2.6.4"
[[package]] [[package]]
name = "configargparse" name = "configargparse"
version = "1.7" version = "1.7"
@ -2842,9 +2823,10 @@ files = [
name = "jsonpatch" name = "jsonpatch"
version = "1.33" version = "1.33"
description = "Apply JSON-Patches (RFC 6902)" description = "Apply JSON-Patches (RFC 6902)"
optional = false optional = true
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*"
groups = ["main"] groups = ["main"]
markers = "extra == \"external-tools\" or extra == \"desktop\" or extra == \"all\""
files = [ files = [
{file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"}, {file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"},
{file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"}, {file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"},
@ -2857,9 +2839,10 @@ jsonpointer = ">=1.9"
name = "jsonpointer" name = "jsonpointer"
version = "3.0.0" version = "3.0.0"
description = "Identify specific nodes in a JSON document (RFC 6901)" description = "Identify specific nodes in a JSON document (RFC 6901)"
optional = false optional = true
python-versions = ">=3.7" python-versions = ">=3.7"
groups = ["main"] groups = ["main"]
markers = "extra == \"external-tools\" or extra == \"desktop\" or extra == \"all\""
files = [ files = [
{file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"}, {file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"},
{file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"}, {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"},
@ -3052,9 +3035,10 @@ files = [
name = "langchain" name = "langchain"
version = "0.3.23" version = "0.3.23"
description = "Building applications with LLMs through composability" description = "Building applications with LLMs through composability"
optional = false optional = true
python-versions = "<4.0,>=3.9" python-versions = "<4.0,>=3.9"
groups = ["main"] groups = ["main"]
markers = "extra == \"external-tools\" or extra == \"desktop\" or extra == \"all\""
files = [ files = [
{file = "langchain-0.3.23-py3-none-any.whl", hash = "sha256:084f05ee7e80b7c3f378ebadd7309f2a37868ce2906fa0ae64365a67843ade3d"}, {file = "langchain-0.3.23-py3-none-any.whl", hash = "sha256:084f05ee7e80b7c3f378ebadd7309f2a37868ce2906fa0ae64365a67843ade3d"},
{file = "langchain-0.3.23.tar.gz", hash = "sha256:d95004afe8abebb52d51d6026270248da3f4b53d93e9bf699f76005e0c83ad34"}, {file = "langchain-0.3.23.tar.gz", hash = "sha256:d95004afe8abebb52d51d6026270248da3f4b53d93e9bf699f76005e0c83ad34"},
@ -3120,9 +3104,10 @@ tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<10"
name = "langchain-core" name = "langchain-core"
version = "0.3.51" version = "0.3.51"
description = "Building applications with LLMs through composability" description = "Building applications with LLMs through composability"
optional = false optional = true
python-versions = "<4.0,>=3.9" python-versions = "<4.0,>=3.9"
groups = ["main"] groups = ["main"]
markers = "extra == \"external-tools\" or extra == \"desktop\" or extra == \"all\""
files = [ files = [
{file = "langchain_core-0.3.51-py3-none-any.whl", hash = "sha256:4bd71e8acd45362aa428953f2a91d8162318014544a2216e4b769463caf68e13"}, {file = "langchain_core-0.3.51-py3-none-any.whl", hash = "sha256:4bd71e8acd45362aa428953f2a91d8162318014544a2216e4b769463caf68e13"},
{file = "langchain_core-0.3.51.tar.gz", hash = "sha256:db76b9cc331411602cb40ba0469a161febe7a0663fbcaddbc9056046ac2d22f4"}, {file = "langchain_core-0.3.51.tar.gz", hash = "sha256:db76b9cc331411602cb40ba0469a161febe7a0663fbcaddbc9056046ac2d22f4"},
@ -3140,30 +3125,14 @@ PyYAML = ">=5.3"
tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<10.0.0" tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<10.0.0"
typing-extensions = ">=4.7" typing-extensions = ">=4.7"
[[package]]
name = "langchain-openai"
version = "0.3.12"
description = "An integration package connecting OpenAI and LangChain"
optional = false
python-versions = "<4.0,>=3.9"
groups = ["main"]
files = [
{file = "langchain_openai-0.3.12-py3-none-any.whl", hash = "sha256:0fab64d58ec95e65ffbaf659470cd362e815685e15edbcb171641e90eca4eb86"},
{file = "langchain_openai-0.3.12.tar.gz", hash = "sha256:c9dbff63551f6bd91913bca9f99a2d057fd95dc58d4778657d67e5baa1737f61"},
]
[package.dependencies]
langchain-core = ">=0.3.49,<1.0.0"
openai = ">=1.68.2,<2.0.0"
tiktoken = ">=0.7,<1"
[[package]] [[package]]
name = "langchain-text-splitters" name = "langchain-text-splitters"
version = "0.3.8" version = "0.3.8"
description = "LangChain text splitting utilities" description = "LangChain text splitting utilities"
optional = false optional = true
python-versions = "<4.0,>=3.9" python-versions = "<4.0,>=3.9"
groups = ["main"] groups = ["main"]
markers = "extra == \"external-tools\" or extra == \"desktop\" or extra == \"all\""
files = [ files = [
{file = "langchain_text_splitters-0.3.8-py3-none-any.whl", hash = "sha256:e75cc0f4ae58dcf07d9f18776400cf8ade27fadd4ff6d264df6278bb302f6f02"}, {file = "langchain_text_splitters-0.3.8-py3-none-any.whl", hash = "sha256:e75cc0f4ae58dcf07d9f18776400cf8ade27fadd4ff6d264df6278bb302f6f02"},
{file = "langchain_text_splitters-0.3.8.tar.gz", hash = "sha256:116d4b9f2a22dda357d0b79e30acf005c5518177971c66a9f1ab0edfdb0f912e"}, {file = "langchain_text_splitters-0.3.8.tar.gz", hash = "sha256:116d4b9f2a22dda357d0b79e30acf005c5518177971c66a9f1ab0edfdb0f912e"},
@ -3172,30 +3141,14 @@ files = [
[package.dependencies] [package.dependencies]
langchain-core = ">=0.3.51,<1.0.0" langchain-core = ">=0.3.51,<1.0.0"
[[package]]
name = "langchainhub"
version = "0.1.21"
description = "The LangChain Hub API client"
optional = false
python-versions = "<4.0,>=3.8.1"
groups = ["main"]
files = [
{file = "langchainhub-0.1.21-py3-none-any.whl", hash = "sha256:1cc002dc31e0d132a776afd044361e2b698743df5202618cf2bad399246b895f"},
{file = "langchainhub-0.1.21.tar.gz", hash = "sha256:723383b3964a47dbaea6ad5d0ef728accefbc9d2c07480e800bdec43510a8c10"},
]
[package.dependencies]
packaging = ">=23.2,<25"
requests = ">=2,<3"
types-requests = ">=2.31.0.2,<3.0.0.0"
[[package]] [[package]]
name = "langsmith" name = "langsmith"
version = "0.3.28" version = "0.3.28"
description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform."
optional = false optional = true
python-versions = "<4.0,>=3.9" python-versions = "<4.0,>=3.9"
groups = ["main"] groups = ["main"]
markers = "extra == \"external-tools\" or extra == \"desktop\" or extra == \"all\""
files = [ files = [
{file = "langsmith-0.3.28-py3-none-any.whl", hash = "sha256:54ac8815514af52d9c801ad7970086693667e266bf1db90fc453c1759e8407cd"}, {file = "langsmith-0.3.28-py3-none-any.whl", hash = "sha256:54ac8815514af52d9c801ad7970086693667e266bf1db90fc453c1759e8407cd"},
{file = "langsmith-0.3.28.tar.gz", hash = "sha256:4666595207131d7f8d83418e54dc86c05e28562e5c997633e7c33fc18f9aeb89"}, {file = "langsmith-0.3.28.tar.gz", hash = "sha256:4666595207131d7f8d83418e54dc86c05e28562e5c997633e7c33fc18f9aeb89"},
@ -3221,14 +3174,14 @@ pytest = ["pytest (>=7.0.0)", "rich (>=13.9.4,<14.0.0)"]
[[package]] [[package]]
name = "letta-client" name = "letta-client"
version = "0.1.124" version = "0.1.129"
description = "" description = ""
optional = false optional = false
python-versions = "<4.0,>=3.8" python-versions = "<4.0,>=3.8"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "letta_client-0.1.124-py3-none-any.whl", hash = "sha256:a7901437ef91f395cd85d24c0312046b7c82e5a4dd8e04de0d39b5ca085c65d3"}, {file = "letta_client-0.1.129-py3-none-any.whl", hash = "sha256:87a5fc32471e5b9fefbfc1e1337fd667d5e2e340ece5d2a6c782afbceab4bf36"},
{file = "letta_client-0.1.124.tar.gz", hash = "sha256:e8b5716930824cc98c62ee01343e358f88619d346578d48a466277bc8282036d"}, {file = "letta_client-0.1.129.tar.gz", hash = "sha256:b00f611c18a2ad802ec9265f384e1666938c5fc5c86364b2c410d72f0331d597"},
] ]
[package.dependencies] [package.dependencies]
@ -4366,10 +4319,10 @@ files = [
name = "orjson" name = "orjson"
version = "3.10.16" version = "3.10.16"
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
optional = false optional = true
python-versions = ">=3.9" python-versions = ">=3.9"
groups = ["main"] groups = ["main"]
markers = "platform_python_implementation != \"PyPy\"" markers = "platform_python_implementation != \"PyPy\" and (extra == \"external-tools\" or extra == \"desktop\" or extra == \"all\")"
files = [ files = [
{file = "orjson-3.10.16-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4cb473b8e79154fa778fb56d2d73763d977be3dcc140587e07dbc545bbfc38f8"}, {file = "orjson-3.10.16-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4cb473b8e79154fa778fb56d2d73763d977be3dcc140587e07dbc545bbfc38f8"},
{file = "orjson-3.10.16-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:622a8e85eeec1948690409a19ca1c7d9fd8ff116f4861d261e6ae2094fe59a00"}, {file = "orjson-3.10.16-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:622a8e85eeec1948690409a19ca1c7d9fd8ff116f4861d261e6ae2094fe59a00"},
@ -6069,9 +6022,10 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
name = "requests-toolbelt" name = "requests-toolbelt"
version = "1.0.0" version = "1.0.0"
description = "A utility belt for advanced users of python-requests" description = "A utility belt for advanced users of python-requests"
optional = false optional = true
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
groups = ["main"] groups = ["main"]
markers = "extra == \"external-tools\" or extra == \"desktop\" or extra == \"all\""
files = [ files = [
{file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"},
{file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"},
@ -6855,21 +6809,6 @@ dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2
doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"] doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"]
test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.971)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.971)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"]
[[package]]
name = "types-requests"
version = "2.32.0.20250328"
description = "Typing stubs for requests"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "types_requests-2.32.0.20250328-py3-none-any.whl", hash = "sha256:72ff80f84b15eb3aa7a8e2625fffb6a93f2ad5a0c20215fc1dcfa61117bcb2a2"},
{file = "types_requests-2.32.0.20250328.tar.gz", hash = "sha256:c9e67228ea103bd811c96984fac36ed2ae8da87a36a633964a21f199d60baf32"},
]
[package.dependencies]
urllib3 = ">=2"
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.13.2" version = "4.13.2"
@ -7438,9 +7377,10 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"]
name = "zstandard" name = "zstandard"
version = "0.23.0" version = "0.23.0"
description = "Zstandard bindings for Python" description = "Zstandard bindings for Python"
optional = false optional = true
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["main"] groups = ["main"]
markers = "extra == \"external-tools\" or extra == \"desktop\" or extra == \"all\""
files = [ files = [
{file = "zstandard-0.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bf0a05b6059c0528477fba9054d09179beb63744355cab9f38059548fedd46a9"}, {file = "zstandard-0.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bf0a05b6059c0528477fba9054d09179beb63744355cab9f38059548fedd46a9"},
{file = "zstandard-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fc9ca1c9718cb3b06634c7c8dec57d24e9438b2aa9a0f02b8bb36bf478538880"}, {file = "zstandard-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fc9ca1c9718cb3b06634c7c8dec57d24e9438b2aa9a0f02b8bb36bf478538880"},
@ -7563,4 +7503,4 @@ tests = ["wikipedia"]
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = "<3.14,>=3.10" python-versions = "<3.14,>=3.10"
content-hash = "75c1c949aa6c0ef8d681bddd91999f97ed4991451be93ca45bf9c01dd19d8a8a" content-hash = "ba9cf0e00af2d5542aa4beecbd727af92b77ba584033f05c222b00ae47f96585"

View File

@ -56,7 +56,6 @@ nltk = "^3.8.1"
jinja2 = "^3.1.5" jinja2 = "^3.1.5"
locust = {version = "^2.31.5", optional = true} locust = {version = "^2.31.5", optional = true}
wikipedia = {version = "^1.4.0", optional = true} wikipedia = {version = "^1.4.0", optional = true}
composio-langchain = "^0.7.7"
composio-core = "^0.7.7" composio-core = "^0.7.7"
alembic = "^1.13.3" alembic = "^1.13.3"
pyhumps = "^3.8.0" pyhumps = "^3.8.0"
@ -74,7 +73,7 @@ llama-index = "^0.12.2"
llama-index-embeddings-openai = "^0.3.1" llama-index-embeddings-openai = "^0.3.1"
e2b-code-interpreter = {version = "^1.0.3", optional = true} e2b-code-interpreter = {version = "^1.0.3", optional = true}
anthropic = "^0.49.0" anthropic = "^0.49.0"
letta_client = "^0.1.124" letta_client = "^0.1.127"
openai = "^1.60.0" openai = "^1.60.0"
opentelemetry-api = "1.30.0" opentelemetry-api = "1.30.0"
opentelemetry-sdk = "1.30.0" opentelemetry-sdk = "1.30.0"

View File

@ -179,7 +179,7 @@ def check_agent_uses_external_tool(filename: str) -> LettaResponse:
Note: This is acting on the Letta response, note the usage of `user_message` Note: This is acting on the Letta response, note the usage of `user_message`
""" """
from composio_langchain import Action from composio import Action
# Set up client # Set up client
client = create_client() client = create_client()

View File

@ -56,7 +56,7 @@ def test_add_composio_tool(fastapi_client):
assert "name" in response.json() assert "name" in response.json()
def test_composio_tool_execution_e2e(check_composio_key_set, composio_get_emojis, server: SyncServer, default_user): async def test_composio_tool_execution_e2e(check_composio_key_set, composio_get_emojis, server: SyncServer, default_user):
agent_state = server.agent_manager.create_agent( agent_state = server.agent_manager.create_agent(
agent_create=CreateAgent( agent_create=CreateAgent(
name="sarah_agent", name="sarah_agent",
@ -67,7 +67,7 @@ def test_composio_tool_execution_e2e(check_composio_key_set, composio_get_emojis
actor=default_user, actor=default_user,
) )
tool_execution_result = ToolExecutionManager(agent_state, actor=default_user).execute_tool( tool_execution_result = await ToolExecutionManager(agent_state, actor=default_user).execute_tool(
function_name=composio_get_emojis.name, function_args={}, tool=composio_get_emojis function_name=composio_get_emojis.name, function_args={}, tool=composio_get_emojis
) )

View File

@ -124,7 +124,7 @@ def test_agent(client: LocalClient):
def test_agent_add_remove_tools(client: LocalClient, agent): def test_agent_add_remove_tools(client: LocalClient, agent):
# Create and add two tools to the client # Create and add two tools to the client
# tool 1 # tool 1
from composio_langchain import Action from composio import Action
github_tool = client.load_composio_tool(action=Action.GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER) github_tool = client.load_composio_tool(action=Action.GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER)
@ -316,7 +316,7 @@ def test_tools(client: LocalClient):
def test_tools_from_composio_basic(client: LocalClient): def test_tools_from_composio_basic(client: LocalClient):
from composio_langchain import Action from composio import Action
# Create a `LocalClient` (you can also use a `RESTClient`, see the letta_rest_client.py example) # Create a `LocalClient` (you can also use a `RESTClient`, see the letta_rest_client.py example)
client = create_client() client = create_client()