From 41c8287c62733c1ab9e3d2183cea34b22146b816 Mon Sep 17 00:00:00 2001 From: Ablaze005 Date: Thu, 3 Sep 2026 13:06:30 +0100 Subject: [PATCH] new features --- docs/v1.15.17/pt-BR/tools/supabase_tool.mdx | 26 +++ lib/crewai/pyproject.toml | 1 + lib/crewai/src/crewai/tools/supabase_tool.py | 209 +++++++++++++++++++ lib/crewai/tests/tools/test_supabase_tool.py | 12 ++ pyproject.toml | 1 + 5 files changed, 249 insertions(+) create mode 100644 docs/v1.15.17/pt-BR/tools/supabase_tool.mdx create mode 100644 lib/crewai/src/crewai/tools/supabase_tool.py create mode 100644 lib/crewai/tests/tools/test_supabase_tool.py diff --git a/docs/v1.15.17/pt-BR/tools/supabase_tool.mdx b/docs/v1.15.17/pt-BR/tools/supabase_tool.mdx new file mode 100644 index 0000000000..886a6e003c --- /dev/null +++ b/docs/v1.15.17/pt-BR/tools/supabase_tool.mdx @@ -0,0 +1,26 @@ +# SupabaseTool + +The `SupabaseTool` allows CrewAI agents to interact with Supabase databases. + +## Supported Actions +- `select` +- `insert` +- `update` +- `delete` + +## Environment Variables +- SUPABASE_URL +- SUPABASE_KEY + +## Example Usage + +```python +from crewai.tools import SupabaseTool + +tool = SupabaseTool() + +result = tool.run({ + "action": "select", + "table": "messages", + "filters": {"id": "eq.1"} +}) diff --git a/lib/crewai/pyproject.toml b/lib/crewai/pyproject.toml index 0b4c513754..72531040ee 100644 --- a/lib/crewai/pyproject.toml +++ b/lib/crewai/pyproject.toml @@ -45,6 +45,7 @@ dependencies = [ "pyyaml~=6.0", "aiofiles~=24.1.0", "lancedb>=0.29.2,<0.30.1", + "supabase>=2.0.0", ] [project.urls] diff --git a/lib/crewai/src/crewai/tools/supabase_tool.py b/lib/crewai/src/crewai/tools/supabase_tool.py new file mode 100644 index 0000000000..5d3c446c0e --- /dev/null +++ b/lib/crewai/src/crewai/tools/supabase_tool.py @@ -0,0 +1,209 @@ +from __future__ import annotations + +import os +from typing import TYPE_CHECKING, Any + +from crewai.tools import BaseTool + +if TYPE_CHECKING: + from supabase import Client + + +class SupabaseTool(BaseTool): + name: str = "SupabaseTool" + description: str = ( + "A tool for performing Supabase database operations such as " + "select, insert, update, and delete." + ) + + def __init__(self) -> None: + super().__init__() + url = os.getenv("SUPABASE_URL") + key = os.getenv("SUPABASE_KEY") + + if not url or not key: + raise ValueError( + "SUPABASE_URL and SUPABASE_KEY must be set in environment variables" + ) + if not url.startswith("https://"): + raise ValueError("SUPABASE_URL must start with 'https://'") + + from supabase import create_client + + self.client: Client = create_client(url, key) + + def run(self, *args: Any, **kwargs: Any) -> Any: + """Execute the tool and return its operation result. + + Args: + *args: Positional arguments passed to the tool. + **kwargs: Keyword arguments passed to the tool. + + Returns: + A normalized JSON-like operation response. + + Raises: + ValueError: If the tool configuration or operation input is invalid. + TypeError: If filters is not a dictionary. + """ + return super().run(*args, **kwargs) + + def _run(self, params: dict[str, Any]) -> dict[str, Any]: + """Dispatch an operation described by ``params``. + + Args: + params: Operation name, table name, optional filters, and data. + + Returns: + A normalized response containing ``data`` and ``error`` keys, or + an ``error`` key for invalid operation input. + + Raises: + ValueError: If ``params`` is not a dictionary or filters use an + unsupported operator format. + TypeError: If filters is not a dictionary. + """ + if not isinstance(params, dict): + raise ValueError("SupabaseTool parameters must be a dictionary") + + action = params.get("action") + table = params.get("table") + if not action or not table: + return {"data": None, "error": "Missing required fields: action, table"} + + if action == "select": + return self.select(table, params.get("filters")) + if action == "insert": + return self.insert(table, params.get("data")) + if action == "update": + return self.update(table, params.get("data"), params.get("filters")) + if action == "delete": + return self.delete(table, params.get("filters")) + return {"data": None, "error": f"Unknown action: {action}"} + + def _apply_filters(self, operation: Any, filters: Any) -> Any: + """Apply validated filters to a Supabase operation.""" + if filters is None or filters == {}: + return operation + if not isinstance(filters, dict): + raise TypeError("filters must be a dictionary") + + operators = { + "eq": "eq", + "neq": "neq", + "gt": "gt", + "gte": "gte", + "lt": "lt", + "lte": "lte", + } + for column, value in filters.items(): + operator = "eq" + filter_value = value + if isinstance(value, dict): + operator = value.get("operator") + if operator not in operators or "value" not in value: + raise ValueError( + "Filter descriptors must contain a supported operator " + "and a value" + ) + filter_value = value["value"] + elif isinstance(value, str) and value.startswith( + ("eq.", "neq.", "gt.", "gte.", "lt.", "lte.") + ): + raise ValueError( + "Filter values must be direct values or descriptors like " + "{'operator': 'neq', 'value': 1}; do not use 'eq.1'" + ) + operation = getattr(operation, operators[operator])(column, filter_value) + return operation + + @staticmethod + def _normalize_response(response: Any) -> dict[str, Any]: + """Convert a Supabase response into a JSON-like dictionary.""" + if isinstance(response, dict): + return response + return { + "data": getattr(response, "data", None), + "error": getattr(response, "error", None), + } + + def select(self, table: str, filters: dict[str, Any] | None = None) -> dict[str, Any]: + """Select rows from a table. + + Args: + table: Supabase table name. + filters: Optional column-to-value equality filters. + + Returns: + A normalized response containing selected rows and any error. + + Raises: + TypeError: If filters is not a dictionary. + """ + operation = self.client.table(table).select("*") + operation = self._apply_filters(operation, filters) + return self._normalize_response(operation.execute()) + + def insert(self, table: str, data: Any) -> dict[str, Any]: + """Insert data into a table. + + Args: + table: Supabase table name. + data: Row dictionary or list of row dictionaries to insert. + + Returns: + A normalized response containing inserted rows and any error. + + Raises: + ValueError: If data is missing. + """ + if data is None: + return {"data": None, "error": "Missing data for insert"} + return self._normalize_response(self.client.table(table).insert(data).execute()) + + def update( + self, + table: str, + data: Any, + filters: dict[str, Any] | None = None, + ) -> dict[str, Any]: + """Update rows in a table. + + Args: + table: Supabase table name. + data: Column values to update. + filters: Optional column-to-value equality filters. + + Returns: + A normalized response containing updated rows and any error. + + Raises: + TypeError: If filters is not a dictionary. + ValueError: If data is missing. + """ + if data is None: + return {"data": None, "error": "Missing data for update"} + operation = self.client.table(table).update(data) + operation = self._apply_filters(operation, filters) + return self._normalize_response(operation.execute()) + + def delete( + self, + table: str, + filters: dict[str, Any] | None = None, + ) -> dict[str, Any]: + """Delete rows from a table. + + Args: + table: Supabase table name. + filters: Optional column-to-value equality filters. + + Returns: + A normalized response containing deleted rows and any error. + + Raises: + TypeError: If filters is not a dictionary. + """ + operation = self.client.table(table).delete() + operation = self._apply_filters(operation, filters) + return self._normalize_response(operation.execute()) diff --git a/lib/crewai/tests/tools/test_supabase_tool.py b/lib/crewai/tests/tools/test_supabase_tool.py new file mode 100644 index 0000000000..9ddd4dcac8 --- /dev/null +++ b/lib/crewai/tests/tools/test_supabase_tool.py @@ -0,0 +1,12 @@ +import os +import pytest + +from crewai.tools.supabase_tool import SupabaseTool + +def test_missing_env_vars(): + # Temporarily remove env vars + os.environ.pop("SUPABASE_URL", None) + os.environ.pop("SUPABASE_KEY", None) + + with pytest.raises(ValueError): + SupabaseTool() diff --git a/pyproject.toml b/pyproject.toml index 52b4b876c2..a7ec15983b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -245,6 +245,7 @@ exclude-newer-package = { msgpack = "2026-06-20T00:00:00Z", pydantic-settings = # Keep OpenAI on the SDK range required by CrewAI when transitive dependencies # loosen or pin their own lower versions. override-dependencies = [ + "supabase>=2.0.0", "openai>=2.30.0,<3", "rich>=13.7.1", "onnxruntime<1.24; python_version < '3.11'",