Skip to content

Commit 7d09b32

Browse files
authored
add get accounts from cloud + lint (#39)
* add linting CI * lint with ruff and fix types * lint both projects * use uv workspaces to handle deps * uv.lock using dev deps * update pyright command * fix types
1 parent a58d05a commit 7d09b32

File tree

14 files changed

+9861
-43
lines changed

14 files changed

+9861
-43
lines changed

.github/workflows/lint.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: Linter
2+
3+
on: [push, pull_request]
4+
5+
env:
6+
PYTHON_VERSION: "3.12"
7+
UV_VERSION: "0.7.8"
8+
UV_PROJECT_ENVIRONMENT: .venv
9+
RUFF_VERSION: "0.11.2"
10+
11+
concurrency:
12+
group: ${{ github.workflow }}-${{ github.ref }}
13+
cancel-in-progress: true
14+
15+
jobs:
16+
check:
17+
strategy:
18+
fail-fast: true
19+
matrix:
20+
project:
21+
- thirdweb-ai
22+
- thirdweb-mcp
23+
24+
name: Linter (${{ matrix.project }})
25+
runs-on: ubuntu-latest
26+
defaults:
27+
run:
28+
working-directory: ./python/${{ matrix.project }}
29+
steps:
30+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
31+
32+
- name: Install uv
33+
uses: astral-sh/setup-uv@22695119d769bdb6f7032ad67b9bca0ef8c4a174 # v5
34+
35+
- name: Install Python
36+
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
37+
with:
38+
cache: true
39+
cache-dependency-path: "python/${{ matrix.project }}/uv.lock" # Update cache if uv.lock changes
40+
41+
- name: Install the project
42+
run: uv sync --group dev --all-extras
43+
44+
- name: Configure path
45+
run: echo "$PWD/.venv/bin" >> $GITHUB_PATH
46+
47+
- name: Run Ruff linter
48+
run: uv run ruff check --output-format=github .
49+
50+
- name: Run Ruff formatter
51+
run: uv run ruff format --check .
52+
53+
- name: Run Pyright check
54+
run: uv run pyright src/

python/pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[tool.uv.workspace]
2+
members = ["thirdweb-ai", "thirdweb-mcp"]

python/thirdweb-ai/pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ dev = [
4949
"pytest-mock>=3.12.0,<4",
5050
"pytest-cov>=4.1.0,<5",
5151
"ipython>=8.34.0",
52+
"thirdweb-mcp",
5253
]
5354

5455
[tool.hatch.build.targets.sdist]
@@ -88,6 +89,9 @@ vcs = "git"
8889
style = "semver"
8990
format = "{base}"
9091

92+
[tool.uv.sources]
93+
thirdweb-mcp = { workspace = true }
94+
9195
[tool.pyright]
9296
include = ["src"]
9397
typeCheckingMode = "strict"

python/thirdweb-ai/src/thirdweb_ai/adapters/google_adk/google_adk.py

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1+
import copy
12
from typing import Any
23

34
from google.adk.tools import BaseTool, ToolContext
45
from google.genai import types
56

67
from thirdweb_ai.tools.tool import Tool, ToolSchema
78

8-
from pydantic import BaseModel
99

1010
class GoogleAdkTool(BaseTool):
1111
"""Adapter for Thirdweb Tool to Google ADK BaseTool.
@@ -40,38 +40,34 @@ def _get_declaration(self) -> types.FunctionDeclaration:
4040
Returns:
4141
A FunctionDeclaration for Google ADK
4242
"""
43-
# Deep copy the parameters to avoid modifying the original
44-
import copy
45-
parameters = copy.deepcopy(self.tool.schema["parameters"])
46-
47-
if "additionalProperties" in parameters:
48-
del parameters["additionalProperties"]
49-
50-
def remove_additional_properties(obj: dict[str, Any]):
51-
if "additionalProperties" in obj:
52-
del obj["additionalProperties"]
53-
54-
if "items" in obj and isinstance(obj["items"], dict):
55-
remove_additional_properties(obj["items"])
56-
57-
if "properties" in obj and isinstance(obj["properties"], dict):
58-
for prop in obj["properties"].values():
59-
if isinstance(prop, dict):
60-
remove_additional_properties(prop)
61-
62-
if "properties" in parameters:
63-
for prop in parameters["properties"].values():
64-
remove_additional_properties(prop)
65-
43+
44+
if "parameters" not in self.tool.schema:
45+
raise ValueError("Tool schema must contain 'parameters'.")
46+
47+
# Create a clean parameters dict without additionalProperties
48+
parameters = copy.deepcopy(dict(self.tool.schema["parameters"]))
49+
50+
# Remove additionalProperties recursively from the entire schema
51+
def clean_schema(obj: dict[str, Any]) -> dict[str, Any]:
52+
cleaned = {k: v for k, v in obj.items() if k != "additionalProperties"}
53+
54+
for key, value in cleaned.items():
55+
if isinstance(value, dict):
56+
cleaned[key] = clean_schema(value)
57+
elif isinstance(value, list):
58+
cleaned[key] = [clean_schema(item) if isinstance(item, dict) else item for item in value]
59+
60+
return cleaned
61+
62+
clean_parameters = clean_schema(parameters)
63+
6664
return types.FunctionDeclaration(
6765
name=self.name,
6866
description=self.description,
69-
parameters=parameters,
67+
parameters=types.Schema(**clean_parameters),
7068
)
7169

72-
# Override the method with the expected signature based on the error message
73-
# and adapting from the reference implementation
74-
async def run_async(self, args: dict[str, Any], tool_context: ToolContext) -> Any:
70+
async def run_async(self, *, args: dict[str, Any], tool_context: ToolContext) -> Any:
7571
"""Execute the tool asynchronously.
7672
7773
This method adapts the Thirdweb tool to work with Google ADK's async execution.

python/thirdweb-ai/src/thirdweb_ai/adapters/mcp/mcp.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Any, cast
2+
13
import mcp.types as types
24
from mcp.server.fastmcp import FastMCP
35
from mcp.server.fastmcp.tools.base import Tool as FastMCPTool
@@ -11,26 +13,27 @@ def get_fastmcp_tools(tools: list[Tool]) -> list[FastMCPTool]:
1113
fn=lambda _t=tool, **kwargs: _t.run_json(kwargs),
1214
name=tool.name,
1315
description=tool.description,
14-
parameters=tool.schema.get("parameters"),
16+
parameters=cast(dict[str, Any], tool.schema.get("parameters") or {}),
1517
fn_metadata=func_metadata(tool._func_definition, skip_names=["self"]), # noqa: SLF001
1618
is_async=False,
1719
context_kwarg=None,
20+
annotations=tool.annotations,
1821
)
1922
for tool in tools
2023
]
2124

2225

2326
def add_fastmcp_tools(fastmcp: FastMCP, tools: list[Tool]):
2427
for tool in get_fastmcp_tools(tools):
25-
fastmcp._tool_manager._tools[tool.name] = tool # noqa: SLF001
28+
fastmcp._tool_manager._tools[tool.name] = tool # type: ignore[reportPrivateUsage] # noqa: SLF001
2629

2730

2831
def get_mcp_tools(tools: list[Tool]) -> list[types.Tool]:
2932
return [
3033
types.Tool(
3134
name=tool.name,
3235
description=tool.description,
33-
inputSchema=tool.schema.get("parameters"),
36+
inputSchema=cast(dict[str, Any], tool.schema.get("parameters") or {}),
3437
)
3538
for tool in tools
3639
]

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,15 @@ def _get_openai_schema(schema: Any):
2929

3030
def get_agents_tools(tools: list[Tool]):
3131
def _get_tool(tool: Tool):
32-
async def _invoke_tool(ctx: RunContextWrapper[Any], tool_input: str, _t: Tool = tool) -> str: # type: ignore # noqa: PGH003
32+
async def _invoke_tool(ctx: RunContextWrapper[Any], tool_input: str, _t: Tool = tool) -> str:
3333
input_args = json.loads(tool_input) if tool_input else {}
3434
return _t.return_value_as_string(_t.run_json(input_args))
3535

3636
schema = _get_openai_schema(tool.schema.get("parameters"))
37-
return FunctionTool( # type: ignore # noqa: PGH003
37+
return FunctionTool(
3838
name=tool.name,
3939
description=tool.description,
40-
params_json_schema=schema,
40+
params_json_schema=schema or {},
4141
on_invoke_tool=_invoke_tool,
4242
)
4343

python/thirdweb-ai/src/thirdweb_ai/adapters/pydantic_ai/pydantic_ai.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ async def execute(**kwargs: Any) -> Any:
1212
return tool.run_json(kwargs)
1313

1414
async def prepare(ctx: RunContext, tool_def: ToolDefinition) -> ToolDefinition:
15+
if "parameters" not in tool.schema:
16+
raise ValueError("Tool schema must contain 'parameters'.")
1517
tool_def.parameters_json_schema = tool.schema["parameters"]
1618
return tool_def
1719

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55
from thirdweb_ai.services.service import Service
66
from thirdweb_ai.services.storage import Storage
77

8-
__all__ = ["Engine", "EngineCloud", "Insight", "Nebula", "Service", "Storage"]
8+
__all__ = ["Engine", "EngineCloud", "Insight", "Nebula", "Service", "Storage"]

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,3 +218,14 @@ def search_transactions(
218218
}
219219

220220
return self._post("transactions/search", payload)
221+
222+
@tool(
223+
description="List all engine server wallets for the current project. Returns an array of EOA addresses with their corresponding predicted smart account addresses."
224+
)
225+
def get_accounts(self) -> dict[str, Any]:
226+
"""Get all engine server wallets for the current project.
227+
228+
Returns:
229+
dict containing list of account objects with address and smartAccountAddress
230+
"""
231+
return self._get("accounts")

python/thirdweb-ai/src/thirdweb_ai/tools/tool.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def args_base_model_from_signature(name: str, sig: inspect.Signature) -> type[Ba
7272
fields: dict[str, tuple[type[Any], Any]] = {}
7373
for param_name, param in sig.parameters.items():
7474
# This is handled externally
75-
if param_name == "cancellation_token" or param_name == "self":
75+
if param_name in ["cancellation_token", "self"]:
7676
continue
7777

7878
if param.annotation is inspect.Parameter.empty:
@@ -196,10 +196,7 @@ def return_type(self) -> type[Any]:
196196
return self._return_type
197197

198198
def return_value_as_string(self, value: Any) -> str:
199-
if isinstance(value, BaseModel):
200-
return value.model_dump_json()
201-
202-
return str(value)
199+
return value.model_dump_json() if isinstance(value, BaseModel) else str(value)
203200

204201
@abstractmethod
205202
def run(self, args: ArgsT | None = None) -> ReturnT: ...
@@ -225,7 +222,7 @@ def __init__(
225222
if isinstance(func_definition, functools.partial)
226223
else name or func_definition.__name__
227224
)
228-
args_model = args_base_model_from_signature(func_name + "args", self._signature)
225+
args_model = args_base_model_from_signature(f"{func_name}args", self._signature)
229226
return_type = self._signature.return_annotation
230227
super().__init__(args_model, return_type, func_name, description, strict)
231228

0 commit comments

Comments
 (0)