From 12ff04f194a9d9efe8739957220a5669a6f78139 Mon Sep 17 00:00:00 2001 From: Andy Li <55300002+cliandy@users.noreply.github.com> Date: Wed, 30 Apr 2025 16:05:03 -0700 Subject: [PATCH] feat: composio async execution (#1941) --- examples/composio_tool_usage.py | 2 +- letta/agent.py | 2 +- letta/agents/letta_agent.py | 5 + letta/functions/composio_helpers.py | 100 ++++++++++++++++ letta/functions/functions.py | 6 +- letta/functions/helpers.py | 108 +++--------------- letta/helpers/tool_execution_helper.py | 2 +- letta/schemas/tool.py | 11 +- .../tool_executor/tool_execution_manager.py | 2 +- letta/services/tool_executor/tool_executor.py | 6 +- poetry.lock | 104 ++++------------- pyproject.toml | 3 +- tests/helpers/endpoints_helper.py | 2 +- tests/integration_test_composio.py | 4 +- tests/test_local_client.py | 4 +- 15 files changed, 161 insertions(+), 200 deletions(-) create mode 100644 letta/functions/composio_helpers.py diff --git a/examples/composio_tool_usage.py b/examples/composio_tool_usage.py index d32546d1f..89c662b00 100644 --- a/examples/composio_tool_usage.py +++ b/examples/composio_tool_usage.py @@ -60,7 +60,7 @@ Last updated Oct 2, 2024. Please check `composio` documentation for any composio def main(): - from composio_langchain import Action + from composio import Action # Add the composio tool tool = client.load_composio_tool(action=Action.GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER) diff --git a/letta/agent.py b/letta/agent.py index 3960ade6a..84e8d0b47 100644 --- a/letta/agent.py +++ b/letta/agent.py @@ -21,8 +21,8 @@ from letta.constants import ( ) from letta.errors import ContextWindowExceededError 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.helpers import execute_composio_action, generate_composio_action_from_func_name from letta.functions.mcp_client.base_client import BaseMCPClient from letta.helpers import ToolRulesSolver from letta.helpers.composio_helpers import get_composio_api_key diff --git a/letta/agents/letta_agent.py b/letta/agents/letta_agent.py index 834098f97..2ac83ec40 100644 --- a/letta/agents/letta_agent.py +++ b/letta/agents/letta_agent.py @@ -179,6 +179,7 @@ class LettaAgent(BaseAgent): 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.EXTERNAL_COMPOSIO) ] 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) log_event(name="finish_send_message_to_agents_matching_tags", attributes=tool_args) 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: tool_execution_manager = ToolExecutionManager(agent_state=agent_state, actor=self.actor) # TODO: Integrate sandbox result diff --git a/letta/functions/composio_helpers.py b/letta/functions/composio_helpers.py new file mode 100644 index 000000000..ae5cbb35a --- /dev/null +++ b/letta/functions/composio_helpers.py @@ -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, "", "exec") + except SyntaxError as e: + print(f"Syntax error in code: {e}") diff --git a/letta/functions/functions.py b/letta/functions/functions.py index 007d587d1..b0c41a86b 100644 --- a/letta/functions/functions.py +++ b/letta/functions/functions.py @@ -1,8 +1,9 @@ import importlib import inspect +from collections.abc import Callable from textwrap import dedent # remove indentation 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.functions.schema_generator import generate_schema @@ -66,7 +67,8 @@ def parse_source_code(func) -> str: 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. diff --git a/letta/functions/helpers.py b/letta/functions/helpers.py index 54ca2740b..705046c42 100644 --- a/letta/functions/helpers.py +++ b/letta/functions/helpers.py @@ -6,10 +6,9 @@ from random import uniform from typing import Any, Dict, List, Optional, Type, Union import humps -from composio.constants import DEFAULT_ENTITY_ID 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.orm.errors import NoResultFound 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 -# 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? def generate_mcp_tool_wrapper(mcp_tool_name: str) -> tuple[str, str]: @@ -58,62 +29,11 @@ def {mcp_tool_name}(**kwargs): """ # 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() -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( tool: "LangChainBaseTool", additional_imports_module_attr_map: dict[str, str] = None ) -> tuple[str, str]: @@ -139,12 +59,12 @@ def {func_name}(**kwargs): """ # Compile safety check - assert_code_gen_compilable(wrapper_function_str) + _assert_code_gen_compilable(wrapper_function_str) return func_name, wrapper_function_str -def assert_code_gen_compilable(code_str): +def _assert_code_gen_compilable(code_str): try: compile(code_str, "", "exec") except SyntaxError as e: @@ -157,7 +77,7 @@ def assert_all_classes_are_imported(tool: Union["LangChainBaseTool"], additional current_class_imports = {tool_name} if additional_imports_module_attr_map: 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): 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) -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`. 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 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 # i.e., if obj('b' = ), we would want to inspect 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 # In the list example above, ['a', 3, None, ], 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 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 # Handle basic types return repr(obj) - elif is_base_model(obj): + elif _is_base_model(obj): # Otherwise, if it is a BaseModel # We want to pull out all the parameters, and reformat them into strings # e.g. {arg}={value} @@ -269,7 +189,7 @@ def generate_imported_tool_instantiation_call_str(obj: Any) -> Optional[str]: return None -def is_base_model(obj: Any): +def _is_base_model(obj: Any): return isinstance(obj, BaseModel) @@ -286,7 +206,7 @@ def generate_import_code(module_attr_map: Optional[dict]): return "\n".join(code_lines) -def parse_letta_response_for_assistant_message( +def _parse_letta_response_for_assistant_message( target_agent_id: str, letta_response: LettaResponse, ) -> 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)) -async def send_message_to_agent_no_stream( +async def _send_message_to_agent_no_stream( server: "SyncServer", agent_id: str, actor: User, @@ -389,7 +309,7 @@ async def async_send_message_with_retries( for attempt in range(1, max_retries + 1): try: response = await asyncio.wait_for( - send_message_to_agent_no_stream( + _send_message_to_agent_no_stream( server=server, agent_id=target_agent_id, actor=sender_agent.user, @@ -399,7 +319,7 @@ async def async_send_message_with_retries( ) # 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: sender_agent.logger.info(f"{logging_prefix} - {assistant_message}") return assistant_message diff --git a/letta/helpers/tool_execution_helper.py b/letta/helpers/tool_execution_helper.py index 2ea281578..1ec3f6462 100644 --- a/letta/helpers/tool_execution_helper.py +++ b/letta/helpers/tool_execution_helper.py @@ -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.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.orm.enums import ToolType from letta.schemas.agent import AgentState diff --git a/letta/schemas/tool.py b/letta/schemas/tool.py index bc2de0e4a..6c8f9bd3e 100644 --- a/letta/schemas/tool.py +++ b/letta/schemas/tool.py @@ -11,13 +11,9 @@ from letta.constants import ( MCP_TOOL_TAG_NAME_PREFIX, ) 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.helpers import ( - generate_composio_tool_wrapper, - generate_langchain_tool_wrapper, - generate_mcp_tool_wrapper, - generate_model_from_args_json_schema, -) +from letta.functions.helpers import 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.schema_generator import ( generate_schema_from_args_schema_v2, @@ -176,8 +172,7 @@ class ToolCreate(LettaBase): Returns: Tool: A Letta Tool initialized with attributes derived from the Composio tool. """ - from composio import LogLevel - from composio_langchain import ComposioToolSet + 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) diff --git a/letta/services/tool_executor/tool_execution_manager.py b/letta/services/tool_executor/tool_execution_manager.py index fcc96759e..6ba8679c3 100644 --- a/letta/services/tool_executor/tool_execution_manager.py +++ b/letta/services/tool_executor/tool_execution_manager.py @@ -100,7 +100,7 @@ class ToolExecutionManager: try: executor = ToolExecutorFactory.get_executor(tool.tool_type) # 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) else: result = executor.execute(function_name, function_args, self.agent_state, tool, self.actor) diff --git a/letta/services/tool_executor/tool_executor.py b/letta/services/tool_executor/tool_executor.py index 7d9cac41f..50879e570 100644 --- a/letta/services/tool_executor/tool_executor.py +++ b/letta/services/tool_executor/tool_executor.py @@ -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.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.json_helpers import json_dumps from letta.schemas.agent import AgentState @@ -486,7 +486,7 @@ class LettaMultiAgentToolExecutor(ToolExecutor): class ExternalComposioToolExecutor(ToolExecutor): """Executor for external Composio tools.""" - def execute( + async def execute( self, function_name: str, function_args: dict, @@ -505,7 +505,7 @@ class ExternalComposioToolExecutor(ToolExecutor): composio_api_key = get_composio_api_key(actor=actor) # 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 ) diff --git a/poetry.lock b/poetry.lock index 8812c8ccf..e67c55d35 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1016,25 +1016,6 @@ e2b = ["e2b (>=0.17.2a37,<1.1.0)", "e2b-code-interpreter"] flyio = ["gql", "requests_toolbelt"] 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]] name = "configargparse" version = "1.7" @@ -2842,9 +2823,10 @@ files = [ name = "jsonpatch" version = "1.33" 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.*" groups = ["main"] +markers = "extra == \"external-tools\" or extra == \"desktop\" or extra == \"all\"" files = [ {file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"}, {file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"}, @@ -2857,9 +2839,10 @@ jsonpointer = ">=1.9" name = "jsonpointer" version = "3.0.0" description = "Identify specific nodes in a JSON document (RFC 6901)" -optional = false +optional = true python-versions = ">=3.7" groups = ["main"] +markers = "extra == \"external-tools\" or extra == \"desktop\" or extra == \"all\"" files = [ {file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"}, {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"}, @@ -3052,9 +3035,10 @@ files = [ name = "langchain" version = "0.3.23" description = "Building applications with LLMs through composability" -optional = false +optional = true python-versions = "<4.0,>=3.9" groups = ["main"] +markers = "extra == \"external-tools\" or extra == \"desktop\" or extra == \"all\"" files = [ {file = "langchain-0.3.23-py3-none-any.whl", hash = "sha256:084f05ee7e80b7c3f378ebadd7309f2a37868ce2906fa0ae64365a67843ade3d"}, {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" version = "0.3.51" description = "Building applications with LLMs through composability" -optional = false +optional = true python-versions = "<4.0,>=3.9" groups = ["main"] +markers = "extra == \"external-tools\" or extra == \"desktop\" or extra == \"all\"" files = [ {file = "langchain_core-0.3.51-py3-none-any.whl", hash = "sha256:4bd71e8acd45362aa428953f2a91d8162318014544a2216e4b769463caf68e13"}, {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" 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]] name = "langchain-text-splitters" version = "0.3.8" description = "LangChain text splitting utilities" -optional = false +optional = true python-versions = "<4.0,>=3.9" groups = ["main"] +markers = "extra == \"external-tools\" or extra == \"desktop\" or extra == \"all\"" files = [ {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"}, @@ -3172,30 +3141,14 @@ files = [ [package.dependencies] 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]] name = "langsmith" version = "0.3.28" description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." -optional = false +optional = true python-versions = "<4.0,>=3.9" groups = ["main"] +markers = "extra == \"external-tools\" or extra == \"desktop\" or extra == \"all\"" files = [ {file = "langsmith-0.3.28-py3-none-any.whl", hash = "sha256:54ac8815514af52d9c801ad7970086693667e266bf1db90fc453c1759e8407cd"}, {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]] name = "letta-client" -version = "0.1.124" +version = "0.1.129" description = "" optional = false python-versions = "<4.0,>=3.8" groups = ["main"] files = [ - {file = "letta_client-0.1.124-py3-none-any.whl", hash = "sha256:a7901437ef91f395cd85d24c0312046b7c82e5a4dd8e04de0d39b5ca085c65d3"}, - {file = "letta_client-0.1.124.tar.gz", hash = "sha256:e8b5716930824cc98c62ee01343e358f88619d346578d48a466277bc8282036d"}, + {file = "letta_client-0.1.129-py3-none-any.whl", hash = "sha256:87a5fc32471e5b9fefbfc1e1337fd667d5e2e340ece5d2a6c782afbceab4bf36"}, + {file = "letta_client-0.1.129.tar.gz", hash = "sha256:b00f611c18a2ad802ec9265f384e1666938c5fc5c86364b2c410d72f0331d597"}, ] [package.dependencies] @@ -4366,10 +4319,10 @@ files = [ name = "orjson" version = "3.10.16" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" -optional = false +optional = true python-versions = ">=3.9" groups = ["main"] -markers = "platform_python_implementation != \"PyPy\"" +markers = "platform_python_implementation != \"PyPy\" and (extra == \"external-tools\" or extra == \"desktop\" or extra == \"all\")" 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-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" version = "1.0.0" 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.*" groups = ["main"] +markers = "extra == \"external-tools\" or extra == \"desktop\" or extra == \"all\"" files = [ {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"}, @@ -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)"] 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]] name = "typing-extensions" version = "4.13.2" @@ -7438,9 +7377,10 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"] name = "zstandard" version = "0.23.0" description = "Zstandard bindings for Python" -optional = false +optional = true python-versions = ">=3.8" groups = ["main"] +markers = "extra == \"external-tools\" or extra == \"desktop\" or extra == \"all\"" 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_11_0_arm64.whl", hash = "sha256:fc9ca1c9718cb3b06634c7c8dec57d24e9438b2aa9a0f02b8bb36bf478538880"}, @@ -7563,4 +7503,4 @@ tests = ["wikipedia"] [metadata] lock-version = "2.1" python-versions = "<3.14,>=3.10" -content-hash = "75c1c949aa6c0ef8d681bddd91999f97ed4991451be93ca45bf9c01dd19d8a8a" +content-hash = "ba9cf0e00af2d5542aa4beecbd727af92b77ba584033f05c222b00ae47f96585" diff --git a/pyproject.toml b/pyproject.toml index 8eea31aca..34a3830be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,6 @@ nltk = "^3.8.1" jinja2 = "^3.1.5" locust = {version = "^2.31.5", optional = true} wikipedia = {version = "^1.4.0", optional = true} -composio-langchain = "^0.7.7" composio-core = "^0.7.7" alembic = "^1.13.3" pyhumps = "^3.8.0" @@ -74,7 +73,7 @@ llama-index = "^0.12.2" llama-index-embeddings-openai = "^0.3.1" e2b-code-interpreter = {version = "^1.0.3", optional = true} anthropic = "^0.49.0" -letta_client = "^0.1.124" +letta_client = "^0.1.127" openai = "^1.60.0" opentelemetry-api = "1.30.0" opentelemetry-sdk = "1.30.0" diff --git a/tests/helpers/endpoints_helper.py b/tests/helpers/endpoints_helper.py index 9b8f9a9f1..f9025c43f 100644 --- a/tests/helpers/endpoints_helper.py +++ b/tests/helpers/endpoints_helper.py @@ -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` """ - from composio_langchain import Action + from composio import Action # Set up client client = create_client() diff --git a/tests/integration_test_composio.py b/tests/integration_test_composio.py index fd6b32cab..e1219d1ea 100644 --- a/tests/integration_test_composio.py +++ b/tests/integration_test_composio.py @@ -56,7 +56,7 @@ def test_add_composio_tool(fastapi_client): 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_create=CreateAgent( name="sarah_agent", @@ -67,7 +67,7 @@ def test_composio_tool_execution_e2e(check_composio_key_set, composio_get_emojis 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 ) diff --git a/tests/test_local_client.py b/tests/test_local_client.py index 0bd9a1401..a3967e4a0 100644 --- a/tests/test_local_client.py +++ b/tests/test_local_client.py @@ -124,7 +124,7 @@ def test_agent(client: LocalClient): def test_agent_add_remove_tools(client: LocalClient, agent): # Create and add two tools to the client # 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) @@ -316,7 +316,7 @@ def test_tools(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) client = create_client()