Skip to content

Commit

Permalink
Initial python draft
Browse files Browse the repository at this point in the history
1. core types
2. web3.py wallet client
3. langchain tool adapter
4. langchain evm web3.py example
  • Loading branch information
i11 committed Dec 12, 2024
1 parent 1abfe45 commit b1e0a3f
Show file tree
Hide file tree
Showing 31 changed files with 5,221 additions and 0 deletions.
49 changes: 49 additions & 0 deletions python/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Environment variables
.env
.env.local
.env.*.local
.venv
venv/
9 changes: 9 additions & 0 deletions python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Goat 🐐 - Python

[Docs](https://ohmygoat.dev) | [Examples](https://github.com/goat-sdk/goat/tree/main/typescript/examples) | [Discord](https://discord.gg/goat-sdk)

## Development

1. Clone the repo
2. Install the dependencies: `poetry install`
3. Build the packages: `poetry build`
6 changes: 6 additions & 0 deletions python/examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Examples

## Langchain

### EVM
- [web3.py](https://github.com/goat-sdk/goat/tree/main/python/examples/langchain/web3)
3 changes: 3 additions & 0 deletions python/examples/langchain/web3/.env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
OPENAI_API_KEY=
WALLET_PRIVATE_KEY=
RPC_PROVIDER_URL=
15 changes: 15 additions & 0 deletions python/examples/langchain/web3/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Langchain with web3.py Example

## Setup

Copy the `.env.template` and populate with your values.

```
cp .env.template .env
```

## Usage

```
poetry run python example.py
```
52 changes: 52 additions & 0 deletions python/examples/langchain/web3/example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import asyncio
import os
import logging
from langchain_openai import OpenAI
from langchain import hub as prompts
from langchain.agents import AgentExecutor, create_structured_chat_agent
from web3 import Web3

from goat_sdk.adapters.langchain import getOnChainTools
from goat_sdk.wallets.web3 import Web3EVMWalletClient

logging.basicConfig(
format="%(asctime)s %(levelname)s: %(message)s", level=logging.DEBUG
)


async def main():
logger = logging.getLogger(__name__)
chain = "sepolia"
prompt = prompts.pull("hwchase17/structured-chat-agent")
llm = OpenAI(model="gpt-4o-mini")

w3 = Web3(
Web3.HTTPProvider(
# TODO: Export RPC_PROVIDER_URL
"https://eth-%s.g.alchemy.com/v2/%s"
% (chain, os.environ["ALCHEMY_API_KEY"])
)
)

walletClient = Web3EVMWalletClient(w3)

tools = await getOnChainTools(wallet=walletClient, plugins=[])
logger.debug("Tools: %s", tools)
agent = create_structured_chat_agent(llm, tools, prompt)
logger.debug("Agent: %s", agent)

logger.debug("Create an agent executor by passing in the agent and tools")
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=True,
handle_parsing_errors=True,
)

response = agent_executor.invoke({"input": "Get my balance in USDC"})
logger.info("Agent response: %s", response)


if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
2,575 changes: 2,575 additions & 0 deletions python/examples/langchain/web3/poetry.lock

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions python/examples/langchain/web3/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[tool.poetry]
name = "goat-examples-langchain-web3"
version = "0.1.0"
description = ""
authors = ["Agustin Armellini Fischer <[email protected]>"]
license = "MIT"
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.13"
langchain-openai = "^0.2.11"
langchain = "^0.3.10"
goat_sdk = {path = "../../../"}
pydantic = "^2.10.3"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
Empty file added python/goat_sdk/__init__.py
Empty file.
Empty file.
8 changes: 8 additions & 0 deletions python/goat_sdk/adapters/langchain/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from goat_sdk.adapters.langchain.tools import (
getOnChainTools,
)


__all__ = [
"getOnChainTools"
]
22 changes: 22 additions & 0 deletions python/goat_sdk/adapters/langchain/tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from typing import List, Optional, Union
from goat_sdk.core.plugins import Plugin
from goat_sdk.core import get_tools
from goat_sdk.core.wallets import WalletClient
from langchain_core.tools import Tool


async def getOnChainTools[TWalletClient: WalletClient](
wallet: TWalletClient,
plugins: Optional[List[Union[Plugin[TWalletClient], Plugin[WalletClient]]]] = [],
wordForTool: Optional[str] = "",
):
tools = await get_tools(wallet, plugins)
return [
Tool(
name=t.name,
description=t.description,
func=t.method,
args_schema=t.parameters,
)
for t in tools
]
8 changes: 8 additions & 0 deletions python/goat_sdk/core/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from goat_sdk.core.core import (
get_tools,
)


__all__ = [
"get_tools"
]
85 changes: 85 additions & 0 deletions python/goat_sdk/core/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import logging
import re
from typing import List, Optional, Union

from goat_sdk.core.plugins import Plugin
from goat_sdk.core.tools import DeferredTool
from goat_sdk.core.wallets import WalletClient, Chain, is_evm_chain, is_solana_chain
from goat_sdk.core.evm import deferredEVMCoreTools


async def filter_plugin_tools[TWalletClient: WalletClient](
plugin: Plugin[TWalletClient],
chain: Chain,
supportsSmartWallets: Optional[bool] = False,
) -> List[DeferredTool[TWalletClient]]:
if not plugin.supports_chain(chain):
logging.warning(
"Plugin %s does not support chain %s. Skipping.", plugin.name, chain.type
)
return []
if supportsSmartWallets and not plugin.supports_smart_wallets():
logging.warning(
"Plugin %s does not support smart wallets. Skipping.", plugin.name
)
return []
tools = await plugin.get_tools(chain)

if len(tools) == 0:
logging.warning("Plugin %s returned no tools. Skipping.", plugin.name)

return tools


def replace_tool_placeholder(template: str, wordForTool: Optional[str] = "tool"):
"""
Replace the placeholder '{{ tool }}' in the template with the provided word.
:param template: The input template string with the placeholder.
:param word_for_tool: The word to replace the placeholder with. Defaults to "tool".
:return: The template with the placeholder replaced by the word_for_tool.
"""
# Use a regular expression to find and replace the placeholder '{{ tool }}'
placeholder_regex = r"\{\{\s*tool\s*\}\}"
return re.sub(placeholder_regex, wordForTool, template)


async def get_deferred_tools[TWalletClient: WalletClient](
chain: Chain,
plugins: Optional[List[Union[Plugin[TWalletClient], Plugin[WalletClient]]]] = [],
supportsSmartWallets: Optional[bool] = False,
wordForTool: Optional[str] = "",
) -> List[DeferredTool[TWalletClient]]:
tools: List[DeferredTool[TWalletClient]] = []
if is_evm_chain(chain):
tools += deferredEVMCoreTools
elif is_solana_chain(chain):
pass
else:
raise ValueError("Unsupported chain type: %s" % chain.type)

for plugin in plugins:
tools += await filter_plugin_tools(plugin, chain, supportsSmartWallets)

for tool in tools:
tool.description = replace_tool_placeholder(tool.description, wordForTool)

return tools


async def get_tools[TWalletClient: WalletClient](
wallet: TWalletClient,
plugins: Optional[List[Union[Plugin[TWalletClient], Plugin[WalletClient]]]] = [],
wordForTool: Optional[str] = "",
) -> List[DeferredTool[TWalletClient]]:
chain = wallet.get_chain()
tools = await get_deferred_tools(
chain,
plugins,
# TODO: isEVMSmartWalletClient(wallet)
supportsSmartWallets=False,
wordForTool=wordForTool,
)
for tool in tools:
tool.method = lambda parameters: tool.method(wallet, parameters)
return tools
8 changes: 8 additions & 0 deletions python/goat_sdk/core/evm/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from goat_sdk.core.evm.tools import (
deferredEVMCoreTools,
)


__all__ = [
"deferredEVMCoreTools",
]
19 changes: 19 additions & 0 deletions python/goat_sdk/core/evm/methods.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from typing import Dict, Optional

from goat_sdk.core.wallets import EVMWalletClient


def get_address(walletClient: EVMWalletClient, params: Dict) -> str:
return walletClient.get_address()


async def get_balance(
walletClient: EVMWalletClient, params: Optional[Dict] = {}
) -> str:
try:
addr = params["address"] if "address" in params else get_address(walletClient)
resAddr = await walletClient.resolve_address(addr)
raw_balance = await walletClient.balance_of(resAddr)
return str(raw_balance.value // raw_balance.decimals)
except Exception as e:
raise ("Failed to fetch balance", e)
12 changes: 12 additions & 0 deletions python/goat_sdk/core/evm/parameters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from typing import Optional
from pydantic import BaseModel, Field


class GetETHBalanceParametersSchema(BaseModel):
address: Optional[str] = Field(
default="",
description="The address to get the balance of, defaults to the address of the wallet",
)

class GetAddressParametersSchema(BaseModel):
pass
30 changes: 30 additions & 0 deletions python/goat_sdk/core/evm/tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from typing import List

from goat_sdk.core.evm.methods import get_address, get_balance
from goat_sdk.core.evm.parameters import GetAddressParametersSchema, GetETHBalanceParametersSchema
from goat_sdk.core.tools import DeferredTool
from goat_sdk.core.wallets import EVMWalletClient


deferredEVMCoreTools: List[DeferredTool[EVMWalletClient]] = [
DeferredTool[EVMWalletClient](
**dict(
{
"name": "get_address",
"description": "This {{tool}} returns the address of the EVM wallet.",
"parameters": GetAddressParametersSchema,
"method": get_address,
}
)
),
DeferredTool[EVMWalletClient](
**dict(
{
"name": "get_eth_balance",
"description": "This {{tool}} returns the ETH balance of an EVM wallet.",
"parameters": GetETHBalanceParametersSchema,
"method": get_balance,
}
)
),
]
8 changes: 8 additions & 0 deletions python/goat_sdk/core/plugins/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from goat_sdk.core.plugins.plugins import (
Plugin,
)


__all__ = [
"Plugin",
]
Loading

0 comments on commit b1e0a3f

Please sign in to comment.