Skip to content

Add pytest #17

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

Open
wants to merge 28 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
44 changes: 44 additions & 0 deletions .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Python Tests

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Install uv
uses: astral-sh/setup-uv@22695119d769bdb6f7032ad67b9bca0ef8c4a174 # v5

- name: Install Python
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
with:
enable-cache: true
cache-dependency-glob: "uv.lock" # Update cache if uv.lock changes

- name: Install the project
run: |
cd python/thirdweb-ai
uv sync --all-extras --dev

- name: Test with pytest
env:
__THIRDWEB_SECRET_KEY_DEV: ${{ secrets.__THIRDWEB_SECRET_KEY_DEV }}
run: |
cd python/thirdweb-ai
uv run pytest tests --cov=thirdweb_ai --cov-report=xml

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
fail_ci_if_error: true
13 changes: 13 additions & 0 deletions python/thirdweb-ai/pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*

# Add markers if needed
markers =
unit: Unit tests
integration: Integration tests

# Configure verbosity and coverage
addopts = -v --cov=src/thirdweb_ai --cov-report=term --cov-report=html
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .coinbase_agentkit import thirdweb_action_provider
from .coinbase_agentkit import ThirdwebActionProvider, thirdweb_action_provider

__all__ = ["thirdweb_action_provider"]
__all__ = ["ThirdwebActionProvider", "thirdweb_action_provider"]
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .agents import get_agents_tools
from .agents import get_agents_tools as get_openai_tools

__all__ = ["get_agents_tools"]
__all__ = ["get_openai_tools"]
6 changes: 6 additions & 0 deletions python/thirdweb-ai/src/thirdweb_ai/common/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import importlib.util
import re
from typing import Any


def has_module(module_name: str) -> bool:
"""Check if module is available."""
return importlib.util.find_spec(module_name) is not None


def extract_digits(value: int | str) -> int:
value_str = str(value).strip("\"'")
digit_match = re.search(r"\d+", value_str)
Expand Down
1 change: 1 addition & 0 deletions python/thirdweb-ai/tests/adapters/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

27 changes: 27 additions & 0 deletions python/thirdweb-ai/tests/adapters/test_autogen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import pytest

from thirdweb_ai.tools.tool import Tool


def test_get_autogen_tools(test_tools: list[Tool]):
"""Test converting thirdweb tools to AutoGen tools."""
pytest.importorskip("autogen_core")
from autogen_core.tools import BaseTool as AutogenBaseTool # type: ignore[import]

from thirdweb_ai.adapters.autogen import get_autogen_tools

# Convert tools to AutoGen tools
autogen_tools = get_autogen_tools(test_tools)

# Assert we got the correct number of tools
assert len(autogen_tools) == len(test_tools)

# Check all tools were properly converted
assert all(isinstance(tool, AutogenBaseTool) for tool in autogen_tools)

# Check properties were preserved
assert [tool.name for tool in autogen_tools] == [tool.name for tool in test_tools]
assert [tool.description for tool in autogen_tools] == [tool.description for tool in test_tools]

# Check all tools have a run method
assert all(callable(getattr(tool, "run", None)) for tool in autogen_tools)
49 changes: 49 additions & 0 deletions python/thirdweb-ai/tests/adapters/test_coinbase_agentkit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import pytest
from coinbase_agentkit import (
EthAccountWalletProvider,
EthAccountWalletProviderConfig,
)
from eth_account import Account

from thirdweb_ai.tools.tool import Tool


def test_get_coinbase_agentkit_tools(test_tools: list[Tool]):
"""Test converting thirdweb tools to Coinbase AgentKit tools."""
pytest.importorskip("coinbase_agentkit")
from coinbase_agentkit import ActionProvider # type: ignore[import]

from thirdweb_ai.adapters.coinbase_agentkit import ThirdwebActionProvider, thirdweb_action_provider

# Convert tools to Coinbase AgentKit provider
provider = thirdweb_action_provider(test_tools)

# Check provider was created with the right type
assert isinstance(provider, ThirdwebActionProvider)
assert isinstance(provider, ActionProvider)

# Check provider name
assert provider.name == "thirdweb"

account = Account.create()
# Initialize Ethereum Account Wallet Provider
wallet_provider = EthAccountWalletProvider(
config=EthAccountWalletProviderConfig(
account=account,
chain_id="8453", # Base mainnet
rpc_url="https://8453.rpc.thirdweb.com",
)
)
actions = provider.get_actions(wallet_provider=wallet_provider)
# Check provider has the expected number of actions
assert len(actions) == len(test_tools)

