Skip to content

Commit 41c8287

Browse files
committed
new features
1 parent c90337b commit 41c8287

5 files changed

Lines changed: 249 additions & 0 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# SupabaseTool
2+
3+
The `SupabaseTool` allows CrewAI agents to interact with Supabase databases.
4+
5+
## Supported Actions
6+
- `select`
7+
- `insert`
8+
- `update`
9+
- `delete`
10+
11+
## Environment Variables
12+
- SUPABASE_URL
13+
- SUPABASE_KEY
14+
15+
## Example Usage
16+
17+
```python
18+
from crewai.tools import SupabaseTool
19+
20+
tool = SupabaseTool()
21+
22+
result = tool.run({
23+
"action": "select",
24+
"table": "messages",
25+
"filters": {"id": "eq.1"}
26+
})

lib/crewai/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ dependencies = [
4545
"pyyaml~=6.0",
4646
"aiofiles~=24.1.0",
4747
"lancedb>=0.29.2,<0.30.1",
48+
"supabase>=2.0.0",
4849
]
4950

5051
[project.urls]
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
from __future__ import annotations
2+
3+
import os
4+
from typing import TYPE_CHECKING, Any
5+
6+
from crewai.tools import BaseTool
7+
8+
if TYPE_CHECKING:
9+
from supabase import Client
10+
11+
12+
class SupabaseTool(BaseTool):
13+
name: str = "SupabaseTool"
14+
description: str = (
15+
"A tool for performing Supabase database operations such as "
16+
"select, insert, update, and delete."
17+
)
18+
19+
def __init__(self) -> None:
20+
super().__init__()
21+
url = os.getenv("SUPABASE_URL")
22+
key = os.getenv("SUPABASE_KEY")
23+
24+
if not url or not key:
25+
raise ValueError(
26+
"SUPABASE_URL and SUPABASE_KEY must be set in environment variables"
27+
)
28+
if not url.startswith("https://"):
29+
raise ValueError("SUPABASE_URL must start with 'https://'")
30+
31+
from supabase import create_client
32+
33+
self.client: Client = create_client(url, key)
34+
35+
def run(self, *args: Any, **kwargs: Any) -> Any:
36+
"""Execute the tool and return its operation result.
37+
38+
Args:
39+
*args: Positional arguments passed to the tool.
40+
**kwargs: Keyword arguments passed to the tool.
41+
42+
Returns:
43+
A normalized JSON-like operation response.
44+
45+
Raises:
46+
ValueError: If the tool configuration or operation input is invalid.
47+
TypeError: If filters is not a dictionary.
48+
"""
49+
return super().run(*args, **kwargs)
50+
51+
def _run(self, params: dict[str, Any]) -> dict[str, Any]:
52+
"""Dispatch an operation described by ``params``.
53+
54+
Args:
55+
params: Operation name, table name, optional filters, and data.
56+
57+
Returns:
58+
A normalized response containing ``data`` and ``error`` keys, or
59+
an ``error`` key for invalid operation input.
60+
61+
Raises:
62+
ValueError: If ``params`` is not a dictionary or filters use an
63+
unsupported operator format.
64+
TypeError: If filters is not a dictionary.
65+
"""
66+
if not isinstance(params, dict):
67+
raise ValueError("SupabaseTool parameters must be a dictionary")
68+
69+
action = params.get("action")
70+
table = params.get("table")
71+
if not action or not table:
72+
return {"data": None, "error": "Missing required fields: action, table"}
73+
74+
if action == "select":
75+
return self.select(table, params.get("filters"))
76+
if action == "insert":
77+
return self.insert(table, params.get("data"))
78+
if action == "update":
79+
return self.update(table, params.get("data"), params.get("filters"))
80+
if action == "delete":
81+
return self.delete(table, params.get("filters"))
82+
return {"data": None, "error": f"Unknown action: {action}"}
83+
84+
def _apply_filters(self, operation: Any, filters: Any) -> Any:
85+
"""Apply validated filters to a Supabase operation."""
86+
if filters is None or filters == {}:
87+
return operation
88+
if not isinstance(filters, dict):
89+
raise TypeError("filters must be a dictionary")
90+
91+
operators = {
92+
"eq": "eq",
93+
"neq": "neq",
94+
"gt": "gt",
95+
"gte": "gte",
96+
"lt": "lt",
97+
"lte": "lte",
98+
}
99+
for column, value in filters.items():
100+
operator = "eq"
101+
filter_value = value
102+
if isinstance(value, dict):
103+
operator = value.get("operator")
104+
if operator not in operators or "value" not in value:
105+
raise ValueError(
106+
"Filter descriptors must contain a supported operator "
107+
"and a value"
108+
)
109+
filter_value = value["value"]
110+
elif isinstance(value, str) and value.startswith(
111+
("eq.", "neq.", "gt.", "gte.", "lt.", "lte.")
112+
):
113+
raise ValueError(
114+
"Filter values must be direct values or descriptors like "
115+
"{'operator': 'neq', 'value': 1}; do not use 'eq.1'"
116+
)
117+
operation = getattr(operation, operators[operator])(column, filter_value)
118+
return operation
119+
120+
@staticmethod
121+
def _normalize_response(response: Any) -> dict[str, Any]:
122+
"""Convert a Supabase response into a JSON-like dictionary."""
123+
if isinstance(response, dict):
124+
return response
125+
return {
126+
"data": getattr(response, "data", None),
127+
"error": getattr(response, "error", None),
128+
}
129+
130+
def select(self, table: str, filters: dict[str, Any] | None = None) -> dict[str, Any]:
131+
"""Select rows from a table.
132+
133+
Args:
134+
table: Supabase table name.
135+
filters: Optional column-to-value equality filters.
136+
137+
Returns:
138+
A normalized response containing selected rows and any error.
139+
140+
Raises:
141+
TypeError: If filters is not a dictionary.
142+
"""
143+
operation = self.client.table(table).select("*")
144+
operation = self._apply_filters(operation, filters)
145+
return self._normalize_response(operation.execute())
146+
147+
def insert(self, table: str, data: Any) -> dict[str, Any]:
148+
"""Insert data into a table.
149+
150+
Args:
151+
table: Supabase table name.
152+
data: Row dictionary or list of row dictionaries to insert.
153+
154+
Returns:
155+
A normalized response containing inserted rows and any error.
156+
157+
Raises:
158+
ValueError: If data is missing.
159+
"""
160+
if data is None:
161+
return {"data": None, "error": "Missing data for insert"}
162+
return self._normalize_response(self.client.table(table).insert(data).execute())
163+
164+
def update(
165+
self,
166+
table: str,
167+
data: Any,
168+
filters: dict[str, Any] | None = None,
169+
) -> dict[str, Any]:
170+
"""Update rows in a table.
171+
172+
Args:
173+
table: Supabase table name.
174+
data: Column values to update.
175+
filters: Optional column-to-value equality filters.
176+
177+
Returns:
178+
A normalized response containing updated rows and any error.
179+
180+
Raises:
181+
TypeError: If filters is not a dictionary.
182+
ValueError: If data is missing.
183+
"""
184+
if data is None:
185+
return {"data": None, "error": "Missing data for update"}
186+
operation = self.client.table(table).update(data)
187+
operation = self._apply_filters(operation, filters)
188+
return self._normalize_response(operation.execute())
189+
190+
def delete(
191+
self,
192+
table: str,
193+
filters: dict[str, Any] | None = None,
194+
) -> dict[str, Any]:
195+
"""Delete rows from a table.
196+
197+
Args:
198+
table: Supabase table name.
199+
filters: Optional column-to-value equality filters.
200+
201+
Returns:
202+
A normalized response containing deleted rows and any error.
203+
204+
Raises:
205+
TypeError: If filters is not a dictionary.
206+
"""
207+
operation = self.client.table(table).delete()
208+
operation = self._apply_filters(operation, filters)
209+
return self._normalize_response(operation.execute())
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import os
2+
import pytest
3+
4+
from crewai.tools.supabase_tool import SupabaseTool
5+
6+
def test_missing_env_vars():
7+
# Temporarily remove env vars
8+
os.environ.pop("SUPABASE_URL", None)
9+
os.environ.pop("SUPABASE_KEY", None)
10+
11+
with pytest.raises(ValueError):
12+
SupabaseTool()

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ exclude-newer-package = { msgpack = "2026-06-20T00:00:00Z", pydantic-settings =
245245
# Keep OpenAI on the SDK range required by CrewAI when transitive dependencies
246246
# loosen or pin their own lower versions.
247247
override-dependencies = [
248+
"supabase>=2.0.0",
248249
"openai>=2.30.0,<3",
249250
"rich>=13.7.1",
250251
"onnxruntime<1.24; python_version < '3.11'",

0 commit comments

Comments
 (0)