Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
18 changes: 18 additions & 0 deletions toolkits/gibsonai/.pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
files: ^.*/gibsonai/.*
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: "v4.4.0"
hooks:
- id: check-case-conflict
- id: check-merge-conflict
- id: check-toml
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.7
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
47 changes: 47 additions & 0 deletions toolkits/gibsonai/.ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
target-version = "py310"
line-length = 100
fix = true

[lint]
select = [
# flake8-2020
"YTT",
# flake8-bandit
"S",
# flake8-bugbear
"B",
# flake8-builtins
"A",
# flake8-comprehensions
"C4",
# flake8-debugger
"T10",
# flake8-simplify
"SIM",
# isort
"I",
# mccabe
"C90",
# pycodestyle
"E", "W",
# pyflakes
"F",
# pygrep-hooks
"PGH",
# pyupgrade
"UP",
# ruff
"RUF",
# tryceratops
"TRY",
]

[lint.per-file-ignores]
"*" = ["TRY003", "B904"]
"**/tests/*" = ["S101", "E501"]
"**/evals/*" = ["S101", "E501"]


[format]
preview = true
skip-magic-trailing-comma = false
55 changes: 55 additions & 0 deletions toolkits/gibsonai/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
.PHONY: help

help:
@echo "🛠️ github Commands:\n"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

.PHONY: install
install: ## Install the uv environment and install all packages with dependencies
@echo "🚀 Creating virtual environment and installing all packages using uv"
@uv sync --active --all-extras --no-sources
@if [ -f .pre-commit-config.yaml ]; then uv run --no-sources pre-commit install; fi
@echo "✅ All packages and dependencies installed via uv"

.PHONY: install-local
install-local: ## Install the uv environment and install all packages with dependencies with local Arcade sources
@echo "🚀 Creating virtual environment and installing all packages using uv"
@uv sync --active --all-extras
@if [ -f .pre-commit-config.yaml ]; then uv run pre-commit install; fi
@echo "✅ All packages and dependencies installed via uv"

.PHONY: build
build: clean-build ## Build wheel file using poetry
@echo "🚀 Creating wheel file"
uv build

.PHONY: clean-build
clean-build: ## clean build artifacts
@echo "🗑️ Cleaning dist directory"
rm -rf dist

.PHONY: test
test: ## Test the code with pytest
@echo "🚀 Testing code: Running pytest"
@uv run --no-sources pytest -W ignore -v --cov --cov-config=pyproject.toml --cov-report=xml

.PHONY: coverage
coverage: ## Generate coverage report
@echo "coverage report"
@uv run --no-sources coverage report
@echo "Generating coverage report"
@uv run --no-sources coverage html

.PHONY: bump-version
bump-version: ## Bump the version in the pyproject.toml file by a patch version
@echo "🚀 Bumping version in pyproject.toml"
uv version --no-sources --bump patch

.PHONY: check
check: ## Run code quality tools.
@if [ -f .pre-commit-config.yaml ]; then\
echo "🚀 Linting code: Running pre-commit";\
uv run --no-sources pre-commit run -a;\
fi
@echo "🚀 Static type checking: Running mypy"
@uv run --no-sources mypy --config-file=pyproject.toml
5 changes: 5 additions & 0 deletions toolkits/gibsonai/arcade_gibsonai/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""GibsonAI Database Tools for Arcade."""

from arcade_gibsonai.tools.query import execute_query

__all__ = ["execute_query"]
73 changes: 73 additions & 0 deletions toolkits/gibsonai/arcade_gibsonai/api_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from typing import Any, Dict, List, Optional
import httpx
from pydantic import BaseModel

from .constants import API_BASE_URL, API_VERSION, MAX_ROWS_RETURNED


class GibsonAIResponse(BaseModel):
"""Response model for GibsonAI API."""

data: List[Dict[str, Any]]
success: bool
error: Optional[str] = None


class GibsonAIClient:
"""Client for interacting with GibsonAI Data API."""

def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = f"{API_BASE_URL}/{API_VERSION}"
self.headers = {"Content-Type": "application/json", "X-Gibson-API-Key": api_key}

async def execute_query(
self, query: str, params: Optional[List[Any]] = None
) -> List[str]:
"""Execute a query against GibsonAI database."""
if params is None:
params = []

payload = {"array_mode": False, "params": params, "query": query}

try:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.base_url}/-/query",
headers=self.headers,
json=payload,
timeout=30.0,
)

if response.status_code != 200:
error_msg = f"HTTP {response.status_code}: {response.text}"
raise Exception(f"GibsonAI API error: {error_msg}")

result = response.json()

# Handle different response formats
if isinstance(result, dict):
if "error" in result and result["error"]:
raise Exception(f"GibsonAI query error: {result['error']}")
elif "data" in result:
results = [str(row) for row in result["data"]]
else:
results = [str(result)]
elif isinstance(result, list):
results = [str(row) for row in result]
else:
results = [str(result)]

# Limit results to avoid memory issues
return results[:MAX_ROWS_RETURNED]

except httpx.TimeoutException:
raise Exception("Request timeout - GibsonAI API took too long to respond")
except httpx.RequestError as e:
raise Exception(f"Network error connecting to GibsonAI API: {e}")
except Exception as e:
# Re-raise if it's already our custom exception
if "GibsonAI" in str(e):
raise
else:
raise Exception(f"Unexpected error: {e}")
8 changes: 8 additions & 0 deletions toolkits/gibsonai/arcade_gibsonai/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Constants for GibsonAI API configuration."""

# API configuration
API_BASE_URL = "https://api.gibsonai.com"
API_VERSION = "v1"

# Maximum number of rows to return from queries
MAX_ROWS_RETURNED = 1000
5 changes: 5 additions & 0 deletions toolkits/gibsonai/arcade_gibsonai/tools/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""GibsonAI database query tools."""

from arcade_gibsonai.tools.query import execute_query

__all__ = ["execute_query"]
48 changes: 48 additions & 0 deletions toolkits/gibsonai/arcade_gibsonai/tools/query.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from typing import Annotated

from arcade_tdk import ToolContext, tool
from arcade_tdk.errors import RetryableToolError

from ..api_client import GibsonAIClient


@tool(requires_secrets=["GIBSONAI_API_KEY"])
async def execute_query(
context: ToolContext,
query: Annotated[
str,
"The SQL query to execute against GibsonAI project database. Supports all SQL operations including SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, DROP, etc.",
],
) -> list[str]:
"""
Execute a SQL query and return the results from the GibsonAI project relational database.

This tool supports all SQL operations including:
* SELECT queries for data retrieval
* INSERT, UPDATE, DELETE for data manipulation
* CREATE, ALTER, DROP for schema management
* Any other valid SQL statements

When running queries, follow these rules which will help avoid errors:
* First discover the database schema in the GibsonAI project database when schema is not known.
* Discover all the tables in the database when the list of tables is not known.
* Always use case-insensitive queries to match strings in the query.
* Always trim strings in the query.
* Prefer LIKE queries over direct string matches or regex queries.
* Only join on columns that are indexed or the primary key.

For SELECT queries, unless otherwise specified, ensure that query has a LIMIT of 100 for all results.
"""
api_key = context.get_secret("GIBSONAI_API_KEY")
client = GibsonAIClient(api_key)

try:
results = await client.execute_query(query)
return results
except Exception as e:
raise RetryableToolError(
f"Query failed: {e}",
developer_message=f"Query '{query}' failed against GibsonAI database.",
additional_prompt_content="Please check your query syntax and try again.",
retry_after_ms=10,
) from e
Loading