-
Notifications
You must be signed in to change notification settings - Fork 46
feat(nodes): add v0 by Vercel tool node for UI generation #557
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
Open
charliegillet
wants to merge
7
commits into
rocketride-org:develop
Choose a base branch
from
charliegillet:feature/v0-vercel-node
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
71d04ef
feat(nodes): add v0 by Vercel tool node for AI-powered UI generation
charliegillet ddc01e8
fix(nodes): address PR #557 feedback on v0 Vercel node
charliegillet ef05dbe
fix(nodes): format services.json and add header comment for v0 node
charliegillet 65a5a4b
refactor(nodes): migrate tool_v0 to @tool_function pattern per PR #599
charliegillet 4de59a5
fix(nodes): address CodeRabbit review feedback on tool_v0 node
charliegillet 7d3f36d
fix: format JSON file
asclearuc 05f29be
fix(nodes): return schema-shaped errors and redact log payloads in to…
charliegillet File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 = '' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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.'}, | ||
| }, | ||
| }, | ||
| 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 | ||
|
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 {} | ||
|
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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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'] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| httpx |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
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.
Keep output schemas aligned with actual error payloads.
Both tools return an
errorfield 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
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.
Fixed — added
'error': {'type': 'string', 'description': 'Error message on failure.'}to bothgenerate_uiandrefine_uioutput schemas.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.
Rate Limit Exceeded
@charliegillethave exceeded the limit for the number of chat messages per hour. Please wait 1 minutes and 41 seconds before sending another message.