-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(gcp): implement native nodes for firestore, gcs, and vertex ai (… #1608
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
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| import base64 | ||
| import json | ||
| from typing import Optional, Tuple, Any | ||
|
|
||
| class GCPAuthError(Exception): | ||
| """Raised when GCP authentication fails or config is invalid.""" | ||
| pass | ||
|
|
||
| def get_gcp_credentials(config: dict, scopes: Optional[list[str]] = None) -> Tuple[Any, Optional[str]]: | ||
| """ | ||
| Resolves GCP credentials from the given node configuration. | ||
|
|
||
| Returns: | ||
| tuple[google.auth.credentials.Credentials, str]: The credentials and the resolved project ID. | ||
|
|
||
| Raises: | ||
| GCPAuthError: If configuration is invalid or missing. | ||
| """ | ||
| try: | ||
| import google.auth | ||
| from google.oauth2 import service_account | ||
| except ImportError: | ||
| raise GCPAuthError("google-auth library is not installed. Ensure node requirements include 'google-auth'.") | ||
|
|
||
| auth_type = config.get("gcp.authType", "adc") | ||
| project_id = config.get("gcp.projectId") | ||
|
|
||
| if auth_type == "service_account": | ||
| key_data = config.get("gcp.serviceAccountKey") | ||
|
Comment on lines
+25
to
+29
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value Use single quotes for string literals. Double quotes are used for several string literals across the Python files. As per path instructions, use single quotes for string literals in
📍 Affects 3 files
🤖 Prompt for AI AgentsSource: Path instructions |
||
| if not key_data: | ||
| raise GCPAuthError("Service Account JSON key is required when authType is 'service_account', but missing.") | ||
|
|
||
| # RocketRide UI usually uploads files as base64 data-url: data:application/json;base64,... | ||
| try: | ||
| if "," in key_data: | ||
| _, b64_data = key_data.split(",", 1) | ||
| json_bytes = base64.b64decode(b64_data) | ||
| key_info = json.loads(json_bytes.decode('utf-8')) | ||
| else: | ||
| # Fallback if raw JSON string is passed directly | ||
| key_info = json.loads(key_data) | ||
| except Exception as e: | ||
| raise GCPAuthError(f"Failed to parse Service Account JSON key: {e}") | ||
|
|
||
| try: | ||
| creds = service_account.Credentials.from_service_account_info(key_info) | ||
| if scopes: | ||
| creds = creds.with_scopes(scopes) | ||
| except Exception as e: | ||
| raise GCPAuthError(f"Invalid Service Account format: {e}") | ||
|
|
||
| if not project_id: | ||
| project_id = key_info.get("project_id") | ||
|
|
||
| return creds, project_id | ||
|
|
||
| elif auth_type == "adc": | ||
| # Application Default Credentials | ||
| try: | ||
| creds, default_project_id = google.auth.default(scopes=scopes) | ||
| if not project_id: | ||
| project_id = default_project_id | ||
| return creds, project_id | ||
| except Exception as e: | ||
| raise GCPAuthError(f"Failed to acquire Application Default Credentials: {e}") | ||
|
|
||
| else: | ||
| raise GCPAuthError(f"Unknown gcp.authType: {auth_type}") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| { | ||
| "fields": { | ||
| "gcp.authType": { | ||
| "type": "string", | ||
| "title": "Authentication Type", | ||
| "default": "adc", | ||
| "enum": ["adc", "service_account"], | ||
| "enumNames": ["Application Default Credentials", "Service Account JSON"], | ||
| "description": "Choose how to authenticate to Google Cloud Platform.", | ||
| "conditional": [ | ||
| { | ||
| "value": "service_account", | ||
| "properties": ["gcp.serviceAccountKey"] | ||
| } | ||
| ] | ||
| }, | ||
| "gcp.serviceAccountKey": { | ||
| "type": "string", | ||
| "format": "data-url", | ||
| "title": "Service Account Key JSON", | ||
| "description": "Upload the JSON key file for your Google Cloud service account.", | ||
| "ui:options:accept": ".json", | ||
| "secure": true | ||
| }, | ||
| "gcp.projectId": { | ||
| "type": "string", | ||
| "title": "Project ID", | ||
| "description": "Optional: Specify the Google Cloud Project ID explicitly. Leave blank to infer from credentials.", | ||
| "optional": true | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from ai.common.config import Config | ||
| from rocketlib import IGlobalBase, OPEN_MODE, debug, warning | ||
| from nodes.core.gcp_auth import get_gcp_credentials, GCPAuthError | ||
|
|
||
| try: | ||
| from google.cloud import firestore | ||
| except ImportError: | ||
| firestore = None | ||
|
|
||
| class IGlobal(IGlobalBase): | ||
| """Global state for Firestore node.""" | ||
|
|
||
| client: firestore.Client | None = None | ||
| database: str = '(default)' | ||
| collection: str = '' | ||
|
|
||
| def beginGlobal(self) -> None: | ||
| if self.IEndpoint.endpoint.openMode == OPEN_MODE.CONFIG: | ||
| return | ||
|
|
||
| if firestore is None: | ||
| raise ImportError("google-cloud-firestore is not installed.") | ||
|
|
||
| cfg = Config.getNodeConfig(self.glb.logicalType, self.glb.connConfig) | ||
|
|
||
| self.database = str((cfg.get('database') or '(default)')).strip() or '(default)' | ||
| self.collection = str((cfg.get('collection') or '')).strip() | ||
|
|
||
| # Auth | ||
| try: | ||
| creds, project_id = get_gcp_credentials(cfg) | ||
| except Exception as e: | ||
| warning(f"Firestore authentication failed: {e}") | ||
| raise | ||
|
|
||
| self.client = firestore.Client( | ||
| project=project_id, | ||
| credentials=creds, | ||
| database=self.database | ||
| ) | ||
|
|
||
| # Fail fast connection check | ||
| try: | ||
| # simple check to verify connectivity/auth | ||
| next(self.client.collections(page_size=1), None) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 bug: |
||
| debug(f'db_firestore: connected to project {self.client.project}, database={self.database}') | ||
| except Exception as e: | ||
| warning(f"Firestore connection check failed: {e}") | ||
| raise | ||
|
|
||
| def validateConfig(self) -> None: | ||
| try: | ||
| cfg = Config.getNodeConfig(self.glb.logicalType, self.glb.connConfig) | ||
| get_gcp_credentials(cfg) # Validates credentials parse successfully | ||
| if not str(cfg.get('collection') or '').strip(): | ||
| warning('collection is recommended') | ||
| except GCPAuthError as e: | ||
| warning(f"Auth configuration error: {e}") | ||
| except Exception as e: | ||
| warning(str(e)) | ||
|
|
||
| def endGlobal(self) -> None: | ||
| if self.client is not None: | ||
| self.client.close() | ||
| self.client = None | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| from ai.common.tool import tool_function | ||
| from rocketlib import IInstanceBase | ||
| from typing import Dict, Any | ||
|
|
||
| class IInstance(IInstanceBase): | ||
| """Firestore instance, providing tool functions for reading and writing documents.""" | ||
|
|
||
| @tool_function( | ||
| description="Insert or update a document in a Firestore collection. If the document exists, it merges the data.", | ||
| args={ | ||
| "collection": "The collection name.", | ||
| "document_id": "The ID of the document to set. If empty, a new ID will be generated.", | ||
| "data": "The JSON dictionary to store in the document." | ||
| } | ||
| ) | ||
| def set_document(self, collection: str, document_id: str, data: Dict[str, Any]) -> str: | ||
| client = self.glb.client | ||
| if not client: | ||
| return "Error: Firestore client is not connected." | ||
|
|
||
| if not collection: | ||
| collection = self.glb.collection | ||
|
|
||
| try: | ||
| coll_ref = client.collection(collection) | ||
| if document_id: | ||
| doc_ref = coll_ref.document(document_id) | ||
| doc_ref.set(data, merge=True) | ||
| return f"Successfully set document {document_id} in {collection}." | ||
| else: | ||
| _, doc_ref = coll_ref.add(data) | ||
| return f"Successfully added document with ID {doc_ref.id} to {collection}." | ||
| except Exception as e: | ||
| return f"Failed to set document: {e}" | ||
|
|
||
| @tool_function( | ||
| description="Get a single document from a Firestore collection.", | ||
| args={ | ||
| "collection": "The collection name.", | ||
| "document_id": "The document ID." | ||
| } | ||
| ) | ||
| def get_document(self, collection: str, document_id: str) -> Dict[str, Any]: | ||
| client = self.glb.client | ||
| if not client: | ||
| return {"error": "Firestore client is not connected."} | ||
|
|
||
| if not collection: | ||
| collection = self.glb.collection | ||
|
|
||
| try: | ||
| doc = client.collection(collection).document(document_id).get() | ||
| if doc.exists: | ||
| return doc.to_dict() | ||
| else: | ||
| return {"error": f"Document {document_id} not found."} | ||
| except Exception as e: | ||
| return {"error": f"Failed to get document: {e}"} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # Firestore Node | ||
|
|
||
| A standard node for interacting with Google Cloud Firestore (Datastore mode or Native mode). | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 risk: |
||
|
|
||
| ## Authentication | ||
| This node uses the shared RocketRide Google Cloud authentication logic. You can authenticate by either: | ||
| 1. Uploading a **Service Account JSON Key**. | ||
| 2. Leaving the key blank and relying on **Application Default Credentials (ADC)** if your RocketRide server is hosted on Google Cloud Run, GKE, or a Compute Engine VM with attached service accounts. | ||
|
|
||
| ## Usage | ||
| When placed in a pipeline as a tool node, the LLM can: | ||
| - **`get_document`**: Fetch an existing document by its collection and document ID. | ||
| - **`set_document`**: Create or update (merge) a document with JSON data. | ||
|
|
||
| <!-- ROCKETRIDE:GENERATED:PARAMS START --> | ||
| <!-- ROCKETRIDE:GENERATED:PARAMS END --> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import os | ||
| from depends import depends # type: ignore | ||
|
|
||
| requirements = os.path.dirname(os.path.realpath(__file__)) + '/requirements.txt' | ||
| depends(requirements) | ||
|
|
||
| from .IGlobal import IGlobal # noqa: E402 | ||
| from .IInstance import IInstance # noqa: E402 | ||
|
|
||
| __all__ = ['IGlobal', 'IInstance'] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| google-auth>=2.23.3 | ||
| google-cloud-firestore>=2.13.0 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| { | ||
| "title": "Firestore", | ||
| "protocol": "db_firestore://", | ||
| "classType": ["database", "tool"], | ||
| "capabilities": ["noremote", "invoke"], | ||
| "register": "filter", | ||
| "node": "python", | ||
| "path": "nodes.db_firestore", | ||
| "prefix": "firestore", | ||
| "description": ["A processing component that connects to Google Cloud Firestore (Datastore Mode or Native)."], | ||
| "icon": "db_firestore.svg", | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 bug: |
||
| "documentation": "https://docs.rocketride.org", | ||
| "invoke": { | ||
| "llm": { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 bug: |
||
| "description": "LLM to use to craft NoSQL queries", | ||
| "min": 1, | ||
| "optional": true | ||
| } | ||
| }, | ||
| "lanes": { | ||
| "answers": [], | ||
| "questions": ["table", "text", "answers"] | ||
| }, | ||
| "preconfig": { | ||
| "default": "default", | ||
| "profiles": { | ||
| "default": { | ||
| "database": "(default)" | ||
| } | ||
| } | ||
| }, | ||
| "fields": { | ||
| "firestore.database": { | ||
| "type": "string", | ||
| "title": "Database ID", | ||
| "default": "(default)", | ||
| "description": "The Firestore database ID. Usually '(default)'." | ||
| }, | ||
| "firestore.collection": { | ||
| "type": "string", | ||
| "title": "Collection", | ||
| "description": "The Firestore collection to query or insert into." | ||
| }, | ||
| "firestore.default": { | ||
| "object": "default", | ||
| "properties": [ | ||
| "gcp.authType", | ||
| "gcp.serviceAccountKey", | ||
| "gcp.projectId", | ||
| "firestore.database", | ||
| "firestore.collection" | ||
| ] | ||
| }, | ||
| "firestore.profile": { | ||
| "hidden": true, | ||
| "type": "string", | ||
| "default": "default", | ||
| "enum": [["default", "Default"]], | ||
| "conditional": [ | ||
| { | ||
| "value": "default", | ||
| "properties": ["firestore.default"] | ||
| } | ||
| ] | ||
| } | ||
| }, | ||
| "shape": [ | ||
| { | ||
| "section": "Pipe", | ||
| "title": "Firestore", | ||
| "properties": ["firestore.profile"] | ||
| } | ||
| ] | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 bug: engine delivers node config with the dotted namespace stripped — keys arrive as
authType,projectId,serviceAccountKey(seetool_gmail/IGlobal.py:64readingcfg.get('authType')for fieldgoogle.authType, and the comment indb_postgres/IGlobal.py:41).config.get("gcp.authType")never matches → always falls back toadc; theservice_accountbranch is dead code andgcp.projectIdis ignored. Fix: readauthType/serviceAccountKey/projectId.