Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat crewai - ETHGlobal #302

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions python/cdp-crewai/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Changelog

All notable changes to this project will be documented in this file.

## [0.0.1] - 2024-03-20

### Added

- Initial release of CDP Agentkit Extension - CrewAI Toolkit
- Added CdpToolkit for CrewAI integration
- Added CdpAgentkitWrapper utility class
- Added support for wallet persistence between sessions
- Added basic example chatbot implementation
- Integration with cdp-agentkit-core version 0.0.11
- Support for all CDP Agentkit core actions
- Support for multiple networks via CDP SDK
27 changes: 27 additions & 0 deletions python/cdp-crewai/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
.PHONY: format
format:
poetry run ruff format .

.PHONY: lint
lint:
poetry run ruff check .

.PHONY: lint-fix
lint-fix:
poetry run ruff check . --fix

.PHONY: test
test:
poetry run pytest

.PHONY: test-coverage
test-coverage:
poetry run pytest --cov=cdp_crewai --cov-report=term-missing

.PHONY: docs
docs:
poetry run sphinx-build -b html docs/source docs/_build/html

.PHONY: local-docs
local-docs: docs
cd docs && make html && open ./_build/html/index.html
122 changes: 122 additions & 0 deletions python/cdp-crewai/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# CDP AgentKit Extension - CrewAI Toolkit

CDP integration with CrewAI to enable agentic workflows using the core primitives defined in `cdp-agentkit-core`.

## Setup

### Prerequisites

