mirror of
https://github.com/cpacker/MemGPT.git
synced 2025-06-03 04:30:22 +00:00
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:
parent
42dbae6459
commit
6d154bfcbd
73
examples/crewai_tool_usage.py
Normal file
73
examples/crewai_tool_usage.py
Normal 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.
|
||||||
|
|
||||||
|
Don’t 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()
|
@ -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`.
|
||||||
"""
|
"""
|
@ -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`.
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user