Skip to content

Commit d543d0e

Browse files
authored
Fix Engine and OpenAI Agents adapter (#3)
* Fix engine tools and headers * Fix openai agents schema fix
1 parent 6f150fc commit d543d0e

File tree

4 files changed

+59
-36
lines changed

4 files changed

+59
-36
lines changed

python/examples/adapter_openai/example.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@
22
import os
33

44
from agents import Agent, Runner
5-
from thirdweb_ai import Insight, Nebula
5+
from thirdweb_ai import Engine, Insight, Nebula
66
from thirdweb_ai.adapters.openai import get_agents_tools
77

88
# Initialize Thirdweb Insight and Nebula with API key
99
insight = Insight(secret_key=os.getenv("THIRDWEB_SECRET_KEY"), chain_id=1)
1010
nebula = Nebula(secret_key=os.getenv("THIRDWEB_SECRET_KEY"))
11+
engine = Engine(
12+
engine_url=os.getenv("THIRDWEB_ENGINE_URL"),
13+
engine_auth_jwt=os.getenv("THIRDWEB_ENGINE_AUTH_JWT"),
14+
backend_wallet_address=os.getenv("THIRDWEB_BACKEND_WALLET_ADDRESS"),
15+
)
1116

1217

1318
async def main():
@@ -17,7 +22,9 @@ async def main():
1722
agent = Agent(
1823
name="Blockchain Assistant",
1924
instructions="You are a helpful blockchain assistant. Use the provided tools to interact with the blockchain.",
20-
tools=get_agents_tools(insight.get_tools() + nebula.get_tools()),
25+
tools=get_agents_tools(
26+
insight.get_tools() + engine.get_tools() + nebula.get_tools()
27+
),
2128
)
2229

2330
# Example queries to demonstrate capabilities

python/thirdweb-ai/src/thirdweb_ai/adapters/openai/agents.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ def _get_openai_schema(schema: Any):
1919
prop.pop("default", None)
2020
_get_openai_schema(prop)
2121

22+
for field in ["anyOf", "oneOf", "allOf"]:
23+
if field in schema:
24+
for subschema in schema[field]:
25+
_get_openai_schema(subschema)
26+
2227
return schema
2328

2429

python/thirdweb-ai/src/thirdweb_ai/services/engine.py

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ def __init__(
1919
self.backend_wallet_address = backend_wallet_address
2020
self.chain_id = str(chain_id) if chain_id else None
2121

22+
def _make_headers(self):
23+
headers = super()._make_headers()
24+
if self.engine_auth_jwt:
25+
headers["Authorization"] = f"Bearer {self.engine_auth_jwt}"
26+
if self.backend_wallet_address:
27+
headers["X-Backend-Wallet-Address"] = self.backend_wallet_address
28+
return headers
29+
2230
@tool(
2331
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."
2432
)
@@ -66,22 +74,22 @@ def get_all_backend_wallet(
6674
)
6775
def get_wallet_balance(
6876
self,
77+
chain_id: Annotated[
78+
str,
79+
"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.",
80+
],
6981
backend_wallet_address: Annotated[
7082
str | None,
7183
"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.",
7284
] = None,
73-
chain_id: Annotated[
74-
str | None,
75-
"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.",
76-
] = None,
7785
) -> dict[str, Any]:
7886
"""Get wallet balance for native or ERC20 tokens."""
7987
chain_id = chain_id or self.chain_id
8088
backend_wallet_address = backend_wallet_address or self.backend_wallet_address
8189
return self._get(f"backend-wallet/{chain_id}/{backend_wallet_address}/get-balance")
8290

8391
@tool(
84-
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."
92+
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."
8593
)
8694
def send_transaction(
8795
self,
@@ -94,28 +102,26 @@ def send_transaction(
94102
"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'.",
95103
],
96104
data: Annotated[
97-
str | None,
105+
str,
98106
"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.",
99-
] = None,
107+
],
108+
chain_id: Annotated[
109+
str,
110+
"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.",
111+
],
100112
backend_wallet_address: Annotated[
101113
str | None,
102114
"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.",
103115
] = None,
104-
chain_id: Annotated[
105-
str | None,
106-
"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.",
107-
] = None,
108116
) -> dict[str, Any]:
109117
"""Send a transaction from a backend wallet."""
110118

111119
payload = {
112-
"to": to_address,
120+
"toAddress": to_address,
113121
"value": value,
122+
"data": data or "0x",
114123
}
115124

116-
if data:
117-
payload["data"] = data
118-
119125
chain_id = chain_id or self.chain_id
120126
backend_wallet_address = backend_wallet_address or self.backend_wallet_address
121127
return self._post(
@@ -135,7 +141,7 @@ def get_transaction_status(
135141
],
136142
) -> dict[str, Any]:
137143
"""Get the status of a transaction by queue ID."""
138-
return self._get(f"transaction/{queue_id}")
144+
return self._get(f"transaction/status/{queue_id}")
139145

140146
@tool(
141147
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."
@@ -151,22 +157,21 @@ def read_contract(
151157
"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.",
152158
],
153159
function_args: Annotated[
154-
list[Any] | None,
160+
list[str | int | bool],
155161
"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.",
156-
] = None,
162+
],
157163
chain_id: Annotated[
158-
str | None,
164+
str,
159165
"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.",
160-
] = None,
166+
],
161167
) -> dict[str, Any]:
162168
"""Read data from a smart contract."""
163169
payload = {
164170
"functionName": function_name,
165171
"args": function_args or [],
166172
}
167-
168173
chain_id = chain_id or self.chain_id
169-
return self._post(f"contract/{chain_id!s}/{contract_address}/read", payload)
174+
return self._get(f"contract/{chain_id!s}/{contract_address}/read", payload)
170175

