feat: Adapt crewAI to also accept parameterized tools and add example (#1817)

Co-authored-by: Matt Zhou <mattzhou@Matts-MacBook-Pro.local>
This commit is contained in:
Matthew Zhou 2024-10-01 18:00:26 -07:00 committed by GitHub
parent 42dbae6459
commit 6d154bfcbd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 139 additions and 17 deletions

View File

@ -0,0 +1,73 @@
import json
import uuid
from letta import create_client
from letta.schemas.memory import ChatMemory
from letta.schemas.tool import Tool
"""
This example show how you can add CrewAI tools .
First, make sure you have CrewAI and some of the extras downloaded.
```
poetry install --extras "external-tools"
```
then setup letta with `letta configure`.
"""
def main():
from crewai_tools import ScrapeWebsiteTool
crewai_tool = ScrapeWebsiteTool(website_url="https://www.example.com")
example_website_scrape_tool = Tool.from_crewai(crewai_tool)
tool_name = example_website_scrape_tool.name
# Create a `LocalClient` (you can also use a `RESTClient`, see the letta_rest_client.py example)
client = create_client()
# create tool
client.add_tool(example_website_scrape_tool)
# Confirm that the tool is in
tools = client.list_tools()
assert example_website_scrape_tool.name in [t.name for t in tools]
# Generate uuid for agent name for this example
namespace = uuid.NAMESPACE_DNS
agent_uuid = str(uuid.uuid5(namespace, "letta-crewai-tooling-example"))
# Clear all agents
for agent_state in client.list_agents():
if agent_state.name == agent_uuid:
client.delete_agent(agent_id=agent_state.id)
print(f"Deleted agent: {agent_state.name} with ID {str(agent_state.id)}")
# google search persona
persona = f"""
My name is Letta.
I am a personal assistant who answers a user's questions about a website `example.com`. When a user asks me a question about `example.com`, I will use a tool called {tool_name} which will search `example.com` and answer the relevant question.
Dont forget - inner monologue / inner thoughts should always be different than the contents of send_message! send_message is how you communicate with the user, whereas inner thoughts are your own personal inner thoughts.
"""
# Create an agent
agent_state = client.create_agent(name=agent_uuid, memory=ChatMemory(human="My name is Matt.", persona=persona), tools=[tool_name])
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="What's on the example.com website?")
for message in send_message_response.messages:
response_json = json.dumps(message.model_dump(), indent=4)
print(f"{response_json}\n")
# Delete agent
client.delete_agent(agent_id=agent_state.id)
print(f"Deleted agent: {agent_state.name} with ID {str(agent_state.id)}")
if __name__ == "__main__":
main()

View File

@ -11,8 +11,7 @@ This example show how you can add LangChain tools .
First, make sure you have LangChain and some of the extras downloaded. First, make sure you have LangChain and some of the extras downloaded.
For this specific example, you will need `wikipedia` installed. For this specific example, you will need `wikipedia` installed.
``` ```
poetry install --extras "tests" poetry install --extras "external-tools"
poetry install langchain
``` ```
then setup letta with `letta configure`. then setup letta with `letta configure`.
""" """

View File

