Skip to content
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
64 changes: 64 additions & 0 deletions nodes/src/nodes/tool_v0/IGlobal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# =============================================================================
# RocketRide Engine
# =============================================================================
# MIT License
# Copyright (c) 2026 Aparavi Software AG
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# =============================================================================

"""
v0 by Vercel tool node - global (shared) state.

Reads the v0 API key from config and stores it for IInstance tool methods.
"""

from __future__ import annotations

from ai.common.config import Config
from rocketlib import IGlobalBase, OPEN_MODE, warning


class IGlobal(IGlobalBase):
"""Global state for tool_v0."""

apikey: str = ''

def beginGlobal(self) -> None:
if self.IEndpoint.endpoint.openMode == OPEN_MODE.CONFIG:
return

cfg = Config.getNodeConfig(self.glb.logicalType, self.glb.connConfig)

self.apikey = str(cfg.get('apikey') or '').strip()

if not self.apikey:
raise Exception('tool_v0: apikey is required')

def validateConfig(self) -> None:
try:
cfg = Config.getNodeConfig(self.glb.logicalType, self.glb.connConfig)
apikey = str(cfg.get('apikey') or '').strip()
if not apikey:
warning('apikey is required')
except Exception as e:
warning(str(e))

def endGlobal(self) -> None:
self.apikey = ''
276 changes: 276 additions & 0 deletions nodes/src/nodes/tool_v0/IInstance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
# =============================================================================
# RocketRide Engine
# =============================================================================
# MIT License
# Copyright (c) 2026 Aparavi Software AG
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# =============================================================================

"""
v0 by Vercel tool node instance.

Exposes ``generate_ui`` and ``refine_ui`` tools for generating React UI
components via Vercel's v0 generative UI API.
"""

from __future__ import annotations

import json
from typing import Any, Dict, List

import httpx

from rocketlib import IInstanceBase, tool_function, warning

from .IGlobal import IGlobal

# ---------------------------------------------------------------------------
# v0 API configuration
# ---------------------------------------------------------------------------

V0_API_BASE = 'https://api.v0.dev/v1'
V0_GENERATE_ENDPOINT = f'{V0_API_BASE}/chat'
V0_REQUEST_TIMEOUT = 120 # seconds — generation can take a while


class IInstance(IInstanceBase):
IGlobal: IGlobal