171176
@tool(
172177
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."
@@ -182,25 +187,25 @@ def write_contract(
182187
"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.",
183188
],
184189
function_args: Annotated[
185-
list[Any] | None,
190+
list[str | int | bool],
186191
"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.",
187-
] = None,
192+
],
188193
value: Annotated[
189-
str | None,
190-
"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.",
191-
] = "0",
194+
str,
195+
"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'.",
196+
],
192197
chain_id: Annotated[
193-
str | None,
198+
str,
194199
"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.",
195-
] = None,
200+
],
196201
) -> dict[str, Any]:
197202
"""Write data to a smart contract."""
198203
payload: dict[str, Any] = {
199204
"functionName": function_name,
200205
"args": function_args or [],
201206
}
202207

203-
if value:
208+
if value and value != "0":
204209
payload["txOverrides"] = {"value": value}
205210

206211
chain_id = chain_id or self.chain_id

python/thirdweb-ai/src/thirdweb_ai/services/service.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,22 @@ def _make_headers(self):
2222
return kwargs
2323

2424
def _get(self, path: str, params: dict[str, Any] | None = None, headers: dict[str, Any] | None = None):
25+
base_url = self.base_url.rstrip("/")
2526
path = path.lstrip("/")
26-
_headers = {**headers, **self._make_headers()} if headers else self._make_headers()
27-
response = self.client.get(f"{self.base_url}/{path}", params=params, headers=_headers)
27+
_headers = self._make_headers()
28+
if headers:
29+
_headers.update(headers)
30+
response = self.client.get(f"{base_url}/{path}", params=params, headers=_headers)
2831
response.raise_for_status()
2932
return response.json()
3033

3134
def _post(self, path: str, data: dict[str, Any] | None = None, headers: dict[str, Any] | None = None):
35+
base_url = self.base_url.rstrip("/")
3236
path = path.lstrip("/")
33-
_headers = {**headers, **self._make_headers()} if headers else self._make_headers()
34-
response = self.client.post(f"{self.base_url}/{path}", json=data, headers=_headers)
37+
_headers = self._make_headers()
38+
if headers:
39+
_headers.update(headers)
40+
response = self.client.post(f"{base_url}/{path}", json=data, headers=_headers)
3541
response.raise_for_status()
3642
return response.json()
3743

0 commit comments

Comments
 (0)