@ -3,21 +3,15 @@ from typing import Any, Optional, Union
from pydantic import BaseModel from pydantic import BaseModel
def generate_langchain_tool_wrapper(tool: "LangChainBaseTool", additional_imports_module_attr_map: dict = None) -> tuple[str, str]: def generate_langchain_tool_wrapper(
tool: "LangChainBaseTool", additional_imports_module_attr_map: dict[str, str] = None
) -> tuple[str, str]:
tool_name = tool.__class__.__name__ tool_name = tool.__class__.__name__
import_statement = f"from langchain_community.tools import {tool_name}" import_statement = f"from langchain_community.tools import {tool_name}"
extra_module_imports = generate_import_code(additional_imports_module_attr_map) extra_module_imports = generate_import_code(additional_imports_module_attr_map)
# Safety check that user has passed in all required imports: # Safety check that user has passed in all required imports:
current_class_imports = {tool_name} assert_all_classes_are_imported(tool, additional_imports_module_attr_map)
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))
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}"
print(err_msg)
raise RuntimeError(err_msg)
tool_instantiation = f"tool = {generate_imported_tool_instantiation_call_str(tool)}" tool_instantiation = f"tool = {generate_imported_tool_instantiation_call_str(tool)}"
run_call = f"return tool._run(**kwargs)" run_call = f"return tool._run(**kwargs)"
@ -37,9 +31,14 @@ def {func_name}(**kwargs):
return func_name, wrapper_function_str return func_name, wrapper_function_str
def generate_crewai_tool_wrapper(tool: "CrewAIBaseTool") -> tuple[str, str]: def generate_crewai_tool_wrapper(tool: "CrewAIBaseTool", additional_imports_module_attr_map: dict[str, str] = None) -> tuple[str, str]:
tool_name = tool.__class__.__name__ tool_name = tool.__class__.__name__
import_statement = f"from crewai_tools import {tool_name}" import_statement = f"from crewai_tools import {tool_name}"
extra_module_imports = generate_import_code(additional_imports_module_attr_map)
# Safety check that user has passed in all required imports:
assert_all_classes_are_imported(tool, additional_imports_module_attr_map)
tool_instantiation = f"tool = {generate_imported_tool_instantiation_call_str(tool)}" tool_instantiation = f"tool = {generate_imported_tool_instantiation_call_str(tool)}"
run_call = f"return tool._run(**kwargs)" run_call = f"return tool._run(**kwargs)"
func_name = f"run_{tool_name.lower()}" func_name = f"run_{tool_name.lower()}"
@ -50,12 +49,29 @@ def {func_name}(**kwargs):
if 'self' in kwargs: if 'self' in kwargs:
del kwargs['self'] del kwargs['self']
{import_statement} {import_statement}
{extra_module_imports}
{tool_instantiation} {tool_instantiation}
{run_call} {run_call}
""" """
return func_name, wrapper_function_str return func_name, wrapper_function_str
def assert_all_classes_are_imported(
tool: Union["LangChainBaseTool", "CrewAIBaseTool"], additional_imports_module_attr_map: dict[str, str]
) -> None:
# Safety check that user has passed in all required imports:
tool_name = tool.__class__.__name__
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))
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}"
print(err_msg)
raise RuntimeError(err_msg)
def find_required_class_names_for_import(obj: Union["LangChainBaseTool", "CrewAIBaseTool", BaseModel]) -> list[str]: def find_required_class_names_for_import(obj: Union["LangChainBaseTool", "CrewAIBaseTool", 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`.

View File

@ -93,7 +93,7 @@ class Tool(BaseTool):
) )
@classmethod @classmethod
def from_crewai(cls, crewai_tool: "CrewAIBaseTool") -> "Tool": def from_crewai(cls, crewai_tool: "CrewAIBaseTool", additional_imports_module_attr_map: dict[str, str] = None) -> "Tool":
""" """
Class method to create an instance of Tool from a crewAI BaseTool object. Class method to create an instance of Tool from a crewAI BaseTool object.
@ -106,7 +106,7 @@ class Tool(BaseTool):
description = crewai_tool.description description = crewai_tool.description
source_type = "python" source_type = "python"
tags = ["crew-ai"] tags = ["crew-ai"]
wrapper_func_name, wrapper_function_str = generate_crewai_tool_wrapper(crewai_tool) wrapper_func_name, wrapper_function_str = generate_crewai_tool_wrapper(crewai_tool, additional_imports_module_attr_map)
json_schema = generate_schema_from_args_schema(crewai_tool.args_schema, name=wrapper_func_name, description=description) json_schema = generate_schema_from_args_schema(crewai_tool.args_schema, name=wrapper_func_name, description=description)
# append heartbeat (necessary for triggering another reasoning step after this tool call) # append heartbeat (necessary for triggering another reasoning step after this tool call)

View File

@ -292,11 +292,45 @@ def test_tools_from_crewai(client):
# Pull a simple HTML website and check that scraping it works # Pull a simple HTML website and check that scraping it works
# TODO: This is very hacky and can break at any time if the website changes. # TODO: This is very hacky and can break at any time if the website changes.
# Host our own websites to test website tool calling on. # Host our own websites to test website tool calling on.
simple_webpage_url = "https://www.york.ac.uk/teaching/cws/wws/webpage1.html" simple_webpage_url = "https://www.example.com"
expected_content = "There are lots of ways to create web pages using already coded programmes." expected_content = "This domain is for use in illustrative examples in documents."
assert expected_content in func(website_url=simple_webpage_url) assert expected_content in func(website_url=simple_webpage_url)
def test_tools_from_crewai_with_params(client):
# create crewAI tool
from crewai_tools import ScrapeWebsiteTool
from letta.schemas.tool import Tool
crewai_tool = ScrapeWebsiteTool(website_url="https://www.example.com")
# Translate to memGPT Tool
tool = Tool.from_crewai(crewai_tool)
# Add the tool
client.add_tool(tool)
# list tools
tools = client.list_tools()
assert tool.name in [t.name for t in tools]
# get tool
tool_id = client.get_tool_id(name=tool.name)
retrieved_tool = client.get_tool(tool_id)
source_code = retrieved_tool.source_code
# Parse the function and attempt to use it
local_scope = {}
exec(source_code, {}, local_scope)
func = local_scope[tool.name]
# Pull a simple HTML website and check that scraping it works
expected_content = "This domain is for use in illustrative examples in documents."
assert expected_content in func()
def test_tools_from_langchain(client): def test_tools_from_langchain(client):
# create langchain tool # create langchain tool
from langchain_community.tools import WikipediaQueryRun from langchain_community.tools import WikipediaQueryRun