feat: Support Langchain tools (#881)

This commit is contained in:
Matthew Zhou
2025-01-31 14:36:35 -10:00
committed by GitHub
parent 50df2ee3c4
commit 24ade60257
10 changed files with 45 additions and 22 deletions

View File

@@ -73,7 +73,7 @@ def main():
print(f"Created agent: {agent_state.name} with ID {str(agent_state.id)}")
# Send a message to the agent
send_message_response = client.user_message(agent_id=agent_state.id, message="How do you pronounce Albert Einstein's name?")
send_message_response = client.user_message(agent_id=agent_state.id, message="Tell me a fun fact about Albert Einstein!")
for message in send_message_response.messages:
response_json = json.dumps(message.model_dump(), indent=4)
print(f"{response_json}\n")

View File

@@ -2950,18 +2950,11 @@ class LocalClient(AbstractClient):
langchain_tool=langchain_tool,
additional_imports_module_attr_map=additional_imports_module_attr_map,
)
return self.server.tool_manager.create_or_update_tool(pydantic_tool=Tool(**tool_create.model_dump()), actor=self.user)
def load_crewai_tool(self, crewai_tool: "CrewAIBaseTool", additional_imports_module_attr_map: dict[str, str] = None) -> Tool:
tool_create = ToolCreate.from_crewai(
crewai_tool=crewai_tool,
additional_imports_module_attr_map=additional_imports_module_attr_map,
)
return self.server.tool_manager.create_or_update_tool(pydantic_tool=Tool(**tool_create.model_dump()), actor=self.user)
return self.server.tool_manager.create_or_update_langchain_tool(tool_create=tool_create, actor=self.user)
def load_composio_tool(self, action: "ActionType") -> Tool:
tool_create = ToolCreate.from_composio(action_name=action.name)
return self.server.tool_manager.create_or_update_composio_tool(pydantic_tool=Tool(**tool_create.model_dump()), actor=self.user)
return self.server.tool_manager.create_or_update_composio_tool(tool_create=tool_create, actor=self.user)
def create_tool(
self,

View File

@@ -230,9 +230,7 @@ def generate_imported_tool_instantiation_call_str(obj: Any) -> Optional[str]:
def is_base_model(obj: Any):
from langchain_core.pydantic_v1 import BaseModel as LangChainBaseModel
return isinstance(obj, BaseModel) or isinstance(obj, LangChainBaseModel)
return isinstance(obj, BaseModel)
def generate_import_code(module_attr_map: Optional[dict]):

View File

@@ -7,6 +7,7 @@ class ToolType(str, Enum):
LETTA_MEMORY_CORE = "letta_memory_core"
LETTA_MULTI_AGENT_CORE = "letta_multi_agent_core"
EXTERNAL_COMPOSIO = "external_composio"
EXTERNAL_LANGCHAIN = "external_langchain"
class JobType(str, Enum):

View File

@@ -79,7 +79,7 @@ class Tool(BaseTool):
self.json_schema = get_json_schema_from_module(module_name=LETTA_MULTI_AGENT_TOOL_MODULE_NAME, function_name=self.name)
elif self.tool_type == ToolType.EXTERNAL_COMPOSIO:
# If it is a composio tool, we generate both the source code and json schema on the fly here
# TODO: This is brittle, need to think long term about how to improve this
# TODO: Deriving the composio action name is brittle, need to think long term about how to improve this
try:
composio_action = generate_composio_action_from_func_name(self.name)
tool_create = ToolCreate.from_composio(composio_action)

View File

@@ -231,7 +231,7 @@ def add_composio_tool(
try:
tool_create = ToolCreate.from_composio(action_name=composio_action_name)
return server.tool_manager.create_or_update_composio_tool(pydantic_tool=Tool(**tool_create.model_dump()), actor=actor)
return server.tool_manager.create_or_update_composio_tool(tool_create=tool_create, actor=actor)
except EnumStringNotFound as e:
raise HTTPException(
status_code=400, # Bad Request

View File

@@ -11,7 +11,7 @@ from letta.orm.enums import ToolType
from letta.orm.errors import NoResultFound
from letta.orm.tool import Tool as ToolModel
from letta.schemas.tool import Tool as PydanticTool
from letta.schemas.tool import ToolUpdate
from letta.schemas.tool import ToolCreate, ToolUpdate
from letta.schemas.user import User as PydanticUser
from letta.utils import enforce_types, printd
@@ -57,9 +57,12 @@ class ToolManager:
return tool
@enforce_types
def create_or_update_composio_tool(self, pydantic_tool: PydanticTool, actor: PydanticUser) -> PydanticTool:
pydantic_tool.tool_type = ToolType.EXTERNAL_COMPOSIO
return self.create_or_update_tool(pydantic_tool, actor)
def create_or_update_composio_tool(self, tool_create: ToolCreate, actor: PydanticUser) -> PydanticTool:
return self.create_or_update_tool(PydanticTool(tool_type=ToolType.EXTERNAL_COMPOSIO, **tool_create.model_dump()), actor)
@enforce_types
def create_or_update_langchain_tool(self, tool_create: ToolCreate, actor: PydanticUser) -> PydanticTool:
return self.create_or_update_tool(PydanticTool(tool_type=ToolType.EXTERNAL_LANGCHAIN, **tool_create.model_dump()), actor)
@enforce_types
def create_tool(self, pydantic_tool: PydanticTool, actor: PydanticUser) -> PydanticTool:

View File

@@ -190,7 +190,7 @@ def list_tool(test_user):
def composio_github_star_tool(test_user):
tool_manager = ToolManager()
tool_create = ToolCreate.from_composio(action_name="GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER")
tool = tool_manager.create_or_update_composio_tool(pydantic_tool=Tool(**tool_create.model_dump()), actor=test_user)
tool = tool_manager.create_or_update_composio_tool(tool_create=tool_create, actor=test_user)
yield tool
@@ -198,7 +198,7 @@ def composio_github_star_tool(test_user):
def composio_gmail_get_profile_tool(test_user):
tool_manager = ToolManager()
tool_create = ToolCreate.from_composio(action_name="GMAIL_GET_PROFILE")
tool = tool_manager.create_or_update_composio_tool(pydantic_tool=Tool(**tool_create.model_dump()), actor=test_user)
tool = tool_manager.create_or_update_composio_tool(tool_create=tool_create, actor=test_user)
yield tool

View File

@@ -198,7 +198,7 @@ def print_tool(server: SyncServer, default_user, default_organization):
@pytest.fixture
def composio_github_star_tool(server, default_user):
tool_create = ToolCreate.from_composio(action_name="GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER")
tool = server.tool_manager.create_or_update_composio_tool(pydantic_tool=PydanticTool(**tool_create.model_dump()), actor=default_user)
tool = server.tool_manager.create_or_update_composio_tool(tool_create=tool_create, actor=default_user)
yield tool

View File

@@ -205,3 +205,31 @@ def test_composio_tool_schema_generation(openai_model: str, structured_output: b
print(f"Failed to call OpenAI using schema {schema} generated from {action_name}\n\n")
raise
@pytest.mark.parametrize("openai_model", ["gpt-4o-mini"])
@pytest.mark.parametrize("structured_output", [True])
def test_langchain_tool_schema_generation(openai_model: str, structured_output: bool):
"""Test that we can generate the schemas for some Langchain tools."""
from langchain_community.tools import WikipediaQueryRun
from langchain_community.utilities import WikipediaAPIWrapper
api_wrapper = WikipediaAPIWrapper(top_k_results=1, doc_content_chars_max=500)
langchain_tool = WikipediaQueryRun(api_wrapper=api_wrapper)
tool_create = ToolCreate.from_langchain(
langchain_tool=langchain_tool,
additional_imports_module_attr_map={"langchain_community.utilities": "WikipediaAPIWrapper"},
)
assert tool_create.json_schema
schema = tool_create.json_schema
print(f"The schema for {langchain_tool.name}: {json.dumps(schema, indent=4)}\n\n")
try:
_openai_payload(openai_model, schema, structured_output)
print(f"Successfully called OpenAI using schema {schema} generated from {langchain_tool.name}\n\n")
except:
print(f"Failed to call OpenAI using schema {schema} generated from {langchain_tool.name}\n\n")
raise