@tool_function(
input_schema={
'type': 'object',
'required': ['prompt'],
'properties': {
'prompt': {
'type': 'string',
'description': 'A natural-language description of the UI component to generate.',
},
'model': {
'type': 'string',
'description': 'The v0 model to use (default: "v0-1.0-md").',
'default': 'v0-1.0-md',
},
},
},
output_schema={
'type': 'object',
'properties': {
'success': {'type': 'boolean'},
'code': {'type': 'string', 'description': 'Generated React component code.'},
'message_id': {'type': 'string', 'description': 'v0 message ID for follow-up refinements.'},
'error': {'type': 'string', 'description': 'Error message on failure.'},
},
},
Comment on lines +72 to +80
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep output schemas aligned with actual error payloads.

Both tools return an error field on failure, but neither output schema declares it. This creates a contract mismatch for schema-driven callers.

♻️ Proposed fix
         output_schema={
             'type': 'object',
             'properties': {
                 'success': {'type': 'boolean'},
                 'code': {'type': 'string', 'description': 'Generated React component code.'},
                 'message_id': {'type': 'string', 'description': 'v0 message ID for follow-up refinements.'},
+                'error': {'type': 'string', 'description': 'Error message when generation fails.'},
             },
         },
@@
         output_schema={
             'type': 'object',
             'properties': {
                 'success': {'type': 'boolean'},
                 'code': {'type': 'string', 'description': 'Refined React component code.'},
                 'message_id': {'type': 'string', 'description': 'Updated message ID for further refinements.'},
+                'error': {'type': 'string', 'description': 'Error message when refinement fails.'},
             },
         },

Also applies to: 99-103, 142-149, 179-182

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nodes/src/nodes/tool_v0/IInstance.py` around lines 72 - 79, The output_schema
dictionaries (the output_schema defined in IInstance with properties 'success',
'code', 'message_id') are missing the 'error' field that the tools actually
return on failures; update each output_schema to include an 'error' property
(e.g., 'error': {'type': ['object','null'], 'description': 'Error payload
returned on failure', 'additionalProperties': True}) so schema-driven callers
see the real contract and allow null when no error is present; apply the same
change to the other output_schema occurrences noted in the review.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — added 'error': {'type': 'string', 'description': 'Error message on failure.'} to both generate_ui and refine_ui output schemas.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rate Limit Exceeded

@charliegillet have exceeded the limit for the number of chat messages per hour. Please wait 1 minutes and 41 seconds before sending another message.

description='Generate a React UI component from a natural-language description. Provide a detailed prompt describing the desired UI and receive production-ready React + Tailwind CSS code.',
)
def generate_ui(self, args):
"""Generate a React UI component from a text prompt."""
args = _normalize_tool_input(args)

prompt = args.get('prompt')
if not prompt:
return {'success': False, 'error': 'generate_ui requires a `prompt` parameter'}

model = args.get('model') or 'v0-1.0-md'

messages = [
{'role': 'user', 'content': prompt},
]

try:
response = self._call_v0_api(messages, model)
code, message_id = _extract_code(response)
except Exception as e:
return {'success': False, 'error': f'v0 API call failed: {e}'}

if not code:
return {
'success': False,
'error': 'No code generated',
}

return {
'success': True,
'code': code,
'message_id': message_id,
}

@tool_function(
input_schema={
'type': 'object',
'required': ['prompt', 'message_id'],
'properties': {
'prompt': {
'type': 'string',
'description': 'Follow-up instructions describing how to change the component.',
},
'message_id': {
'type': 'string',
'description': 'The message_id returned from a previous generate_ui or refine_ui call.',
},
'prior_messages': {
'type': 'array',
'description': 'Prior conversation messages (user/assistant pairs) for stateless API fallback. Include the original prompt and response so the server has full context.',
'items': {
'type': 'object',
'properties': {
'role': {'type': 'string'},
'content': {'type': 'string'},
},
},
},
'model': {
'type': 'string',
'description': 'The v0 model to use (default: "v0-1.0-md").',
'default': 'v0-1.0-md',
},
},
},
output_schema={
'type': 'object',
'properties': {
'success': {'type': 'boolean'},
'code': {'type': 'string', 'description': 'Refined React component code.'},
'message_id': {'type': 'string', 'description': 'Updated message ID for further refinements.'},
'error': {'type': 'string', 'description': 'Error message on failure.'},
},
},
description='Refine a previously generated UI component by providing follow-up instructions. Requires the message_id from a prior generate_ui call.',
)
def refine_ui(self, args):
"""Refine a previously generated UI component."""
args = _normalize_tool_input(args)

prompt = args.get('prompt')
if not prompt:
return {'success': False, 'error': 'refine_ui requires a `prompt` parameter'}

message_id = args.get('message_id')
if not message_id:
return {'success': False, 'error': 'refine_ui requires a `message_id` from a prior generation'}

model = args.get('model') or 'v0-1.0-md'

# Build the messages array with prior history as a stateless fallback.
# The v0 /v1/chat endpoint may be stateful (server-side history keyed by
# parent_message_id) or stateless (standard OpenAI-compatible, requiring
# the full conversation in messages). We include both: the prior context
# in `messages` and `parent_message_id` as an extra parameter so the
# request works correctly regardless of the server's behaviour.
prior_messages: List[Dict[str, str]] = args.get('prior_messages') or []
messages = [*prior_messages, {'role': 'user', 'content': prompt}]

try:
response = self._call_v0_api(messages, model, parent_message_id=message_id)
code, new_message_id = _extract_code(response)
except Exception as e:
return {'success': False, 'error': f'v0 API call failed: {e}'}

if not code:
return {
'success': False,
'error': 'No code generated',
}

return {
'success': True,
'code': code,
'message_id': new_message_id or message_id,
}

def _call_v0_api(self, messages: List[Dict[str, str]], model: str, **extra: Any) -> Dict[str, Any]:
"""Send a chat-style request to the v0 API and return the parsed response."""
payload = {
'model': model,
'messages': messages,
'stream': False,
**extra,
}

headers = {
'Authorization': f'Bearer {self.IGlobal.apikey}',
'Content-Type': 'application/json',
}

try:
with httpx.Client(timeout=V0_REQUEST_TIMEOUT) as client:
resp = client.post(
V0_GENERATE_ENDPOINT,
headers=headers,
json=payload,
)
resp.raise_for_status()
try:
return resp.json()
except (json.JSONDecodeError, ValueError) as exc:
warning(f'v0 API returned non-JSON response: {exc}')
raise ValueError('v0 API returned non-JSON response') from exc
except httpx.HTTPStatusError as e:
warning(f'v0 API error: status={e.response.status_code}')
raise
except Exception as e:
warning(f'v0 API request failed: {e}')
raise
Comment thread
coderabbitai[bot] marked this conversation as resolved.


def _extract_code(response: Dict[str, Any]) -> tuple[str, str]:
"""Extract generated code and message ID from the v0 API response."""
message_id = ''
code = ''

choices = response.get('choices') or []
if choices:
message = choices[0].get('message') or {}
code = message.get('content') or ''
message_id = response.get('id') or ''

return code, message_id


def _normalize_tool_input(input_obj: Any) -> Dict[str, Any]:
"""Normalize whatever the engine/framework passes as tool input into a plain dict."""
if input_obj is None:
return {}

if hasattr(input_obj, 'model_dump') and callable(getattr(input_obj, 'model_dump')):
input_obj = input_obj.model_dump()
elif hasattr(input_obj, 'dict') and callable(getattr(input_obj, 'dict')):
input_obj = input_obj.dict()

if isinstance(input_obj, str):
try:
parsed = json.loads(input_obj)
if isinstance(parsed, dict):
input_obj = parsed
except Exception:
pass

if not isinstance(input_obj, dict):
warning(f'v0: unexpected input type {type(input_obj).__name__} (content redacted)')
return {}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if 'input' in input_obj and isinstance(input_obj['input'], dict):
inner = input_obj['input']
extras = {k: v for k, v in input_obj.items() if k != 'input'}
input_obj = {**inner, **extras}

input_obj.pop('security_context', None)

return input_obj
35 changes: 35 additions & 0 deletions nodes/src/nodes/tool_v0/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# =============================================================================
# RocketRide Engine
# =============================================================================
# MIT License
# Copyright (c) 2026 Aparavi Software AG
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# =============================================================================

from os.path import dirname, join, realpath
from depends import depends

requirements = join(dirname(realpath(__file__)), 'requirements.txt')
depends(requirements)

from .IGlobal import IGlobal
from .IInstance import IInstance

__all__ = ['IGlobal', 'IInstance']
1 change: 1 addition & 0 deletions nodes/src/nodes/tool_v0/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
httpx
Loading
Loading