Skip to content

Fix Engine and OpenAI Agents adapter #3

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

Merged
merged 2 commits into from
Mar 20, 2025
Merged
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
11 changes: 9 additions & 2 deletions python/examples/adapter_openai/example.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@
import os

from agents import Agent, Runner
from thirdweb_ai import Insight, Nebula
from thirdweb_ai import Engine, Insight, Nebula
from thirdweb_ai.adapters.openai import get_agents_tools

# Initialize Thirdweb Insight and Nebula with API key
insight = Insight(secret_key=os.getenv("THIRDWEB_SECRET_KEY"), chain_id=1)
nebula = Nebula(secret_key=os.getenv("THIRDWEB_SECRET_KEY"))
engine = Engine(
engine_url=os.getenv("THIRDWEB_ENGINE_URL"),
engine_auth_jwt=os.getenv("THIRDWEB_ENGINE_AUTH_JWT"),
backend_wallet_address=os.getenv("THIRDWEB_BACKEND_WALLET_ADDRESS"),
)


async def main():
Expand All @@ -17,7 +22,9 @@ async def main():
agent = Agent(
name="Blockchain Assistant",
instructions="You are a helpful blockchain assistant. Use the provided tools to interact with the blockchain.",
tools=get_agents_tools(insight.get_tools() + nebula.get_tools()),
tools=get_agents_tools(
insight.get_tools() + engine.get_tools() + nebula.get_tools()
),
)

# Example queries to demonstrate capabilities
Expand Down
5 changes: 5 additions & 0 deletions python/thirdweb-ai/src/thirdweb_ai/adapters/openai/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ def _get_openai_schema(schema: Any):
prop.pop("default", None)
_get_openai_schema(prop)

for field in ["anyOf", "oneOf", "allOf"]:
if field in schema:
for subschema in schema[field]:
_get_openai_schema(subschema)

return schema


Expand Down
65 changes: 35 additions & 30 deletions python/thirdweb-ai/src/thirdweb_ai/services/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ def __init__(
self.backend_wallet_address = backend_wallet_address
self.chain_id = str(chain_id) if chain_id else None

def _make_headers(self):
headers = super()._make_headers()
if self.engine_auth_jwt:
headers["Authorization"] = f"Bearer {self.engine_auth_jwt}"
if self.backend_wallet_address:
headers["X-Backend-Wallet-Address"] = self.backend_wallet_address
return headers

@tool(
description="Create and initialize a new backend wallet controlled by thirdweb Engine. These wallets are securely managed by the Engine service and can be used to sign blockchain transactions, deploy contracts, and execute on-chain operations without managing private keys directly."
)
Expand Down Expand Up @@ -66,22 +74,22 @@ def get_all_backend_wallet(
)
def get_wallet_balance(
self,
chain_id: Annotated[
str,
"The numeric blockchain network ID to query (e.g., '1' for Ethereum mainnet, '137' for Polygon). If not provided, uses the default chain ID configured in the Engine instance.",
],
backend_wallet_address: Annotated[
str | None,
"The Ethereum address of the wallet to check (e.g., '0x1234...'). If not provided, uses the default backend wallet address configured in the Engine instance.",
] = None,
chain_id: Annotated[
str | None,
"The numeric blockchain network ID to query (e.g., '1' for Ethereum mainnet, '137' for Polygon). If not provided, uses the default chain ID configured in the Engine instance.",
] = None,
) -> dict[str, Any]:
"""Get wallet balance for native or ERC20 tokens."""
chain_id = chain_id or self.chain_id
backend_wallet_address = backend_wallet_address or self.backend_wallet_address
return self._get(f"backend-wallet/{chain_id}/{backend_wallet_address}/get-balance")

@tool(
description="Send an on-chain transaction from a backend wallet. This powerful function can transfer native currency (ETH, MATIC), ERC20 tokens, or execute any arbitrary contract interaction. The transaction is signed and broadcast to the blockchain automatically by the Engine service."
description="Send an on-chain transaction. This powerful function can transfer native currency (ETH, MATIC), ERC20 tokens, or execute any arbitrary contract interaction. The transaction is signed and broadcast to the blockchain automatically."
)
def send_transaction(
self,
Expand All @@ -94,28 +102,26 @@ def send_transaction(
"The amount of native currency to send, specified in wei (e.g., '1000000000000000000' for 1 ETH). For token transfers or contract interactions that don't need to send value, use '0'.",
],
data: Annotated[
str | None,
str,
"The hexadecimal transaction data payload for contract interactions (e.g., '0x23b872dd...'). For simple native currency transfers, leave this empty. For ERC20 transfers or contract calls, this contains the ABI-encoded function call.",
] = None,
],
chain_id: Annotated[
str,
"The numeric blockchain network ID to send the transaction on (e.g., '1' for Ethereum mainnet, '137' for Polygon). If not provided, uses the default chain ID configured in the Engine instance.",
],
backend_wallet_address: Annotated[
str | None,
"The sender wallet address to use (must be a wallet created through Engine). If not provided, uses the default backend wallet configured in the Engine instance.",
] = None,
chain_id: Annotated[
str | None,
"The numeric blockchain network ID to send the transaction on (e.g., '1' for Ethereum mainnet, '137' for Polygon). If not provided, uses the default chain ID configured in the Engine instance.",
] = None,
) -> dict[str, Any]:
"""Send a transaction from a backend wallet."""