# Check properties were preserved by getting actions and checking names/descriptions
assert [action.name for action in actions] == [tool.name for tool in test_tools]
assert [action.description for action in actions] == [tool.description for tool in test_tools]

# Verify that args_schema is set correctly
assert [action.args_schema for action in actions] == [tool.args_type() for tool in test_tools]

# Check all actions have callable invoke functions
assert all(callable(action.invoke) for action in actions)
29 changes: 29 additions & 0 deletions python/thirdweb-ai/tests/adapters/test_goat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import pytest

from thirdweb_ai.tools.tool import Tool


def test_get_goat_tools(test_tools: list[Tool]):
"""Test converting thirdweb tools to GOAT tools."""
# Skip this test if module not fully installed
pytest.importorskip("goat.tools")

from goat.tools import BaseTool as GoatBaseTool # type: ignore[import]

from thirdweb_ai.adapters.goat import get_goat_tools

# Convert tools to GOAT tools
goat_tools = get_goat_tools(test_tools)

# Assert we got the correct number of tools
assert len(goat_tools) == len(test_tools)

# Check all tools were properly converted
assert all(isinstance(tool, GoatBaseTool) for tool in goat_tools)

# Check properties were preserved
assert [tool.name for tool in goat_tools] == [tool.name for tool in test_tools]
assert [tool.description for tool in goat_tools] == [tool.description for tool in test_tools]

# Check all tools have a callable run method
assert all(callable(getattr(tool, "run", None)) for tool in goat_tools)
30 changes: 30 additions & 0 deletions python/thirdweb-ai/tests/adapters/test_langchain.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import pytest

from thirdweb_ai.tools.tool import Tool


def test_get_langchain_tools(test_tools: list[Tool]):
"""Test converting thirdweb tools to LangChain tools."""
pytest.importorskip("langchain_core")
from langchain_core.tools.structured import StructuredTool # type: ignore[import]

from thirdweb_ai.adapters.langchain import get_langchain_tools

# Convert tools to LangChain tools
langchain_tools = get_langchain_tools(test_tools)

# Assert we got the correct number of tools
assert len(langchain_tools) == len(test_tools)

# Check all tools were properly converted
assert all(isinstance(tool, StructuredTool) for tool in langchain_tools)

# Check properties were preserved
assert [tool.name for tool in langchain_tools] == [tool.name for tool in test_tools]
assert [tool.description for tool in langchain_tools] == [tool.description for tool in test_tools]

# Check schemas were preserved
assert [tool.args_schema for tool in langchain_tools] == [tool.args_type() for tool in test_tools]

# Check all tools have callable run methods
assert all(callable(getattr(tool, "func", None)) for tool in langchain_tools)
28 changes: 28 additions & 0 deletions python/thirdweb-ai/tests/adapters/test_llama_index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import pytest

from thirdweb_ai.tools.tool import Tool


def test_get_llama_index_tools(test_tools: list[Tool]):
"""Test converting thirdweb tools to LlamaIndex tools."""
pytest.importorskip("llama_index")
from llama_index.core.tools import FunctionTool # type: ignore[import]

from thirdweb_ai.adapters.llama_index import get_llama_index_tools

# Convert tools to LlamaIndex tools
llama_tools = get_llama_index_tools(test_tools)

# Assert we got the correct number of tools
assert len(llama_tools) == len(test_tools)

# Check all tools were properly converted
assert all(isinstance(tool, FunctionTool) for tool in llama_tools)

# Check properties were preserved
assert [tool.metadata.name for tool in llama_tools] == [tool.name for tool in test_tools]
assert [tool.metadata.description for tool in llama_tools] == [tool.description for tool in test_tools]
assert [tool.metadata.fn_schema for tool in llama_tools] == [tool.args_type() for tool in test_tools]

# Check all tools are callable
assert all(callable(tool) for tool in llama_tools)
29 changes: 29 additions & 0 deletions python/thirdweb-ai/tests/adapters/test_mcp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import pytest

from thirdweb_ai.tools.tool import Tool


def test_get_mcp_tools(test_tools: list[Tool]):
"""Test converting thirdweb tools to MCP tools."""
pytest.importorskip("mcp.types")

import mcp.types as mcp_types # type: ignore[import]

from thirdweb_ai.adapters.mcp import get_mcp_tools