- [CDP API Key](https://portal.cdp.coinbase.com/access/api)
- [OpenAI API Key](https://platform.openai.com/docs/quickstart#create-and-export-an-api-key)
- Python 3.10 or higher

### Installation

```bash
pip install cdp-crewai
```


### Environment Setup

Set the following environment variables:

```bash
export CDP_API_KEY_NAME="your_api_key_name"
export CDP_API_KEY_PRIVATE_KEY="your_api_key_private_key"
export MNEMONIC_PHRASE="your_mnemonic_phrase"
export NETWORK_ID="your_network_id"
```


## Usage

### Basic Setup

```python
from cdp_crewai.agent_toolkits import CdpToolkit
from cdp_crewai.utils import CdpAgentkitWrapper

# Initialize the wrapper
wrapper = CdpAgentkitWrapper()

# Create toolkit from wrapper
toolkit = CdpToolkit.from_cdp_agentkit_wrapper(cdp)

# Get available tools
tools = toolkit.get_tools()
for tool in tools:
print(tool.name)
```

The toolkit provides the following tools:
[List of tools from CDP AgentKit Core]

### Using with CrewAI

```python
from crewai import Agent, Task, Crew
from langchain_openai import ChatOpenAI

# Initialize LLM
llm = ChatOpenAI(model="gpt-4")

# Create an agent with CDP tools
crypto_agent = Agent(
role='Crypto Expert',
goal='Execute crypto transactions efficiently',
backstory='Expert in blockchain and cryptocurrency operations',
tools=toolkit.get_tools(),
llm=llm
)

# Create a task
task = Task(
description='Transfer 0.01 ETH to the address 0x1234567890123456789012345678901234567890',
agent=crypto_agent
)

# Create and run the crew
crew = Crew(
agents=[crypto_agent],
tasks=[task]
)
result = crew.kickoff()
```


## CDP Toolkit Specific Features

### Wallet Management

The toolkit maintains an MPC wallet that persists between sessions:

```python
# Export wallet data
wallet_data = cdp.export_wallet()

# Import wallet data
values = {"cdp_wallet_data": wallet_data}
cdp = CdpAgentkitWrapper(**values)
```


### Network Support

The toolkit supports [multiple networks](https://docs.cdp.coinbase.com/cdp-apis/docs/networks).

### Gasless Transactions

The following operations support gasless transactions on Base Mainnet:
- USDC transfers
- EURC transfers
- cbBTC transfers

## Examples

Check out [../examples](../examples) for inspiration and help getting started!
- [Chatbot Python](../examples/cdp-crewai-chatbot/README.md): Simple example of a Python Chatbot that can perform complex onchain interactions using CrewAI.

## Contributing

See [CONTRIBUTING.md](../../CONTRIBUTING.md) for detailed setup instructions and contribution guidelines.
4 changes: 4 additions & 0 deletions python/cdp-crewai/cdp_crewai/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .agent_toolkits.cdp_toolkit import CdpToolkit
from .utils.cdp_agentkit_wrapper import CdpAgentkitWrapper

__all__ = ["CdpToolkit", "CdpAgentkitWrapper"]
1 change: 1 addition & 0 deletions python/cdp-crewai/cdp_crewai/__version__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.0.1"
3 changes: 3 additions & 0 deletions python/cdp-crewai/cdp_crewai/agent_toolkits/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from cdp_crewai.agent_toolkits.cdp_toolkit import CdpToolkit

__all__ = ["CdpToolkit"]
57 changes: 57 additions & 0 deletions python/cdp-crewai/cdp_crewai/agent_toolkits/cdp_toolkit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""CDP Toolkit."""


from crewai.tools import BaseTool

from cdp_agentkit_core.actions import CDP_ACTIONS

from ..tools import CdpTool
from ..utils import CdpAgentkitWrapper


class CdpToolkit:
"""Coinbase Developer Platform (CDP) Toolkit.

*Security Note*: This toolkit contains tools that can read and modify
the state of a service; e.g., by creating, deleting, or updating,
reading underlying data.

For example, this toolkit can be used to create wallets, transactions,
and smart contract invocations on CDP supported blockchains.
"""

tools: list[BaseTool] = [] # noqa: RUF012

def __init__(self, tools: list[BaseTool]| None = None):
"""Initialize the toolkit with tools."""
self.tools = tools or []

@classmethod
def from_cdp_agentkit_wrapper(cls, cdp_agentkit_wrapper: CdpAgentkitWrapper) -> "CdpToolkit":
"""Create a CdpToolkit from a CdpAgentkitWrapper.

Args:
cdp_agentkit_wrapper: CdpAgentkitWrapper. The CDP Agentkit wrapper.

Returns:
CdpToolkit. The CDP toolkit.

"""
actions = CDP_ACTIONS

tools = [
CdpTool(
name=action.name,
description=action.description,
cdp_agentkit_wrapper=cdp_agentkit_wrapper,
args_schema=action.args_schema,
func=action.func,
)
for action in actions
]

return cls(tools=tools)

def get_tools(self) -> list[BaseTool]:
"""Get the tools in the toolkit."""
return self.tools
4 changes: 4 additions & 0 deletions python/cdp-crewai/cdp_crewai/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""Specifies package level constants used throughout the package."""

# CDP_CREWAI_DEFAULT_SOURCE (str): Denotes the default source for CDP CrewAI extensions.
CDP_CREWAI_DEFAULT_SOURCE = "cdp-crewai"
5 changes: 5 additions & 0 deletions python/cdp-crewai/cdp_crewai/tools/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""CDP Tool."""

from cdp_crewai.tools.cdp_tool import CdpTool

__all__ = ["CdpTool"]
36 changes: 36 additions & 0 deletions python/cdp-crewai/cdp_crewai/tools/cdp_tool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Tool allows agents to interact with the cdp-sdk library and control an MPC Wallet onchain."""

from collections.abc import Callable
from typing import Any

from crewai.tools import BaseTool
from pydantic import BaseModel, Field

from ..utils import CdpAgentkitWrapper


class CdpTool(BaseTool):
"""Tool for interacting with the CDP SDK."""

cdp_agentkit_wrapper: CdpAgentkitWrapper = Field(
..., description="CDP AgentKit wrapper instance"
)
name: str = Field(default="")
description: str = Field(default="")
args_schema: type[BaseModel] | None = Field(default=None)
func: Callable[..., str] = Field(..., description="Function to execute")

def _run(self, instructions: str | None = "", **kwargs: Any) -> str:
"""Use the CDP SDK to run an operation."""
if not instructions or instructions == "{}":
instructions = ""

if self.args_schema is not None:
# Include instructions in kwargs when using schema
kwargs["instructions"] = instructions
validated_input_data = self.args_schema(**kwargs)
parsed_input_args = validated_input_data.model_dump()
else:
parsed_input_args = {"instructions": instructions}

return self.cdp_agentkit_wrapper.run_action(self.func, **parsed_input_args)
3 changes: 3 additions & 0 deletions python/cdp-crewai/cdp_crewai/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .cdp_agentkit_wrapper import CdpAgentkitWrapper

__all__ = ["CdpAgentkitWrapper"]
99 changes: 99 additions & 0 deletions python/cdp-crewai/cdp_crewai/utils/cdp_agentkit_wrapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""Util that calls CDP."""

import inspect
import json
from collections.abc import Callable
from typing import Any

from langchain.utils import get_from_dict_or_env
from pydantic import BaseModel, model_validator

from cdp import MnemonicSeedPhrase, Wallet
from cdp_crewai import __version__
from cdp_crewai.constants import CDP_CREWAI_DEFAULT_SOURCE


class CdpAgentkitWrapper(BaseModel):
"""Wrapper for CDP AgentKit to be used with CrewAI tools."""

wallet: Any = None #: :meta private:
cdp_api_key_name: str | None = None
cdp_api_key_private_key: str | None = None
network_id: str | None = None

@model_validator(mode="before")
@classmethod
def validate_environment(cls, values: dict) -> Any:
"""Validate CDP configuration and initialize SDK."""
cdp_api_key_name = get_from_dict_or_env(values, "cdp_api_key_name", "CDP_API_KEY_NAME")
cdp_api_key_private_key = get_from_dict_or_env(
values, "cdp_api_key_private_key", "CDP_API_KEY_PRIVATE_KEY"
).replace("\\n", "\n")
mnemonic_phrase = get_from_dict_or_env(values, "mnemonic_phrase", "MNEMONIC_PHRASE", "")
network_id = get_from_dict_or_env(values, "network_id", "NETWORK_ID", "base-sepolia")
wallet_data_json = values.get("cdp_wallet_data")

try:
from cdp import Cdp, Wallet, WalletData
except Exception:
raise ImportError(
"CDP SDK is not installed. " "Please install it with `pip install cdp-sdk`"
) from None

Cdp.configure(
api_key_name=cdp_api_key_name,
private_key=cdp_api_key_private_key,
source=CDP_CREWAI_DEFAULT_SOURCE,
source_version=__version__,
)

if wallet_data_json:
wallet_data = WalletData.from_dict(json.loads(wallet_data_json))
wallet = Wallet.import_data(wallet_data)
elif mnemonic_phrase:
phrase = MnemonicSeedPhrase(mnemonic_phrase)
wallet = Wallet.import_wallet(phrase, network_id)
else:
wallet = Wallet.create(network_id=network_id)

values["wallet"] = wallet
values["cdp_api_key_name"] = cdp_api_key_name
values["cdp_api_key_private_key"] = cdp_api_key_private_key
values["mnemonic_phrase"] = mnemonic_phrase
values["network_id"] = network_id

return values

def get_wallet_info(self) -> str:
"""Get information about the current wallet."""
return f"Wallet address: {self.wallet.address}"

def transfer_eth(self, to_address: str, amount: float) -> str:
"""Transfer ETH to a specified address."""
tx = self.wallet.transfer_eth(to_address=to_address, amount=amount)
return f"Transferred {amount} ETH to {to_address}. Transaction hash: {tx.hash}"

def export_wallet(self) -> str:
"""Export wallet data required to re-instantiate the wallet.

Returns:
str: The json string of wallet data including the wallet_id and seed.

"""
wallet_data_dict = self.wallet.export_data().to_dict()
wallet_data_dict["default_address_id"] = self.wallet.default_address.address_id
return json.dumps(wallet_data_dict)

def run_action(self, func: Callable[..., str], **kwargs) -> str:
"""Run a CDP Action."""
func_signature = inspect.signature(func)
first_kwarg = next(iter(func_signature.parameters.values()), None)

if first_kwarg and first_kwarg.annotation is Wallet:
return func(self.wallet, **kwargs)
else:
return func(**kwargs)

# # Add all CDP actions dynamically
# for action in CDP_ACTIONS:
# locals()[action.name] = action.function
Loading
Loading