payload = {
"to": to_address,
"toAddress": to_address,
"value": value,
"data": data or "0x",
}

if data:
payload["data"] = data

chain_id = chain_id or self.chain_id
backend_wallet_address = backend_wallet_address or self.backend_wallet_address
return self._post(
Expand All @@ -135,7 +141,7 @@ def get_transaction_status(
],
) -> dict[str, Any]:
"""Get the status of a transaction by queue ID."""
return self._get(f"transaction/{queue_id}")
return self._get(f"transaction/status/{queue_id}")

@tool(
description="Call a read-only function on a smart contract to query its current state without modifying the blockchain or spending gas. Perfect for retrieving information like token balances, contract configuration, or any view/pure functions from Solidity contracts."
Expand All @@ -151,22 +157,21 @@ def read_contract(
"The exact name of the function to call on the contract (e.g., 'balanceOf', 'totalSupply'). Must match the function name in the contract's ABI exactly, including correct capitalization.",
],
function_args: Annotated[
list[Any] | None,
list[str | int | bool],
"An ordered list of arguments to pass to the function (e.g., [address, tokenId]). Must match the types and order expected by the function. For functions with no parameters, use an empty list or None.",
] = None,
],
chain_id: Annotated[
str | None,
str,
"The numeric blockchain network ID where the contract is deployed (e.g., '1' for Ethereum mainnet, '137' for Polygon). If not provided, uses the default chain ID configured in the Engine instance.",
] = None,
],
) -> dict[str, Any]:
"""Read data from a smart contract."""
payload = {
"functionName": function_name,
"args": function_args or [],
}

chain_id = chain_id or self.chain_id
return self._post(f"contract/{chain_id!s}/{contract_address}/read", payload)
return self._get(f"contract/{chain_id!s}/{contract_address}/read", payload)

@tool(
description="Execute a state-changing function on a smart contract by sending a transaction. This allows you to modify on-chain data, such as transferring tokens, minting NFTs, or updating contract configuration. The transaction is automatically signed by your backend wallet and submitted to the blockchain."
Expand All @@ -182,25 +187,25 @@ def write_contract(
"The exact name of the function to call on the contract (e.g., 'mint', 'transfer', 'setApprovalForAll'). Must match the function name in the contract's ABI exactly, including correct capitalization.",
],
function_args: Annotated[
list[Any] | None,
list[str | int | bool],
"An ordered list of arguments to pass to the function (e.g., ['0x1234...', 5] for transferring 5 tokens to address '0x1234...'). Must match the types and order expected by the function. For functions with no parameters, use an empty list.",
] = None,
],
value: Annotated[
str | None,
"The amount of native currency (ETH, MATIC, etc.) to send with the transaction, in wei (e.g., '1000000000000000000' for 1 ETH). Required for payable functions, use '0' for non-payable functions.",
] = "0",
str,
"The amount of native currency (ETH, MATIC, etc.) to send with the transaction, in wei (e.g., '1000000000000000000' for 1 ETH). Required for payable functions, use '0' for non-payable functions. Default to '0'.",
],
chain_id: Annotated[
str | None,
str,
"The numeric blockchain network ID where the contract is deployed (e.g., '1' for Ethereum mainnet, '137' for Polygon). If not provided, uses the default chain ID configured in the Engine instance.",
] = None,
],
) -> dict[str, Any]:
"""Write data to a smart contract."""
payload: dict[str, Any] = {
"functionName": function_name,
"args": function_args or [],
}

if value:
if value and value != "0":
payload["txOverrides"] = {"value": value}

chain_id = chain_id or self.chain_id
Expand Down
14 changes: 10 additions & 4 deletions python/thirdweb-ai/src/thirdweb_ai/services/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,22 @@ def _make_headers(self):
return kwargs

def _get(self, path: str, params: dict[str, Any] | None = None, headers: dict[str, Any] | None = None):
base_url = self.base_url.rstrip("/")
path = path.lstrip("/")
_headers = {**headers, **self._make_headers()} if headers else self._make_headers()
response = self.client.get(f"{self.base_url}/{path}", params=params, headers=_headers)
_headers = self._make_headers()
if headers:
_headers.update(headers)
response = self.client.get(f"{base_url}/{path}", params=params, headers=_headers)
response.raise_for_status()
return response.json()

def _post(self, path: str, data: dict[str, Any] | None = None, headers: dict[str, Any] | None = None):
base_url = self.base_url.rstrip("/")
path = path.lstrip("/")
_headers = {**headers, **self._make_headers()} if headers else self._make_headers()
response = self.client.post(f"{self.base_url}/{path}", json=data, headers=_headers)
_headers = self._make_headers()
if headers:
_headers.update(headers)
response = self.client.post(f"{base_url}/{path}", json=data, headers=_headers)
response.raise_for_status()
return response.json()

Expand Down