# Convert tools to MCP tools
mcp_tools = get_mcp_tools(test_tools)

# Assert we got the correct number of tools
assert len(mcp_tools) == len(test_tools)

# Check all tools were properly converted
assert all(isinstance(tool, mcp_types.Tool) for tool in mcp_tools)

# Check properties were preserved
assert [tool.name for tool in mcp_tools] == [tool.name for tool in test_tools]
assert [tool.description for tool in mcp_tools] == [tool.description for tool in test_tools]

# Check that input schemas were set correctly
for i, tool in enumerate(mcp_tools):
assert tool.inputSchema == test_tools[i].schema.get("parameters")
28 changes: 28 additions & 0 deletions python/thirdweb-ai/tests/adapters/test_openai.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import pytest

from thirdweb_ai.tools.tool import Tool


def test_get_openai_tools(test_tools: list[Tool]):
"""Test converting thirdweb tools to OpenAI tools."""
pytest.importorskip("openai")
from agents import FunctionTool

from thirdweb_ai.adapters.openai import get_openai_tools

# Convert tools to OpenAI tools
openai_tools = get_openai_tools(test_tools)

# Assert we got the correct number of tools
assert len(openai_tools) == len(test_tools)

# Check all required properties exist in the tools
for i, tool in enumerate(openai_tools):
assert isinstance(tool, FunctionTool)
assert hasattr(tool, "name")
assert hasattr(tool, "description")
assert hasattr(tool, "params_json_schema")

# Check name and description match
assert tool.name == test_tools[i].name
assert tool.description == test_tools[i].description
28 changes: 28 additions & 0 deletions python/thirdweb-ai/tests/adapters/test_pydantic_ai.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import pytest

from thirdweb_ai.tools.tool import Tool


def test_get_pydantic_ai_tools(test_tools: list[Tool]):
"""Test converting thirdweb tools to Pydantic AI tools."""
pytest.importorskip("pydantic_ai")
from pydantic_ai import Tool as PydanticTool # type: ignore[import]

from thirdweb_ai.adapters.pydantic_ai import get_pydantic_ai_tools

# Convert tools to Pydantic AI tools
pydantic_ai_tools = get_pydantic_ai_tools(test_tools)

# Assert we got the correct number of tools
assert len(pydantic_ai_tools) == len(test_tools)

# Check all tools were properly converted
assert all(isinstance(tool, PydanticTool) for tool in pydantic_ai_tools)

# Check properties were preserved
assert [tool.name for tool in pydantic_ai_tools] == [tool.name for tool in test_tools]
assert [tool.description for tool in pydantic_ai_tools] == [tool.description for tool in test_tools]

# Check all tools have function and prepare methods
assert all(callable(getattr(tool, "function", None)) for tool in pydantic_ai_tools)
assert all(callable(getattr(tool, "prepare", None)) for tool in pydantic_ai_tools)
28 changes: 28 additions & 0 deletions python/thirdweb-ai/tests/adapters/test_smolagents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import pytest

from thirdweb_ai.tools.tool import Tool


def test_get_smolagents_tools(test_tools: list[Tool]):
"""Test converting thirdweb tools to SmolaGents tools."""
pytest.importorskip("smolagents")

from smolagents import Tool as SmolagentTool # type: ignore[import]

from thirdweb_ai.adapters.smolagents import get_smolagents_tools

# Convert tools to SmolaGents tools
smolagents_tools = get_smolagents_tools(test_tools)

# Assert we got the correct number of tools
assert len(smolagents_tools) == len(test_tools)

# Check all tools were properly converted (using duck typing with SmolagentTool)
assert all(isinstance(tool, SmolagentTool) for tool in smolagents_tools)

# Check properties were preserved
assert [tool.name for tool in smolagents_tools] == [tool.name for tool in test_tools]
assert [tool.description for tool in smolagents_tools] == [tool.description for tool in test_tools]

# Check all tools have a callable forward method
assert all(callable(getattr(tool, "forward", None)) for tool in smolagents_tools)
7 changes: 0 additions & 7 deletions python/thirdweb-ai/tests/common/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,3 @@ def test_no_digits(self):

with pytest.raises(ValueError, match="does not contain any digits"):
normalize_chain_id(["ethereum", "polygon"])

def test_invalid_digit_string(self):
# This test is for completeness, but the current implementation
# doesn't trigger this error case since re.search('\d+') always
# returns a valid digit string if it matches
pass

Loading
Loading