fix(tool-input): added tool input, visibility enum for tool params, fixed google provider bugs (#674)

* transfrom from block-centric tool input component to tool-centric tool input component for agent tools

* added additional type safety, created generic wrapper for tool input & reused across all subblock types

* stop retries if tool call fails, implemented for all providers except google

* bug fix with tool name extraction

* another bug fix

* ran script to update docs

* update contributing guide tool/block add example to reflect new param structure

* update README

* add key control to combobox, fixed google

* fixed google provider, fixed combobox

* fixed a ton of tools, ensured that the agent tool has full parity with actual tool for all tools

* update docs to reflect new structure

* updated visibility for gmail draft

* standardize dropdown values for tool definitions

* add asterisk for user-only + required fields

* updated visibility for tools

* consolidate redactApiKey util, fixed console entry bug that overwrites previous block logs

* updated docs

* update contributing guide to guide users to point their branches at staging instead of main

* nits

* move socket tests
This commit is contained in:
Waleed Latif
2025-07-13 20:16:49 -07:00
committed by GitHub
parent db22e26662
commit ff2b1d33c8
223 changed files with 4089 additions and 4920 deletions

View File

@@ -15,8 +15,6 @@ Thank you for your interest in contributing to Sim Studio! Our goal is to provid
- [Commit Message Guidelines](#commit-message-guidelines)
- [Local Development Setup](#local-development-setup)
- [Adding New Blocks and Tools](#adding-new-blocks-and-tools)
- [Local Storage Mode](#local-storage-mode)
- [Standalone Build](#standalone-build)
- [License](#license)
- [Contributor License Agreement (CLA)](#contributor-license-agreement-cla)
@@ -57,7 +55,7 @@ We strive to keep our workflow as simple as possible. To contribute:
```
7. **Create a Pull Request**
Open a pull request against the `main` branch on GitHub. Please provide a clear description of the changes and reference any relevant issues (e.g., `fixes #123`).
Open a pull request against the `staging` branch on GitHub. Please provide a clear description of the changes and reference any relevant issues (e.g., `fixes #123`).
---
@@ -85,7 +83,7 @@ If you discover a bug or have a feature request, please open an issue in our Git
Before creating a pull request:
- **Ensure Your Branch Is Up-to-Date:**
Rebase your branch onto the latest `main` branch to prevent merge conflicts.
Rebase your branch onto the latest `staging` branch to prevent merge conflicts.
- **Follow the Guidelines:**
Make sure your changes are well-tested, follow our coding standards, and include relevant documentation if necessary.
@@ -209,13 +207,14 @@ Dev Containers provide a consistent and easy-to-use development environment:
3. **Start Developing:**
- Run `bun run dev` in the terminal or use the `sim-start` alias
- Run `bun run dev:full` in the terminal or use the `sim-start` alias
- This starts both the main application and the realtime socket server
- All dependencies and configurations are automatically set up
- Your changes will be automatically hot-reloaded
4. **GitHub Codespaces:**
- This setup also works with GitHub Codespaces if you prefer development in the browser
- Just click "Code" → "Codespaces" → "Create codespace on main"
- Just click "Code" → "Codespaces" → "Create codespace on staging"
### Option 4: Manual Setup
@@ -246,9 +245,11 @@ If you prefer not to use Docker or Dev Containers:
4. **Run the Development Server:**
```bash
bun run dev
bun run dev:full
```
This command starts both the main application and the realtime socket server required for full functionality.
5. **Make Your Changes and Test Locally.**
### Email Template Development
@@ -379,7 +380,18 @@ In addition, you will need to update the registries:
provider: 'pinecone', // ID of the OAuth provider
params: {
// Tool parameters
parameterName: {
type: 'string',
required: true,
visibility: 'user-or-llm', // Controls parameter visibility
description: 'Description of the parameter',
},
optionalParam: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Optional parameter only user can set',
},
},
request: {
// Request configuration
@@ -429,11 +441,57 @@ Maintaining consistent naming across the codebase is critical for auto-generatio
- **Tool Exports:** Should be named `{toolName}Tool` (e.g., `fetchTool`)
- **Tool IDs:** Should follow the format `{provider}_{tool_name}` (e.g., `pinecone_fetch`)
### Parameter Visibility System
Sim Studio implements a sophisticated parameter visibility system that controls how parameters are exposed to users and LLMs in agent workflows. Each parameter can have one of four visibility levels:
| Visibility | User Sees | LLM Sees | How It Gets Set |
|-------------|-----------|----------|--------------------------------|
| `user-only` | ✅ Yes | ❌ No | User provides in UI |
| `user-or-llm` | ✅ Yes | ✅ Yes | User provides OR LLM generates |
| `llm-only` | ❌ No | ✅ Yes | LLM generates only |
| `hidden` | ❌ No | ❌ No | Application injects at runtime |
#### Visibility Guidelines
- **`user-or-llm`**: Use for core parameters that can be provided by users or intelligently filled by the LLM (e.g., search queries, email subjects)
- **`user-only`**: Use for configuration parameters, API keys, and settings that only users should control (e.g., number of results, authentication credentials)
- **`llm-only`**: Use for computed values that the LLM should handle internally (e.g., dynamic calculations, contextual data)
- **`hidden`**: Use for system-level parameters injected at runtime (e.g., OAuth tokens, internal identifiers)
#### Example Implementation
```typescript
params: {
query: {
type: 'string',
required: true,
visibility: 'user-or-llm', // User can provide or LLM can generate
description: 'Search query to execute',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only', // Only user provides this
description: 'API key for authentication',
},
internalId: {
type: 'string',
required: false,
visibility: 'hidden', // System provides this at runtime
description: 'Internal tracking identifier',
},
}
```
This visibility system ensures clean user interfaces while maintaining full flexibility for LLM-driven workflows.
### Guidelines & Best Practices
- **Code Style:** Follow the project's ESLint and Prettier configurations. Use meaningful variable names and small, focused functions.
- **Documentation:** Clearly document the purpose, inputs, outputs, and any special behavior for your block/tool.
- **Error Handling:** Implement robust error handling and provide user-friendly error messages.
- **Parameter Visibility:** Always specify the appropriate visibility level for each parameter to ensure proper UI behavior and LLM integration.
- **Testing:** Add unit or integration tests to verify your changes when possible.
- **Commit Changes:** Update all related components and registries, and describe your changes in your pull request.

View File

@@ -87,6 +87,7 @@ docker compose -f docker-compose.prod.yml up -d
1. Open VS Code with the [Remote - Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
2. Open the project and click "Reopen in Container" when prompted
3. Run `bun run dev:full` in the terminal or use the `sim-start` alias
- This starts both the main application and the realtime socket server
### Option 4: Manual Setup
@@ -113,24 +114,27 @@ bunx drizzle-kit push
4. Start the development servers:
Next.js app:
**Recommended approach - run both servers together (from project root):**
```bash
bun run dev:full
```
This starts both the main Next.js application and the realtime socket server required for full functionality.
**Alternative - run servers separately:**
Next.js app (from project root):
```bash
bun run dev
```
Start the realtime server:
Realtime socket server (from `apps/sim` directory in a separate terminal):
```bash
cd apps/sim
bun run dev:sockets
```
Run both together (recommended):
```bash
bun run dev:full
```
## Tech Stack
- **Framework**: [Next.js](https://nextjs.org/) (App Router)

View File

@@ -1,186 +0,0 @@
---
title: Autoblocks
description: Manage and use versioned prompts with Autoblocks
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="autoblocks"
color="#0D2929"
icon={true}
iconSvg={`<svg className="block-icon"
version='1.1'
id='Layer_1'
xmlns='http://www.w3.org/2000/svg'
xmlnsXlink='http://www.w3.org/1999/xlink'
x='0px'
y='0px'
viewBox='0 0 1250 1250'
enableBackground='new 0 0 1250 1250'
xmlSpace='preserve'
>
<path
fill='#FFFFFF'
opacity='1.000000'
stroke='none'
d='
M671.222290,1079.959839
C671.176025,1077.962891 671.089233,1075.965820 671.089111,1073.968872
C671.082825,918.318481 671.062683,762.668091 671.192322,607.017761
C671.195862,602.748474 669.789551,600.693787 666.180847,598.638306
C636.091125,581.500183 606.140991,564.117126 576.145508,546.813599
C556.393311,535.419128 536.677856,523.960449 516.869568,512.664307
C495.246002,500.332977 473.461487,488.282806 451.883911,475.872253
C434.220825,465.713257 416.802856,455.129089 399.195587,444.871857
C379.466736,433.378601 359.648438,422.038818 339.866608,410.636597
C320.229004,399.317505 300.588470,388.003510 280.948822,376.688019
C271.840149,371.440033 262.730530,366.193695 253.057938,360.622070
C267.185272,352.478241 280.655273,344.713531 294.125092,336.948517
C329.023163,316.830566 363.943237,296.750366 398.783295,276.532349
C402.073059,274.623260 404.534790,274.139191 408.118988,276.252319
C435.683502,292.503723 463.371948,308.546082 491.084290,324.545258
C509.340118,335.084839 527.725525,345.399719 546.006958,355.895203
C585.713440,378.690979 625.427124,401.474670 665.069397,424.381744
C705.530884,447.762177 745.895203,471.310669 786.336243,494.726715
C796.959717,500.877930 807.667236,506.888184 818.432190,512.787903
C820.966064,514.176636 821.763611,515.816772 821.762329,518.659241
C821.692932,676.145020 821.688171,833.630737 821.793762,991.116455
C821.795837,994.184937 820.514771,995.521545 818.222412,996.837891
C782.578491,1017.306641 746.954346,1037.809570 711.333679,1058.318848
C698.839661,1065.512573 686.367554,1072.744629 673.219116,1079.994141
C672.109314,1080.006104 671.665771,1079.982910 671.222290,1079.959839
z'
/>
<path
fill='#FFFFFF'
opacity='1.000000'
stroke='none'
d='
M684.421631,400.605865
C600.749390,352.376038 517.388306,304.342010 433.717010,256.129181
C455.858643,243.338989 477.724731,230.689346 499.608948,218.071136
C526.744324,202.425217 553.916504,186.842911 581.002014,171.111252
C583.487793,169.667450 585.282104,169.727783 587.700562,171.126724
C627.018250,193.870560 666.389465,216.521790 705.739136,239.210449
C744.537903,261.581543 783.343262,283.941437 822.113525,306.361786
C854.544006,325.115936 886.886658,344.022156 919.345703,362.726379
C945.337769,377.704102 971.415039,392.534851 997.539551,407.280151
C1001.126465,409.304749 1002.459045,411.581146 1002.455444,415.839966
C1002.322388,571.647339 1002.315430,727.454834 1002.468750,883.262207
C1002.473694,888.329590 1001.184082,891.101135 996.646118,893.690186
C949.437134,920.624695 902.383667,947.831665 855.284607,974.958862
C854.453491,975.437500 853.591980,975.863708 851.884216,976.772095
C851.884216,974.236023 851.884216,972.347290 851.884216,970.458557
C851.884216,814.817688 851.876099,659.176880 851.927551,503.536011
C851.928955,499.372650 851.416870,497.004883 846.802246,494.523651
C829.014954,484.959839 811.879517,474.190002 794.417969,464.012421
C774.549316,452.431854 754.597900,440.993225 734.670959,429.512817
C718.033508,419.927551 701.379517,410.370911 684.421631,400.605865
z'
/>
<path
fill='#FFFFFF'
opacity='1.000000'
stroke='none'
d='
M398.927063,451.754761
C400.510162,450.940521 401.764893,450.328430 403.700867,449.383972
C403.700867,452.154175 403.700897,454.096252 403.700897,456.038330
C403.700897,554.021851 403.720520,652.005371 403.628479,749.988831
C403.624847,753.876892 404.584320,756.067810 408.236908,758.155518
C451.188324,782.705505 493.996735,807.505737 536.834656,832.254150
C575.355164,854.508362 613.866882,876.777893 652.379028,899.046387
C658.236328,902.433167 664.075500,905.851257 670.506531,909.594543
C660.506226,915.396240 650.958069,920.955383 641.391357,926.482483
C602.367798,949.028442 563.293213,971.486938 524.376099,994.215210
C520.155334,996.680237 517.203247,996.930176 512.863708,994.408752
C454.421143,960.451721 395.851410,926.713562 337.314575,892.918823
C319.777893,882.794556 302.245758,872.662292 284.710938,862.534790
C274.721008,856.764954 264.759888,850.944214 254.717163,845.267761
C252.338959,843.923462 251.216995,842.476929 251.219849,839.499817
C251.315567,739.849976 251.312408,640.200073 251.234558,540.550232
C251.232254,537.601685 252.346344,536.241150 254.806610,534.827820
C302.775909,507.271362 350.680695,479.602600 398.927063,451.754761
z'
/>
</svg>`}
/>
{/* MANUAL-CONTENT-START:intro */}
[Autoblocks](https://www.autoblocks.ai/) is a comprehensive platform for managing, monitoring, and optimizing AI applications. It provides robust tools for prompt management that enable teams to collaborate effectively on AI prompts while maintaining version control and type safety.
With Autoblocks, you can:
- **Version and manage prompts**: Track changes, roll back to previous versions, and maintain a history of prompt iterations
- **Collaborate across teams**: Enable product, engineering, and AI teams to work together on prompt development
- **Ensure type safety**: Get autocomplete and validation for prompt variables
- **Monitor prompt performance**: Track metrics and analyze how changes affect outcomes
- **Test prompts**: Compare different versions and evaluate results before deployment
Autoblocks integrates seamlessly with your existing AI workflows in Sim Studio, providing a structured approach to prompt engineering that improves consistency and reduces errors.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Collaborate on prompts with type safety, autocomplete, and backwards-incompatibility protection. Autoblocks prompt management allows product teams to collaborate while maintaining excellent developer experience.
## Tools
### `autoblocks_prompt_manager`
Manage and render prompts using Autoblocks prompt management system
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `promptId` | string | Yes | The ID of the prompt to retrieve |
| `version` | string | Yes | Version strategy \(latest or specific\) |
| `specificVersion` | string | No | Specific version to use \(e.g., |
| `templateParams` | object | No | Parameters to render the template with |
| `apiKey` | string | Yes | Autoblocks API key |
| `enableABTesting` | boolean | No | Whether to enable A/B testing between versions |
| `abTestConfig` | object | No | Configuration for A/B testing between versions |
| `environment` | string | Yes | Environment to use \(production, staging, development\) |
#### Output
| Parameter | Type |
| --------- | ---- |
| `promptId` | string |
| `version` | string |
| `renderedPrompt` | string |
| `templates` | string |
## Block Configuration
### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `promptId` | string | Yes | Prompt ID - Enter the Autoblocks prompt ID |
### Outputs
| Output | Type | Description |
| ------ | ---- | ----------- |
| `promptId` | string | promptId output from the block |
| `version` | string | version output from the block |
| `renderedPrompt` | string | renderedPrompt output from the block |
| `templates` | json | templates output from the block |
## Notes
- Category: `tools`
- Type: `autoblocks`

View File

@@ -214,7 +214,7 @@ Populate Clay with data from a JSON file. Enables direct communication and notif
| --------- | ---- | -------- | ----------- |
| `webhookURL` | string | Yes | The webhook URL to populate |
| `data` | json | Yes | The data to populate |
| `authToken` | string | No | Optional auth token for WebhookURL |
| `authToken` | string | Yes | Auth token for Clay webhook authentication |
#### Output

View File

@@ -53,10 +53,10 @@ Convert TTS using ElevenLabs voices
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Your ElevenLabs API key |
| `text` | string | Yes | The text to convert to speech |
| `voiceId` | string | Yes | The ID of the voice to use |
| `modelId` | string | No | The ID of the model to use \(defaults to eleven_monolingual_v1\) |
| `apiKey` | string | Yes | Your ElevenLabs API key |
#### Output

View File

@@ -65,9 +65,9 @@ Extract structured content from web pages with comprehensive metadata support. C
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Firecrawl API key |
| `url` | string | Yes | The URL to scrape content from |
| `scrapeOptions` | json | No | Options for content scraping |
| `apiKey` | string | Yes | Firecrawl API key |
#### Output
@@ -85,8 +85,8 @@ Search for information on the web using Firecrawl
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Firecrawl API key |
| `query` | string | Yes | The search query to use |
| `apiKey` | string | Yes | Firecrawl API key |
#### Output

View File

@@ -85,15 +85,15 @@ Create comments on GitHub PRs
| --------- | ---- | -------- | ----------- |
| `owner` | string | Yes | Repository owner |
| `repo` | string | Yes | Repository name |
| `pullNumber` | number | Yes | Pull request number |
| `body` | string | Yes | Comment content |
| `pullNumber` | number | Yes | Pull request number |
| `path` | string | No | File path for review comment |
| `position` | number | No | Line number for review comment |
| `apiKey` | string | Yes | GitHub API token |
| `commentType` | string | No | Type of comment \(pr_comment or file_comment\) |
| `line` | number | No | Line number for review comment |
| `side` | string | No | Side of the diff \(LEFT or RIGHT\) |
| `commitId` | string | No | The SHA of the commit to comment on |
| `apiKey` | string | Yes | GitHub API token |
#### Output

View File

@@ -110,7 +110,7 @@ Draft emails using Gmail
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `operation` | string | Yes | Operation (e.g., 'send', 'draft') |
| `operation` | string | Yes | Operation |

View File

@@ -135,7 +135,8 @@ Create a new Google Docs document
| `accessToken` | string | Yes | The access token for the Google Docs API |
| `title` | string | Yes | The title of the document to create |
| `content` | string | No | The content of the document to create |
| `folderId` | string | No | The ID of the folder to create the document in |
| `folderSelector` | string | No | Select the folder to create the document in |
| `folderId` | string | No | The ID of the folder to create the document in \(internal use\) |
#### Output

View File

@@ -91,7 +91,8 @@ Upload a file to Google Drive
| `fileName` | string | Yes | The name of the file to upload |
| `content` | string | Yes | The content of the file to upload |
| `mimeType` | string | No | The MIME type of the file to upload |
| `folderId` | string | No | The ID of the folder to upload the file to |
| `folderSelector` | string | No | Select the folder to upload the file to |
| `folderId` | string | No | The ID of the folder to upload the file to \(internal use\) |
#### Output
@@ -117,7 +118,8 @@ Create a new folder in Google Drive
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Google Drive API |
| `fileName` | string | Yes | Name of the folder to create |
| `folderId` | string | No | ID of the parent folder \(leave empty for root folder\) |
| `folderSelector` | string | No | Select the parent folder to create the folder in |
| `folderId` | string | No | ID of the parent folder \(internal use\) |
#### Output
@@ -142,7 +144,8 @@ List files and folders in Google Drive
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Google Drive API |
| `folderId` | string | No | The ID of the folder to list files from |
| `folderSelector` | string | No | Select the folder to list files from |
| `folderId` | string | No | The ID of the folder to list files from \(internal use\) |
| `query` | string | No | A query to filter the files |
| `pageSize` | number | No | The number of files to return |
| `pageToken` | string | No | The page token to use for pagination |

View File

@@ -73,9 +73,9 @@ Search the web with the Custom Search API
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `query` | string | Yes | The search query to execute |
| `apiKey` | string | Yes | Google API key |
| `searchEngineId` | string | Yes | Custom Search Engine ID |
| `num` | string | No | Number of results to return \(default: 10, max: 10\) |
| `apiKey` | string | Yes | Google API key |
#### Output

View File

@@ -1,123 +0,0 @@
---
title: Guesty
description: Interact with Guesty property management system
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="guesty"
color="#0051F8"
icon={true}
iconSvg={`<svg className="block-icon"
viewBox='0 0 101 100'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M56.6019 2.6685C53.2445 0.339792 48.8025 0.308905 45.413 2.5907L44.1323 3.45286C44.1309 3.45379 44.1296 3.45471 44.1282 3.45564L5.37916 29.5416C5.37801 29.5424 5.37687 29.5431 5.37572 29.5439L4.37839 30.2153C1.64126 32.058 0 35.1414 0 38.441V90.0841C0 95.5599 4.4395 100 9.91593 100H67.4737C72.9501 100 77.389 95.5605 77.389 90.0841V49.6765C77.389 46.3038 75.675 43.1622 72.8385 41.3373L56.3027 30.6989C53.0908 28.6325 48.9777 28.5944 45.728 30.6009L28.3986 41.301C25.4732 43.1073 23.6922 46.3001 23.6922 49.7382V75.553H33.3248V51.0025C33.3248 50.1189 33.7823 49.2983 34.5337 48.8337L34.535 48.8329L49.5731 39.5476C50.408 39.0322 51.4645 39.0414 52.29 39.5714L66.5886 48.7705C67.3167 49.24 67.7564 50.0471 67.7564 50.9134V87.8176C67.7564 89.2256 66.6152 90.3674 65.2072 90.3674H12.1824C10.7742 90.3674 9.63262 89.2256 9.63262 87.8176V39.6474C9.63262 38.7995 10.0541 38.0071 10.7571 37.5331L49.5075 11.4463C50.3783 10.8601 51.5192 10.8675 52.3822 11.4646L89.8995 37.4867C89.9007 37.4877 89.9024 37.4886 89.9035 37.4896C90.588 37.9663 90.9959 38.7476 90.9959 39.5819V100H100.629V38.3956C100.629 35.1448 99.0352 32.1005 96.3641 30.2478L95.3969 29.5767C95.3941 29.575 95.3918 29.5733 95.3895 29.5717L56.6019 2.6685Z'
fill='currentColor'
/>
</svg>`}
/>
{/* MANUAL-CONTENT-START:intro */}
[Guesty](https://www.guesty.com) is a comprehensive property management platform designed for short-term and vacation rental property managers. It provides a centralized system to manage listings, reservations, guest communications, and operations across multiple booking channels like Airbnb, Booking.com, and VRBO.
With Guesty, property managers can:
- **Centralize operations**: Manage multiple properties and listings from a single dashboard
- **Automate workflows**: Set up automated messaging, task assignments, and cleaning schedules
- **Synchronize calendars**: Keep availability updated across all booking channels
- **Process payments**: Handle secure payment processing and financial reporting
- **Manage guest communications**: Streamline guest interactions through unified inbox
- **Generate reports**: Access analytics and insights to optimize property performance
In Sim Studio, the Guesty integration enables your agents to interact directly with your property management system programmatically. This allows for powerful automation scenarios such as reservation management, guest communication, and operational workflows. Your agents can retrieve detailed reservation information by ID, including guest details, booking dates, and property information. They can also search for guests by phone number to access their profiles and booking history. This integration bridges the gap between your AI workflows and your property management operations, enabling seamless handling of hospitality tasks without manual intervention. By connecting Sim Studio with Guesty, you can automate guest communications, streamline check-in processes, manage reservation details, and enhance the overall guest experience through intelligent automation.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Access Guesty property management data including reservations and guest information. Retrieve reservation details by ID or search for guests by phone number.
## Tools
### `guesty_reservation`
Fetch reservation details from Guesty by reservation ID
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Your Guesty API token |
| `reservationId` | string | Yes | The ID of the reservation to fetch |
#### Output
| Parameter | Type |
| --------- | ---- |
| `id` | string |
| `guest` | string |
| `email` | string |
| `phone` | string |
### `guesty_guest`
Search for guests in Guesty by phone number
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Your Guesty API token |
| `phoneNumber` | string | Yes | The phone number to search for |
#### Output
| Parameter | Type |
| --------- | ---- |
| `guests` | string |
| `fullName` | string |
| `email` | string |
| `phone` | string |
| `address` | string |
| `city` | string |
| `country` | string |
## Block Configuration
### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `action` | string | Yes | Action |
### Outputs
| Output | Type | Description |
| ------ | ---- | ----------- |
| `id` | string | id output from the block |
| `guest` | json | guest output from the block |
| `checkIn` | string | checkIn output from the block |
| `checkOut` | string | checkOut output from the block |
| `status` | string | status output from the block |
| `listing` | json | listing output from the block |
| `money` | json | money output from the block |
| `guests` | json | guests output from the block |
## Notes
- Category: `tools`
- Type: `guesty`

View File

@@ -80,14 +80,13 @@ Generate completions using Hugging Face Inference API
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Hugging Face API token |
| `systemPrompt` | string | No | System prompt to guide the model behavior |
| `content` | string | Yes | The user message content to send to the model |
| `provider` | string | Yes | The provider to use for the API request \(e.g., novita, cerebras, etc.\) |
| `model` | string | Yes | Model to use for chat completions \(e.g., deepseek/deepseek-v3-0324\) |
| `content` | string | Yes | The user message content to send to the model |
| `systemPrompt` | string | No | System prompt to guide the model behavior |
| `maxTokens` | number | No | Maximum number of tokens to generate |
| `temperature` | number | No | Sampling temperature \(0-2\). Higher values make output more random |
| `stream` | boolean | No | Whether to stream the response |
| `apiKey` | string | Yes | Hugging Face API token |
#### Output

View File

@@ -60,8 +60,8 @@ Generate images using OpenAI
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `prompt` | string | Yes | A text description of the desired image |
| `model` | string | Yes | The model to use \(gpt-image-1 or dall-e-3\) |
| `prompt` | string | Yes | A text description of the desired image |
| `size` | string | Yes | The size of the generated images \(1024x1024, 1024x1792, or 1792x1024\) |
| `quality` | string | No | The quality of the image \(standard or hd\) |
| `style` | string | No | The style of the image \(vivid or natural\) |

View File

@@ -78,6 +78,10 @@ Extract and process web content into clean, LLM-friendly text using Jina AI Read
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `url` | string | Yes | The URL to read and convert to markdown |
| `useReaderLMv2` | boolean | No | Whether to use ReaderLM-v2 for better quality |
| `gatherLinks` | boolean | No | Whether to gather all links at the end |
| `jsonResponse` | boolean | No | Whether to return response in JSON format |
| `apiKey` | string | Yes | Your Jina AI API key |
#### Output

View File

@@ -81,6 +81,7 @@ Search for similar content in one or more knowledge bases using vector similarit
| `results` | string |
| `query` | string |
| `totalResults` | string |
| `cost` | string |
### `knowledge_upload_chunk`

View File

@@ -65,9 +65,9 @@ Search the web for information using Linkup
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `q` | string | Yes | The search query |
| `apiKey` | string | Yes | Enter your Linkup API key |
| `depth` | string | Yes | Search depth \(has to either be |
| `outputType` | string | Yes | Type of output to return \(has to either be |
| `apiKey` | string | Yes | Enter your Linkup API key |
#### Output

View File

@@ -58,9 +58,9 @@ Add memories to Mem0 for persistent storage and retrieval
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Your Mem0 API key |
| `userId` | string | Yes | User ID associated with the memory |
| `messages` | json | Yes | Array of message objects with role and content |
| `apiKey` | string | Yes | Your Mem0 API key |
#### Output
@@ -76,10 +76,10 @@ Search for memories in Mem0 using semantic search
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Your Mem0 API key |
| `userId` | string | Yes | User ID to search memories for |
| `query` | string | Yes | Search query to find relevant memories |
| `limit` | number | No | Maximum number of results to return |
| `apiKey` | string | Yes | Your Mem0 API key |
#### Output
@@ -96,12 +96,12 @@ Retrieve memories from Mem0 by ID or filter criteria
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Your Mem0 API key |
| `userId` | string | Yes | User ID to retrieve memories for |
| `memoryId` | string | No | Specific memory ID to retrieve |
| `startDate` | string | No | Start date for filtering by created_at \(format: YYYY-MM-DD\) |
| `endDate` | string | No | End date for filtering by created_at \(format: YYYY-MM-DD\) |
| `limit` | number | No | Maximum number of results to return |
| `apiKey` | string | Yes | Your Mem0 API key |
#### Output

View File

@@ -2,7 +2,6 @@
"items": [
"index",
"airtable",
"autoblocks",
"browser_use",
"clay",
"confluence",
@@ -18,7 +17,6 @@
"google_drive",
"google_search",
"google_sheets",
"guesty",
"huggingface",
"image_generator",
"jina",

View File

@@ -96,11 +96,11 @@ Parse PDF documents using Mistral OCR API
| `filePath` | string | Yes | URL to a PDF document to be processed |
| `fileUpload` | object | No | File upload data from file-upload component |
| `resultType` | string | No | Type of parsed result \(markdown, text, or json\). Defaults to markdown. |
| `apiKey` | string | Yes | Mistral API key \(MISTRAL_API_KEY\) |
| `includeImageBase64` | boolean | No | Include base64-encoded images in the response |
| `pages` | array | No | Specific pages to process \(array of page numbers, starting from 0\) |
| `imageLimit` | number | No | Maximum number of images to extract from the PDF |
| `imageMinSize` | number | No | Minimum height and width of images to extract from the PDF |
| `apiKey` | string | Yes | Mistral API key \(MISTRAL_API_KEY\) |
#### Output

View File

@@ -49,8 +49,8 @@ Read content from a Notion page
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `pageId` | string | Yes | The ID of the Notion page to read |
| `accessToken` | string | Yes | Notion OAuth access token |
| `pageId` | string | Yes | The ID of the Notion page to read |
#### Output
@@ -70,9 +70,9 @@ Append content to a Notion page
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | Notion OAuth access token |
| `pageId` | string | Yes | The ID of the Notion page to append content to |
| `content` | string | Yes | The content to append to the page |
| `accessToken` | string | Yes | Notion OAuth access token |
#### Output
@@ -88,12 +88,12 @@ Create a new page in Notion
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | Notion OAuth access token |
| `parentType` | string | Yes | Type of parent: |
| `parentId` | string | Yes | ID of the parent page or database |
| `title` | string | No | Title of the page \(required for parent pages, not for databases\) |
| `properties` | json | No | JSON object of properties for database pages |
| `content` | string | No | Optional content to add to the page upon creation |
| `accessToken` | string | Yes | Notion OAuth access token |
#### Output

View File

@@ -57,11 +57,10 @@ Generate embeddings from text using OpenAI
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | OpenAI API key |
| `input` | string | Yes | Text to generate embeddings for |
| `model` | string | No | Model to use for embeddings |
| `encoding_format` | string | No | The format to return the embeddings in |
| `user` | string | No | A unique identifier for the end-user |
| `encodingFormat` | string | No | The format to return the embeddings in |
| `apiKey` | string | Yes | OpenAI API key |
#### Output

View File

@@ -51,11 +51,12 @@ Generate completions using Perplexity AI chat models
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Perplexity API key |
| `systemPrompt` | string | No | System prompt to guide the model behavior |
| `content` | string | Yes | The user message content to send to the model |
| `model` | string | Yes | Model to use for chat completions \(e.g., sonar, mistral\) |
| `messages` | array | Yes | Array of message objects with role and content |
| `max_tokens` | number | No | Maximum number of tokens to generate |
| `temperature` | number | No | Sampling temperature between 0 and 1 |
| `apiKey` | string | Yes | Perplexity API key |
#### Output
@@ -75,7 +76,7 @@ Generate completions using Perplexity AI chat models
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `prompt` | string | Yes | User Prompt - Enter your prompt here... |
| `content` | string | Yes | User Prompt - Enter your prompt here... |

View File

@@ -59,9 +59,9 @@ Generate embeddings from text using Pinecone
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Pinecone API key |
| `model` | string | Yes | Model to use for generating embeddings |
| `inputs` | array | Yes | Array of text inputs to generate embeddings for |
| `apiKey` | string | Yes | Pinecone API key |
#### Output
@@ -80,10 +80,10 @@ Insert or update text records in a Pinecone index
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Pinecone API key |
| `indexHost` | string | Yes | Full Pinecone index host URL |
| `namespace` | string | Yes | Namespace to upsert records into |
| `records` | array | Yes | Record or array of records to upsert, each containing _id, text, and optional metadata |
| `apiKey` | string | Yes | Pinecone API key |
#### Output
@@ -99,7 +99,6 @@ Search for similar text in a Pinecone index
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Pinecone API key |
| `indexHost` | string | Yes | Full Pinecone index host URL |
| `namespace` | string | No | Namespace to search in |
| `searchQuery` | string | Yes | Text to search for |
@@ -107,6 +106,7 @@ Search for similar text in a Pinecone index
| `fields` | array | No | Fields to return in the results |
| `filter` | object | No | Filter to apply to the search |
| `rerank` | object | No | Reranking parameters |
| `apiKey` | string | Yes | Pinecone API key |
#### Output
@@ -124,7 +124,6 @@ Search for similar vectors in a Pinecone index
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Pinecone API key |
| `indexHost` | string | Yes | Full Pinecone index host URL |
| `namespace` | string | No | Namespace to search in |
| `vector` | array | Yes | Vector to search for |
@@ -132,6 +131,7 @@ Search for similar vectors in a Pinecone index
| `filter` | object | No | Filter to apply to the search |
| `includeValues` | boolean | No | Include vector values in response |
| `includeMetadata` | boolean | No | Include metadata in response |
| `apiKey` | string | Yes | Pinecone API key |
#### Output
@@ -150,10 +150,10 @@ Fetch vectors by ID from a Pinecone index
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Pinecone API key |
| `indexHost` | string | Yes | Full Pinecone index host URL |
| `ids` | array | Yes | Array of vector IDs to fetch |
| `namespace` | string | No | Namespace to fetch vectors from |
| `apiKey` | string | Yes | Pinecone API key |
#### Output

View File

@@ -50,24 +50,6 @@ Access Reddit data to retrieve posts and comments from any subreddit. Get post t
## Tools
### `reddit_hot_posts`
Fetch the most popular (hot) posts from a specified subreddit.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `subreddit` | string | Yes | The name of the subreddit to fetch posts from \(without the r/ prefix\) |
| `limit` | number | No | Maximum number of posts to return \(default: 10, max: 100\) |
#### Output
| Parameter | Type |
| --------- | ---- |
| `subreddit` | string |
| `posts` | string |
### `reddit_get_posts`
Fetch posts from a subreddit with different sorting options
@@ -76,6 +58,7 @@ Fetch posts from a subreddit with different sorting options
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | Access token for Reddit API |
| `subreddit` | string | Yes | The name of the subreddit to fetch posts from \(without the r/ prefix\) |
| `sort` | string | No | Sort method for posts: |
| `limit` | number | No | Maximum number of posts to return \(default: 10, max: 100\) |
@@ -96,6 +79,7 @@ Fetch comments from a specific Reddit post
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | Access token for Reddit API |
| `postId` | string | Yes | The ID of the Reddit post to fetch comments from |
| `subreddit` | string | Yes | The subreddit where the post is located \(without the r/ prefix\) |
| `sort` | string | No | Sort method for comments: |
@@ -121,7 +105,7 @@ Fetch comments from a specific Reddit post
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `action` | string | Yes | Action |
| `operation` | string | Yes | Operation |

View File

@@ -93,11 +93,11 @@ A powerful web search tool that provides access to Google search results through
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `query` | string | Yes | The search query |
| `apiKey` | string | Yes | Serper API Key |
| `num` | number | No | Number of results to return |
| `gl` | string | No | Country code for search results |
| `hl` | string | No | Language code for search results |
| `type` | string | No | Type of search to perform |
| `apiKey` | string | Yes | Serper API Key |
#### Output

View File

@@ -70,6 +70,7 @@ Send messages to Slack channels or users through the Slack API. Supports Slack m
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `authMethod` | string | No | Authentication method: oauth or bot_token |
| `botToken` | string | No | Bot token for Custom Bot |
| `accessToken` | string | No | OAuth access token or bot token for Slack API |
| `channel` | string | Yes | Target Slack channel \(e.g., #general\) |

View File

@@ -205,10 +205,10 @@ Extract structured data from a webpage using Stagehand
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `instruction` | string | Yes | Instructions for extraction |
| `schema` | json | Yes | JSON schema defining the structure of the data to extract |
| `apiKey` | string | Yes | OpenAI API key for extraction \(required by Stagehand\) |
| `url` | string | Yes | URL of the webpage to extract data from |
| `instruction` | string | Yes | Instructions for extraction |
| `apiKey` | string | Yes | OpenAI API key for extraction \(required by Stagehand\) |
| `schema` | json | Yes | JSON schema defining the structure of the data to extract |
#### Output

View File

@@ -209,11 +209,11 @@ Run an autonomous web agent to complete tasks and extract structured data
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `task` | string | Yes | The task to complete or goal to achieve on the website |
| `startUrl` | string | Yes | URL of the webpage to start the agent on |
| `outputSchema` | json | No | Optional JSON schema defining the structure of data the agent should return |
| `task` | string | Yes | The task to complete or goal to achieve on the website |
| `variables` | json | No | Optional variables to substitute in the task \(format: \{key: value\}\). Reference in task using %key% |
| `apiKey` | string | Yes | OpenAI API key for agent execution \(required by Stagehand\) |
| `outputSchema` | json | No | Optional JSON schema defining the structure of data the agent should return |
#### Output

View File

@@ -83,8 +83,10 @@ Query data from a Supabase table
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Your Supabase client anon key |
| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) |
| `table` | string | Yes | The name of the Supabase table to query |
| `filter` | object | No | Filter to apply to the query |
| `apiKey` | string | Yes | Your Supabase client anon key |
#### Output
@@ -101,8 +103,10 @@ Insert data into a Supabase table
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Your Supabase client anon key |
| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) |
| `table` | string | Yes | The name of the Supabase table to insert data into |
| `data` | any | Yes | The data to insert |
| `apiKey` | string | Yes | Your Supabase client anon key |
#### Output

View File

@@ -95,8 +95,8 @@ Extract raw content from multiple web pages simultaneously using Tavily
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `urls` | string | Yes | URL or array of URLs to extract content from |
| `apiKey` | string | Yes | Tavily API Key |
| `extract_depth` | string | No | The depth of extraction \(basic=1 credit/5 URLs, advanced=2 credits/5 URLs\) |
| `apiKey` | string | Yes | Tavily API Key |
#### Output

View File

@@ -55,8 +55,8 @@ Search for videos on YouTube using the YouTube Data API.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `query` | string | Yes | Search query for YouTube videos |
| `apiKey` | string | Yes | YouTube API Key |
| `maxResults` | number | No | Maximum number of videos to return |
| `apiKey` | string | Yes | YouTube API Key |
#### Output

View File

@@ -513,7 +513,6 @@ export async function POST(req: NextRequest) {
// } else {
logger.info(`[${requestId}] Using VM for code execution`, {
resolvedCode,
executionParams,
hasEnvVars: Object.keys(envVars).length > 0,
})

View File

@@ -10,7 +10,7 @@ import {
ConditionalIcon,
ConnectIcon,
} from '@/components/icons'
import { cn } from '@/lib/utils'
import { cn, redactApiKeys } from '@/lib/utils'
import type { TraceSpan } from '../../stores/types'
interface TraceSpansDisplayProps {
@@ -25,24 +25,7 @@ function transformBlockData(data: any, blockType: string, isInput: boolean) {
// For input data, filter out sensitive information
if (isInput) {
const cleanInput = { ...data }
// Remove sensitive fields (common API keys and tokens)
if (cleanInput.apiKey) {
cleanInput.apiKey = '***'
}
if (cleanInput.azureApiKey) {
cleanInput.azureApiKey = '***'
}
if (cleanInput.token) {
cleanInput.token = '***'
}
if (cleanInput.accessToken) {
cleanInput.accessToken = '***'
}
if (cleanInput.authorization) {
cleanInput.authorization = '***'
}
const cleanInput = redactApiKeys(data)
// Remove null/undefined values for cleaner display
Object.keys(cleanInput).forEach((key) => {
@@ -112,7 +95,7 @@ function CollapsibleInputOutput({ span, spanId }: CollapsibleInputOutputProps) {
<div>
<button
onClick={() => setInputExpanded(!inputExpanded)}
className='flex items-center gap-2 mb-2 font-medium text-muted-foreground text-xs hover:text-foreground transition-colors'
className='mb-2 flex items-center gap-2 font-medium text-muted-foreground text-xs transition-colors hover:text-foreground'
>
{inputExpanded ? (
<ChevronDown className='h-3 w-3' />
@@ -134,7 +117,7 @@ function CollapsibleInputOutput({ span, spanId }: CollapsibleInputOutputProps) {
<div>
<button
onClick={() => setOutputExpanded(!outputExpanded)}
className='flex items-center gap-2 mb-2 font-medium text-muted-foreground text-xs hover:text-foreground transition-colors'
className='mb-2 flex items-center gap-2 font-medium text-muted-foreground text-xs transition-colors hover:text-foreground'
>
{outputExpanded ? (
<ChevronDown className='h-3 w-3' />

View File

@@ -51,9 +51,11 @@ export function ComboBox({
const [searchTerm, setSearchTerm] = useState('')
const [cursorPosition, setCursorPosition] = useState(0)
const [activeSourceBlockId, setActiveSourceBlockId] = useState<string | null>(null)
const [highlightedIndex, setHighlightedIndex] = useState(-1)
const inputRef = useRef<HTMLInputElement>(null)
const overlayRef = useRef<HTMLDivElement>(null)
const dropdownRef = useRef<HTMLDivElement>(null)
const reactFlowInstance = useReactFlow()
// Use preview value when in preview mode, otherwise use store value or prop value
@@ -167,6 +169,7 @@ export function ComboBox({
setStoreValue(selectedValue)
}
setOpen(false)
setHighlightedIndex(-1)
inputRef.current?.blur()
}
@@ -184,9 +187,10 @@ export function ComboBox({
const handleFocus = () => {
setIsFocused(true)
setOpen(true)
setHighlightedIndex(-1)
}
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
const handleBlur = () => {
setIsFocused(false)
setShowEnvVars(false)
setShowTags(false)
@@ -196,6 +200,7 @@ export function ComboBox({
const activeElement = document.activeElement
if (!activeElement || !activeElement.closest('.absolute.top-full')) {
setOpen(false)
setHighlightedIndex(-1)
}
}, 150)
}
@@ -205,12 +210,33 @@ export function ComboBox({
setShowEnvVars(false)
setShowTags(false)
setOpen(false)
setHighlightedIndex(-1)
return
}
if (e.key === 'ArrowDown' && !open) {
setOpen(true)
if (e.key === 'ArrowDown') {
e.preventDefault()
if (!open) {
setOpen(true)
setHighlightedIndex(0)
} else {
setHighlightedIndex((prev) => (prev < filteredOptions.length - 1 ? prev + 1 : 0))
}
}
if (e.key === 'ArrowUp') {
e.preventDefault()
if (open) {
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : filteredOptions.length - 1))
}
}
if (e.key === 'Enter' && open && highlightedIndex >= 0) {
e.preventDefault()
const selectedOption = filteredOptions[highlightedIndex]
if (selectedOption) {
handleSelect(getOptionValue(selectedOption))
}
}
}
@@ -315,6 +341,31 @@ export function ComboBox({
}
}, [value])
// Reset highlighted index when filtered options change, but preserve if within bounds
useEffect(() => {
setHighlightedIndex((prev) => {
if (prev >= 0 && prev < filteredOptions.length) {
return prev
}
return -1
})
}, [filteredOptions])
// Scroll highlighted option into view
useEffect(() => {
if (highlightedIndex >= 0 && dropdownRef.current) {
const highlightedElement = dropdownRef.current.querySelector(
`[data-option-index="${highlightedIndex}"]`
)
if (highlightedElement) {
highlightedElement.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
})
}
}
}, [highlightedIndex])
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Element
@@ -325,6 +376,7 @@ export function ComboBox({
!target.closest('.absolute.top-full')
) {
setOpen(false)
setHighlightedIndex(-1)
}
}
@@ -394,6 +446,7 @@ export function ComboBox({
<div className='absolute top-full left-0 z-[100] mt-1 w-full min-w-[286px]'>
<div className='allow-scroll fade-in-0 zoom-in-95 animate-in rounded-md border bg-popover text-popover-foreground shadow-lg'>
<div
ref={dropdownRef}
className='allow-scroll max-h-48 overflow-y-auto p-1'
style={{ scrollbarWidth: 'thin' }}
>
@@ -402,7 +455,7 @@ export function ComboBox({
No matching options found.
</div>
) : (
filteredOptions.map((option) => {
filteredOptions.map((option, index) => {
const optionValue = getOptionValue(option)
const optionLabel = getOptionLabel(option)
const OptionIcon =
@@ -410,16 +463,22 @@ export function ComboBox({
? (option.icon as React.ComponentType<{ className?: string }>)
: null
const isSelected = displayValue === optionValue || displayValue === optionLabel
const isHighlighted = index === highlightedIndex
return (
<div
key={optionValue}
data-option-index={index}
onClick={() => handleSelect(optionValue)}
onMouseDown={(e) => {
e.preventDefault()
handleSelect(optionValue)
}}
className='relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground'
onMouseEnter={() => setHighlightedIndex(index)}
className={cn(
'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground',
isHighlighted && 'bg-accent text-accent-foreground'
)}
>
{OptionIcon && <OptionIcon className='mr-2 h-3 w-3 opacity-60' />}
<span className='flex-1 truncate'>{optionLabel}</span>

View File

@@ -105,7 +105,7 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
'messages.read': 'Read your Discord messages',
guilds: 'Read your Discord guilds',
'guilds.members.read': 'Read your Discord guild members',
read: 'Read access to your Linear workspace',
read: 'Read access to your workspace',
write: 'Write access to your Linear workspace',
'channels:read': 'Read your Slack channels',
'groups:read': 'Read your Slack private channels',

View File

@@ -113,8 +113,8 @@ export function Dropdown({
}}
disabled={isPreview || disabled}
>
<SelectTrigger className='text-left'>
<SelectValue placeholder='Select an option' />
<SelectTrigger className='min-w-0 text-left'>
<SelectValue placeholder='Select an option' className='truncate' />
</SelectTrigger>
<SelectContent className='max-h-48'>
{evaluatedOptions.map((option) => (

View File

@@ -376,20 +376,22 @@ export function ConfluenceFileSelector({
variant='outline'
role='combobox'
aria-expanded={open}
className='w-full justify-between'
className='h-10 w-full min-w-0 justify-between'
disabled={disabled || !domain}
>
{selectedFile ? (
<div className='flex items-center gap-2 overflow-hidden'>
<ConfluenceIcon className='h-4 w-4' />
<span className='truncate font-normal'>{selectedFile.name}</span>
</div>
) : (
<div className='flex items-center gap-2'>
<ConfluenceIcon className='h-4 w-4' />
<span className='text-muted-foreground'>{label}</span>
</div>
)}
<div className='flex min-w-0 items-center gap-2 overflow-hidden'>
{selectedFile ? (
<>
<ConfluenceIcon className='h-4 w-4' />
<span className='truncate font-normal'>{selectedFile.name}</span>
</>
) : (
<>
<ConfluenceIcon className='h-4 w-4' />
<span className='truncate text-muted-foreground'>{label}</span>
</>
)}
</div>
<ChevronDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
</Button>
</PopoverTrigger>

View File

@@ -220,20 +220,22 @@ export function DiscordChannelSelector({
variant='outline'
role='combobox'
aria-expanded={open}
className='w-full justify-between'
className='h-10 w-full min-w-0 justify-between'
disabled={disabled || !botToken || !serverId}
>
{selectedChannel ? (
<div className='flex items-center gap-2 overflow-hidden'>
<span className='text-muted-foreground'>#</span>
<span className='truncate font-normal'>{selectedChannel.name}</span>
</div>
) : (
<div className='flex items-center gap-2'>
<DiscordIcon className='h-4 w-4' />
<span className='text-muted-foreground'>{label}</span>
</div>
)}
<div className='flex min-w-0 items-center gap-2 overflow-hidden'>
{selectedChannel ? (
<>
<span className='text-muted-foreground'>#</span>
<span className='truncate font-normal'>{selectedChannel.name}</span>
</>
) : (
<>
<DiscordIcon className='h-4 w-4' />
<span className='truncate text-muted-foreground'>{label}</span>
</>
)}
</div>
<ChevronDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
</Button>
</PopoverTrigger>

View File

@@ -206,27 +206,29 @@ export function GoogleCalendarSelector({
variant='outline'
role='combobox'
aria-expanded={open}
className='w-full justify-between'
className='h-10 w-full min-w-0 justify-between'
disabled={disabled || !credentialId}
>
{selectedCalendar ? (
<div className='flex items-center gap-2 overflow-hidden'>
<div
className='h-3 w-3 flex-shrink-0 rounded-full'
style={{
backgroundColor: selectedCalendar.backgroundColor || '#4285f4',
}}
/>
<span className='truncate font-normal'>
{getCalendarDisplayName(selectedCalendar)}
</span>
</div>
) : (
<div className='flex items-center gap-2'>
<GoogleCalendarIcon className='h-4 w-4' />
<span className='text-muted-foreground'>{label}</span>
</div>
)}
<div className='flex min-w-0 items-center gap-2 overflow-hidden'>
{selectedCalendar ? (
<>
<div
className='h-3 w-3 flex-shrink-0 rounded-full'
style={{
backgroundColor: selectedCalendar.backgroundColor || '#4285f4',
}}
/>
<span className='truncate font-normal'>
{getCalendarDisplayName(selectedCalendar)}
</span>
</>
) : (
<>
<GoogleCalendarIcon className='h-4 w-4' />
<span className='truncate text-muted-foreground'>{label}</span>
</>
)}
</div>
<ChevronDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
</Button>
</PopoverTrigger>

View File

@@ -432,25 +432,27 @@ export function GoogleDrivePicker({
variant='outline'
role='combobox'
aria-expanded={open}
className='w-full justify-between'
className='h-10 w-full min-w-0 justify-between'
disabled={disabled}
>
{selectedFile ? (
<div className='flex items-center gap-2 overflow-hidden'>
{getFileIcon(selectedFile, 'sm')}
<span className='truncate font-normal'>{selectedFile.name}</span>
</div>
) : selectedFileId && isLoadingSelectedFile && selectedCredentialId ? (
<div className='flex items-center gap-2'>
<RefreshCw className='h-4 w-4 animate-spin' />
<span className='text-muted-foreground'>Loading document...</span>
</div>
) : (
<div className='flex items-center gap-2'>
{getProviderIcon(provider)}
<span className='text-muted-foreground'>{label}</span>
</div>
)}
<div className='flex min-w-0 items-center gap-2 overflow-hidden'>
{selectedFile ? (
<>
{getFileIcon(selectedFile, 'sm')}
<span className='truncate font-normal'>{selectedFile.name}</span>
</>
) : selectedFileId && isLoadingSelectedFile && selectedCredentialId ? (
<>
<RefreshCw className='h-4 w-4 animate-spin' />
<span className='truncate text-muted-foreground'>Loading document...</span>
</>
) : (
<>
{getProviderIcon(provider)}
<span className='truncate text-muted-foreground'>{label}</span>
</>
)}
</div>
<ChevronDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
</Button>
</PopoverTrigger>

View File

@@ -442,20 +442,22 @@ export function JiraIssueSelector({
variant='outline'
role='combobox'
aria-expanded={open}
className='w-full justify-between'
className='h-10 w-full min-w-0 justify-between'
disabled={disabled || !domain}
>
{selectedIssue ? (
<div className='flex items-center gap-2 overflow-hidden'>
<JiraIcon className='h-4 w-4' />
<span className='truncate font-normal'>{selectedIssue.name}</span>
</div>
) : (
<div className='flex items-center gap-2'>
<JiraIcon className='h-4 w-4' />
<span className='text-muted-foreground'>{label}</span>
</div>
)}
<div className='flex min-w-0 items-center gap-2 overflow-hidden'>
{selectedIssue ? (
<>
<JiraIcon className='h-4 w-4' />
<span className='truncate font-normal'>{selectedIssue.name}</span>
</>
) : (
<>
<JiraIcon className='h-4 w-4' />
<span className='truncate text-muted-foreground'>{label}</span>
</>
)}
</div>
<ChevronDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
</Button>
</PopoverTrigger>

View File

@@ -414,25 +414,27 @@ export function MicrosoftFileSelector({
variant='outline'
role='combobox'
aria-expanded={open}
className='w-full justify-between'
className='h-10 w-full min-w-0 justify-between'
disabled={disabled}
>
{selectedFile ? (
<div className='flex items-center gap-2 overflow-hidden'>
{getFileIcon(selectedFile, 'sm')}
<span className='truncate font-normal'>{selectedFile.name}</span>
</div>
) : selectedFileId && isLoadingSelectedFile && selectedCredentialId ? (
<div className='flex items-center gap-2'>
<RefreshCw className='h-4 w-4 animate-spin' />
<span className='text-muted-foreground'>Loading document...</span>
</div>
) : (
<div className='flex items-center gap-2'>
{getProviderIcon(provider)}
<span className='text-muted-foreground'>{label}</span>
</div>
)}
<div className='flex min-w-0 items-center gap-2 overflow-hidden'>
{selectedFile ? (
<>
{getFileIcon(selectedFile, 'sm')}
<span className='truncate font-normal'>{selectedFile.name}</span>
</>
) : selectedFileId && isLoadingSelectedFile && selectedCredentialId ? (
<>
<RefreshCw className='h-4 w-4 animate-spin' />
<span className='truncate text-muted-foreground'>Loading document...</span>
</>
) : (
<>
{getProviderIcon(provider)}
<span className='truncate text-muted-foreground'>{label}</span>
</>
)}
</div>
<ChevronDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
</Button>
</PopoverTrigger>

View File

@@ -701,24 +701,26 @@ export function TeamsMessageSelector({
variant='outline'
role='combobox'
aria-expanded={open}
className='w-full justify-between'
className='h-10 w-full min-w-0 justify-between'
disabled={disabled}
>
{selectedMessage ? (
<div className='flex items-center gap-2 overflow-hidden'>
<MicrosoftTeamsIcon className='h-4 w-4' />
<span className='truncate font-normal'>{selectedMessage.displayName}</span>
</div>
) : (
<div className='flex items-center gap-2'>
<MicrosoftTeamsIcon className='h-4 w-4' />
<span className='text-muted-foreground'>
{selectionType === 'channel' && selectionStage === 'team'
? 'Select a team first'
: label}
</span>
</div>
)}
<div className='flex min-w-0 items-center gap-2 overflow-hidden'>
{selectedMessage ? (
<>
<MicrosoftTeamsIcon className='h-4 w-4' />
<span className='truncate font-normal'>{selectedMessage.displayName}</span>
</>
) : (
<>
<MicrosoftTeamsIcon className='h-4 w-4' />
<span className='truncate text-muted-foreground'>
{selectionType === 'channel' && selectionStage === 'team'
? 'Select a team first'
: label}
</span>
</>
)}
</div>
<ChevronDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
</Button>
</PopoverTrigger>

View File

@@ -20,13 +20,15 @@ export function SliderInput({
subBlockId,
min = 0,
max = 100,
defaultValue = 50,
defaultValue,
step = 0.1,
integer = false,
isPreview = false,
previewValue,
disabled = false,
}: SliderInputProps) {
// Smart default value: if no default provided, use midpoint or 0.7 for 0-1 ranges
const computedDefaultValue = defaultValue ?? (max <= 1 ? 0.7 : (min + max) / 2)
const [storeValue, setStoreValue] = useSubBlockValue<number>(blockId, subBlockId)
// Use preview value when in preview mode, otherwise use store value
@@ -34,9 +36,11 @@ export function SliderInput({
// Clamp the value within bounds while preserving relative position when possible
const normalizedValue =
value !== null && value !== undefined ? Math.max(min, Math.min(max, value)) : defaultValue
value !== null && value !== undefined
? Math.max(min, Math.min(max, value))
: computedDefaultValue
const displayValue = normalizedValue ?? defaultValue
const displayValue = normalizedValue ?? computedDefaultValue
// Ensure the normalized value is set if it differs from the current value
useEffect(() => {

View File

@@ -86,10 +86,28 @@ export function ToolCredentialSelector({
const data = await response.json()
setCredentials(data.credentials || [])
// If we have a selected value but it's not in the credentials list, clear it
// If we have a value but it's not in the credentials, reset it
if (value && !data.credentials?.some((cred: Credential) => cred.id === value)) {
onChange('')
}
// Auto-selection logic (like credential-selector):
// 1. If we already have a valid selection, keep it
// 2. If there's a default credential, select it
// 3. If there's only one credential, select it
if (
(!value || !data.credentials?.some((cred: Credential) => cred.id === value)) &&
data.credentials &&
data.credentials.length > 0
) {
const defaultCred = data.credentials.find((cred: Credential) => cred.isDefault)
if (defaultCred) {
onChange(defaultCred.id)
} else if (data.credentials.length === 1) {
// If only one credential, select it
onChange(data.credentials[0].id)
}
}
} else {
logger.error('Error fetching credentials:', { error: await response.text() })
setCredentials([])
@@ -102,9 +120,26 @@ export function ToolCredentialSelector({
}
}, [provider, value, onChange])
// Fetch credentials on mount and when provider changes
// Fetch credentials on initial mount only
useEffect(() => {
fetchCredentials()
// This effect should only run once on mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// Listen for visibility changes to update credentials when user returns from settings
useEffect(() => {
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible') {
fetchCredentials()
}
}
document.addEventListener('visibilitychange', handleVisibilityChange)
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange)
}
}, [fetchCredentials])
const handleSelect = (credentialId: string) => {
@@ -119,30 +154,41 @@ export function ToolCredentialSelector({
fetchCredentials()
}
// Handle popover open to fetch fresh credentials
const handleOpenChange = (isOpen: boolean) => {
setOpen(isOpen)
if (isOpen) {
// Fetch fresh credentials when opening the dropdown
fetchCredentials()
}
}
const selectedCredential = credentials.find((cred) => cred.id === selectedId)
return (
<>
<Popover open={open} onOpenChange={setOpen}>
<Popover open={open} onOpenChange={handleOpenChange}>
<PopoverTrigger asChild>
<Button
variant='outline'
role='combobox'
aria-expanded={open}
className='w-full justify-between'
className='h-10 w-full min-w-0 justify-between'
disabled={disabled}
>
{selectedCredential ? (
<div className='flex items-center gap-2 overflow-hidden'>
{getProviderIcon(provider)}
<span className='truncate font-normal'>{selectedCredential.name}</span>
</div>
) : (
<div className='flex items-center gap-2'>
{getProviderIcon(provider)}
<span className='text-muted-foreground'>{label}</span>
</div>
)}
<div className='flex min-w-0 items-center gap-2 overflow-hidden'>
{selectedCredential ? (
<>
{getProviderIcon(provider)}
<span className='truncate font-normal'>{selectedCredential.name}</span>
</>
) : (
<>
{getProviderIcon(provider)}
<span className='truncate text-muted-foreground'>{label}</span>
</>
)}
</div>
<ChevronDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
</Button>
</PopoverTrigger>

View File

@@ -115,7 +115,7 @@ export function SubBlock({
<Dropdown
blockId={blockId}
subBlockId={config.id}
options={config.options as string[]}
options={config.options as { label: string; id: string }[]}
isPreview={isPreview}
previewValue={previewValue}
disabled={isDisabled}
@@ -128,7 +128,7 @@ export function SubBlock({
<ComboBox
blockId={blockId}
subBlockId={config.id}
options={config.options as string[]}
options={config.options as { label: string; id: string }[]}
placeholder={config.placeholder}
isPreview={isPreview}
previewValue={previewValue}

View File

@@ -33,6 +33,7 @@ interface ExecutorOptions {
selectedOutputIds?: string[]
edges?: Array<{ source: string; target: string }>
onStream?: (streamingExecution: StreamingExecution) => Promise<void>
executionId?: string
}
}
@@ -203,7 +204,7 @@ export function useWorkflowExecution() {
}
try {
const result = await executeWorkflow(workflowInput, onStream)
const result = await executeWorkflow(workflowInput, onStream, executionId)
await Promise.all(streamReadingPromises)
@@ -216,14 +217,18 @@ export function useWorkflowExecution() {
if (result.logs) {
result.logs.forEach((log: BlockLog) => {
if (streamedContent.has(log.blockId)) {
const content = streamedContent.get(log.blockId) || ''
// For console display, show the actual structured block output instead of formatted streaming content
// This ensures console logs match the block state structure
// Use replaceOutput to completely replace the output instead of merging
useConsoleStore.getState().updateConsole(log.blockId, {
replaceOutput: log.output,
success: true,
})
// Use the executionId from this execution context
useConsoleStore.getState().updateConsole(
log.blockId,
{
replaceOutput: log.output,
success: true,
},
executionId
)
}
})
@@ -262,7 +267,7 @@ export function useWorkflowExecution() {
// For manual (non-chat) execution
const executionId = uuidv4()
try {
const result = await executeWorkflow(workflowInput)
const result = await executeWorkflow(workflowInput, undefined, executionId)
if (result && 'metadata' in result && result.metadata?.isDebugSession) {
setDebugContext(result.metadata.context || null)
if (result.metadata.pendingBlocks) {
@@ -330,7 +335,8 @@ export function useWorkflowExecution() {
const executeWorkflow = async (
workflowInput?: any,
onStream?: (se: StreamingExecution) => Promise<void>
onStream?: (se: StreamingExecution) => Promise<void>,
executionId?: string
): Promise<ExecutionResult | StreamingExecution> => {
// Use the mergeSubblockState utility to get all block states
const mergedStates = mergeSubblockState(blocks)
@@ -401,6 +407,7 @@ export function useWorkflowExecution() {
target: conn.target,
})),
onStream,
executionId,
},
}

View File

@@ -25,7 +25,13 @@ export const ApiBlock: BlockConfig<RequestResponse> = {
title: 'Method',
type: 'dropdown',
layout: 'half',
options: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
options: [
{ label: 'GET', id: 'GET' },
{ label: 'POST', id: 'POST' },
{ label: 'PUT', id: 'PUT' },
{ label: 'DELETE', id: 'DELETE' },
{ label: 'PATCH', id: 'PATCH' },
],
},
{
id: 'params',

View File

@@ -1,120 +0,0 @@
import { AutoblocksIcon } from '@/components/icons'
import type { ToolResponse } from '@/tools/types'
import type { BlockConfig } from '../types'
interface AutoblocksResponse extends ToolResponse {
output: {
promptId: string
version: string
renderedPrompt: string
templates: Record<string, string>
}
}
export const AutoblocksBlock: BlockConfig<AutoblocksResponse> = {
type: 'autoblocks',
name: 'Autoblocks',
description: 'Manage and use versioned prompts with Autoblocks',
longDescription:
'Collaborate on prompts with type safety, autocomplete, and backwards-incompatibility protection. Autoblocks prompt management allows product teams to collaborate while maintaining excellent developer experience.',
category: 'tools',
bgColor: '#0D2929',
icon: AutoblocksIcon,
subBlocks: [
{
id: 'promptId',
title: 'Prompt ID',
type: 'short-input',
layout: 'full',
placeholder: 'Enter the Autoblocks prompt ID',
},
{
id: 'version',
title: 'Version',
type: 'dropdown',
layout: 'half',
options: [
{ label: 'Latest Minor', id: 'latest' },
{ label: 'Specific Version', id: 'specific' },
],
value: () => 'latest',
},
{
id: 'specificVersion',
title: 'Specific Version',
type: 'short-input',
layout: 'half',
placeholder: 'e.g. 1.2 or 1.latest',
condition: {
field: 'version',
value: 'specific',
},
},
{
id: 'apiKey',
title: 'API Key',
type: 'short-input',
layout: 'full',
placeholder: 'Enter your Autoblocks API key',
password: true,
},
{
id: 'templateParams',
title: 'Template Parameters',
type: 'code',
layout: 'full',
language: 'json',
placeholder: '{"param1": "value1", "param2": "value2"}',
},
{
id: 'enableABTesting',
title: 'Enable A/B Testing',
type: 'switch',
layout: 'half',
},
{
id: 'abTestConfig',
title: 'A/B Test Configuration',
type: 'code',
layout: 'full',
language: 'json',
placeholder:
'{"versions": [{"version": "0", "weight": 95}, {"version": "latest", "weight": 5}]}',
condition: {
field: 'enableABTesting',
value: true,
},
},
{
id: 'environment',
title: 'Environment',
type: 'dropdown',
layout: 'half',
options: [
{ label: 'Production', id: 'production' },
{ label: 'Staging', id: 'staging' },
{ label: 'Development', id: 'development' },
],
value: () => 'development',
},
],
tools: {
access: ['autoblocks_prompt_manager'],
},
inputs: {
promptId: { type: 'string', required: true },
version: { type: 'string', required: true },
specificVersion: { type: 'string', required: false },
templateParams: { type: 'json', required: false },
apiKey: { type: 'string', required: true },
enableABTesting: { type: 'boolean', required: false },
abTestConfig: { type: 'json', required: false },
environment: { type: 'string', required: true },
},
outputs: {
promptId: 'string',
version: 'string',
renderedPrompt: 'string',
templates: 'json',
},
}

View File

@@ -57,6 +57,19 @@ export const ElevenLabsBlock: BlockConfig<ElevenLabsBlockResponse> = {
layout: 'full',
placeholder: 'Enter the voice ID',
},
{
id: 'modelId',
title: 'Model ID',
type: 'dropdown',
layout: 'half',
options: [
{ label: 'eleven_monolingual_v1', id: 'eleven_monolingual_v1' },
{ label: 'eleven_multilingual_v2', id: 'eleven_multilingual_v2' },
{ label: 'eleven_turbo_v2', id: 'eleven_turbo_v2' },
{ label: 'eleven_turbo_v2_5', id: 'eleven_turbo_v2_5' },
{ label: 'eleven_flash_v2_5', id: 'eleven_flash_v2_5' },
],
},
{
id: 'apiKey',
title: 'API Key',
@@ -65,18 +78,5 @@ export const ElevenLabsBlock: BlockConfig<ElevenLabsBlockResponse> = {
placeholder: 'Enter your ElevenLabs API key',
password: true,
},
{
id: 'modelId',
title: 'Model ID (Optional)',
type: 'dropdown',
layout: 'half',
options: [
'eleven_monolingual_v1',
'eleven_multilingual_v2',
'eleven_turbo_v2',
'eleven_turbo_v2_5',
'eleven_flash_v2_5',
],
},
],
}

View File

@@ -176,7 +176,10 @@ export const EvaluatorBlock: BlockConfig<EvaluatorResponse> = {
options: () => {
const ollamaModels = useOllamaStore.getState().models
const baseModels = Object.keys(getBaseModelProviders())
return [...baseModels, ...ollamaModels]
return [...baseModels, ...ollamaModels].map((model) => ({
label: model,
id: model,
}))
},
},
{

View File

@@ -1,94 +0,0 @@
import { GuestyIcon } from '@/components/icons'
import type { GuestyGuestResponse, GuestyReservationResponse } from '@/tools/guesty/types'
import type { BlockConfig } from '../types'
export const GuestyBlock: BlockConfig<GuestyReservationResponse | GuestyGuestResponse> = {
type: 'guesty',
name: 'Guesty',
description: 'Interact with Guesty property management system',
longDescription:
'Access Guesty property management data including reservations and guest information. Retrieve reservation details by ID or search for guests by phone number.',
docsLink: 'https://docs.simstudio.ai/tools/guesty',
category: 'tools',
bgColor: '#0051F8', // Guesty brand color
icon: GuestyIcon,
subBlocks: [
{
id: 'action',
title: 'Action',
type: 'dropdown',
layout: 'full',
options: [
{ label: 'Get Reservation', id: 'reservation' },
{ label: 'Search Guest', id: 'guest' },
],
},
{
id: 'reservationId',
title: 'Reservation ID',
type: 'short-input',
layout: 'full',
placeholder: 'Enter reservation ID',
condition: {
field: 'action',
value: 'reservation',
},
},
{
id: 'phoneNumber',
title: 'Phone Number',
type: 'short-input',
layout: 'full',
placeholder: 'Enter phone number',
condition: {
field: 'action',
value: 'guest',
},
},
{
id: 'apiKey',
title: 'API Key',
type: 'short-input',
layout: 'full',
placeholder: 'Enter your Guesty API key',
password: true,
connectionDroppable: false,
},
],
tools: {
access: ['guesty_reservation', 'guesty_guest'],
config: {
tool: (params) => {
return params.action === 'reservation' ? 'guesty_reservation' : 'guesty_guest'
},
params: (params) => {
if (params.action === 'reservation') {
return {
apiKey: params.apiKey,
reservationId: params.reservationId,
}
}
return {
apiKey: params.apiKey,
phoneNumber: params.phoneNumber,
}
},
},
},
inputs: {
action: { type: 'string', required: true },
apiKey: { type: 'string', required: true },
reservationId: { type: 'string', required: false },
phoneNumber: { type: 'string', required: false },
},
outputs: {
id: 'string',
guest: 'json',
checkIn: 'string',
checkOut: 'string',
status: 'string',
listing: 'json',
money: 'json',
guests: 'json',
},
}

View File

@@ -19,9 +19,9 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
type: 'dropdown',
layout: 'full',
options: [
{ label: 'Read Page', id: 'read_notion' },
{ label: 'Append Content', id: 'write_notion' },
{ label: 'Create Page', id: 'create_notion' },
{ label: 'Read Page', id: 'notion_read' },
{ label: 'Append Content', id: 'notion_write' },
{ label: 'Create Page', id: 'notion_create_page' },
],
},
{
@@ -43,7 +43,7 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
placeholder: 'Enter Notion page ID',
condition: {
field: 'operation',
value: 'read_notion',
value: 'notion_read',
},
},
{
@@ -54,7 +54,7 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
placeholder: 'Enter Notion page ID',
condition: {
field: 'operation',
value: 'write_notion',
value: 'notion_write',
},
},
// Create operation fields
@@ -67,7 +67,7 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
{ label: 'Page', id: 'page' },
{ label: 'Database', id: 'database' },
],
condition: { field: 'operation', value: 'create_notion' },
condition: { field: 'operation', value: 'notion_create_page' },
},
{
id: 'parentId',
@@ -75,7 +75,7 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
type: 'short-input',
layout: 'full',
placeholder: 'ID of parent page or database',
condition: { field: 'operation', value: 'create_notion' },
condition: { field: 'operation', value: 'notion_create_page' },
},
{
id: 'title',
@@ -85,7 +85,7 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
placeholder: 'Title for the new page',
condition: {
field: 'operation',
value: 'create_notion',
value: 'notion_create_page',
and: { field: 'parentType', value: 'page' },
},
},
@@ -97,7 +97,7 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
placeholder: 'Enter page properties as JSON object',
condition: {
field: 'operation',
value: 'create_notion',
value: 'notion_create_page',
},
},
// Content input for write/create operations
@@ -109,7 +109,7 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
placeholder: 'Enter content to add to the page',
condition: {
field: 'operation',
value: 'write_notion',
value: 'notion_write',
},
},
{
@@ -120,7 +120,7 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
placeholder: 'Enter content to add to the page',
condition: {
field: 'operation',
value: 'create_notion',
value: 'notion_create_page',
},
},
],
@@ -129,11 +129,11 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
config: {
tool: (params) => {
switch (params.operation) {
case 'read_notion':
case 'notion_read':
return 'notion_read'
case 'write_notion':
case 'notion_write':
return 'notion_write'
case 'create_notion':
case 'notion_create_page':
return 'notion_create_page'
default:
return 'notion_read'
@@ -144,7 +144,7 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
// Parse properties from JSON string for create operations
let parsedProperties
if (operation === 'create_notion' && properties) {
if (operation === 'notion_create_page' && properties) {
try {
parsedProperties = JSON.parse(properties)
} catch (error) {

View File

@@ -26,14 +26,14 @@ export const PerplexityBlock: BlockConfig<PerplexityChatResponse> = {
icon: PerplexityIcon,
subBlocks: [
{
id: 'system',
id: 'systemPrompt',
title: 'System Prompt',
type: 'long-input',
layout: 'full',
placeholder: 'Optional system prompt to guide the model behavior...',
},
{
id: 'prompt',
id: 'content',
title: 'User Prompt',
type: 'long-input',
layout: 'full',
@@ -87,8 +87,8 @@ export const PerplexityBlock: BlockConfig<PerplexityChatResponse> = {
const toolParams = {
apiKey: params.apiKey,
model: params.model,
prompt: params.prompt,
system: params.system,
content: params.content,
systemPrompt: params.systemPrompt,
max_tokens: params.max_tokens ? Number.parseInt(params.max_tokens) : undefined,
temperature: params.temperature ? Number.parseFloat(params.temperature) : undefined,
}
@@ -98,8 +98,8 @@ export const PerplexityBlock: BlockConfig<PerplexityChatResponse> = {
},
},
inputs: {
prompt: { type: 'string', required: true },
system: { type: 'string', required: false },
content: { type: 'string', required: true },
systemPrompt: { type: 'string', required: false },
model: { type: 'string', required: true },
max_tokens: { type: 'string', required: false },
temperature: { type: 'string', required: false },

View File

@@ -19,10 +19,10 @@ export const RedditBlock: BlockConfig<
bgColor: '#FF5700',
icon: RedditIcon,
subBlocks: [
// Action selection
// Operation selection
{
id: 'action',
title: 'Action',
id: 'operation',
title: 'Operation',
type: 'dropdown',
layout: 'full',
options: [
@@ -51,7 +51,7 @@ export const RedditBlock: BlockConfig<
layout: 'full',
placeholder: 'Enter subreddit name (without r/)',
condition: {
field: 'action',
field: 'operation',
value: ['get_posts', 'get_comments'],
},
},
@@ -69,7 +69,7 @@ export const RedditBlock: BlockConfig<
{ label: 'Rising', id: 'rising' },
],
condition: {
field: 'action',
field: 'operation',
value: 'get_posts',
},
},
@@ -86,7 +86,7 @@ export const RedditBlock: BlockConfig<
{ label: 'All Time', id: 'all' },
],
condition: {
field: 'action',
field: 'operation',
value: 'get_posts',
and: {
field: 'sort',
@@ -101,7 +101,7 @@ export const RedditBlock: BlockConfig<
layout: 'full',
placeholder: '10',
condition: {
field: 'action',
field: 'operation',
value: 'get_posts',
},
},
@@ -114,7 +114,7 @@ export const RedditBlock: BlockConfig<
layout: 'full',
placeholder: 'Enter post ID',
condition: {
field: 'action',
field: 'operation',
value: 'get_comments',
},
},
@@ -133,7 +133,7 @@ export const RedditBlock: BlockConfig<
{ label: 'Q&A', id: 'qa' },
],
condition: {
field: 'action',
field: 'operation',
value: 'get_comments',
},
},
@@ -144,28 +144,28 @@ export const RedditBlock: BlockConfig<
layout: 'full',
placeholder: '50',
condition: {
field: 'action',
field: 'operation',
value: 'get_comments',
},
},
],
tools: {
access: ['reddit_hot_posts', 'reddit_get_posts', 'reddit_get_comments'],
access: ['reddit_get_posts', 'reddit_get_comments'],
config: {
tool: (inputs) => {
const action = inputs.action || 'get_posts'
const operation = inputs.operation || 'get_posts'
if (action === 'get_comments') {
if (operation === 'get_comments') {
return 'reddit_get_comments'
}
return 'reddit_get_posts'
},
params: (inputs) => {
const action = inputs.action || 'get_posts'
const operation = inputs.operation || 'get_posts'
const { credential, ...rest } = inputs
if (action === 'get_comments') {
if (operation === 'get_comments') {
return {
postId: rest.postId,
subreddit: rest.subreddit,
@@ -186,7 +186,7 @@ export const RedditBlock: BlockConfig<
},
},
inputs: {
action: { type: 'string', required: true },
operation: { type: 'string', required: true },
credential: { type: 'string', required: true },
subreddit: { type: 'string', required: true },
sort: { type: 'string', required: true },

View File

@@ -120,7 +120,10 @@ export const RouterBlock: BlockConfig<RouterResponse> = {
options: () => {
const ollamaModels = useOllamaStore.getState().models
const baseModels = Object.keys(getBaseModelProviders())
return [...baseModels, ...ollamaModels]
return [...baseModels, ...ollamaModels].map((model) => ({
label: model,
id: model,
}))
},
},
{

View File

@@ -25,28 +25,53 @@ export const SerperBlock: BlockConfig<SearchResponse> = {
title: 'Search Type',
type: 'dropdown',
layout: 'half',
options: ['search', 'news', 'places', 'images'],
options: [
{ label: 'search', id: 'search' },
{ label: 'news', id: 'news' },
{ label: 'places', id: 'places' },
{ label: 'images', id: 'images' },
],
},
{
id: 'num',
title: 'Number of Results',
type: 'dropdown',
layout: 'half',
options: ['10', '20', '30', '40', '50', '100'],
options: [
{ label: '10', id: '10' },
{ label: '20', id: '20' },
{ label: '30', id: '30' },
{ label: '40', id: '40' },
{ label: '50', id: '50' },
{ label: '100', id: '100' },
],
},
{
id: 'gl',
title: 'Country',
type: 'dropdown',
layout: 'half',
options: ['US', 'GB', 'CA', 'AU', 'DE', 'FR', 'ES', 'IT', 'JP', 'KR'],
options: [
{ label: 'US', id: 'US' },
{ label: 'GB', id: 'GB' },
{ label: 'CA', id: 'CA' },
{ label: 'AU', id: 'AU' },
{ label: 'DE', id: 'DE' },
{ label: 'FR', id: 'FR' },
],
},
{
id: 'hl',
title: 'Language',
type: 'dropdown',
layout: 'half',
options: ['en', 'es', 'fr', 'de', 'it', 'pt', 'ja', 'ko', 'zh'],
options: [
{ label: 'en', id: 'en' },
{ label: 'es', id: 'es' },
{ label: 'fr', id: 'fr' },
{ label: 'de', id: 'de' },
{ label: 'it', id: 'it' },
],
},
{
id: 'apiKey',

View File

@@ -76,10 +76,6 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
layout: 'full',
provider: 'slack',
placeholder: 'Select Slack channel',
condition: {
field: 'operation',
value: ['send'],
},
},
{
id: 'text',
@@ -87,10 +83,6 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
type: 'long-input',
layout: 'full',
placeholder: 'Enter your message (supports Slack mrkdwn)',
condition: {
field: 'operation',
value: ['send'],
},
},
],
tools: {

View File

@@ -101,12 +101,12 @@ export const SupabaseBlock: BlockConfig<SupabaseResponse> = {
},
},
inputs: {
operation: { type: 'string', required: true, requiredForToolCall: true },
projectId: { type: 'string', required: true, requiredForToolCall: true },
table: { type: 'string', required: true, requiredForToolCall: true },
apiKey: { type: 'string', required: true, requiredForToolCall: true },
operation: { type: 'string', required: true },
projectId: { type: 'string', required: true },
table: { type: 'string', required: true },
apiKey: { type: 'string', required: true },
// Insert operation inputs
data: { type: 'string', required: false, requiredForToolCall: true },
data: { type: 'string', required: false },
},
outputs: {
message: 'string',

View File

@@ -67,7 +67,10 @@ export const TavilyBlock: BlockConfig<TavilyResponse> = {
title: 'Extract Depth',
type: 'dropdown',
layout: 'full',
options: ['basic', 'advanced'],
options: [
{ label: 'basic', id: 'basic' },
{ label: 'advanced', id: 'advanced' },
],
value: () => 'basic',
condition: { field: 'operation', value: 'tavily_extract' },
},

View File

@@ -44,7 +44,7 @@ export const TranslateBlock: BlockConfig = {
title: 'Model',
type: 'dropdown',
layout: 'half',
options: Object.keys(getBaseModelProviders()),
options: Object.keys(getBaseModelProviders()).map((key) => ({ label: key, id: key })),
},
{
id: 'apiKey',

View File

@@ -25,7 +25,11 @@ export const VisionBlock: BlockConfig<VisionResponse> = {
title: 'Vision Model',
type: 'dropdown',
layout: 'half',
options: ['gpt-4o', 'claude-3-opus-20240229', 'claude-3-sonnet-20240229'],
options: [
{ label: 'gpt-4o', id: 'gpt-4o' },
{ label: 'claude-3-opus', id: 'claude-3-opus-20240229' },
{ label: 'claude-3-sonnet', id: 'claude-3-sonnet-20240229' },
],
},
{
id: 'prompt',

View File

@@ -79,7 +79,10 @@ export const XBlock: BlockConfig<XResponse> = {
title: 'Include Replies',
type: 'dropdown',
layout: 'full',
options: ['true', 'false'],
options: [
{ label: 'true', id: 'true' },
{ label: 'false', id: 'false' },
],
value: () => 'false',
condition: { field: 'operation', value: 'x_read' },
},
@@ -105,7 +108,10 @@ export const XBlock: BlockConfig<XResponse> = {
title: 'Sort Order',
type: 'dropdown',
layout: 'full',
options: ['recency', 'relevancy'],
options: [
{ label: 'recency', id: 'recency' },
{ label: 'relevancy', id: 'relevancy' },
],
value: () => 'recency',
condition: { field: 'operation', value: 'x_search' },
},

View File

@@ -6,7 +6,6 @@
import { AgentBlock } from './blocks/agent'
import { AirtableBlock } from './blocks/airtable'
import { ApiBlock } from './blocks/api'
// import { AutoblocksBlock } from './blocks/autoblocks'
import { BrowserUseBlock } from './blocks/browser_use'
import { ClayBlock } from './blocks/clay'
import { ConditionBlock } from './blocks/condition'
@@ -26,7 +25,6 @@ import { GoogleDocsBlock } from './blocks/google_docs'
import { GoogleDriveBlock } from './blocks/google_drive'
import { GoogleSheetsBlock } from './blocks/google_sheets'
import { HuggingFaceBlock } from './blocks/huggingface'
// import { GuestyBlock } from './blocks/guesty'
import { ImageGeneratorBlock } from './blocks/image_generator'
import { JinaBlock } from './blocks/jina'
import { JiraBlock } from './blocks/jira'
@@ -34,7 +32,6 @@ import { KnowledgeBlock } from './blocks/knowledge'
import { LinearBlock } from './blocks/linear'
import { LinkupBlock } from './blocks/linkup'
import { Mem0Block } from './blocks/mem0'
// import { GuestyBlock } from './blocks/guesty'
import { MemoryBlock } from './blocks/memory'
import { MicrosoftExcelBlock } from './blocks/microsoft_excel'
import { MicrosoftTeamsBlock } from './blocks/microsoft_teams'
@@ -72,7 +69,6 @@ export const registry: Record<string, BlockConfig> = {
agent: AgentBlock,
airtable: AirtableBlock,
api: ApiBlock,
// autoblocks: AutoblocksBlock,
browser_use: BrowserUseBlock,
clay: ClayBlock,
condition: ConditionBlock,
@@ -91,15 +87,17 @@ export const registry: Record<string, BlockConfig> = {
google_drive: GoogleDriveBlock,
google_search: GoogleSearchBlock,
google_sheets: GoogleSheetsBlock,
microsoft_excel: MicrosoftExcelBlock,
microsoft_teams: MicrosoftTeamsBlock,
// guesty: GuestyBlock,
huggingface: HuggingFaceBlock,
image_generator: ImageGeneratorBlock,
jina: JinaBlock,
jira: JiraBlock,
knowledge: KnowledgeBlock,
linear: LinearBlock,
linkup: LinkupBlock,
mem0: Mem0Block,
memory: MemoryBlock,
microsoft_excel: MicrosoftExcelBlock,
microsoft_teams: MicrosoftTeamsBlock,
mistral_parse: MistralParseBlock,
notion: NotionBlock,
openai: OpenAIBlock,
@@ -107,15 +105,14 @@ export const registry: Record<string, BlockConfig> = {
perplexity: PerplexityBlock,
pinecone: PineconeBlock,
reddit: RedditBlock,
response: ResponseBlock,
router: RouterBlock,
memory: MemoryBlock,
s3: S3Block,
serper: SerperBlock,
stagehand: StagehandBlock,
stagehand_agent: StagehandAgentBlock,
slack: SlackBlock,
starter: StarterBlock,
knowledge: KnowledgeBlock,
supabase: SupabaseBlock,
tavily: TavilyBlock,
telegram: TelegramBlock,
@@ -128,8 +125,6 @@ export const registry: Record<string, BlockConfig> = {
workflow: WorkflowBlock,
x: XBlock,
youtube: YouTubeBlock,
huggingface: HuggingFaceBlock,
response: ResponseBlock,
}
// Helper functions to access the registry

View File

@@ -68,7 +68,6 @@ export type BlockOutput =
export interface ParamConfig {
type: ParamType
required: boolean
requiredForToolCall?: boolean
description?: string
schema?: {
type: string
@@ -92,11 +91,8 @@ export interface SubBlockConfig {
layout?: SubBlockLayout
mode?: 'basic' | 'advanced' | 'both' // Default is 'both' if not specified
options?:
| string[]
| { label: string; id: string; icon?: React.ComponentType<{ className?: string }> }[]
| (() =>
| string[]
| { label: string; id: string; icon?: React.ComponentType<{ className?: string }> }[])
| (() => { label: string; id: string; icon?: React.ComponentType<{ className?: string }> }[])
min?: number
max?: number
columns?: string[]

View File

@@ -1773,24 +1773,6 @@ export function StripeIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function GuestyIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
width='101'
height='100'
viewBox='0 0 101 100'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M56.6019 2.6685C53.2445 0.339792 48.8025 0.308905 45.413 2.5907L44.1323 3.45286C44.1309 3.45379 44.1296 3.45471 44.1282 3.45564L5.37916 29.5416C5.37801 29.5424 5.37687 29.5431 5.37572 29.5439L4.37839 30.2153C1.64126 32.058 0 35.1414 0 38.441V90.0841C0 95.5599 4.4395 100 9.91593 100H67.4737C72.9501 100 77.389 95.5605 77.389 90.0841V49.6765C77.389 46.3038 75.675 43.1622 72.8385 41.3373L56.3027 30.6989C53.0908 28.6325 48.9777 28.5944 45.728 30.6009L28.3986 41.301C25.4732 43.1073 23.6922 46.3001 23.6922 49.7382V75.553H33.3248V51.0025C33.3248 50.1189 33.7823 49.2983 34.5337 48.8337L34.535 48.8329L49.5731 39.5476C50.408 39.0322 51.4645 39.0414 52.29 39.5714L66.5886 48.7705C67.3167 49.24 67.7564 50.0471 67.7564 50.9134V87.8176C67.7564 89.2256 66.6152 90.3674 65.2072 90.3674H12.1824C10.7742 90.3674 9.63262 89.2256 9.63262 87.8176V39.6474C9.63262 38.7995 10.0541 38.0071 10.7571 37.5331L49.5075 11.4463C50.3783 10.8601 51.5192 10.8675 52.3822 11.4646L89.8995 37.4867C89.9007 37.4877 89.9024 37.4886 89.9035 37.4896C90.588 37.9663 90.9959 38.7476 90.9959 39.5819V100H100.629V38.3956C100.629 35.1448 99.0352 32.1005 96.3641 30.2478L95.3969 29.5767C95.3941 29.575 95.3918 29.5733 95.3895 29.5717L56.6019 2.6685Z'
fill='currentColor'
/>
</svg>
)
}
export function EyeIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
@@ -2165,109 +2147,6 @@ export function StagehandIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function AutoblocksIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
version='1.1'
id='Layer_1'
xmlns='http://www.w3.org/2000/svg'
xmlnsXlink='http://www.w3.org/1999/xlink'
x='0px'
y='0px'
width='100%'
viewBox='0 0 1250 1250'
enableBackground='new 0 0 1250 1250'
xmlSpace='preserve'
>
<path
fill='#FFFFFF'
opacity='1.000000'
stroke='none'
d='
M671.222290,1079.959839
C671.176025,1077.962891 671.089233,1075.965820 671.089111,1073.968872
C671.082825,918.318481 671.062683,762.668091 671.192322,607.017761
C671.195862,602.748474 669.789551,600.693787 666.180847,598.638306
C636.091125,581.500183 606.140991,564.117126 576.145508,546.813599
C556.393311,535.419128 536.677856,523.960449 516.869568,512.664307
C495.246002,500.332977 473.461487,488.282806 451.883911,475.872253
C434.220825,465.713257 416.802856,455.129089 399.195587,444.871857
C379.466736,433.378601 359.648438,422.038818 339.866608,410.636597
C320.229004,399.317505 300.588470,388.003510 280.948822,376.688019
C271.840149,371.440033 262.730530,366.193695 253.057938,360.622070
C267.185272,352.478241 280.655273,344.713531 294.125092,336.948517
C329.023163,316.830566 363.943237,296.750366 398.783295,276.532349
C402.073059,274.623260 404.534790,274.139191 408.118988,276.252319
C435.683502,292.503723 463.371948,308.546082 491.084290,324.545258
C509.340118,335.084839 527.725525,345.399719 546.006958,355.895203
C585.713440,378.690979 625.427124,401.474670 665.069397,424.381744
C705.530884,447.762177 745.895203,471.310669 786.336243,494.726715
C796.959717,500.877930 807.667236,506.888184 818.432190,512.787903
C820.966064,514.176636 821.763611,515.816772 821.762329,518.659241
C821.692932,676.145020 821.688171,833.630737 821.793762,991.116455
C821.795837,994.184937 820.514771,995.521545 818.222412,996.837891
C782.578491,1017.306641 746.954346,1037.809570 711.333679,1058.318848
C698.839661,1065.512573 686.367554,1072.744629 673.219116,1079.994141
C672.109314,1080.006104 671.665771,1079.982910 671.222290,1079.959839
z'
/>
<path
fill='#FFFFFF'
opacity='1.000000'
stroke='none'
d='
M684.421631,400.605865
C600.749390,352.376038 517.388306,304.342010 433.717010,256.129181
C455.858643,243.338989 477.724731,230.689346 499.608948,218.071136
C526.744324,202.425217 553.916504,186.842911 581.002014,171.111252
C583.487793,169.667450 585.282104,169.727783 587.700562,171.126724
C627.018250,193.870560 666.389465,216.521790 705.739136,239.210449
C744.537903,261.581543 783.343262,283.941437 822.113525,306.361786
C854.544006,325.115936 886.886658,344.022156 919.345703,362.726379
C945.337769,377.704102 971.415039,392.534851 997.539551,407.280151
C1001.126465,409.304749 1002.459045,411.581146 1002.455444,415.839966
C1002.322388,571.647339 1002.315430,727.454834 1002.468750,883.262207
C1002.473694,888.329590 1001.184082,891.101135 996.646118,893.690186
C949.437134,920.624695 902.383667,947.831665 855.284607,974.958862
C854.453491,975.437500 853.591980,975.863708 851.884216,976.772095
C851.884216,974.236023 851.884216,972.347290 851.884216,970.458557
C851.884216,814.817688 851.876099,659.176880 851.927551,503.536011
C851.928955,499.372650 851.416870,497.004883 846.802246,494.523651
C829.014954,484.959839 811.879517,474.190002 794.417969,464.012421
C774.549316,452.431854 754.597900,440.993225 734.670959,429.512817
C718.033508,419.927551 701.379517,410.370911 684.421631,400.605865
z'
/>
<path
fill='#FFFFFF'
opacity='1.000000'
stroke='none'
d='
M398.927063,451.754761
C400.510162,450.940521 401.764893,450.328430 403.700867,449.383972
C403.700867,452.154175 403.700897,454.096252 403.700897,456.038330
C403.700897,554.021851 403.720520,652.005371 403.628479,749.988831
C403.624847,753.876892 404.584320,756.067810 408.236908,758.155518
C451.188324,782.705505 493.996735,807.505737 536.834656,832.254150
C575.355164,854.508362 613.866882,876.777893 652.379028,899.046387
C658.236328,902.433167 664.075500,905.851257 670.506531,909.594543
C660.506226,915.396240 650.958069,920.955383 641.391357,926.482483
C602.367798,949.028442 563.293213,971.486938 524.376099,994.215210
C520.155334,996.680237 517.203247,996.930176 512.863708,994.408752
C454.421143,960.451721 395.851410,926.713562 337.314575,892.918823
C319.777893,882.794556 302.245758,872.662292 284.710938,862.534790
C274.721008,856.764954 264.759888,850.944214 254.717163,845.267761
C252.338959,843.923462 251.216995,842.476929 251.219849,839.499817
C251.315567,739.849976 251.312408,640.200073 251.234558,540.550232
C251.232254,537.601685 252.346344,536.241150 254.806610,534.827820
C302.775909,507.271362 350.680695,479.602600 398.927063,451.754761
z'
/>
</svg>
)
}
export function BrowserUseIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg

View File

@@ -128,7 +128,7 @@ export class AgentBlockHandler implements BlockHandler {
})
.map(async (tool) => {
if (tool.type === 'custom-tool' && tool.schema) {
return this.createCustomTool(tool, context)
return await this.createCustomTool(tool, context)
}
return this.transformBlockTool(tool, context)
})
@@ -139,26 +139,36 @@ export class AgentBlockHandler implements BlockHandler {
)
}
private createCustomTool(tool: ToolInput, context: ExecutionContext): any {
private async createCustomTool(tool: ToolInput, context: ExecutionContext): Promise<any> {
const userProvidedParams = tool.params || {}
// Import the utility function
const { filterSchemaForLLM, mergeToolParameters } = await import('../../../tools/params')
// Create schema excluding user-provided parameters
const filteredSchema = filterSchemaForLLM(tool.schema.function.parameters, userProvidedParams)
const toolId = `${CUSTOM_TOOL_PREFIX}${tool.title}`
const base: any = {
id: `${CUSTOM_TOOL_PREFIX}${tool.title}`,
id: toolId,
name: tool.schema.function.name,
description: tool.schema.function.description || '',
params: tool.params || {},
params: userProvidedParams,
parameters: {
...filteredSchema,
type: tool.schema.function.parameters.type,
properties: tool.schema.function.parameters.properties,
required: tool.schema.function.parameters.required || [],
},
usageControl: tool.usageControl || 'auto',
}
if (tool.code) {
base.executeFunction = async (callParams: Record<string, any>) => {
// Merge user-provided parameters with LLM-generated parameters
const mergedParams = mergeToolParameters(userProvidedParams, callParams)
const result = await executeTool('function_execute', {
code: tool.code,
...tool.params,
...callParams,
...mergedParams,
timeout: tool.timeout ?? DEFAULT_FUNCTION_TIMEOUT,
envVars: context.environmentVariables || {},
isCustomTool: true,
@@ -753,13 +763,16 @@ export class AgentBlockHandler implements BlockHandler {
}
private formatToolCall(tc: any) {
const toolName = this.stripCustomToolPrefix(tc.name)
return {
...tc,
name: this.stripCustomToolPrefix(tc.name),
name: toolName,
startTime: tc.startTime,
endTime: tc.endTime,
duration: tc.duration,
input: tc.arguments || tc.input,
arguments: tc.arguments || tc.input || {},
input: tc.arguments || tc.input || {}, // Keep both for backward compatibility
output: tc.result || tc.output,
}
}

View File

@@ -84,6 +84,7 @@ export class Executor {
selectedOutputIds?: string[]
edges?: Array<{ source: string; target: string }>
onStream?: (streamingExecution: StreamingExecution) => Promise<void>
executionId?: string
}
},
private initialBlockStates: Record<string, BlockOutput> = {},
@@ -1353,6 +1354,7 @@ export class Executor {
endedAt: blockLog.endedAt,
workflowId: context.workflowId,
blockId: parallelInfo ? blockId : block.id,
executionId: this.contextExtensions.executionId,
blockName: parallelInfo
? `${block.metadata?.name || 'Unnamed Block'} (iteration ${
parallelInfo.iterationIndex + 1
@@ -1421,6 +1423,7 @@ export class Executor {
endedAt: blockLog.endedAt,
workflowId: context.workflowId,
blockId: parallelInfo ? blockId : block.id,
executionId: this.contextExtensions.executionId,
blockName: parallelInfo
? `${block.metadata?.name || 'Unnamed Block'} (iteration ${
parallelInfo.iterationIndex + 1
@@ -1490,6 +1493,7 @@ export class Executor {
endedAt: blockLog.endedAt,
workflowId: context.workflowId,
blockId: parallelInfo ? blockId : block.id,
executionId: this.contextExtensions.executionId,
blockName: parallelInfo
? `${block.metadata?.name || 'Unnamed Block'} (iteration ${parallelInfo.iterationIndex + 1})`
: block.metadata?.name || 'Unnamed Block',

View File

@@ -449,19 +449,25 @@ ${fieldDescriptions}
// Execute the tool
const toolCallStartTime = Date.now()
const mergedArgs = {
// Only merge actual tool parameters for logging
const toolParams = {
...tool.params,
...toolArgs,
}
// Add system parameters for execution
const executionParams = {
...toolParams,
...(request.workflowId ? { _context: { workflowId: request.workflowId } } : {}),
...(request.environmentVariables ? { envVars: request.environmentVariables } : {}),
}
const result = await executeTool(toolName, mergedArgs, true)
const result = await executeTool(toolName, executionParams, true)
const toolCallEndTime = Date.now()
const toolCallDuration = toolCallEndTime - toolCallStartTime
if (!result.success) continue
// Add to time segments
// Add to time segments for both success and failure
timeSegments.push({
type: 'tool',
name: toolName,
@@ -470,17 +476,31 @@ ${fieldDescriptions}
duration: toolCallDuration,
})
toolResults.push(result.output)
// Prepare result content for the LLM
let resultContent: any
if (result.success) {
toolResults.push(result.output)
resultContent = result.output
} else {
// Include error information so LLM can respond appropriately
resultContent = {
error: true,
message: result.error || 'Tool execution failed',
tool: toolName,
}
}
toolCalls.push({
name: toolName,
arguments: toolArgs,
arguments: toolParams,
startTime: new Date(toolCallStartTime).toISOString(),
endTime: new Date(toolCallEndTime).toISOString(),
duration: toolCallDuration,
result: result.output,
result: resultContent,
success: result.success,
})
// Add the tool call and result to messages
// Add the tool call and result to messages (both success and failure)
const toolUseId = generateToolUseId(toolName)
currentMessages.push({
@@ -501,7 +521,7 @@ ${fieldDescriptions}
{
type: 'tool_result',
tool_use_id: toolUseId,
content: JSON.stringify(result.output),
content: JSON.stringify(resultContent),
} as any,
],
})

View File

@@ -265,7 +265,7 @@ export const azureOpenAIProvider: ProviderConfig = {
},
],
},
// Cost will be calculated in execution-logger.ts
// Cost will be calculated in logger
},
logs: [], // No block logs for direct streaming
metadata: {
@@ -373,20 +373,25 @@ export const azureOpenAIProvider: ProviderConfig = {
// Execute the tool
const toolCallStartTime = Date.now()
const mergedArgs = {
// Only merge actual tool parameters for logging
const toolParams = {
...tool.params,
...toolArgs,
}
// Add system parameters for execution
const executionParams = {
...toolParams,
...(request.workflowId ? { _context: { workflowId: request.workflowId } } : {}),
...(request.environmentVariables ? { envVars: request.environmentVariables } : {}),
}
const result = await executeTool(toolName, mergedArgs, true)
const result = await executeTool(toolName, executionParams, true)
const toolCallEndTime = Date.now()
const toolCallDuration = toolCallEndTime - toolCallStartTime
if (!result.success) continue
// Add to time segments
// Add to time segments for both success and failure
timeSegments.push({
type: 'tool',
name: toolName,
@@ -395,17 +400,31 @@ export const azureOpenAIProvider: ProviderConfig = {
duration: toolCallDuration,
})
toolResults.push(result.output)
// Prepare result content for the LLM
let resultContent: any
if (result.success) {
toolResults.push(result.output)
resultContent = result.output
} else {
// Include error information so LLM can respond appropriately
resultContent = {
error: true,
message: result.error || 'Tool execution failed',
tool: toolName,
}
}
toolCalls.push({
name: toolName,
arguments: toolArgs,
arguments: toolParams,
startTime: new Date(toolCallStartTime).toISOString(),
endTime: new Date(toolCallEndTime).toISOString(),
duration: toolCallDuration,
result: result.output,
result: resultContent,
success: result.success,
})
// Add the tool call and result to messages
// Add the tool call and result to messages (both success and failure)
currentMessages.push({
role: 'assistant',
content: null,
@@ -424,7 +443,7 @@ export const azureOpenAIProvider: ProviderConfig = {
currentMessages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(result.output),
content: JSON.stringify(resultContent),
})
} catch (error) {
logger.error('Error processing tool call:', {
@@ -565,7 +584,7 @@ export const azureOpenAIProvider: ProviderConfig = {
iterations: iterationCount + 1,
timeSegments: timeSegments,
},
// Cost will be calculated in execution-logger.ts
// Cost will be calculated in logger
},
logs: [], // No block logs at provider level
metadata: {

View File

@@ -281,18 +281,25 @@ export const cerebrasProvider: ProviderConfig = {
// Execute the tool
const toolCallStartTime = Date.now()
const mergedArgs = {
// Only merge actual tool parameters for logging
const toolParams = {
...tool.params,
...toolArgs,
...(request.workflowId ? { _context: { workflowId: request.workflowId } } : {}),
}
const result = await executeTool(toolName, mergedArgs, true)
// Add system parameters for execution
const executionParams = {
...toolParams,
...(request.workflowId ? { _context: { workflowId: request.workflowId } } : {}),
...(request.environmentVariables ? { envVars: request.environmentVariables } : {}),
}
const result = await executeTool(toolName, executionParams, true)
const toolCallEndTime = Date.now()
const toolCallDuration = toolCallEndTime - toolCallStartTime
if (!result.success) continue
// Add to time segments
// Add to time segments for both success and failure
timeSegments.push({
type: 'tool',
name: toolName,
@@ -301,17 +308,31 @@ export const cerebrasProvider: ProviderConfig = {
duration: toolCallDuration,
})
toolResults.push(result.output)
// Prepare result content for the LLM
let resultContent: any
if (result.success) {
toolResults.push(result.output)
resultContent = result.output
} else {
// Include error information so LLM can respond appropriately
resultContent = {
error: true,
message: result.error || 'Tool execution failed',
tool: toolName,
}
}
toolCalls.push({
name: toolName,
arguments: toolArgs,
arguments: toolParams,
startTime: new Date(toolCallStartTime).toISOString(),
endTime: new Date(toolCallEndTime).toISOString(),
duration: toolCallDuration,
result: result.output,
result: resultContent,
success: result.success,
})
// Add the tool call and result to messages
// Add the tool call and result to messages (both success and failure)
currentMessages.push({
role: 'assistant',
content: null,
@@ -330,7 +351,7 @@ export const cerebrasProvider: ProviderConfig = {
currentMessages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(result.output),
content: JSON.stringify(resultContent),
})
} catch (error) {
logger.error('Error processing tool call:', { error })

View File

@@ -283,19 +283,25 @@ export const deepseekProvider: ProviderConfig = {
// Execute the tool
const toolCallStartTime = Date.now()
const mergedArgs = {
// Only merge actual tool parameters for logging
const toolParams = {
...tool.params,
...toolArgs,
}
// Add system parameters for execution
const executionParams = {
...toolParams,
...(request.workflowId ? { _context: { workflowId: request.workflowId } } : {}),
...(request.environmentVariables ? { envVars: request.environmentVariables } : {}),
}
const result = await executeTool(toolName, mergedArgs, true)
const result = await executeTool(toolName, executionParams, true)
const toolCallEndTime = Date.now()
const toolCallDuration = toolCallEndTime - toolCallStartTime
if (!result.success) continue
// Add to time segments
// Add to time segments for both success and failure
timeSegments.push({
type: 'tool',
name: toolName,
@@ -304,17 +310,31 @@ export const deepseekProvider: ProviderConfig = {
duration: toolCallDuration,
})
toolResults.push(result.output)
// Prepare result content for the LLM
let resultContent: any
if (result.success) {
toolResults.push(result.output)
resultContent = result.output
} else {
// Include error information so LLM can respond appropriately
resultContent = {
error: true,
message: result.error || 'Tool execution failed',
tool: toolName,
}
}
toolCalls.push({
name: toolName,
arguments: toolArgs,
arguments: toolParams,
startTime: new Date(toolCallStartTime).toISOString(),
endTime: new Date(toolCallEndTime).toISOString(),
duration: toolCallDuration,
result: result.output,
result: resultContent,
success: result.success,
})
// Add the tool call and result to messages
// Add the tool call and result to messages (both success and failure)
currentMessages.push({
role: 'assistant',
content: null,
@@ -333,7 +353,7 @@ export const deepseekProvider: ProviderConfig = {
currentMessages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(result.output),
content: JSON.stringify(resultContent),
})
} catch (error) {
logger.error('Error processing tool call:', { error })

View File

@@ -3,6 +3,7 @@ import type { StreamingExecution } from '@/executor/types'
import { executeTool } from '@/tools'
import { getProviderDefaultModel, getProviderModels } from '../models'
import type { ProviderConfig, ProviderRequest, ProviderResponse, TimeSegment } from '../types'
import { prepareToolExecution, prepareToolsWithUsageControl, trackForcedToolUsage } from '../utils'
const logger = createLogger('GoogleProvider')
@@ -23,6 +24,66 @@ function createReadableStreamFromGeminiStream(response: Response): ReadableStrea
while (true) {
const { done, value } = await reader.read()
if (done) {
// Try to parse any remaining buffer as complete JSON
if (buffer.trim()) {
// Processing final buffer
try {
const data = JSON.parse(buffer.trim())
const candidate = data.candidates?.[0]
if (candidate?.content?.parts) {
// Check if this is a function call
const functionCall = extractFunctionCall(candidate)
if (functionCall) {
logger.debug(
'Function call detected in final buffer, ending stream to execute tool',
{
functionName: functionCall.name,
}
)
// Function calls should not be streamed - end the stream early
controller.close()
return
}
const content = extractTextContent(candidate)
if (content) {
controller.enqueue(new TextEncoder().encode(content))
}
}
} catch (e) {
// Final buffer not valid JSON, checking if it contains JSON array
// Try parsing as JSON array if it starts with [
if (buffer.trim().startsWith('[')) {
try {
const dataArray = JSON.parse(buffer.trim())
if (Array.isArray(dataArray)) {
for (const item of dataArray) {
const candidate = item.candidates?.[0]
if (candidate?.content?.parts) {
// Check if this is a function call
const functionCall = extractFunctionCall(candidate)
if (functionCall) {
logger.debug(
'Function call detected in array item, ending stream to execute tool',
{
functionName: functionCall.name,
}
)
controller.close()
return
}
const content = extractTextContent(candidate)
if (content) {
controller.enqueue(new TextEncoder().encode(content))
}
}
}
}
} catch (arrayError) {
// Buffer is not valid JSON array
}
}
}
}
controller.close()
break
}
@@ -30,47 +91,98 @@ function createReadableStreamFromGeminiStream(response: Response): ReadableStrea
const text = new TextDecoder().decode(value)
buffer += text
try {
const lines = buffer.split('\n')
buffer = ''
// Try to find complete JSON objects in buffer
// Look for patterns like: {...}\n{...} or just a single {...}
let searchIndex = 0
while (searchIndex < buffer.length) {
const openBrace = buffer.indexOf('{', searchIndex)
if (openBrace === -1) break
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim()
// Try to find the matching closing brace
let braceCount = 0
let inString = false
let escaped = false
let closeBrace = -1
if (i === lines.length - 1 && line !== '') {
buffer = line
continue
}
for (let i = openBrace; i < buffer.length; i++) {
const char = buffer[i]
if (!line) continue
if (line.startsWith('data: ')) {
const jsonStr = line.substring(6)
if (jsonStr === '[DONE]') continue
try {
const data = JSON.parse(jsonStr)
const candidate = data.candidates?.[0]
if (candidate?.content?.parts) {
const content = extractTextContent(candidate)
if (content) {
controller.enqueue(new TextEncoder().encode(content))
}
if (!inString) {
if (char === '"' && !escaped) {
inString = true
} else if (char === '{') {
braceCount++
} else if (char === '}') {
braceCount--
if (braceCount === 0) {
closeBrace = i
break
}
} catch (e) {
logger.error('Error parsing Gemini SSE JSON data', {
error: e instanceof Error ? e.message : String(e),
data: jsonStr,
})
}
} else {
if (char === '"' && !escaped) {
inString = false
}
}
escaped = char === '\\' && !escaped
}
if (closeBrace !== -1) {
// Found a complete JSON object
const jsonStr = buffer.substring(openBrace, closeBrace + 1)
try {
const data = JSON.parse(jsonStr)
// JSON parsed successfully from stream
const candidate = data.candidates?.[0]
// Handle specific finish reasons
if (candidate?.finishReason === 'UNEXPECTED_TOOL_CALL') {
logger.warn('Gemini returned UNEXPECTED_TOOL_CALL in streaming mode', {
finishReason: candidate.finishReason,
hasContent: !!candidate?.content,
hasParts: !!candidate?.content?.parts,
})
// This indicates a configuration issue - tools might be improperly configured for streaming
continue
}
if (candidate?.content?.parts) {
// Check if this is a function call
const functionCall = extractFunctionCall(candidate)
if (functionCall) {
logger.debug(
'Function call detected in stream, ending stream to execute tool',
{
functionName: functionCall.name,
}
)
// Function calls should not be streamed - we need to end the stream
// and let the non-streaming tool execution flow handle this
controller.close()
return
}
const content = extractTextContent(candidate)
if (content) {
controller.enqueue(new TextEncoder().encode(content))
}
}
} catch (e) {
logger.error('Error parsing JSON from stream', {
error: e instanceof Error ? e.message : String(e),
jsonPreview: jsonStr.substring(0, 200),
})
}
// Remove processed JSON from buffer and continue searching
buffer = buffer.substring(closeBrace + 1)
searchIndex = 0
} else {
// No complete JSON object found, wait for more data
break
}
} catch (e) {
logger.error('Error processing Gemini SSE stream', {
error: e instanceof Error ? e.message : String(e),
chunk: text,
})
}
}
} catch (e) {
@@ -142,8 +254,8 @@ export const googleProvider: ProviderConfig = {
payload.systemInstruction = systemInstruction
}
// Add structured output format if requested
if (request.responseFormat) {
// Add structured output format if requested (but not when tools are present)
if (request.responseFormat && !tools?.length) {
const responseFormatSchema = request.responseFormat.schema || request.responseFormat
// Clean the schema using our helper function
@@ -157,31 +269,60 @@ export const googleProvider: ProviderConfig = {
hasSchema: !!cleanSchema,
mimeType: 'application/json',
})
} else if (request.responseFormat && tools?.length) {
logger.warn(
'Gemini does not support structured output (responseFormat) with function calling (tools). Structured output will be ignored.'
)
}
// Add tools if provided
if (tools?.length) {
payload.tools = [
{
functionDeclarations: tools,
},
]
// Handle tools and tool usage control
let preparedTools: ReturnType<typeof prepareToolsWithUsageControl> | null = null
logger.info('Google Gemini request with tools:', {
toolCount: tools.length,
model: requestedModel,
tools: tools.map((t) => t.name),
})
if (tools?.length) {
preparedTools = prepareToolsWithUsageControl(tools, request.tools, logger, 'google')
const { tools: filteredTools, toolConfig } = preparedTools
if (filteredTools?.length) {
payload.tools = [
{
functionDeclarations: filteredTools,
},
]
// Add Google-specific tool configuration
if (toolConfig) {
payload.toolConfig = toolConfig
}
logger.info('Google Gemini request with tools:', {
toolCount: filteredTools.length,
model: requestedModel,
tools: filteredTools.map((t) => t.name),
hasToolConfig: !!toolConfig,
toolConfig: toolConfig,
})
}
}
// Make the API request
const initialCallTime = Date.now()
// For streaming requests, add the alt=sse parameter to the URL
const endpoint = request.stream
? `https://generativelanguage.googleapis.com/v1beta/models/${requestedModel}:generateContent?key=${request.apiKey}&alt=sse`
// Disable streaming for initial requests when tools are present to avoid function calls in streams
// Only enable streaming for the final response after tool execution
const shouldStream = request.stream && !tools?.length
// Use streamGenerateContent for streaming requests
const endpoint = shouldStream
? `https://generativelanguage.googleapis.com/v1beta/models/${requestedModel}:streamGenerateContent?key=${request.apiKey}`
: `https://generativelanguage.googleapis.com/v1beta/models/${requestedModel}:generateContent?key=${request.apiKey}`
if (request.stream && tools?.length) {
logger.info('Streaming disabled for initial request due to tools presence', {
toolCount: tools.length,
willStreamAfterTools: true,
})
}
const response = await fetch(endpoint, {
method: 'POST',
headers: {
@@ -203,7 +344,7 @@ export const googleProvider: ProviderConfig = {
const firstResponseTime = Date.now() - initialCallTime
// Handle streaming response
if (request.stream) {
if (shouldStream) {
logger.info('Handling Google Gemini streaming response')
// Create a ReadableStream from the Google Gemini stream
@@ -239,11 +380,7 @@ export const googleProvider: ProviderConfig = {
duration: firstResponseTime,
},
],
cost: {
total: 0.0, // Initial estimate, updated as tokens are processed
input: 0.0,
output: 0.0,
},
// Cost will be calculated in logger
},
},
logs: [],
@@ -288,6 +425,38 @@ export const googleProvider: ProviderConfig = {
let iterationCount = 0
const MAX_ITERATIONS = 10 // Prevent infinite loops
// Track forced tools and their usage (similar to OpenAI pattern)
const originalToolConfig = preparedTools?.toolConfig
const forcedTools = preparedTools?.forcedTools || []
let usedForcedTools: string[] = []
let hasUsedForcedTool = false
let currentToolConfig = originalToolConfig
// Helper function to check for forced tool usage in responses
const checkForForcedToolUsage = (functionCall: { name: string; args: any }) => {
if (currentToolConfig && forcedTools.length > 0) {
const toolCallsForTracking = [{ name: functionCall.name, arguments: functionCall.args }]
const result = trackForcedToolUsage(
toolCallsForTracking,
currentToolConfig,
logger,
'google',
forcedTools,
usedForcedTools
)
hasUsedForcedTool = result.hasUsedForcedTool
usedForcedTools = result.usedForcedTools
if (result.nextToolConfig) {
currentToolConfig = result.nextToolConfig
logger.info('Updated tool config for next iteration', {
hasNextToolConfig: !!currentToolConfig,
usedForcedTools: usedForcedTools,
})
}
}
}
// Track time spent in model vs tools
let modelTime = firstResponseTime
let toolsTime = 0
@@ -343,70 +512,15 @@ export const googleProvider: ProviderConfig = {
break
}
// First, identify parameters marked as requiredForToolCall
const requiredToolCallParams: Record<string, any> = {}
if (tool.params) {
Object.entries(tool.params).forEach(([key, value]) => {
// Check if this parameter is marked as requiredForToolCall
if (value?.requiredForToolCall) {
requiredToolCallParams[key] = value
}
})
}
// Execute the tool
const toolCallStartTime = Date.now()
// Merge arguments in the correct order of precedence:
// 1. Default parameters from tool.params
// 2. Arguments from the model's function call (toolArgs)
// 3. Parameters marked as requiredForToolCall (these should always be preserved)
// 4. Workflow context if needed
const mergedArgs = {
...tool.params, // Default parameters defined for the tool
...toolArgs, // Arguments from the model's function call
...requiredToolCallParams, // Required parameters from the tool definition (take precedence)
...(request.workflowId ? { _context: { workflowId: request.workflowId } } : {}),
...(request.environmentVariables ? { envVars: request.environmentVariables } : {}),
}
// For debugging only - don't log actual API keys
logger.debug(`Executing tool ${toolName} with parameters:`, {
parameterKeys: Object.keys(mergedArgs),
hasRequiredParams: Object.keys(requiredToolCallParams).length > 0,
requiredParamKeys: Object.keys(requiredToolCallParams),
})
const result = await executeTool(toolName, mergedArgs, true)
const { toolParams, executionParams } = prepareToolExecution(tool, toolArgs, request)
const result = await executeTool(toolName, executionParams, true)
const toolCallEndTime = Date.now()
const toolCallDuration = toolCallEndTime - toolCallStartTime
if (!result.success) {
// Check for API key related errors
const errorMessage = result.error?.toLowerCase() || ''
if (
errorMessage.includes('api key') ||
errorMessage.includes('apikey') ||
errorMessage.includes('x-api-key') ||
errorMessage.includes('authentication')
) {
logger.error(`Tool ${toolName} failed with API key error:`, {
error: result.error,
toolRequiresKey: true,
})
// Add a more helpful error message for the user
content = `Error: The ${toolName} tool requires a valid API key. Please ensure you've provided the correct API key for this specific service.`
} else {
// Regular error handling
logger.warn(`Tool ${toolName} execution failed`, {
error: result.error,
duration: toolCallDuration,
})
}
break
}
// Add to time segments
// Add to time segments for both success and failure
timeSegments.push({
type: 'tool',
name: toolName,
@@ -415,15 +529,28 @@ export const googleProvider: ProviderConfig = {
duration: toolCallDuration,
})
// Track results
toolResults.push(result.output)
// Prepare result content for the LLM
let resultContent: any
if (result.success) {
toolResults.push(result.output)
resultContent = result.output
} else {
// Include error information so LLM can respond appropriately
resultContent = {
error: true,
message: result.error || 'Tool execution failed',
tool: toolName,
}
}
toolCalls.push({
name: toolName,
arguments: toolArgs,
arguments: toolParams,
startTime: new Date(toolCallStartTime).toISOString(),
endTime: new Date(toolCallEndTime).toISOString(),
duration: toolCallDuration,
result: result.output,
result: resultContent,
success: result.success,
})
// Prepare for next request with simplified messages
@@ -450,7 +577,7 @@ export const googleProvider: ProviderConfig = {
role: 'user',
parts: [
{
text: `Function ${latestFunctionCall.name} result: ${JSON.stringify(toolResults[toolResults.length - 1])}`,
text: `Function ${latestFunctionCall.name} result: ${JSON.stringify(resultContent)}`,
},
],
},
@@ -460,6 +587,9 @@ export const googleProvider: ProviderConfig = {
const thisToolsTime = Date.now() - toolsStartTime
toolsTime += thisToolsTime
// Check for forced tool usage and update configuration
checkForForcedToolUsage(latestFunctionCall)
// Make the next request with updated messages
const nextModelStartTime = Date.now()
@@ -470,17 +600,118 @@ export const googleProvider: ProviderConfig = {
const streamingPayload = {
...payload,
contents: simplifiedMessages,
tool_config: { mode: 'AUTO' }, // Always use AUTO mode for streaming after tools
}
// Remove any forced tool configuration to prevent issues with streaming
if ('tool_config' in streamingPayload) {
streamingPayload.tool_config = { mode: 'AUTO' }
// Check if we should remove tools and enable structured output for final response
const allForcedToolsUsed =
forcedTools.length > 0 && usedForcedTools.length === forcedTools.length
if (allForcedToolsUsed && request.responseFormat) {
// All forced tools have been used, we can now remove tools and enable structured output
streamingPayload.tools = undefined
streamingPayload.toolConfig = undefined
// Add structured output format for final response
const responseFormatSchema =
request.responseFormat.schema || request.responseFormat
const cleanSchema = cleanSchemaForGemini(responseFormatSchema)
if (!streamingPayload.generationConfig) {
streamingPayload.generationConfig = {}
}
streamingPayload.generationConfig.responseMimeType = 'application/json'
streamingPayload.generationConfig.responseSchema = cleanSchema
logger.info('Using structured output for final response after tool execution')
} else {
// Use updated tool configuration if available, otherwise default to AUTO
if (currentToolConfig) {
streamingPayload.toolConfig = currentToolConfig
} else {
streamingPayload.toolConfig = { functionCallingConfig: { mode: 'AUTO' } }
}
}
// Make the streaming request with alt=sse parameter
// Check if we should handle this as a potential forced tool call
// First make a non-streaming request to see if we get a function call
const checkPayload = {
...streamingPayload,
// Remove stream property to get non-streaming response
}
checkPayload.stream = undefined
const checkResponse = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/${requestedModel}:generateContent?key=${request.apiKey}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(checkPayload),
}
)
if (!checkResponse.ok) {
const errorBody = await checkResponse.text()
logger.error('Error in Gemini check request:', {
status: checkResponse.status,
statusText: checkResponse.statusText,
responseBody: errorBody,
})
throw new Error(
`Gemini API check error: ${checkResponse.status} ${checkResponse.statusText}`
)
}
const checkResult = await checkResponse.json()
const checkCandidate = checkResult.candidates?.[0]
const checkFunctionCall = extractFunctionCall(checkCandidate)
if (checkFunctionCall) {
// We have a function call - handle it in non-streaming mode
logger.info(
'Function call detected in follow-up, handling in non-streaming mode',
{
functionName: checkFunctionCall.name,
}
)
// Update geminiResponse to continue the tool execution loop
geminiResponse = checkResult
// Update token counts if available
if (checkResult.usageMetadata) {
tokens.prompt += checkResult.usageMetadata.promptTokenCount || 0
tokens.completion += checkResult.usageMetadata.candidatesTokenCount || 0
tokens.total +=
(checkResult.usageMetadata.promptTokenCount || 0) +
(checkResult.usageMetadata.candidatesTokenCount || 0)
}
// Calculate timing for this model call
const nextModelEndTime = Date.now()
const thisModelTime = nextModelEndTime - nextModelStartTime
modelTime += thisModelTime
// Add to time segments
timeSegments.push({
type: 'model',
name: `Model response (iteration ${iterationCount + 1})`,
startTime: nextModelStartTime,
endTime: nextModelEndTime,
duration: thisModelTime,
})
// Continue the loop to handle the function call
iterationCount++
continue
}
// No function call - proceed with streaming
logger.info('No function call detected, proceeding with streaming response')
// Make the streaming request with streamGenerateContent endpoint
const streamingResponse = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/${requestedModel}:generateContent?key=${request.apiKey}&alt=sse`,
`https://generativelanguage.googleapis.com/v1beta/models/${requestedModel}:streamGenerateContent?key=${request.apiKey}`,
{
method: 'POST',
headers: {
@@ -546,11 +777,7 @@ export const googleProvider: ProviderConfig = {
iterations: iterationCount + 1,
timeSegments,
},
cost: {
total: (tokens.total || 0) * 0.0001, // Estimate cost based on tokens
input: (tokens.prompt || 0) * 0.0001,
output: (tokens.completion || 0) * 0.0001,
},
// Cost will be calculated in logger
},
logs: [],
metadata: {
@@ -566,6 +793,41 @@ export const googleProvider: ProviderConfig = {
}
// Make the next request for non-streaming response
const nextPayload = {
...payload,
contents: simplifiedMessages,
}
// Check if we should remove tools and enable structured output for final response
const allForcedToolsUsed =
forcedTools.length > 0 && usedForcedTools.length === forcedTools.length
if (allForcedToolsUsed && request.responseFormat) {
// All forced tools have been used, we can now remove tools and enable structured output
nextPayload.tools = undefined
nextPayload.toolConfig = undefined
// Add structured output format for final response
const responseFormatSchema =
request.responseFormat.schema || request.responseFormat
const cleanSchema = cleanSchemaForGemini(responseFormatSchema)
if (!nextPayload.generationConfig) {
nextPayload.generationConfig = {}
}
nextPayload.generationConfig.responseMimeType = 'application/json'
nextPayload.generationConfig.responseSchema = cleanSchema
logger.info(
'Using structured output for final non-streaming response after tool execution'
)
} else {
// Add updated tool configuration if available
if (currentToolConfig) {
nextPayload.toolConfig = currentToolConfig
}
}
const nextResponse = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/${requestedModel}:generateContent?key=${request.apiKey}`,
{
@@ -573,10 +835,7 @@ export const googleProvider: ProviderConfig = {
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...payload,
contents: simplifiedMessages,
}),
body: JSON.stringify(nextPayload),
}
)
@@ -681,6 +940,7 @@ export const googleProvider: ProviderConfig = {
iterations: iterationCount + 1,
timeSegments: timeSegments,
},
// Cost will be calculated in logger
}
} catch (error) {
// Include timing information even for errors
@@ -866,7 +1126,7 @@ function convertToGeminiFormat(request: ProviderRequest): {
// Process schema properties
if (toolParameters.properties) {
const properties = { ...toolParameters.properties }
let required = toolParameters.required ? [...toolParameters.required] : []
const required = toolParameters.required ? [...toolParameters.required] : []
// Remove defaults and optional parameters
for (const key in properties) {
@@ -876,10 +1136,6 @@ function convertToGeminiFormat(request: ProviderRequest): {
const { default: _, ...cleanProp } = prop
properties[key] = cleanProp
}
if (tool.params?.[key]?.requiredForToolCall && required.includes(key)) {
required = required.filter((r) => r !== key)
}
}
// Build Gemini-compatible parameters schema

View File

@@ -252,18 +252,25 @@ export const groqProvider: ProviderConfig = {
// Execute the tool
const toolCallStartTime = Date.now()
const mergedArgs = {
// Only merge actual tool parameters for logging
const toolParams = {
...tool.params,
...toolArgs,
...(request.workflowId ? { _context: { workflowId: request.workflowId } } : {}),
}
const result = await executeTool(toolName, mergedArgs, true)
// Add system parameters for execution
const executionParams = {
...toolParams,
...(request.workflowId ? { _context: { workflowId: request.workflowId } } : {}),
...(request.environmentVariables ? { envVars: request.environmentVariables } : {}),
}
const result = await executeTool(toolName, executionParams, true)
const toolCallEndTime = Date.now()
const toolCallDuration = toolCallEndTime - toolCallStartTime
if (!result.success) continue
// Add to time segments
// Add to time segments for both success and failure
timeSegments.push({
type: 'tool',
name: toolName,
@@ -272,17 +279,31 @@ export const groqProvider: ProviderConfig = {
duration: toolCallDuration,
})
toolResults.push(result.output)
// Prepare result content for the LLM
let resultContent: any
if (result.success) {
toolResults.push(result.output)
resultContent = result.output
} else {
// Include error information so LLM can respond appropriately
resultContent = {
error: true,
message: result.error || 'Tool execution failed',
tool: toolName,
}
}
toolCalls.push({
name: toolName,
arguments: toolArgs,
arguments: toolParams,
startTime: new Date(toolCallStartTime).toISOString(),
endTime: new Date(toolCallEndTime).toISOString(),
duration: toolCallDuration,
result: result.output,
result: resultContent,
success: result.success,
})
// Add the tool call and result to messages
// Add the tool call and result to messages (both success and failure)
currentMessages.push({
role: 'assistant',
content: null,
@@ -301,7 +322,7 @@ export const groqProvider: ProviderConfig = {
currentMessages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(result.output),
content: JSON.stringify(resultContent),
})
} catch (error) {
logger.error('Error processing tool call:', { error })

View File

@@ -307,7 +307,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
},
capabilities: {
temperature: { min: 0, max: 2 },
toolUsageControl: false,
toolUsageControl: true,
},
},
{
@@ -320,7 +320,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
},
capabilities: {
temperature: { min: 0, max: 2 },
toolUsageControl: false,
toolUsageControl: true,
},
},
],

View File

@@ -185,18 +185,25 @@ export const ollamaProvider: ProviderConfig = {
// Execute the tool
const toolCallStartTime = Date.now()
const mergedArgs = {
// Only merge actual tool parameters for logging
const toolParams = {
...tool.params,
...toolArgs,
...(request.workflowId ? { _context: { workflowId: request.workflowId } } : {}),
}
const result = await executeTool(toolName, mergedArgs, true)
// Add system parameters for execution
const executionParams = {
...toolParams,
...(request.workflowId ? { _context: { workflowId: request.workflowId } } : {}),
...(request.environmentVariables ? { envVars: request.environmentVariables } : {}),
}
const result = await executeTool(toolName, executionParams, true)
const toolCallEndTime = Date.now()
const toolCallDuration = toolCallEndTime - toolCallStartTime
if (!result.success) continue
// Add to time segments
// Add to time segments for both success and failure
timeSegments.push({
type: 'tool',
name: toolName,
@@ -205,17 +212,31 @@ export const ollamaProvider: ProviderConfig = {
duration: toolCallDuration,
})
toolResults.push(result.output)
// Prepare result content for the LLM
let resultContent: any
if (result.success) {
toolResults.push(result.output)
resultContent = result.output
} else {
// Include error information so LLM can respond appropriately
resultContent = {
error: true,
message: result.error || 'Tool execution failed',
tool: toolName,
}
}
toolCalls.push({
name: toolName,
arguments: toolArgs,
arguments: toolParams,
startTime: new Date(toolCallStartTime).toISOString(),
endTime: new Date(toolCallEndTime).toISOString(),
duration: toolCallDuration,
result: result.output,
result: resultContent,
success: result.success,
})
// Add the tool call and result to messages
// Add the tool call and result to messages (both success and failure)
currentMessages.push({
role: 'assistant',
content: null,
@@ -234,7 +255,7 @@ export const ollamaProvider: ProviderConfig = {
currentMessages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(result.output),
content: JSON.stringify(resultContent),
})
} catch (error) {
logger.error('Error processing tool call:', { error })

View File

@@ -4,7 +4,7 @@ import type { StreamingExecution } from '@/executor/types'
import { executeTool } from '@/tools'
import { getProviderDefaultModel, getProviderModels } from '../models'
import type { ProviderConfig, ProviderRequest, ProviderResponse, TimeSegment } from '../types'
import { prepareToolsWithUsageControl, trackForcedToolUsage } from '../utils'
import { prepareToolExecution, prepareToolsWithUsageControl, trackForcedToolUsage } from '../utils'
const logger = createLogger('OpenAIProvider')
@@ -247,7 +247,7 @@ export const openaiProvider: ProviderConfig = {
},
],
},
// Cost will be calculated in execution-logger.ts
// Cost will be calculated in logger
},
logs: [], // No block logs for direct streaming
metadata: {
@@ -355,20 +355,13 @@ export const openaiProvider: ProviderConfig = {
// Execute the tool
const toolCallStartTime = Date.now()
const mergedArgs = {
...tool.params,
...toolArgs,
...(request.workflowId ? { _context: { workflowId: request.workflowId } } : {}),
...(request.environmentVariables ? { envVars: request.environmentVariables } : {}),
}
const result = await executeTool(toolName, mergedArgs, true)
const { toolParams, executionParams } = prepareToolExecution(tool, toolArgs, request)
const result = await executeTool(toolName, executionParams, true)
const toolCallEndTime = Date.now()
const toolCallDuration = toolCallEndTime - toolCallStartTime
if (!result.success) continue
// Add to time segments
// Add to time segments for both success and failure
timeSegments.push({
type: 'tool',
name: toolName,
@@ -377,17 +370,31 @@ export const openaiProvider: ProviderConfig = {
duration: toolCallDuration,
})
toolResults.push(result.output)
// Prepare result content for the LLM
let resultContent: any
if (result.success) {
toolResults.push(result.output)
resultContent = result.output
} else {
// Include error information so LLM can respond appropriately
resultContent = {
error: true,
message: result.error || 'Tool execution failed',
tool: toolName,
}
}
toolCalls.push({
name: toolName,
arguments: toolArgs,
arguments: toolParams,
startTime: new Date(toolCallStartTime).toISOString(),
endTime: new Date(toolCallEndTime).toISOString(),
duration: toolCallDuration,
result: result.output,
result: resultContent,
success: result.success,
})
// Add the tool call and result to messages
// Add the tool call and result to messages (both success and failure)
currentMessages.push({
role: 'assistant',
content: null,
@@ -406,7 +413,7 @@ export const openaiProvider: ProviderConfig = {
currentMessages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(result.output),
content: JSON.stringify(resultContent),
})
} catch (error) {
logger.error('Error processing tool call:', {
@@ -547,7 +554,7 @@ export const openaiProvider: ProviderConfig = {
iterations: iterationCount + 1,
timeSegments: timeSegments,
},
// Cost will be calculated in execution-logger.ts
// Cost will be calculated in logger
},
logs: [], // No block logs at provider level
metadata: {

View File

@@ -208,7 +208,14 @@ describe('Model Capabilities', () => {
describe('supportsToolUsageControl', () => {
it.concurrent('should return true for providers that support tool usage control', () => {
const supportedProviders = ['openai', 'azure-openai', 'anthropic', 'deepseek', 'xai']
const supportedProviders = [
'openai',
'azure-openai',
'anthropic',
'deepseek',
'xai',
'google',
]
for (const provider of supportedProviders) {
expect(supportsToolUsageControl(provider)).toBe(true)
@@ -218,13 +225,7 @@ describe('Model Capabilities', () => {
it.concurrent(
'should return false for providers that do not support tool usage control',
() => {
const unsupportedProviders = [
'google',
'ollama',
'cerebras',
'groq',
'non-existent-provider',
]
const unsupportedProviders = ['ollama', 'cerebras', 'groq', 'non-existent-provider']
for (const provider of unsupportedProviders) {
expect(supportsToolUsageControl(provider)).toBe(false)
@@ -251,7 +252,7 @@ describe('Model Capabilities', () => {
expect(PROVIDERS_WITH_TOOL_USAGE_CONTROL).toContain('openai')
expect(PROVIDERS_WITH_TOOL_USAGE_CONTROL).toContain('anthropic')
expect(PROVIDERS_WITH_TOOL_USAGE_CONTROL).toContain('deepseek')
expect(PROVIDERS_WITH_TOOL_USAGE_CONTROL).not.toContain('google')
expect(PROVIDERS_WITH_TOOL_USAGE_CONTROL).toContain('google')
expect(PROVIDERS_WITH_TOOL_USAGE_CONTROL).not.toContain('ollama')
})
@@ -709,8 +710,10 @@ describe('Tool Management', () => {
const result = prepareToolsWithUsageControl(tools, providerTools, mockLogger, 'google')
expect(result.toolConfig).toEqual({
mode: 'ANY',
allowed_function_names: ['forcedTool'],
functionCallingConfig: {
mode: 'ANY',
allowedFunctionNames: ['forcedTool'],
},
})
})

View File

@@ -404,29 +404,22 @@ export async function transformBlockTool(
return null
}
// Import the new tool parameter utilities
const { createLLMToolSchema } = await import('../tools/params')
// Get user-provided parameters from the block
const userProvidedParams = block.params || {}
// Create LLM schema that excludes user-provided parameters
const llmSchema = createLLMToolSchema(toolConfig, userProvidedParams)
// Return formatted tool config
return {
id: toolConfig.id,
name: toolConfig.name,
description: toolConfig.description,
params: block.params || {},
parameters: {
type: 'object',
properties: Object.entries(toolConfig.params).reduce(
(acc, [key, config]: [string, any]) => ({
...acc,
[key]: {
type: config.type === 'json' ? 'object' : config.type,
description: config.description || '',
...(key in block.params && { default: block.params[key] }),
},
}),
{}
),
required: Object.entries(toolConfig.params)
.filter(([_, config]: [string, any]) => config.required)
.map(([key]) => key),
},
params: userProvidedParams,
parameters: llmSchema,
}
}
@@ -608,8 +601,10 @@ export function prepareToolsWithUsageControl(
| undefined
toolConfig?: {
// Add toolConfig for Google's format
mode: 'AUTO' | 'ANY' | 'NONE'
allowed_function_names?: string[]
functionCallingConfig: {
mode: 'AUTO' | 'ANY' | 'NONE'
allowedFunctionNames?: string[]
}
}
hasFilteredTools: boolean
forcedTools: string[] // Return all forced tool IDs
@@ -665,8 +660,10 @@ export function prepareToolsWithUsageControl(
// For Google, we'll use a separate toolConfig object
let toolConfig:
| {
mode: 'AUTO' | 'ANY' | 'NONE'
allowed_function_names?: string[]
functionCallingConfig: {
mode: 'AUTO' | 'ANY' | 'NONE'
allowedFunctionNames?: string[]
}
}
| undefined
@@ -681,13 +678,15 @@ export function prepareToolsWithUsageControl(
name: forcedTool.id,
}
} else if (provider === 'google') {
// Google Gemini format uses a separate tool_config object
// Google Gemini format uses a separate toolConfig object
toolConfig = {
mode: 'ANY',
allowed_function_names:
forcedTools.length === 1
? [forcedTool.id] // If only one tool, specify just that one
: forcedToolIds, // If multiple tools, include all of them
functionCallingConfig: {
mode: 'ANY',
allowedFunctionNames:
forcedTools.length === 1
? [forcedTool.id] // If only one tool, specify just that one
: forcedToolIds, // If multiple tools, include all of them
},
}
// Keep toolChoice as 'auto' since we use toolConfig instead
toolChoice = 'auto'
@@ -710,7 +709,7 @@ export function prepareToolsWithUsageControl(
// Default to auto if no forced tools
toolChoice = 'auto'
if (provider === 'google') {
toolConfig = { mode: 'AUTO' }
toolConfig = { functionCallingConfig: { mode: 'AUTO' } }
}
logger.info('Setting tool_choice to auto - letting model decide which tools to use')
}
@@ -752,8 +751,10 @@ export function trackForcedToolUsage(
| { type: 'any'; any: { model: string; name: string } }
| null
nextToolConfig?: {
mode: 'AUTO' | 'ANY' | 'NONE'
allowed_function_names?: string[]
functionCallingConfig: {
mode: 'AUTO' | 'ANY' | 'NONE'
allowedFunctionNames?: string[]
}
}
} {
// Default to keeping the original tool_choice
@@ -761,8 +762,10 @@ export function trackForcedToolUsage(
let nextToolChoice = originalToolChoice
let nextToolConfig:
| {
mode: 'AUTO' | 'ANY' | 'NONE'
allowed_function_names?: string[]
functionCallingConfig: {
mode: 'AUTO' | 'ANY' | 'NONE'
allowedFunctionNames?: string[]
}
}
| undefined
@@ -773,9 +776,9 @@ export function trackForcedToolUsage(
// Get the name of the current forced tool(s)
let forcedToolNames: string[] = []
if (isGoogleFormat && originalToolChoice?.allowed_function_names) {
if (isGoogleFormat && originalToolChoice?.functionCallingConfig?.allowedFunctionNames) {
// For Google format
forcedToolNames = originalToolChoice.allowed_function_names
forcedToolNames = originalToolChoice.functionCallingConfig.allowedFunctionNames
} else if (
typeof originalToolChoice === 'object' &&
(originalToolChoice?.function?.name ||
@@ -818,11 +821,13 @@ export function trackForcedToolUsage(
}
} else if (provider === 'google') {
nextToolConfig = {
mode: 'ANY',
allowed_function_names:
remainingTools.length === 1
? [nextToolToForce] // If only one tool left, specify just that one
: remainingTools, // If multiple tools, include all remaining
functionCallingConfig: {
mode: 'ANY',
allowedFunctionNames:
remainingTools.length === 1
? [nextToolToForce] // If only one tool left, specify just that one
: remainingTools, // If multiple tools, include all remaining
},
}
} else {
// Default OpenAI format
@@ -840,7 +845,7 @@ export function trackForcedToolUsage(
if (provider === 'anthropic') {
nextToolChoice = null // Anthropic requires null to remove the parameter
} else if (provider === 'google') {
nextToolConfig = { mode: 'AUTO' }
nextToolConfig = { functionCallingConfig: { mode: 'AUTO' } }
} else {
nextToolChoice = 'auto'
}
@@ -888,3 +893,30 @@ export function getMaxTemperature(model: string): number | undefined {
export function supportsToolUsageControl(provider: string): boolean {
return supportsToolUsageControlFromDefinitions(provider)
}
/**
* Prepare tool execution parameters, separating tool parameters from system parameters
*/
export function prepareToolExecution(
tool: { params?: Record<string, any> },
llmArgs: Record<string, any>,
request: { workflowId?: string; environmentVariables?: Record<string, any> }
): {
toolParams: Record<string, any>
executionParams: Record<string, any>
} {
// Only merge actual tool parameters for logging
const toolParams = {
...tool.params,
...llmArgs,
}
// Add system parameters for execution
const executionParams = {
...toolParams,
...(request.workflowId ? { _context: { workflowId: request.workflowId } } : {}),
...(request.environmentVariables ? { envVars: request.environmentVariables } : {}),
}
return { toolParams, executionParams }
}

View File

@@ -319,24 +319,25 @@ export const xAIProvider: ProviderConfig = {
}
const toolCallStartTime = Date.now()
const mergedArgs = {
// Only merge actual tool parameters for logging
const toolParams = {
...tool.params,
...toolArgs,
...(request.workflowId ? { _context: { workflowId: request.workflowId } } : {}),
}
const result = await executeTool(toolName, mergedArgs, true)
// Add system parameters for execution
const executionParams = {
...toolParams,
...(request.workflowId ? { _context: { workflowId: request.workflowId } } : {}),
...(request.environmentVariables ? { envVars: request.environmentVariables } : {}),
}
const result = await executeTool(toolName, executionParams, true)
const toolCallEndTime = Date.now()
const toolCallDuration = toolCallEndTime - toolCallStartTime
if (!result.success) {
logger.warn('XAI Provider - Tool execution failed:', {
toolName,
error: result.error,
})
continue
}
// Add to time segments
// Add to time segments for both success and failure
timeSegments.push({
type: 'tool',
name: toolName,
@@ -345,16 +346,36 @@ export const xAIProvider: ProviderConfig = {
duration: toolCallDuration,
})
toolResults.push(result.output)
// Prepare result content for the LLM
let resultContent: any
if (result.success) {
toolResults.push(result.output)
resultContent = result.output
} else {
// Include error information so LLM can respond appropriately
resultContent = {
error: true,
message: result.error || 'Tool execution failed',
tool: toolName,
}
logger.warn('XAI Provider - Tool execution failed:', {
toolName,
error: result.error,
})
}
toolCalls.push({
name: toolName,
arguments: toolArgs,
arguments: toolParams,
startTime: new Date(toolCallStartTime).toISOString(),
endTime: new Date(toolCallEndTime).toISOString(),
duration: toolCallDuration,
result: result.output,
result: resultContent,
success: result.success,
})
// Add the tool call and result to messages (both success and failure)
currentMessages.push({
role: 'assistant',
content: null,
@@ -373,7 +394,7 @@ export const xAIProvider: ProviderConfig = {
currentMessages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(result.output),
content: JSON.stringify(resultContent),
})
} catch (error) {
logger.error('XAI Provider - Error processing tool call:', {

View File

@@ -176,10 +176,18 @@ export const useConsoleStore = create<ConsoleStore>()(
set((state) => ({ isOpen: !state.isOpen }))
},
updateConsole: (blockId: string, update: string | import('./types').ConsoleUpdate) => {
updateConsole: (
blockId: string,
update: string | import('./types').ConsoleUpdate,
executionId?: string
) => {
set((state) => {
const updatedEntries = state.entries.map((entry) => {
if (entry.blockId === blockId) {
// Match by executionId if provided, otherwise fall back to blockId for backward compatibility
const isMatch = executionId
? entry.executionId === executionId
: entry.blockId === blockId
if (isMatch) {
if (typeof update === 'string') {
// Simple content update for backward compatibility
const newOutput = updateBlockOutput(entry.output, update)

View File

@@ -5,6 +5,7 @@ export interface ConsoleEntry {
timestamp: string
workflowId: string
blockId: string
executionId?: string
blockName?: string
blockType?: string
startedAt?: string
@@ -36,5 +37,5 @@ export interface ConsoleStore {
clearConsole: (workflowId: string | null) => void
getWorkflowEntries: (workflowId: string) => ConsoleEntry[]
toggleConsole: () => void
updateConsole: (blockId: string, update: string | ConsoleUpdate) => void
updateConsole: (blockId: string, update: string | ConsoleUpdate, executionId?: string) => void
}

View File

@@ -1,8 +1,6 @@
import type { ToolConfig } from '../types'
import type { AirtableCreateParams, AirtableCreateResponse } from './types'
// import { logger } from '@/utils/logger' // Removed logger due to import issues
export const airtableCreateRecordsTool: ToolConfig<AirtableCreateParams, AirtableCreateResponse> = {
id: 'airtable_create_records',
name: 'Airtable Create Records',
@@ -18,21 +16,25 @@ export const airtableCreateRecordsTool: ToolConfig<AirtableCreateParams, Airtabl
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token',
},
baseId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ID of the Airtable base',
},
tableId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ID or name of the table',
},
records: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description: 'Array of records to create, each with a `fields` object',
// Example: [{ fields: { "Field 1": "Value1", "Field 2": "Value2" } }]
},

View File

@@ -18,21 +18,25 @@ export const airtableGetRecordTool: ToolConfig<AirtableGetParams, AirtableGetRes
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token',
},
baseId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ID of the Airtable base',
},
tableId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ID or name of the table',
},
recordId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ID of the record to retrieve',
},
},

View File

@@ -1,220 +0,0 @@
/**
* @vitest-environment jsdom
*
* Airtable Tools Integration Tests
*/
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { ToolTester } from '../__test-utils__/test-tools'
import {
airtableCreateRecordsTool,
airtableGetRecordTool,
airtableListRecordsTool,
airtableUpdateRecordTool,
} from './index'
describe('Airtable Tools Integration', () => {
let tester: ToolTester
beforeEach(() => {
process.env.NEXT_PUBLIC_APP_URL = 'http://localhost:3000'
})
afterEach(() => {
vi.resetAllMocks()
process.env.NEXT_PUBLIC_APP_URL = undefined
})
describe('Airtable List Records Tool', () => {
beforeEach(() => {
tester = new ToolTester(airtableListRecordsTool)
})
test('should construct correct list request', () => {
const params = {
baseId: 'base123',
tableId: 'table456',
accessToken: 'token789',
maxRecords: 100,
filterFormula: "Status='Active'",
}
const url = tester.getRequestUrl(params)
const headers = tester.getRequestHeaders(params)
expect(url).toContain('/base123/table456')
expect(url).toContain('maxRecords=100')
expect(url).toContain(`filterByFormula=Status='Active'`)
expect(headers.Authorization).toBe('Bearer token789')
})
test('should handle successful list response', async () => {
const mockData = {
records: [
{ id: 'rec1', fields: { Name: 'Test 1' } },
{ id: 'rec2', fields: { Name: 'Test 2' } },
],
offset: 'next_page_token',
}
tester.setup(mockData)
const result = await tester.execute({
baseId: 'base123',
tableId: 'table456',
accessToken: 'token789',
})
expect(result.success).toBe(true)
expect(result.output.records).toHaveLength(2)
expect(result.output.metadata.offset).toBe('next_page_token')
expect(result.output.metadata.totalRecords).toBe(2)
})
})
describe('Airtable Get Record Tool', () => {
beforeEach(() => {
tester = new ToolTester(airtableGetRecordTool)
})
test('should construct correct get request', () => {
const params = {
baseId: 'base123',
tableId: 'table456',
recordId: 'rec789',
accessToken: 'token789',
}
const url = tester.getRequestUrl(params)
const headers = tester.getRequestHeaders(params)
expect(url).toContain('/base123/table456/rec789')
expect(headers.Authorization).toBe('Bearer token789')
})
test('should handle successful get response', async () => {
const mockData = {
id: 'rec789',
createdTime: '2023-01-01T00:00:00.000Z',
fields: { Name: 'Test Record' },
}
tester.setup(mockData)
const result = await tester.execute({
baseId: 'base123',
tableId: 'table456',
recordId: 'rec789',
accessToken: 'token789',
})
expect(result.success).toBe(true)
expect(result.output.record.id).toBe('rec789')
expect(result.output.record.fields.Name).toBe('Test Record')
expect(result.output.metadata.recordCount).toBe(1)
})
})
describe('Airtable Create Records Tool', () => {
beforeEach(() => {
tester = new ToolTester(airtableCreateRecordsTool)
})
test('should construct correct create request', () => {
const params = {
baseId: 'base123',
tableId: 'table456',
accessToken: 'token789',
records: [{ fields: { Name: 'New Record' } }],
}
const url = tester.getRequestUrl(params)
const headers = tester.getRequestHeaders(params)
const body = tester.getRequestBody(params)
expect(url).toContain('/base123/table456')
expect(headers.Authorization).toBe('Bearer token789')
expect(body).toEqual({ records: [{ fields: { Name: 'New Record' } }] })
})
test('should handle successful create response', async () => {
const mockData = {
records: [{ id: 'rec1', fields: { Name: 'New Record' } }],
}
tester.setup(mockData)
const result = await tester.execute({
baseId: 'base123',
tableId: 'table456',
accessToken: 'token789',
records: [{ fields: { Name: 'New Record' } }],
})
expect(result.success).toBe(true)
expect(result.output.records).toHaveLength(1)
expect(result.output.metadata.recordCount).toBe(1)
})
})
describe('Airtable Update Record Tool', () => {
beforeEach(() => {
tester = new ToolTester(airtableUpdateRecordTool)
})
test('should construct correct update request', () => {
const params = {
baseId: 'base123',
tableId: 'table456',
recordId: 'rec789',
accessToken: 'token789',
fields: { Name: 'Updated Record' },
}
const url = tester.getRequestUrl(params)
const headers = tester.getRequestHeaders(params)
const body = tester.getRequestBody(params)
expect(url).toContain('/base123/table456/rec789')
expect(headers.Authorization).toBe('Bearer token789')
expect(body).toEqual({ fields: { Name: 'Updated Record' } })
})
test('should handle successful update response', async () => {
const mockData = {
id: 'rec789',
fields: { Name: 'Updated Record' },
}
tester.setup(mockData)
const result = await tester.execute({
baseId: 'base123',
tableId: 'table456',
recordId: 'rec789',
accessToken: 'token789',
fields: { Name: 'Updated Record' },
})
expect(result.success).toBe(true)
expect(result.output.record.id).toBe('rec789')
expect(result.output.metadata.recordCount).toBe(1)
expect(result.output.metadata.updatedFields).toContain('Name')
})
})
test('should handle error responses', async () => {
tester = new ToolTester(airtableListRecordsTool)
const errorMessage = 'Invalid API key'
tester.setup({ error: errorMessage }, { ok: false, status: 401 })
const result = await tester.execute({
baseId: 'base123',
tableId: 'table456',
accessToken: 'invalid_token',
})
expect(result.success).toBe(false)
expect(result.error).toContain('Failed to list Airtable records')
})
})

View File

@@ -10,36 +10,39 @@ export const airtableListRecordsTool: ToolConfig<AirtableListParams, AirtableLis
oauth: {
required: true,
provider: 'airtable',
// Define required scopes if different from default write/read
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token',
},
baseId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ID of the Airtable base',
},
tableId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ID of the table',
},
maxRecords: {
type: 'number',
required: false,
visibility: 'user-only',
description: 'Maximum number of records to return',
},
filterFormula: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Formula to filter records (e.g., "({Field Name} = \'Value\')")',
},
// TODO: Add other list parameters like pageSize, offset, view, sort, fields, returnFieldsByFieldId, recordMetadata
},
request: {

View File

@@ -17,7 +17,6 @@ interface AirtableBaseParams {
export interface AirtableListParams extends AirtableBaseParams {
maxRecords?: number
filterFormula?: string
// TODO: Add other list parameters like pageSize, offset, view, sort, fields, returnFieldsByFieldId, recordMetadata
}
export interface AirtableListResponse extends ToolResponse {
@@ -47,7 +46,6 @@ export interface AirtableGetResponse extends ToolResponse {
// Create Records Types
export interface AirtableCreateParams extends AirtableBaseParams {
records: Array<{ fields: Record<string, any> }>
// TODO: Add typecast parameter
}
export interface AirtableCreateResponse extends ToolResponse {
@@ -63,7 +61,6 @@ export interface AirtableCreateResponse extends ToolResponse {
export interface AirtableUpdateParams extends AirtableBaseParams {
recordId: string
fields: Record<string, any>
// TODO: Add typecast parameter
}
export interface AirtableUpdateResponse extends ToolResponse {
@@ -79,7 +76,6 @@ export interface AirtableUpdateResponse extends ToolResponse {
// Update Multiple Records Types
export interface AirtableUpdateMultipleParams extends AirtableBaseParams {
records: Array<{ id: string; fields: Record<string, any> }>
// TODO: Add typecast, performUpsert parameters
}
export interface AirtableUpdateMultipleResponse extends ToolResponse {

View File

@@ -21,21 +21,25 @@ export const airtableUpdateMultipleRecordsTool: ToolConfig<
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token',
},
baseId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ID of the Airtable base',
},
tableId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ID or name of the table',
},
records: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description: 'Array of records to update, each with an `id` and a `fields` object',
// Example: [{ id: "rec123", fields: { "Status": "Done" } }, { id: "rec456", fields: { "Priority": "High" } }]
},

View File

@@ -18,26 +18,31 @@ export const airtableUpdateRecordTool: ToolConfig<AirtableUpdateParams, Airtable
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token',
},
baseId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ID of the Airtable base',
},
tableId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ID or name of the table',
},
recordId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ID of the record to update',
},
fields: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description: 'An object containing the field names and their new values',
// Example: { "Field 1": "NewValue1", "Status": "Completed" }
},

View File

@@ -1,3 +0,0 @@
import { promptManagerTool } from './prompt_manager'
export const autoblocksPromptManagerTool = promptManagerTool

View File

@@ -1,113 +0,0 @@
import { createLogger } from '@/lib/logs/console-logger'
import type { ToolConfig } from '../types'
import type { PromptManagerParams, PromptManagerResponse } from './types'
const logger = createLogger('AutoblocksPromptManagerTool')
export const promptManagerTool: ToolConfig<PromptManagerParams, PromptManagerResponse> = {
id: 'autoblocks_prompt_manager',
name: 'Autoblocks Prompt Manager',
description: 'Manage and render prompts using Autoblocks prompt management system',
version: '1.0.0',
params: {
promptId: {
type: 'string',
required: true,
description: 'The ID of the prompt to retrieve',
},
version: {
type: 'string',
required: true,
description: 'Version strategy (latest or specific)',
},
specificVersion: {
type: 'string',
required: false,
description: 'Specific version to use (e.g., "1.2" or "1.latest")',
},
templateParams: {
type: 'object',
required: false,
description: 'Parameters to render the template with',
},
apiKey: {
type: 'string',
required: true,
description: 'Autoblocks API key',
},
enableABTesting: {
type: 'boolean',
required: false,
description: 'Whether to enable A/B testing between versions',
},
abTestConfig: {
type: 'object',
required: false,
description: 'Configuration for A/B testing between versions',
},
environment: {
type: 'string',
required: true,
description: 'Environment to use (production, staging, development)',
},
},
request: {
url: 'https://api.autoblocks.ai/v1/prompts',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
'X-Environment': params.environment,
}),
body: (params) => {
const requestBody: Record<string, any> = {
promptId: params.promptId,
templateParams: params.templateParams || {},
}
// Handle version selection
if (params.version === 'specific' && params.specificVersion) {
requestBody.version = params.specificVersion
} else {
requestBody.version = 'latest'
}
// Handle A/B testing
if (params.enableABTesting && params.abTestConfig) {
requestBody.versions = params.abTestConfig.versions
}
return requestBody
},
},
transformResponse: async (response) => {
try {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || `Error: ${response.status} ${response.statusText}`)
}
return {
success: true,
output: {
promptId: data.promptId,
version: data.version,
renderedPrompt: data.renderedPrompt,
templates: data.templates || {},
},
}
} catch (error) {
logger.error('Error transforming Autoblocks response', error)
throw error
}
},
transformError: (error) => {
logger.error('Autoblocks prompt manager error', error)
return `Error processing Autoblocks prompt: ${error.message || String(error)}`
},
}

View File

@@ -1,26 +0,0 @@
import type { ToolResponse } from '../types'
export interface PromptManagerParams {
promptId: string
version: string
specificVersion?: string
templateParams?: Record<string, any>
apiKey: string
enableABTesting?: boolean
abTestConfig?: {
versions: Array<{
version: string
weight: number
}>
}
environment: string
}
export interface PromptManagerResponse extends ToolResponse {
output: {
promptId: string
version: string
renderedPrompt: string
templates: Record<string, string>
}
}

View File

@@ -17,27 +17,31 @@ export const runTaskTool: ToolConfig<BrowserUseRunTaskParams, BrowserUseRunTaskR
task: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'What should the browser agent do',
},
variables: {
type: 'json',
required: false,
visibility: 'user-only',
description: 'Optional variables to use as secrets (format: {key: value})',
},
save_browser_data: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Whether to save browser data',
},
model: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'LLM model to use (default: gpt-4o)',
},
apiKey: {
type: 'string',
required: true,
requiredForToolCall: true,
visibility: 'user-only',
description: 'API key for BrowserUse API',
},
},

View File

@@ -12,19 +12,20 @@ export const clayPopulateTool: ToolConfig<ClayPopulateParams, ClayPopulateRespon
webhookURL: {
type: 'string',
required: true,
requiredForToolCall: true,
visibility: 'user-only',
description: 'The webhook URL to populate',
},
data: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description: 'The data to populate',
optionalToolInput: true,
},
authToken: {
type: 'string',
required: false,
description: 'Optional auth token for WebhookURL',
required: true,
visibility: 'user-only',
description: 'Auth token for Clay webhook authentication',
},
},

View File

@@ -19,22 +19,25 @@ export const confluenceRetrieveTool: ToolConfig<
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Confluence',
},
domain: {
type: 'string',
required: true,
requiredForToolCall: true,
visibility: 'user-only',
description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)',
},
pageId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Confluence page ID to retrieve',
},
cloudId: {
type: 'string',
required: false,
visibility: 'user-only',
description:
'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.',
},

View File

@@ -16,37 +16,43 @@ export const confluenceUpdateTool: ToolConfig<ConfluenceUpdateParams, Confluence
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Confluence',
},
domain: {
type: 'string',
required: true,
requiredForToolCall: true,
visibility: 'user-only',
description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)',
},
pageId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Confluence page ID to update',
},
title: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New title for the page',
},
content: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New content for the page in Confluence storage format',
},
version: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Version number of the page (required for preventing conflicts)',
},
cloudId: {
type: 'string',
required: false,
visibility: 'user-only',
description:
'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.',
},

View File

@@ -22,18 +22,19 @@ export const discordGetMessagesTool: ToolConfig<
botToken: {
type: 'string',
required: true,
requiredForToolCall: true,
visibility: 'user-only',
description: 'The bot token for authentication',
},
channelId: {
type: 'string',
required: true,
optionalToolInput: true,
visibility: 'user-only',
description: 'The Discord channel ID to retrieve messages from',
},
limit: {
type: 'number',
required: false,
visibility: 'user-only',
description: 'Maximum number of messages to retrieve (default: 10, max: 100)',
},
},

Some files were not shown because too many files have changed in this diff Show More