From ff2b1d33c87eb616adca7a133672f4dbfa99ddf6 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 13 Jul 2025 20:16:49 -0700 Subject: [PATCH] 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 --- .github/CONTRIBUTING.md | 74 +- README.md | 22 +- apps/docs/content/docs/tools/autoblocks.mdx | 186 -- apps/docs/content/docs/tools/clay.mdx | 2 +- apps/docs/content/docs/tools/elevenlabs.mdx | 2 +- apps/docs/content/docs/tools/firecrawl.mdx | 4 +- apps/docs/content/docs/tools/github.mdx | 4 +- apps/docs/content/docs/tools/gmail.mdx | 2 +- apps/docs/content/docs/tools/google_docs.mdx | 3 +- apps/docs/content/docs/tools/google_drive.mdx | 9 +- .../docs/content/docs/tools/google_search.mdx | 2 +- apps/docs/content/docs/tools/guesty.mdx | 123 -- apps/docs/content/docs/tools/huggingface.mdx | 7 +- .../content/docs/tools/image_generator.mdx | 2 +- apps/docs/content/docs/tools/jina.mdx | 4 + apps/docs/content/docs/tools/knowledge.mdx | 1 + apps/docs/content/docs/tools/linkup.mdx | 2 +- apps/docs/content/docs/tools/mem0.mdx | 6 +- apps/docs/content/docs/tools/meta.json | 2 - .../docs/content/docs/tools/mistral_parse.mdx | 2 +- apps/docs/content/docs/tools/notion.mdx | 6 +- apps/docs/content/docs/tools/openai.mdx | 5 +- apps/docs/content/docs/tools/perplexity.mdx | 7 +- apps/docs/content/docs/tools/pinecone.mdx | 10 +- apps/docs/content/docs/tools/reddit.mdx | 22 +- apps/docs/content/docs/tools/serper.mdx | 2 +- apps/docs/content/docs/tools/slack.mdx | 1 + apps/docs/content/docs/tools/stagehand.mdx | 6 +- .../content/docs/tools/stagehand_agent.mdx | 4 +- apps/docs/content/docs/tools/supabase.mdx | 8 +- apps/docs/content/docs/tools/tavily.mdx | 2 +- apps/docs/content/docs/tools/youtube.mdx | 2 +- apps/sim/app/api/function/execute/route.ts | 1 - .../trace-spans/trace-spans-display.tsx | 25 +- .../sub-block/components/combobox.tsx | 69 +- .../components/oauth-required-modal.tsx | 2 +- .../sub-block/components/dropdown.tsx | 4 +- .../components/confluence-file-selector.tsx | 26 +- .../components/discord-channel-selector.tsx | 26 +- .../components/google-calendar-selector.tsx | 40 +- .../components/google-drive-picker.tsx | 36 +- .../components/jira-issue-selector.tsx | 26 +- .../components/microsoft-file-selector.tsx | 36 +- .../components/teams-message-selector.tsx | 34 +- .../sub-block/components/slider-input.tsx | 10 +- .../components/tool-credential-selector.tsx | 76 +- .../components/tool-input/tool-input.tsx | 1538 +++++++++++------ .../components/sub-block/sub-block.tsx | 4 +- .../hooks/use-workflow-execution.ts | 23 +- apps/sim/blocks/blocks/api.ts | 8 +- apps/sim/blocks/blocks/autoblocks.ts | 120 -- apps/sim/blocks/blocks/elevenlabs.ts | 26 +- apps/sim/blocks/blocks/evaluator.ts | 5 +- apps/sim/blocks/blocks/guesty.ts | 94 - apps/sim/blocks/blocks/notion.ts | 30 +- apps/sim/blocks/blocks/perplexity.ts | 12 +- apps/sim/blocks/blocks/reddit.ts | 32 +- apps/sim/blocks/blocks/router.ts | 5 +- apps/sim/blocks/blocks/serper.ts | 33 +- apps/sim/blocks/blocks/slack.ts | 8 - apps/sim/blocks/blocks/supabase.ts | 10 +- apps/sim/blocks/blocks/tavily.ts | 5 +- apps/sim/blocks/blocks/translate.ts | 2 +- apps/sim/blocks/blocks/vision.ts | 6 +- apps/sim/blocks/blocks/x.ts | 10 +- apps/sim/blocks/registry.ts | 17 +- apps/sim/blocks/types.ts | 6 +- apps/sim/components/icons.tsx | 121 -- .../executor/handlers/agent/agent-handler.ts | 33 +- apps/sim/executor/index.ts | 4 + apps/sim/providers/anthropic/index.ts | 40 +- apps/sim/providers/azure-openai/index.ts | 43 +- apps/sim/providers/cerebras/index.ts | 43 +- apps/sim/providers/deepseek/index.ts | 40 +- apps/sim/providers/google/index.ts | 536 ++++-- apps/sim/providers/groq/index.ts | 43 +- apps/sim/providers/models.ts | 4 +- apps/sim/providers/ollama/index.ts | 43 +- apps/sim/providers/openai/index.ts | 43 +- apps/sim/providers/utils.test.ts | 25 +- apps/sim/providers/utils.ts | 114 +- apps/sim/providers/xai/index.ts | 53 +- .../tests/socket-server.test.ts | 0 apps/sim/stores/panel/console/store.ts | 12 +- apps/sim/stores/panel/console/types.ts | 3 +- apps/sim/tools/airtable/create_records.ts | 6 +- apps/sim/tools/airtable/get_record.ts | 4 + apps/sim/tools/airtable/index.test.ts | 220 --- apps/sim/tools/airtable/list_records.ts | 7 +- apps/sim/tools/airtable/types.ts | 4 - .../tools/airtable/update_multiple_records.ts | 4 + apps/sim/tools/airtable/update_record.ts | 5 + apps/sim/tools/autoblocks/index.ts | 3 - apps/sim/tools/autoblocks/prompt_manager.ts | 113 -- apps/sim/tools/autoblocks/types.ts | 26 - apps/sim/tools/browser_use/run_task.ts | 6 +- apps/sim/tools/clay/populate.ts | 9 +- apps/sim/tools/confluence/retrieve.ts | 5 +- apps/sim/tools/confluence/update.ts | 8 +- apps/sim/tools/discord/get_messages.ts | 5 +- apps/sim/tools/discord/get_server.ts | 4 +- apps/sim/tools/discord/get_user.ts | 4 +- apps/sim/tools/discord/send_message.ts | 7 +- apps/sim/tools/elevenlabs/tts.ts | 16 +- apps/sim/tools/exa/answer.ts | 4 +- apps/sim/tools/exa/find_similar_links.ts | 5 +- apps/sim/tools/exa/get_contents.ts | 5 +- apps/sim/tools/exa/search.ts | 6 +- apps/sim/tools/file/parser.ts | 2 + apps/sim/tools/firecrawl/scrape.ts | 15 +- apps/sim/tools/firecrawl/search.ts | 13 +- apps/sim/tools/function/execute.ts | 5 + apps/sim/tools/github/comment.ts | 32 +- apps/sim/tools/github/latest_commit.ts | 5 +- apps/sim/tools/github/pr.test.ts | 211 --- apps/sim/tools/github/pr.ts | 5 +- apps/sim/tools/github/repo_info.test.ts | 241 --- apps/sim/tools/github/repo_info.ts | 4 +- apps/sim/tools/gmail/draft.ts | 4 + apps/sim/tools/gmail/read.test.ts | 409 ----- apps/sim/tools/gmail/read.ts | 10 +- apps/sim/tools/gmail/search.ts | 8 +- apps/sim/tools/gmail/send.ts | 4 + apps/sim/tools/google/search.ts | 16 +- apps/sim/tools/google_calendar/create.ts | 10 + apps/sim/tools/google_calendar/get.ts | 3 + apps/sim/tools/google_calendar/invite.ts | 6 + apps/sim/tools/google_calendar/list.ts | 6 + apps/sim/tools/google_calendar/quick_add.ts | 5 + apps/sim/tools/google_calendar/update.ts | 11 + apps/sim/tools/google_docs/create.ts | 25 +- apps/sim/tools/google_docs/read.ts | 8 +- apps/sim/tools/google_docs/write.ts | 1 + apps/sim/tools/google_drive/create_folder.ts | 24 +- apps/sim/tools/google_drive/get_content.ts | 3 + apps/sim/tools/google_drive/list.ts | 25 +- apps/sim/tools/google_drive/upload.ts | 38 +- apps/sim/tools/google_sheets/append.ts | 19 +- apps/sim/tools/google_sheets/read.ts | 9 +- apps/sim/tools/google_sheets/update.ts | 18 +- apps/sim/tools/google_sheets/write.ts | 18 +- apps/sim/tools/guesty/guest.ts | 66 - apps/sim/tools/guesty/index.ts | 4 - apps/sim/tools/guesty/reservation.ts | 70 - apps/sim/tools/guesty/types.ts | 47 - apps/sim/tools/hubspot/contacts.ts | 137 -- apps/sim/tools/huggingface/chat.ts | 37 +- apps/sim/tools/jina/read_url.ts | 10 +- apps/sim/tools/jira/bulk_read.ts | 5 +- apps/sim/tools/jira/retrieve.ts | 6 +- apps/sim/tools/jira/update.ts | 11 +- apps/sim/tools/jira/write.ts | 10 +- apps/sim/tools/linear/create_issue.ts | 28 +- apps/sim/tools/linear/read_issues.ts | 14 +- apps/sim/tools/linkup/search.ts | 15 +- apps/sim/tools/mem0/add_memories.ts | 15 +- apps/sim/tools/mem0/get_memories.ts | 19 +- apps/sim/tools/mem0/search_memories.ts | 16 +- apps/sim/tools/microsoft_excel/read.ts | 9 +- apps/sim/tools/microsoft_excel/table_add.ts | 4 + apps/sim/tools/microsoft_excel/write.ts | 18 +- .../sim/tools/microsoft_teams/read_channel.ts | 5 +- apps/sim/tools/microsoft_teams/read_chat.ts | 4 +- .../{attachment-utils.ts => utils.ts} | 0 .../tools/microsoft_teams/write_channel.ts | 4 + apps/sim/tools/microsoft_teams/write_chat.ts | 3 + apps/sim/tools/mistral/parser.ts | 19 +- apps/sim/tools/notion/create_page.ts | 16 +- apps/sim/tools/notion/read.ts | 12 +- apps/sim/tools/notion/update_page.ts | 15 +- apps/sim/tools/notion/write.ts | 14 +- apps/sim/tools/openai/embeddings.ts | 16 +- apps/sim/tools/openai/image.ts | 18 +- apps/sim/tools/openai/types.ts | 2 +- apps/sim/tools/outlook/draft.ts | 4 + apps/sim/tools/outlook/read.ts | 3 + apps/sim/tools/outlook/send.ts | 4 + apps/sim/tools/params.test.ts | 273 +++ apps/sim/tools/params.ts | 468 +++++ apps/sim/tools/perplexity/chat.ts | 70 +- apps/sim/tools/perplexity/types.ts | 8 +- apps/sim/tools/pinecone/fetch.ts | 17 +- .../sim/tools/pinecone/generate_embeddings.ts | 14 +- apps/sim/tools/pinecone/search_text.ts | 20 +- apps/sim/tools/pinecone/search_vector.ts | 20 +- apps/sim/tools/pinecone/upsert_text.ts | 16 +- apps/sim/tools/reddit/get_comments.ts | 11 +- apps/sim/tools/reddit/get_posts.ts | 10 + apps/sim/tools/reddit/hot_posts.ts | 8 + apps/sim/tools/registry.ts | 12 +- apps/sim/tools/s3/get_object.ts | 81 +- apps/sim/tools/s3/utils.ts | 73 + apps/sim/tools/salesforce/opportunities.ts | 144 -- apps/sim/tools/serper/search.ts | 17 +- apps/sim/tools/slack/message.ts | 12 +- apps/sim/tools/stagehand/agent.ts | 23 +- apps/sim/tools/stagehand/extract.ts | 20 +- apps/sim/tools/supabase/insert.ts | 28 +- apps/sim/tools/supabase/query.ts | 28 +- apps/sim/tools/tavily/extract.ts | 14 +- apps/sim/tools/tavily/search.ts | 4 +- apps/sim/tools/telegram/message.ts | 22 +- apps/sim/tools/telegram/utils.ts | 15 + apps/sim/tools/thinking/tool.ts | 2 +- apps/sim/tools/twilio/send_sms.ts | 7 +- apps/sim/tools/typeform/files.test.ts | 194 --- apps/sim/tools/typeform/files.ts | 6 + apps/sim/tools/typeform/index.test.ts | 325 ---- apps/sim/tools/typeform/insights.test.ts | 188 -- apps/sim/tools/typeform/insights.ts | 2 + apps/sim/tools/typeform/responses.test.ts | 265 --- apps/sim/tools/typeform/responses.ts | 6 + apps/sim/tools/types.ts | 9 +- apps/sim/tools/utils.test.ts | 8 +- apps/sim/tools/utils.ts | 15 +- apps/sim/tools/vision/tool.ts | 5 +- apps/sim/tools/whatsapp/send_message.ts | 5 +- apps/sim/tools/x/read.ts | 4 +- apps/sim/tools/x/search.ts | 6 + apps/sim/tools/x/user.ts | 2 + apps/sim/tools/x/write.ts | 5 + apps/sim/tools/youtube/search.ts | 14 +- package.json | 5 +- 223 files changed, 4089 insertions(+), 4920 deletions(-) delete mode 100644 apps/docs/content/docs/tools/autoblocks.mdx delete mode 100644 apps/docs/content/docs/tools/guesty.mdx delete mode 100644 apps/sim/blocks/blocks/autoblocks.ts delete mode 100644 apps/sim/blocks/blocks/guesty.ts rename apps/sim/{ => socket-server}/tests/socket-server.test.ts (100%) delete mode 100644 apps/sim/tools/airtable/index.test.ts delete mode 100644 apps/sim/tools/autoblocks/index.ts delete mode 100644 apps/sim/tools/autoblocks/prompt_manager.ts delete mode 100644 apps/sim/tools/autoblocks/types.ts delete mode 100644 apps/sim/tools/github/pr.test.ts delete mode 100644 apps/sim/tools/github/repo_info.test.ts delete mode 100644 apps/sim/tools/gmail/read.test.ts delete mode 100644 apps/sim/tools/guesty/guest.ts delete mode 100644 apps/sim/tools/guesty/index.ts delete mode 100644 apps/sim/tools/guesty/reservation.ts delete mode 100644 apps/sim/tools/guesty/types.ts delete mode 100644 apps/sim/tools/hubspot/contacts.ts rename apps/sim/tools/microsoft_teams/{attachment-utils.ts => utils.ts} (100%) create mode 100644 apps/sim/tools/params.test.ts create mode 100644 apps/sim/tools/params.ts create mode 100644 apps/sim/tools/s3/utils.ts delete mode 100644 apps/sim/tools/salesforce/opportunities.ts create mode 100644 apps/sim/tools/telegram/utils.ts delete mode 100644 apps/sim/tools/typeform/files.test.ts delete mode 100644 apps/sim/tools/typeform/index.test.ts delete mode 100644 apps/sim/tools/typeform/insights.test.ts delete mode 100644 apps/sim/tools/typeform/responses.test.ts diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 3eb3e5cd6..9f5e1c845 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -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. diff --git a/README.md b/README.md index b889c598f..f868d3f1e 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/apps/docs/content/docs/tools/autoblocks.mdx b/apps/docs/content/docs/tools/autoblocks.mdx deleted file mode 100644 index a4f611869..000000000 --- a/apps/docs/content/docs/tools/autoblocks.mdx +++ /dev/null @@ -1,186 +0,0 @@ ---- -title: Autoblocks -description: Manage and use versioned prompts with Autoblocks ---- - -import { BlockInfoCard } from "@/components/ui/block-info-card" - - - - - - `} -/> - -{/* 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` diff --git a/apps/docs/content/docs/tools/clay.mdx b/apps/docs/content/docs/tools/clay.mdx index 5693a9bcc..7cea769be 100644 --- a/apps/docs/content/docs/tools/clay.mdx +++ b/apps/docs/content/docs/tools/clay.mdx @@ -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 diff --git a/apps/docs/content/docs/tools/elevenlabs.mdx b/apps/docs/content/docs/tools/elevenlabs.mdx index 68df5c143..67a1d3f08 100644 --- a/apps/docs/content/docs/tools/elevenlabs.mdx +++ b/apps/docs/content/docs/tools/elevenlabs.mdx @@ -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 diff --git a/apps/docs/content/docs/tools/firecrawl.mdx b/apps/docs/content/docs/tools/firecrawl.mdx index 9f9b7e4f4..94ea6caa6 100644 --- a/apps/docs/content/docs/tools/firecrawl.mdx +++ b/apps/docs/content/docs/tools/firecrawl.mdx @@ -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 diff --git a/apps/docs/content/docs/tools/github.mdx b/apps/docs/content/docs/tools/github.mdx index 6784fee00..97fb1d490 100644 --- a/apps/docs/content/docs/tools/github.mdx +++ b/apps/docs/content/docs/tools/github.mdx @@ -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 diff --git a/apps/docs/content/docs/tools/gmail.mdx b/apps/docs/content/docs/tools/gmail.mdx index 91a21eb55..1d4fa4d1f 100644 --- a/apps/docs/content/docs/tools/gmail.mdx +++ b/apps/docs/content/docs/tools/gmail.mdx @@ -110,7 +110,7 @@ Draft emails using Gmail | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `operation` | string | Yes | Operation (e.g., 'send', 'draft') | +| `operation` | string | Yes | Operation | diff --git a/apps/docs/content/docs/tools/google_docs.mdx b/apps/docs/content/docs/tools/google_docs.mdx index c6ca2893a..730bde40a 100644 --- a/apps/docs/content/docs/tools/google_docs.mdx +++ b/apps/docs/content/docs/tools/google_docs.mdx @@ -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 diff --git a/apps/docs/content/docs/tools/google_drive.mdx b/apps/docs/content/docs/tools/google_drive.mdx index 3b00aa8e3..8de1744f7 100644 --- a/apps/docs/content/docs/tools/google_drive.mdx +++ b/apps/docs/content/docs/tools/google_drive.mdx @@ -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 | diff --git a/apps/docs/content/docs/tools/google_search.mdx b/apps/docs/content/docs/tools/google_search.mdx index c2d811ef3..1e4f400ef 100644 --- a/apps/docs/content/docs/tools/google_search.mdx +++ b/apps/docs/content/docs/tools/google_search.mdx @@ -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 diff --git a/apps/docs/content/docs/tools/guesty.mdx b/apps/docs/content/docs/tools/guesty.mdx deleted file mode 100644 index 864dca2d5..000000000 --- a/apps/docs/content/docs/tools/guesty.mdx +++ /dev/null @@ -1,123 +0,0 @@ ---- -title: Guesty -description: Interact with Guesty property management system ---- - -import { BlockInfoCard } from "@/components/ui/block-info-card" - - - - `} -/> - -{/* 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` diff --git a/apps/docs/content/docs/tools/huggingface.mdx b/apps/docs/content/docs/tools/huggingface.mdx index 0cea4603b..9eefb76e5 100644 --- a/apps/docs/content/docs/tools/huggingface.mdx +++ b/apps/docs/content/docs/tools/huggingface.mdx @@ -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 diff --git a/apps/docs/content/docs/tools/image_generator.mdx b/apps/docs/content/docs/tools/image_generator.mdx index 6be17223f..1b3f4e4ad 100644 --- a/apps/docs/content/docs/tools/image_generator.mdx +++ b/apps/docs/content/docs/tools/image_generator.mdx @@ -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\) | diff --git a/apps/docs/content/docs/tools/jina.mdx b/apps/docs/content/docs/tools/jina.mdx index af649f976..8d796c48f 100644 --- a/apps/docs/content/docs/tools/jina.mdx +++ b/apps/docs/content/docs/tools/jina.mdx @@ -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 diff --git a/apps/docs/content/docs/tools/knowledge.mdx b/apps/docs/content/docs/tools/knowledge.mdx index 1ffd84723..df6aec4f7 100644 --- a/apps/docs/content/docs/tools/knowledge.mdx +++ b/apps/docs/content/docs/tools/knowledge.mdx @@ -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` diff --git a/apps/docs/content/docs/tools/linkup.mdx b/apps/docs/content/docs/tools/linkup.mdx index 99d91fb04..1d8d25958 100644 --- a/apps/docs/content/docs/tools/linkup.mdx +++ b/apps/docs/content/docs/tools/linkup.mdx @@ -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 diff --git a/apps/docs/content/docs/tools/mem0.mdx b/apps/docs/content/docs/tools/mem0.mdx index 8efc9f1b8..7d5a52ad4 100644 --- a/apps/docs/content/docs/tools/mem0.mdx +++ b/apps/docs/content/docs/tools/mem0.mdx @@ -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 diff --git a/apps/docs/content/docs/tools/meta.json b/apps/docs/content/docs/tools/meta.json index 803328796..7ba2fc513 100644 --- a/apps/docs/content/docs/tools/meta.json +++ b/apps/docs/content/docs/tools/meta.json @@ -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", diff --git a/apps/docs/content/docs/tools/mistral_parse.mdx b/apps/docs/content/docs/tools/mistral_parse.mdx index e6be7f2dc..b26d96ca8 100644 --- a/apps/docs/content/docs/tools/mistral_parse.mdx +++ b/apps/docs/content/docs/tools/mistral_parse.mdx @@ -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 diff --git a/apps/docs/content/docs/tools/notion.mdx b/apps/docs/content/docs/tools/notion.mdx index 59bc3ea3b..50eace34d 100644 --- a/apps/docs/content/docs/tools/notion.mdx +++ b/apps/docs/content/docs/tools/notion.mdx @@ -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 diff --git a/apps/docs/content/docs/tools/openai.mdx b/apps/docs/content/docs/tools/openai.mdx index 2461ca304..98407b55f 100644 --- a/apps/docs/content/docs/tools/openai.mdx +++ b/apps/docs/content/docs/tools/openai.mdx @@ -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 diff --git a/apps/docs/content/docs/tools/perplexity.mdx b/apps/docs/content/docs/tools/perplexity.mdx index 64a5c920b..2fd7ed65d 100644 --- a/apps/docs/content/docs/tools/perplexity.mdx +++ b/apps/docs/content/docs/tools/perplexity.mdx @@ -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... | diff --git a/apps/docs/content/docs/tools/pinecone.mdx b/apps/docs/content/docs/tools/pinecone.mdx index bc7216820..95346367a 100644 --- a/apps/docs/content/docs/tools/pinecone.mdx +++ b/apps/docs/content/docs/tools/pinecone.mdx @@ -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 diff --git a/apps/docs/content/docs/tools/reddit.mdx b/apps/docs/content/docs/tools/reddit.mdx index cea576bf4..68d80440a 100644 --- a/apps/docs/content/docs/tools/reddit.mdx +++ b/apps/docs/content/docs/tools/reddit.mdx @@ -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 | diff --git a/apps/docs/content/docs/tools/serper.mdx b/apps/docs/content/docs/tools/serper.mdx index af518d680..acc3e6e31 100644 --- a/apps/docs/content/docs/tools/serper.mdx +++ b/apps/docs/content/docs/tools/serper.mdx @@ -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 diff --git a/apps/docs/content/docs/tools/slack.mdx b/apps/docs/content/docs/tools/slack.mdx index c0021e1a8..77f35f65a 100644 --- a/apps/docs/content/docs/tools/slack.mdx +++ b/apps/docs/content/docs/tools/slack.mdx @@ -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\) | diff --git a/apps/docs/content/docs/tools/stagehand.mdx b/apps/docs/content/docs/tools/stagehand.mdx index 412e54134..6a1742576 100644 --- a/apps/docs/content/docs/tools/stagehand.mdx +++ b/apps/docs/content/docs/tools/stagehand.mdx @@ -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 diff --git a/apps/docs/content/docs/tools/stagehand_agent.mdx b/apps/docs/content/docs/tools/stagehand_agent.mdx index 4a86d4b31..7f62b5ede 100644 --- a/apps/docs/content/docs/tools/stagehand_agent.mdx +++ b/apps/docs/content/docs/tools/stagehand_agent.mdx @@ -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 diff --git a/apps/docs/content/docs/tools/supabase.mdx b/apps/docs/content/docs/tools/supabase.mdx index d40a0a831..97cc3c189 100644 --- a/apps/docs/content/docs/tools/supabase.mdx +++ b/apps/docs/content/docs/tools/supabase.mdx @@ -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 diff --git a/apps/docs/content/docs/tools/tavily.mdx b/apps/docs/content/docs/tools/tavily.mdx index fb025a610..6cab82126 100644 --- a/apps/docs/content/docs/tools/tavily.mdx +++ b/apps/docs/content/docs/tools/tavily.mdx @@ -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 diff --git a/apps/docs/content/docs/tools/youtube.mdx b/apps/docs/content/docs/tools/youtube.mdx index 25fa51d94..3a34d97b0 100644 --- a/apps/docs/content/docs/tools/youtube.mdx +++ b/apps/docs/content/docs/tools/youtube.mdx @@ -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 diff --git a/apps/sim/app/api/function/execute/route.ts b/apps/sim/app/api/function/execute/route.ts index 774b5c76d..dfadd365c 100644 --- a/apps/sim/app/api/function/execute/route.ts +++ b/apps/sim/app/api/function/execute/route.ts @@ -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, }) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/trace-spans/trace-spans-display.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/trace-spans/trace-spans-display.tsx index a4fd4f4a8..795a52ecc 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/trace-spans/trace-spans-display.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/trace-spans/trace-spans-display.tsx @@ -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) {
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/discord-channel-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/discord-channel-selector.tsx index 97f3c97d3..03053fe38 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/discord-channel-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/discord-channel-selector.tsx @@ -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 ? ( -
- # - {selectedChannel.name} -
- ) : ( -
- - {label} -
- )} +
+ {selectedChannel ? ( + <> + # + {selectedChannel.name} + + ) : ( + <> + + {label} + + )} +
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/google-calendar-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/google-calendar-selector.tsx index b45e141a4..32d2a8a7e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/google-calendar-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/google-calendar-selector.tsx @@ -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 ? ( -
-
- - {getCalendarDisplayName(selectedCalendar)} - -
- ) : ( -
- - {label} -
- )} +
+ {selectedCalendar ? ( + <> +
+ + {getCalendarDisplayName(selectedCalendar)} + + + ) : ( + <> + + {label} + + )} +
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/google-drive-picker.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/google-drive-picker.tsx index c8b999069..58c40c5a8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/google-drive-picker.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/google-drive-picker.tsx @@ -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 ? ( -
- {getFileIcon(selectedFile, 'sm')} - {selectedFile.name} -
- ) : selectedFileId && isLoadingSelectedFile && selectedCredentialId ? ( -
- - Loading document... -
- ) : ( -
- {getProviderIcon(provider)} - {label} -
- )} +
+ {selectedFile ? ( + <> + {getFileIcon(selectedFile, 'sm')} + {selectedFile.name} + + ) : selectedFileId && isLoadingSelectedFile && selectedCredentialId ? ( + <> + + Loading document... + + ) : ( + <> + {getProviderIcon(provider)} + {label} + + )} +
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/jira-issue-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/jira-issue-selector.tsx index cd69f8cc7..071e251ad 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/jira-issue-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/jira-issue-selector.tsx @@ -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 ? ( -
- - {selectedIssue.name} -
- ) : ( -
- - {label} -
- )} +
+ {selectedIssue ? ( + <> + + {selectedIssue.name} + + ) : ( + <> + + {label} + + )} +
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx index fc2da8b8a..d87ed7d31 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx @@ -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 ? ( -
- {getFileIcon(selectedFile, 'sm')} - {selectedFile.name} -
- ) : selectedFileId && isLoadingSelectedFile && selectedCredentialId ? ( -
- - Loading document... -
- ) : ( -
- {getProviderIcon(provider)} - {label} -
- )} +
+ {selectedFile ? ( + <> + {getFileIcon(selectedFile, 'sm')} + {selectedFile.name} + + ) : selectedFileId && isLoadingSelectedFile && selectedCredentialId ? ( + <> + + Loading document... + + ) : ( + <> + {getProviderIcon(provider)} + {label} + + )} +
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/teams-message-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/teams-message-selector.tsx index 1df58305a..de6f1add0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/teams-message-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/teams-message-selector.tsx @@ -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 ? ( -
- - {selectedMessage.displayName} -
- ) : ( -
- - - {selectionType === 'channel' && selectionStage === 'team' - ? 'Select a team first' - : label} - -
- )} +
+ {selectedMessage ? ( + <> + + {selectedMessage.displayName} + + ) : ( + <> + + + {selectionType === 'channel' && selectionStage === 'team' + ? 'Select a team first' + : label} + + + )} +
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/slider-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/slider-input.tsx index 703f7b2d3..a8eb48106 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/slider-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/slider-input.tsx @@ -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(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(() => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/components/tool-credential-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/components/tool-credential-selector.tsx index 0083bde7f..19f06e517 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/components/tool-credential-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/components/tool-credential-selector.tsx @@ -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 ( <> - + diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/tool-input.tsx index 8dff74d00..28a866ff0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/tool-input.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { PlusIcon, WrenchIcon, XIcon } from 'lucide-react' import { Button } from '@/components/ui/button' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' @@ -9,9 +9,10 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select' +import { Switch } from '@/components/ui/switch' import { Toggle } from '@/components/ui/toggle' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' -import type { OAuthProvider } from '@/lib/oauth/oauth' +import type { OAuthProvider, OAuthService } from '@/lib/oauth/oauth' import { cn } from '@/lib/utils' import { getAllBlocks } from '@/blocks' import { getProviderFromModel, supportsToolUsageControl } from '@/providers/utils' @@ -19,10 +20,26 @@ import { useCustomToolsStore } from '@/stores/custom-tools/store' import { useGeneralStore } from '@/stores/settings/general/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' -import { getTool } from '@/tools/utils' +import { + formatParameterLabel, + getToolParametersConfig, + isPasswordParameter, + type ToolParameterConfig, +} from '@/tools/params' import { useSubBlockValue } from '../../hooks/use-sub-block-value' import { ChannelSelectorInput } from '../channel-selector/channel-selector-input' +import { CheckboxList } from '../checkbox-list' +import { Code } from '../code' +import { ComboBox } from '../combobox' +import { DateInput } from '../date-input' +import { FileSelectorInput } from '../file-selector/file-selector-input' +import { FileUpload } from '../file-upload' +import { LongInput } from '../long-input' +import { ProjectSelectorInput } from '../project-selector/project-selector-input' import { ShortInput } from '../short-input' +import { SliderInput } from '../slider-input' +import { Table } from '../table' +import { TimeInput } from '../time-input' import { type CustomTool, CustomToolModal } from './components/custom-tool-modal/custom-tool-modal' import { ToolCommand } from './components/tool-command/tool-command' import { ToolCredentialSelector } from './components/tool-credential-selector' @@ -38,301 +55,332 @@ interface ToolInputProps { interface StoredTool { type: string title: string + toolId: string // Direct tool ID instead of relying on block mapping params: Record isExpanded?: boolean schema?: any // For custom tools code?: string // For custom tools implementation operation?: string // For tools with multiple operations - usageControl?: 'auto' | 'force' | 'none' // Control how the tool is used + usageControl?: 'auto' | 'force' | 'none' } -interface ToolParam { - id: string - type: string - description?: string - requiredForToolCall: boolean - optionalToolInput?: boolean -} +function GenericSyncWrapper({ + blockId, + paramId, + value, + onChange, + children, + transformer, +}: { + blockId: string + paramId: string + value: string + onChange: (value: string) => void + children: React.ReactNode + transformer?: (storeValue: T) => string +}) { + const [storeValue] = useSubBlockValue(blockId, paramId) -// Assumes the first tool in the access array is the tool to be used -// TODO: Switch to getting tools instead of tool blocks once we switch to providers -const getToolIdFromBlock = (blockType: string): string | undefined => { - const block = getAllBlocks().find((block) => block.type === blockType) - return block?.tools.access[0] -} - -// Get parameters that need to be displayed in the tool input UI -const getToolDisplayParams = (toolId: string): ToolParam[] => { - const tool = getTool(toolId) - if (!tool) return [] - - return Object.entries(tool.params) - .filter(([_, param]) => param.requiredForToolCall || param.optionalToolInput) - .map(([paramId, param]) => ({ - id: paramId, - type: param.type, - description: param.description, - requiredForToolCall: param.requiredForToolCall ?? false, - optionalToolInput: param.optionalToolInput ?? false, - })) -} - -// Get filtered parameters based on block conditions -const getFilteredToolParams = ( - blockType: string, - toolId: string, - currentOperation?: string, - toolParams?: Record -): ToolParam[] => { - const tool = getTool(toolId) - const block = getAllBlocks().find((block) => block.type === blockType) - - if (!tool || !block) return [] - - const allParams = Object.entries(tool.params) - .filter(([_, param]) => param.requiredForToolCall || param.optionalToolInput) - .map(([paramId, param]) => ({ - id: paramId, - type: param.type, - description: param.description, - requiredForToolCall: param.requiredForToolCall ?? false, - optionalToolInput: param.optionalToolInput ?? false, - })) - - const evaluateCondition = (condition: any, currentValues: Record): boolean => { - const fieldValue = currentValues[condition.field] - let result = false - - if (Array.isArray(condition.value)) { - result = condition.value.includes(fieldValue) - } else { - result = fieldValue === condition.value - } - - if (condition.not) { - result = !result - } - - return result - } - - return allParams.filter((param) => { - const subBlock = block.subBlocks.find((sb) => sb.id === param.id) - - if (!subBlock || !subBlock.condition) { - return true - } - - const currentValues: Record = { - operation: currentOperation, - ...toolParams, - } - - const condition = subBlock.condition - let mainConditionResult = evaluateCondition(condition, currentValues) - - if (condition.and) { - const andConditionResult = evaluateCondition(condition.and, currentValues) - mainConditionResult = mainConditionResult && andConditionResult - } - - return mainConditionResult - }) -} - -// Keep this for backward compatibility - only get strictly required parameters -const getRequiredToolParams = (toolId: string): ToolParam[] => { - const tool = getTool(toolId) - if (!tool) return [] - - return Object.entries(tool.params) - .filter(([_, param]) => param.requiredForToolCall || param.optionalToolInput) - .map(([paramId, param]) => ({ - id: paramId, - type: param.type, - description: param.description, - requiredForToolCall: param.requiredForToolCall ?? false, - optionalToolInput: param.optionalToolInput ?? false, - })) -} - -// Check if a tool requires OAuth -const getOAuthConfig = (toolId: string) => { - const tool = getTool(toolId) - return tool?.oauth -} - -// For custom tools, extract parameters from the schema -const getCustomToolParams = (schema: any): ToolParam[] => { - if (!schema?.function?.parameters?.properties) return [] - - const properties = schema.function.parameters.properties - const required = schema.function.parameters.required || [] - const optionalInputs = schema.function.parameters.optionalToolInputs || [] - - return Object.entries(properties).map(([paramId, param]: [string, any]) => ({ - id: paramId, - type: param.type || 'string', - description: param.description || '', - requiredForToolCall: required.includes(paramId), - optionalToolInput: optionalInputs.includes(paramId), - })) -} - -// Check if a block has multiple operations -const hasMultipleOperations = (blockType: string): boolean => { - const block = getAllBlocks().find((block) => block.type === blockType) - return (block?.tools?.access?.length || 0) > 1 -} - -// Get operation options for a block -const getOperationOptions = (blockType: string): { label: string; id: string }[] => { - const block = getAllBlocks().find((block) => block.type === blockType) - if (!block || !block.tools?.access) return [] - - // Look for an operation dropdown in the block's subBlocks - const operationSubBlock = block.subBlocks.find((sb) => sb.id === 'operation') - if ( - operationSubBlock && - operationSubBlock.type === 'dropdown' && - Array.isArray(operationSubBlock.options) - ) { - return operationSubBlock.options as { label: string; id: string }[] - } - - // Fallback: create options from tools.access - return block.tools.access.map((toolId) => { - const tool = getTool(toolId) - return { - id: toolId, - label: tool?.name || toolId, - } - }) -} - -// Helper function to initialize tool parameters -const initializeToolParams = ( - toolId: string, - params: ToolParam[], - subBlockStore: { - resolveToolParamValue: ( - toolId: string, - paramId: string, - instanceId?: string - ) => string | undefined - }, - isAutoFillEnabled: boolean, - instanceId?: string -): Record => { - const initialParams: Record = {} - - // Only auto-fill parameters if the setting is enabled - if (isAutoFillEnabled) { - // For each parameter, check if we have a stored/resolved value - params.forEach((param) => { - const resolvedValue = subBlockStore.resolveToolParamValue(toolId, param.id, instanceId) - if (resolvedValue) { - initialParams[param.id] = resolvedValue + useEffect(() => { + if (storeValue) { + const transformedValue = transformer ? transformer(storeValue) : String(storeValue) + if (transformedValue !== value) { + onChange(transformedValue) } - }) - } - - return initialParams -} - -// Helper function to check if a tool has expandable content -const hasExpandableContent = ( - isCustomTool: boolean, - hasOperations: boolean, - operationOptions: { label: string; id: string }[], - toolId: string | null | undefined, - requiredParams: ToolParam[] -): boolean => { - // Custom tools are always expandable and handle their own content - if (isCustomTool) return true - - // Check if it has operations - if (hasOperations && operationOptions.length > 0) return true - - // Check if it has OAuth requirements - if (toolId) { - const oauthConfig = getOAuthConfig(toolId) - if (oauthConfig?.required) return true - } - - // Check if it has required parameters - if (requiredParams.length > 0) return true - - // No expandable content - return false -} - -// Helper to format parameter IDs into human-readable labels -const formatParamId = (paramId: string): string => { - // Special case for common parameter names - if (paramId === 'apiKey') return 'API Key' - if (paramId === 'apiVersion') return 'API Version' - - // Handle underscore and hyphen separated words - if (paramId.includes('_') || paramId.includes('-')) { - return paramId - .split(/[-_]/) - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' ') - } - - // Handle single character parameters - if (paramId.length === 1) return paramId.toUpperCase() - - // Handle camelCase - if (/[A-Z]/.test(paramId)) { - const result = paramId.replace(/([A-Z])/g, ' $1') - return ( - result.charAt(0).toUpperCase() + - result - .slice(1) - .replace(/ Api/g, ' API') - .replace(/ Id/g, ' ID') - .replace(/ Url/g, ' URL') - .replace(/ Uri/g, ' URI') - .replace(/ Ui/g, ' UI') - ) - } - - // Simple case - just capitalize first letter - return paramId.charAt(0).toUpperCase() + paramId.slice(1) -} - -// Helper function to check if a parameter should use a channel selector -const shouldUseChannelSelector = (blockType: string, paramId: string): boolean => { - const block = getAllBlocks().find((block) => block.type === blockType) - if (!block) return false - - // Look for a subBlock with the same ID that has type 'channel-selector' - const subBlock = block.subBlocks.find((sb) => sb.id === paramId) - return subBlock?.type === 'channel-selector' -} - -// Helper function to get channel selector configuration from block definition -const getChannelSelectorConfig = (blockType: string, paramId: string) => { - const block = getAllBlocks().find((block) => block.type === blockType) - if (!block) return null - - const subBlock = block.subBlocks.find((sb) => sb.id === paramId && sb.type === 'channel-selector') - return subBlock || null -} - -// Helper function to check if a parameter should be treated as a password field -const shouldBePasswordField = (blockType: string, paramId: string): boolean => { - const block = getAllBlocks().find((block) => block.type === blockType) - if (block) { - const subBlock = block.subBlocks.find((sb) => sb.id === paramId) - if (subBlock?.password) { - return true } - } + }, [storeValue, value, onChange, transformer]) - return false + return <>{children} +} + +function FileSelectorSyncWrapper({ + blockId, + paramId, + value, + onChange, + uiComponent, + disabled, +}: { + blockId: string + paramId: string + value: string + onChange: (value: string) => void + uiComponent: any + disabled: boolean +}) { + return ( + + + + ) +} + +function TableSyncWrapper({ + blockId, + paramId, + value, + onChange, + uiComponent, + disabled, +}: { + blockId: string + paramId: string + value: string + onChange: (value: string) => void + uiComponent: any + disabled: boolean +}) { + return ( + JSON.stringify(storeValue)} + > + + + ) +} + +function DateInputSyncWrapper({ + blockId, + paramId, + value, + onChange, + uiComponent, + disabled, +}: { + blockId: string + paramId: string + value: string + onChange: (value: string) => void + uiComponent: any + disabled: boolean +}) { + return ( + + + + ) +} + +function TimeInputSyncWrapper({ + blockId, + paramId, + value, + onChange, + uiComponent, + disabled, +}: { + blockId: string + paramId: string + value: string + onChange: (value: string) => void + uiComponent: any + disabled: boolean +}) { + return ( + + + + ) +} + +function SliderInputSyncWrapper({ + blockId, + paramId, + value, + onChange, + uiComponent, + disabled, +}: { + blockId: string + paramId: string + value: string + onChange: (value: string) => void + uiComponent: any + disabled: boolean +}) { + return ( + String(storeValue)} + > + + + ) +} + +function CheckboxListSyncWrapper({ + blockId, + paramId, + value, + onChange, + uiComponent, + disabled, +}: { + blockId: string + paramId: string + value: string + onChange: (value: string) => void + uiComponent: any + disabled: boolean +}) { + return ( + JSON.stringify(storeValue)} + > + + + ) +} + +function CodeSyncWrapper({ + blockId, + paramId, + value, + onChange, + uiComponent, + disabled, +}: { + blockId: string + paramId: string + value: string + onChange: (value: string) => void + uiComponent: any + disabled: boolean +}) { + return ( + + + + ) +} + +function ComboboxSyncWrapper({ + blockId, + paramId, + value, + onChange, + uiComponent, + disabled, +}: { + blockId: string + paramId: string + value: string + onChange: (value: string) => void + uiComponent: any + disabled: boolean +}) { + return ( + + + + ) +} + +function FileUploadSyncWrapper({ + blockId, + paramId, + value, + onChange, + uiComponent, + disabled, +}: { + blockId: string + paramId: string + value: string + onChange: (value: string) => void + uiComponent: any + disabled: boolean +}) { + return ( + JSON.stringify(storeValue)} + > + + + ) } export function ToolInput({ @@ -390,9 +438,95 @@ export function ToolInput({ ? (value as unknown as StoredTool[]) : [] - // Check if a tool is already selected - const isToolAlreadySelected = (toolType: string) => { - return selectedTools.some((tool) => tool.type === toolType) + // Check if a tool is already selected (allowing multiple instances for multi-operation tools) + const isToolAlreadySelected = (toolId: string, blockType: string) => { + // For tools with multiple operations, allow multiple instances + if (hasMultipleOperations(blockType)) { + return false + } + // For single-operation tools, prevent duplicates + return selectedTools.some((tool) => tool.toolId === toolId) + } + + // Check if a block has multiple operations + const hasMultipleOperations = (blockType: string): boolean => { + const block = getAllBlocks().find((block) => block.type === blockType) + return (block?.tools?.access?.length || 0) > 1 + } + + // Get operation options for a block + const getOperationOptions = (blockType: string): { label: string; id: string }[] => { + const block = getAllBlocks().find((block) => block.type === blockType) + if (!block || !block.tools?.access) return [] + + // Look for an operation dropdown in the block's subBlocks + const operationSubBlock = block.subBlocks.find((sb) => sb.id === 'operation') + if ( + operationSubBlock && + operationSubBlock.type === 'dropdown' && + Array.isArray(operationSubBlock.options) + ) { + return operationSubBlock.options as { label: string; id: string }[] + } + + // Fallback: create options from tools.access + return block.tools.access.map((toolId) => { + const toolParams = getToolParametersConfig(toolId) + return { + id: toolId, + label: toolParams?.toolConfig?.name || toolId, + } + }) + } + + // Get the correct tool ID based on operation + const getToolIdForOperation = (blockType: string, operation?: string): string | undefined => { + const block = getAllBlocks().find((block) => block.type === blockType) + if (!block || !block.tools?.access) return undefined + + // If there's only one tool, return it + if (block.tools.access.length === 1) { + return block.tools.access[0] + } + + // If there's an operation and a tool selection function, use it + if (operation && block.tools?.config?.tool) { + try { + return block.tools.config.tool({ operation }) + } catch (error) { + console.error('Error selecting tool for operation:', error) + } + } + + // If there's an operation that matches a tool ID, use it + if (operation && block.tools.access.includes(operation)) { + return operation + } + + // Default to first tool + return block.tools.access[0] + } + + // Initialize tool parameters with auto-fill if enabled + const initializeToolParams = ( + toolId: string, + params: ToolParameterConfig[], + instanceId?: string + ): Record => { + const initialParams: Record = {} + + // Only auto-fill parameters if the setting is enabled + if (isAutoFillEnvVarsEnabled) { + // For each parameter, check if we have a stored/resolved value + params.forEach((param) => { + const resolvedValue = subBlockStore.resolveToolParamValue(toolId, param.id, instanceId) + if (resolvedValue) { + initialParams[param.id] = resolvedValue + } + }) + } + + return initialParams } const handleSelectTool = (toolBlock: (typeof toolBlocks)[0]) => { @@ -402,41 +536,50 @@ export function ToolInput({ const operationOptions = hasOperations ? getOperationOptions(toolBlock.type) : [] const defaultOperation = operationOptions.length > 0 ? operationOptions[0].id : undefined - const toolId = getToolIdFromBlock(toolBlock.type) || toolBlock.type - const displayParams = toolId - ? getFilteredToolParams(toolBlock.type, toolId, defaultOperation, {}) - : [] + const toolId = getToolIdForOperation(toolBlock.type, defaultOperation) + if (!toolId) return - // Use the helper function to initialize parameters with blockId as instanceId - const initialParams = initializeToolParams( - toolId, - displayParams, - subBlockStore, - isAutoFillEnvVarsEnabled, - blockId - ) + // Check if tool is already selected + if (isToolAlreadySelected(toolId, toolBlock.type)) return + + // Get tool parameters using the new utility with block type for UI components + const toolParams = getToolParametersConfig(toolId, toolBlock.type) + if (!toolParams) return + + // Initialize parameters with auto-fill and default values + const initialParams = initializeToolParams(toolId, toolParams.userInputParameters, blockId) + + // Add default values from UI component configurations + toolParams.userInputParameters.forEach((param) => { + if (param.uiComponent?.value && !initialParams[param.id]) { + const defaultValue = + typeof param.uiComponent.value === 'function' + ? param.uiComponent.value() + : param.uiComponent.value + initialParams[param.id] = defaultValue + } + }) const newTool: StoredTool = { type: toolBlock.type, title: toolBlock.name, + toolId: toolId, params: initialParams, isExpanded: true, operation: defaultOperation, usageControl: 'auto', } - // If isWide, keep tools in the same row expanded + // Add tool to selection if (isWide) { setStoreValue([ ...selectedTools.map((tool, index) => ({ ...tool, - // Keep expanded if it's in the same row as the new tool isExpanded: Math.floor(selectedTools.length / 2) === Math.floor(index / 2), })), newTool, ]) } else { - // Original behavior for non-wide mode setStoreValue([...selectedTools.map((tool) => ({ ...tool, isExpanded: false })), newTool]) } @@ -446,54 +589,29 @@ export function ToolInput({ const handleAddCustomTool = (customTool: CustomTool) => { if (isPreview || disabled) return - // Check if a tool with the same name already exists - if ( - selectedTools.some( - (tool) => - tool.type === 'custom-tool' && - tool.schema?.function?.name === customTool.schema.function.name - ) - ) { - return - } - - // Get custom tool parameters from schema - const toolParams = getCustomToolParams(customTool.schema) - - // Create tool ID for the custom tool - const toolId = `custom-${customTool.schema.function.name}` - - // Use the helper function to initialize parameters with blockId as instanceId - const initialParams = initializeToolParams( - toolId, - toolParams, - subBlockStore, - isAutoFillEnvVarsEnabled, - blockId - ) + const customToolId = `custom-${customTool.schema.function.name}` const newTool: StoredTool = { type: 'custom-tool', title: customTool.title, - params: initialParams, + toolId: customToolId, + params: {}, isExpanded: true, schema: customTool.schema, code: customTool.code || '', usageControl: 'auto', } - // If isWide, keep tools in the same row expanded + // Add tool to selection if (isWide) { setStoreValue([ ...selectedTools.map((tool, index) => ({ ...tool, - // Keep expanded if it's in the same row as the new tool isExpanded: Math.floor(selectedTools.length / 2) === Math.floor(index / 2), })), newTool, ]) } else { - // Original behavior for non-wide mode setStoreValue([...selectedTools.map((tool) => ({ ...tool, isExpanded: false })), newTool]) } } @@ -502,12 +620,6 @@ export function ToolInput({ const tool = selectedTools[toolIndex] if (tool.type !== 'custom-tool' || !tool.schema) return - // Find the tool ID from the custom tools store based on the function name - const customToolsList = useCustomToolsStore.getState().getAllTools() - const _existingTool = customToolsList.find( - (customTool) => customTool.schema.function.name === tool.schema.function.name - ) - setEditingToolIndex(toolIndex) setCustomToolModalOpen(true) } @@ -536,16 +648,15 @@ export function ToolInput({ } } - const handleRemoveTool = (toolType: string, toolIndex: number) => { + const handleRemoveTool = (toolIndex: number) => { if (isPreview || disabled) return setStoreValue(selectedTools.filter((_, index) => index !== toolIndex)) } - // New handler for when a custom tool is completely deleted from the store const handleDeleteTool = (toolId: string) => { // Find any instances of this tool in the current workflow and remove them const updatedTools = selectedTools.filter((tool) => { - // For custom tools, we need to check if it matches the deleted tool + // For custom tools, check if it matches the deleted tool if ( tool.type === 'custom-tool' && tool.schema?.function?.name && @@ -569,16 +680,11 @@ export function ToolInput({ const handleParamChange = (toolIndex: number, paramId: string, paramValue: string) => { if (isPreview || disabled) return - // Store the value in the tool params store for future use const tool = selectedTools[toolIndex] - const toolId = - tool.type === 'custom-tool' - ? `custom-${tool.schema?.function?.name || 'tool'}` - : getToolIdFromBlock(tool.type) || tool.type - // Only store non-empty values + // Store the value in the tool params store for future use if (paramValue.trim()) { - subBlockStore.setToolParam(toolId, paramId, paramValue) + subBlockStore.setToolParam(tool.toolId, paramId, paramValue) } // Update the value in the workflow @@ -598,13 +704,43 @@ export function ToolInput({ } const handleOperationChange = (toolIndex: number, operation: string) => { - if (isPreview || disabled) return + console.log('🔄 handleOperationChange called:', { toolIndex, operation, isPreview, disabled }) + + if (isPreview || disabled) { + console.log('❌ Early return: preview or disabled') + return + } const tool = selectedTools[toolIndex] - const subBlockStore = useSubBlockStore.getState() + console.log('🔧 Current tool:', tool) + + const newToolId = getToolIdForOperation(tool.type, operation) + console.log('🆔 getToolIdForOperation result:', { toolType: tool.type, operation, newToolId }) + + if (!newToolId) { + console.log('❌ Early return: no newToolId') + return + } + + // Get parameters for the new tool + const toolParams = getToolParametersConfig(newToolId, tool.type) + console.log('📋 getToolParametersConfig result:', { + newToolId, + toolType: tool.type, + toolParams, + }) + + if (!toolParams) { + console.log('❌ Early return: no toolParams') + return + } + + // Initialize parameters for the new operation + const initialParams = initializeToolParams(newToolId, toolParams.userInputParameters, blockId) // Clear fields when operation changes for Jira if (tool.type === 'jira') { + const subBlockStore = useSubBlockStore.getState() // Clear all fields that might be shared between operations subBlockStore.setValue(blockId, 'summary', '') subBlockStore.setValue(blockId, 'description', '') @@ -618,27 +754,9 @@ export function ToolInput({ index === toolIndex ? { ...tool, + toolId: newToolId, operation, - // Reset params when operation changes - params: {}, - } - : tool - ) - ) - } - - const handleCredentialChange = (toolIndex: number, credentialId: string) => { - if (isPreview || disabled) return - - setStoreValue( - selectedTools.map((tool, index) => - index === toolIndex - ? { - ...tool, - params: { - ...tool.params, - credential: credentialId, - }, + params: initialParams, // Reset params when operation changes } : tool ) @@ -715,6 +833,371 @@ export function ToolInput({ return } + // Check if tool has OAuth requirements + const toolRequiresOAuth = (toolId: string): boolean => { + const toolParams = getToolParametersConfig(toolId) + return toolParams?.toolConfig?.oauth?.required || false + } + + // Get OAuth configuration for tool + const getToolOAuthConfig = (toolId: string) => { + const toolParams = getToolParametersConfig(toolId) + return toolParams?.toolConfig?.oauth + } + + // Evaluate parameter conditions to determine if parameter should be shown + const evaluateParameterCondition = (param: any, tool: StoredTool): boolean => { + if (!('uiComponent' in param) || !param.uiComponent?.condition) return true + + const condition = param.uiComponent.condition + const currentValues: Record = { + operation: tool.operation, + ...tool.params, + } + + const fieldValue = currentValues[condition.field] + let result = false + + if (Array.isArray(condition.value)) { + result = condition.value.includes(fieldValue) + } else { + result = fieldValue === condition.value + } + + if (condition.not) { + result = !result + } + + // Handle 'and' conditions + if (condition.and) { + const andFieldValue = currentValues[condition.and.field] + let andResult = false + + if (Array.isArray(condition.and.value)) { + andResult = condition.and.value.includes(andFieldValue) + } else { + andResult = andFieldValue === condition.and.value + } + + if (condition.and.not) { + andResult = !andResult + } + + result = result && andResult + } + + return result + } + + // Helper function to get credential for channel selector + const getCredentialForChannelSelector = (paramId: string): string => { + // Look for the tool that contains this parameter + const currentToolIndex = selectedTools.findIndex((tool) => { + const toolParams = getToolParametersConfig(tool.toolId) + return toolParams?.userInputParameters.some((p) => p.id === paramId) + }) + + if (currentToolIndex === -1) return '' + + const currentTool = selectedTools[currentToolIndex] + + // Enhanced credential detection logic from legacy implementation + // Check for bot token first, then OAuth credential + const botToken = + currentTool.params.botToken || (subBlockStore.getValue(blockId, 'botToken') as string) + const oauthCredential = + currentTool.params.credential || (subBlockStore.getValue(blockId, 'credential') as string) + + if (botToken?.trim()) { + return botToken + } + if (oauthCredential?.trim()) { + return oauthCredential + } + + // Fallback: check for other common credential parameter names + const credentialKeys = ['accessToken', 'token', 'apiKey', 'authToken'] + for (const key of credentialKeys) { + const value = currentTool.params[key] || (subBlockStore.getValue(blockId, key) as string) + if (value?.trim()) { + return value + } + } + + return '' + } + + // Render the appropriate UI component based on parameter configuration + const renderParameterInput = ( + param: ToolParameterConfig, + value: string, + onChange: (value: string) => void, + toolIndex?: number + ) => { + // Create unique blockId for tool parameters to avoid conflicts with main block + const uniqueBlockId = toolIndex !== undefined ? `${blockId}-tool-${toolIndex}` : blockId + const uiComponent = param.uiComponent + + // If no UI component info, fall back to basic input + if (!uiComponent) { + return ( + + ) + } + + // Render based on UI component type + switch (uiComponent.type) { + case 'dropdown': + return ( + + ) + + case 'switch': + return ( + onChange(checked ? 'true' : 'false')} + /> + ) + + case 'long-input': + return ( + + ) + + case 'short-input': + return ( + + ) + + case 'channel-selector': + return ( + + ) + + case 'project-selector': + return ( + + ) + + case 'oauth-input': + return ( + + ) + + case 'file-selector': + return ( + + ) + + case 'table': + return ( + + ) + + case 'combobox': + return ( + + ) + + case 'slider': + return ( + + ) + + case 'code': + return ( + + ) + + case 'checkbox-list': + return ( + + ) + + case 'date-input': + return ( + + ) + + case 'time-input': + return ( + + ) + + case 'file-upload': + return ( + + ) + + default: + return ( + + ) + } + } + return (
{selectedTools.length === 0 ? ( @@ -766,6 +1249,7 @@ export function ToolInput({ const newTool: StoredTool = { type: 'custom-tool', title: customTool.title, + toolId: `custom-${customTool.schema.function.name}`, params: {}, isExpanded: true, schema: customTool.schema, @@ -846,29 +1330,47 @@ export function ToolInput({ const toolBlock = !isCustomTool ? toolBlocks.find((block) => block.type === tool.type) : null - const toolId = !isCustomTool ? getToolIdFromBlock(tool.type) : null - const hasOperations = !isCustomTool && hasMultipleOperations(tool.type) - const operationOptions = hasOperations ? getOperationOptions(tool.type) : [] - // Get parameters based on tool type - const requiredParams = isCustomTool - ? getCustomToolParams(tool.schema) - : toolId - ? getFilteredToolParams(tool.type, toolId, tool.operation, tool.params) + // Get the current tool ID (may change based on operation) + const currentToolId = !isCustomTool + ? getToolIdForOperation(tool.type, tool.operation) || tool.toolId + : tool.toolId + + // Get tool parameters using the new utility with block type for UI components + const toolParams = !isCustomTool + ? getToolParametersConfig(currentToolId, tool.type) + : null + + // For custom tools, extract parameters from schema + const customToolParams = + isCustomTool && tool.schema + ? Object.entries(tool.schema.function.parameters.properties || {}).map( + ([paramId, param]: [string, any]) => ({ + id: paramId, + type: param.type || 'string', + description: param.description || '', + visibility: (tool.schema.function.parameters.required?.includes(paramId) + ? 'user-or-llm' + : 'user-only') as 'user-or-llm' | 'user-only' | 'llm-only' | 'hidden', + }) + ) : [] + // Get all parameters to display + const displayParams = isCustomTool + ? customToolParams + : toolParams?.userInputParameters || [] + + // Check if tool requires OAuth + const requiresOAuth = !isCustomTool && toolRequiresOAuth(currentToolId) + const oauthConfig = !isCustomTool ? getToolOAuthConfig(currentToolId) : null + // Check if the tool has any expandable content - const isExpandable = hasExpandableContent( - isCustomTool, - hasOperations, - operationOptions, - toolId, - requiredParams - ) + const hasExpandableContent = isCustomTool || displayParams.length > 0 || requiresOAuth return (
handleDrop(e, toolIndex)} > - {/* Subtle drop indicator - use border highlight instead of separate line */}
{ if (isCustomTool) { handleEditCustomTool(toolIndex) - } else if (isExpandable) { + } else if (hasExpandableContent) { toggleToolExpansion(toolIndex) } }} @@ -950,7 +1451,6 @@ export function ToolInput({ }} aria-label='Toggle tool usage control' > - {/* Text boxes instead of icons */} { e.stopPropagation() - handleRemoveTool(tool.type, toolIndex) + handleRemoveTool(toolIndex) }} className='text-muted-foreground hover:text-foreground' > @@ -1015,126 +1515,163 @@ export function ToolInput({
- {!isCustomTool && isExpandable && ( -
{ - if (e.target === e.currentTarget) { - toggleToolExpansion(toolIndex) - } - }} - > - {/* Add operation dropdown for tools with multiple operations */} - {hasOperations && operationOptions.length > 0 && ( -
-
Operation
- + {!isCustomTool && hasExpandableContent && tool.isExpanded && ( +
+ {/* Operation dropdown for tools with multiple operations */} + {(() => { + const hasOperations = hasMultipleOperations(tool.type) + const operationOptions = hasOperations ? getOperationOptions(tool.type) : [] + + return hasOperations && operationOptions.length > 0 ? ( +
+
+ Operation +
+
+ +
+
+ ) : null + })()} + + {/* OAuth credential selector if required */} + {requiresOAuth && oauthConfig && ( +
+
Account
+
+ + handleParamChange(toolIndex, 'credential', value) + } + provider={oauthConfig.provider as OAuthProvider} + requiredScopes={oauthConfig.additionalScopes || []} + label={`Select ${oauthConfig.provider} account`} + serviceId={oauthConfig.provider} + disabled={disabled} + /> +
)} - {/* Add OAuth credential selector if the tool requires OAuth */} - {toolId && - (() => { - const oauthConfig = getOAuthConfig(toolId) - if (oauthConfig?.required) { - return ( -
-
- Account -
- handleCredentialChange(toolIndex, value)} - provider={oauthConfig.provider as OAuthProvider} - requiredScopes={oauthConfig.additionalScopes || []} - label={`Select ${oauthConfig.provider} account`} - serviceId={oauthConfig.provider} + {/* Tool parameters */} + {(() => { + const filteredParams = displayParams.filter((param) => + evaluateParameterCondition(param, tool) + ) + const groupedParams: { [key: string]: ToolParameterConfig[] } = {} + const standaloneParams: ToolParameterConfig[] = [] + + // Group checkbox-list parameters by their UI component title + filteredParams.forEach((param) => { + const paramConfig = param as ToolParameterConfig + if ( + paramConfig.uiComponent?.type === 'checkbox-list' && + paramConfig.uiComponent?.title + ) { + const groupKey = paramConfig.uiComponent.title + if (!groupedParams[groupKey]) { + groupedParams[groupKey] = [] + } + groupedParams[groupKey].push(paramConfig) + } else { + standaloneParams.push(paramConfig) + } + }) + + const renderedElements: React.ReactNode[] = [] + + // Render grouped checkbox-lists + Object.entries(groupedParams).forEach(([groupTitle, params]) => { + const firstParam = params[0] as ToolParameterConfig + const groupValue = JSON.stringify( + params.reduce( + (acc, p) => ({ ...acc, [p.id]: tool.params[p.id] === 'true' }), + {} + ) + ) + + renderedElements.push( +
+
+ {groupTitle} +
+
+ { + try { + const parsed = JSON.parse(value) + params.forEach((param) => { + handleParamChange( + toolIndex, + param.id, + parsed[param.id] ? 'true' : 'false' + ) + }) + } catch (e) { + // Handle error + } + }} + uiComponent={firstParam.uiComponent} disabled={disabled} />
- ) - } - return null - })()} +
+ ) + }) - {/* Existing parameters */} - {requiredParams.map((param) => { - // Check if this parameter should use a channel selector - const useChannelSelector = - !isCustomTool && shouldUseChannelSelector(tool.type, param.id) - const channelSelectorConfig = useChannelSelector - ? getChannelSelectorConfig(tool.type, param.id) - : null - - // Determine the correct credential to pass for channel selector - let credentialForChannelSelector = '' - if (useChannelSelector) { - const botToken = - tool.params.botToken || - (subBlockStore.getValue(blockId, 'botToken') as string) - const oauthCredential = - tool.params.credential || - (subBlockStore.getValue(blockId, 'credential') as string) - - if (botToken?.trim()) { - credentialForChannelSelector = botToken - } else if (oauthCredential?.trim()) { - credentialForChannelSelector = oauthCredential - } - } - - return ( -
-
+ // Render standalone parameters + standaloneParams.forEach((param) => { + renderedElements.push( +
- {formatParamId(param.id)} - {param.optionalToolInput && !param.requiredForToolCall && ( + {param.uiComponent?.title || formatParameterLabel(param.id)} + {param.required && param.visibility === 'user-only' && ( + * + )} + {!param.required && ( (Optional) )}
-
- {useChannelSelector && channelSelectorConfig ? ( - { - handleParamChange(toolIndex, param.id, channelId) - }} - /> +
+ {param.uiComponent ? ( + renderParameterInput( + param, + tool.params[param.id] || '', + (value) => handleParamChange(toolIndex, param.id, value), + toolIndex + ) ) : (
-
- ) - })} + ) + }) + + return renderedElements + })()}
)}
@@ -1220,6 +1759,7 @@ export function ToolInput({ const newTool: StoredTool = { type: 'custom-tool', title: customTool.title, + toolId: `custom-${customTool.schema.function.name}`, params: {}, isExpanded: true, schema: customTool.schema, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx index 54efcb75b..a88ddeeb7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx @@ -115,7 +115,7 @@ export function SubBlock({ onStream?: (streamingExecution: StreamingExecution) => Promise + 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 + onStream?: (se: StreamingExecution) => Promise, + executionId?: string ): Promise => { // 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, }, } diff --git a/apps/sim/blocks/blocks/api.ts b/apps/sim/blocks/blocks/api.ts index 68dbaca29..98b175281 100644 --- a/apps/sim/blocks/blocks/api.ts +++ b/apps/sim/blocks/blocks/api.ts @@ -25,7 +25,13 @@ export const ApiBlock: BlockConfig = { 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', diff --git a/apps/sim/blocks/blocks/autoblocks.ts b/apps/sim/blocks/blocks/autoblocks.ts deleted file mode 100644 index 502585c33..000000000 --- a/apps/sim/blocks/blocks/autoblocks.ts +++ /dev/null @@ -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 - } -} - -export const AutoblocksBlock: BlockConfig = { - 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', - }, -} diff --git a/apps/sim/blocks/blocks/elevenlabs.ts b/apps/sim/blocks/blocks/elevenlabs.ts index 61fe71c5f..73a2a7caf 100644 --- a/apps/sim/blocks/blocks/elevenlabs.ts +++ b/apps/sim/blocks/blocks/elevenlabs.ts @@ -57,6 +57,19 @@ export const ElevenLabsBlock: BlockConfig = { 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 = { 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', - ], - }, ], } diff --git a/apps/sim/blocks/blocks/evaluator.ts b/apps/sim/blocks/blocks/evaluator.ts index 218629145..013e6f179 100644 --- a/apps/sim/blocks/blocks/evaluator.ts +++ b/apps/sim/blocks/blocks/evaluator.ts @@ -176,7 +176,10 @@ export const EvaluatorBlock: BlockConfig = { options: () => { const ollamaModels = useOllamaStore.getState().models const baseModels = Object.keys(getBaseModelProviders()) - return [...baseModels, ...ollamaModels] + return [...baseModels, ...ollamaModels].map((model) => ({ + label: model, + id: model, + })) }, }, { diff --git a/apps/sim/blocks/blocks/guesty.ts b/apps/sim/blocks/blocks/guesty.ts deleted file mode 100644 index a8e0346b0..000000000 --- a/apps/sim/blocks/blocks/guesty.ts +++ /dev/null @@ -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 = { - 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', - }, -} diff --git a/apps/sim/blocks/blocks/notion.ts b/apps/sim/blocks/blocks/notion.ts index 8f3596cdf..10447873f 100644 --- a/apps/sim/blocks/blocks/notion.ts +++ b/apps/sim/blocks/blocks/notion.ts @@ -19,9 +19,9 @@ export const NotionBlock: BlockConfig = { 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 = { placeholder: 'Enter Notion page ID', condition: { field: 'operation', - value: 'read_notion', + value: 'notion_read', }, }, { @@ -54,7 +54,7 @@ export const NotionBlock: BlockConfig = { 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 = { { 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 = { 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 = { 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 = { 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 = { 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 = { 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 = { 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 = { // 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) { diff --git a/apps/sim/blocks/blocks/perplexity.ts b/apps/sim/blocks/blocks/perplexity.ts index 3035b3d5a..b36856a5e 100644 --- a/apps/sim/blocks/blocks/perplexity.ts +++ b/apps/sim/blocks/blocks/perplexity.ts @@ -26,14 +26,14 @@ export const PerplexityBlock: BlockConfig = { 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 = { 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 = { }, }, 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 }, diff --git a/apps/sim/blocks/blocks/reddit.ts b/apps/sim/blocks/blocks/reddit.ts index b82595d05..eb129f4e1 100644 --- a/apps/sim/blocks/blocks/reddit.ts +++ b/apps/sim/blocks/blocks/reddit.ts @@ -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 }, diff --git a/apps/sim/blocks/blocks/router.ts b/apps/sim/blocks/blocks/router.ts index 20221f331..c986cb3b4 100644 --- a/apps/sim/blocks/blocks/router.ts +++ b/apps/sim/blocks/blocks/router.ts @@ -120,7 +120,10 @@ export const RouterBlock: BlockConfig = { options: () => { const ollamaModels = useOllamaStore.getState().models const baseModels = Object.keys(getBaseModelProviders()) - return [...baseModels, ...ollamaModels] + return [...baseModels, ...ollamaModels].map((model) => ({ + label: model, + id: model, + })) }, }, { diff --git a/apps/sim/blocks/blocks/serper.ts b/apps/sim/blocks/blocks/serper.ts index 92a94b1e7..731724cdc 100644 --- a/apps/sim/blocks/blocks/serper.ts +++ b/apps/sim/blocks/blocks/serper.ts @@ -25,28 +25,53 @@ export const SerperBlock: BlockConfig = { 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', diff --git a/apps/sim/blocks/blocks/slack.ts b/apps/sim/blocks/blocks/slack.ts index c87c5e683..c8a58182c 100644 --- a/apps/sim/blocks/blocks/slack.ts +++ b/apps/sim/blocks/blocks/slack.ts @@ -76,10 +76,6 @@ export const SlackBlock: BlockConfig = { layout: 'full', provider: 'slack', placeholder: 'Select Slack channel', - condition: { - field: 'operation', - value: ['send'], - }, }, { id: 'text', @@ -87,10 +83,6 @@ export const SlackBlock: BlockConfig = { type: 'long-input', layout: 'full', placeholder: 'Enter your message (supports Slack mrkdwn)', - condition: { - field: 'operation', - value: ['send'], - }, }, ], tools: { diff --git a/apps/sim/blocks/blocks/supabase.ts b/apps/sim/blocks/blocks/supabase.ts index 5f2f88598..2baa32c62 100644 --- a/apps/sim/blocks/blocks/supabase.ts +++ b/apps/sim/blocks/blocks/supabase.ts @@ -101,12 +101,12 @@ export const SupabaseBlock: BlockConfig = { }, }, 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', diff --git a/apps/sim/blocks/blocks/tavily.ts b/apps/sim/blocks/blocks/tavily.ts index b33e08d83..119298997 100644 --- a/apps/sim/blocks/blocks/tavily.ts +++ b/apps/sim/blocks/blocks/tavily.ts @@ -67,7 +67,10 @@ export const TavilyBlock: BlockConfig = { 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' }, }, diff --git a/apps/sim/blocks/blocks/translate.ts b/apps/sim/blocks/blocks/translate.ts index 6579588ba..6d083a7a3 100644 --- a/apps/sim/blocks/blocks/translate.ts +++ b/apps/sim/blocks/blocks/translate.ts @@ -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', diff --git a/apps/sim/blocks/blocks/vision.ts b/apps/sim/blocks/blocks/vision.ts index 53279a48e..a06cf762f 100644 --- a/apps/sim/blocks/blocks/vision.ts +++ b/apps/sim/blocks/blocks/vision.ts @@ -25,7 +25,11 @@ export const VisionBlock: BlockConfig = { 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', diff --git a/apps/sim/blocks/blocks/x.ts b/apps/sim/blocks/blocks/x.ts index 6fac15183..24d3140d0 100644 --- a/apps/sim/blocks/blocks/x.ts +++ b/apps/sim/blocks/blocks/x.ts @@ -79,7 +79,10 @@ export const XBlock: BlockConfig = { 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 = { 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' }, }, diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 9b45d9c3d..886ade4c3 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -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 = { agent: AgentBlock, airtable: AirtableBlock, api: ApiBlock, - // autoblocks: AutoblocksBlock, browser_use: BrowserUseBlock, clay: ClayBlock, condition: ConditionBlock, @@ -91,15 +87,17 @@ export const registry: Record = { 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 = { 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 = { workflow: WorkflowBlock, x: XBlock, youtube: YouTubeBlock, - huggingface: HuggingFaceBlock, - response: ResponseBlock, } // Helper functions to access the registry diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index 36ceb018a..d68c2158f 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -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[] diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 48a297c42..ab39a603c 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -1773,24 +1773,6 @@ export function StripeIcon(props: SVGProps) { ) } -export function GuestyIcon(props: SVGProps) { - return ( - - - - ) -} - export function EyeIcon(props: SVGProps) { return ( ) { ) } -export function AutoblocksIcon(props: SVGProps) { - return ( - - - - - - ) -} - export function BrowserUseIcon(props: SVGProps) { return ( { 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 { + 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) => { + // 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, } } diff --git a/apps/sim/executor/index.ts b/apps/sim/executor/index.ts index a75201d20..b4af914e0 100644 --- a/apps/sim/executor/index.ts +++ b/apps/sim/executor/index.ts @@ -84,6 +84,7 @@ export class Executor { selectedOutputIds?: string[] edges?: Array<{ source: string; target: string }> onStream?: (streamingExecution: StreamingExecution) => Promise + executionId?: string } }, private initialBlockStates: Record = {}, @@ -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', diff --git a/apps/sim/providers/anthropic/index.ts b/apps/sim/providers/anthropic/index.ts index 41476db91..901a3eae1 100644 --- a/apps/sim/providers/anthropic/index.ts +++ b/apps/sim/providers/anthropic/index.ts @@ -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, ], }) diff --git a/apps/sim/providers/azure-openai/index.ts b/apps/sim/providers/azure-openai/index.ts index 91639241d..a0d8de0a8 100644 --- a/apps/sim/providers/azure-openai/index.ts +++ b/apps/sim/providers/azure-openai/index.ts @@ -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: { diff --git a/apps/sim/providers/cerebras/index.ts b/apps/sim/providers/cerebras/index.ts index 5fdca4c74..14c57ddfa 100644 --- a/apps/sim/providers/cerebras/index.ts +++ b/apps/sim/providers/cerebras/index.ts @@ -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 }) diff --git a/apps/sim/providers/deepseek/index.ts b/apps/sim/providers/deepseek/index.ts index d91b7008d..cc5f64f33 100644 --- a/apps/sim/providers/deepseek/index.ts +++ b/apps/sim/providers/deepseek/index.ts @@ -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 }) diff --git a/apps/sim/providers/google/index.ts b/apps/sim/providers/google/index.ts index f2413d27a..f648e36b5 100644 --- a/apps/sim/providers/google/index.ts +++ b/apps/sim/providers/google/index.ts @@ -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 | 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 = {} - 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 diff --git a/apps/sim/providers/groq/index.ts b/apps/sim/providers/groq/index.ts index d928924ea..e092578d8 100644 --- a/apps/sim/providers/groq/index.ts +++ b/apps/sim/providers/groq/index.ts @@ -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 }) diff --git a/apps/sim/providers/models.ts b/apps/sim/providers/models.ts index a7e436872..7b5155aed 100644 --- a/apps/sim/providers/models.ts +++ b/apps/sim/providers/models.ts @@ -307,7 +307,7 @@ export const PROVIDER_DEFINITIONS: Record = { }, capabilities: { temperature: { min: 0, max: 2 }, - toolUsageControl: false, + toolUsageControl: true, }, }, { @@ -320,7 +320,7 @@ export const PROVIDER_DEFINITIONS: Record = { }, capabilities: { temperature: { min: 0, max: 2 }, - toolUsageControl: false, + toolUsageControl: true, }, }, ], diff --git a/apps/sim/providers/ollama/index.ts b/apps/sim/providers/ollama/index.ts index 845830786..05e45b983 100644 --- a/apps/sim/providers/ollama/index.ts +++ b/apps/sim/providers/ollama/index.ts @@ -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 }) diff --git a/apps/sim/providers/openai/index.ts b/apps/sim/providers/openai/index.ts index 43452c8b7..6a5faea9a 100644 --- a/apps/sim/providers/openai/index.ts +++ b/apps/sim/providers/openai/index.ts @@ -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: { diff --git a/apps/sim/providers/utils.test.ts b/apps/sim/providers/utils.test.ts index dbe29da82..30d8e18a8 100644 --- a/apps/sim/providers/utils.test.ts +++ b/apps/sim/providers/utils.test.ts @@ -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'], + }, }) }) diff --git a/apps/sim/providers/utils.ts b/apps/sim/providers/utils.ts index fdf58e92d..87ffef5b0 100644 --- a/apps/sim/providers/utils.ts +++ b/apps/sim/providers/utils.ts @@ -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 }, + llmArgs: Record, + request: { workflowId?: string; environmentVariables?: Record } +): { + toolParams: Record + executionParams: Record +} { + // 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 } +} diff --git a/apps/sim/providers/xai/index.ts b/apps/sim/providers/xai/index.ts index 957d49574..a1c5e4f9d 100644 --- a/apps/sim/providers/xai/index.ts +++ b/apps/sim/providers/xai/index.ts @@ -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:', { diff --git a/apps/sim/tests/socket-server.test.ts b/apps/sim/socket-server/tests/socket-server.test.ts similarity index 100% rename from apps/sim/tests/socket-server.test.ts rename to apps/sim/socket-server/tests/socket-server.test.ts diff --git a/apps/sim/stores/panel/console/store.ts b/apps/sim/stores/panel/console/store.ts index 91661d998..d562a5ca5 100644 --- a/apps/sim/stores/panel/console/store.ts +++ b/apps/sim/stores/panel/console/store.ts @@ -176,10 +176,18 @@ export const useConsoleStore = create()( 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) diff --git a/apps/sim/stores/panel/console/types.ts b/apps/sim/stores/panel/console/types.ts index 363b03364..3c86f6f9c 100644 --- a/apps/sim/stores/panel/console/types.ts +++ b/apps/sim/stores/panel/console/types.ts @@ -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 } diff --git a/apps/sim/tools/airtable/create_records.ts b/apps/sim/tools/airtable/create_records.ts index 6b9134271..163f8e247 100644 --- a/apps/sim/tools/airtable/create_records.ts +++ b/apps/sim/tools/airtable/create_records.ts @@ -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 = { id: 'airtable_create_records', name: 'Airtable Create Records', @@ -18,21 +16,25 @@ export const airtableCreateRecordsTool: ToolConfig { - 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') - }) -}) diff --git a/apps/sim/tools/airtable/list_records.ts b/apps/sim/tools/airtable/list_records.ts index 1889126c2..15736fefd 100644 --- a/apps/sim/tools/airtable/list_records.ts +++ b/apps/sim/tools/airtable/list_records.ts @@ -10,36 +10,39 @@ export const airtableListRecordsTool: ToolConfig }> - // 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 - // 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 }> - // TODO: Add typecast, performUpsert parameters } export interface AirtableUpdateMultipleResponse extends ToolResponse { diff --git a/apps/sim/tools/airtable/update_multiple_records.ts b/apps/sim/tools/airtable/update_multiple_records.ts index 2263a6ede..9e754f6c3 100644 --- a/apps/sim/tools/airtable/update_multiple_records.ts +++ b/apps/sim/tools/airtable/update_multiple_records.ts @@ -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" } }] }, diff --git a/apps/sim/tools/airtable/update_record.ts b/apps/sim/tools/airtable/update_record.ts index 606ae8d88..9cf443aa0 100644 --- a/apps/sim/tools/airtable/update_record.ts +++ b/apps/sim/tools/airtable/update_record.ts @@ -18,26 +18,31 @@ export const airtableUpdateRecordTool: ToolConfig = { - 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 = { - 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)}` - }, -} diff --git a/apps/sim/tools/autoblocks/types.ts b/apps/sim/tools/autoblocks/types.ts deleted file mode 100644 index 14f2c4f6c..000000000 --- a/apps/sim/tools/autoblocks/types.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { ToolResponse } from '../types' - -export interface PromptManagerParams { - promptId: string - version: string - specificVersion?: string - templateParams?: Record - 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 - } -} diff --git a/apps/sim/tools/browser_use/run_task.ts b/apps/sim/tools/browser_use/run_task.ts index ccb92e5af..fc8aacf88 100644 --- a/apps/sim/tools/browser_use/run_task.ts +++ b/apps/sim/tools/browser_use/run_task.ts @@ -17,27 +17,31 @@ export const runTaskTool: ToolConfig = { query: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'The question to answer', }, text: { type: 'boolean', required: false, + visibility: 'user-only', description: 'Whether to include the full text of the answer', }, apiKey: { type: 'string', required: true, - requiredForToolCall: true, + visibility: 'user-only', description: 'Exa AI API Key', }, }, diff --git a/apps/sim/tools/exa/find_similar_links.ts b/apps/sim/tools/exa/find_similar_links.ts index 7cc95d389..e153b6408 100644 --- a/apps/sim/tools/exa/find_similar_links.ts +++ b/apps/sim/tools/exa/find_similar_links.ts @@ -15,22 +15,25 @@ export const findSimilarLinksTool: ToolConfig< url: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'The URL to find similar links for', }, numResults: { type: 'number', required: false, + visibility: 'user-only', description: 'Number of similar links to return (default: 10, max: 25)', }, text: { type: 'boolean', required: false, + visibility: 'user-or-llm', description: 'Whether to include the full text of the similar pages', }, apiKey: { type: 'string', required: true, - requiredForToolCall: true, + visibility: 'user-only', description: 'Exa AI API Key', }, }, diff --git a/apps/sim/tools/exa/get_contents.ts b/apps/sim/tools/exa/get_contents.ts index 293bd8f41..84d900218 100644 --- a/apps/sim/tools/exa/get_contents.ts +++ b/apps/sim/tools/exa/get_contents.ts @@ -12,23 +12,26 @@ export const getContentsTool: ToolConfig = { query: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'The search query to execute', }, numResults: { type: 'number', required: false, + visibility: 'user-only', description: 'Number of results to return (default: 10, max: 25)', }, useAutoprompt: { type: 'boolean', required: false, + visibility: 'user-only', description: 'Whether to use autoprompt to improve the query (default: false)', }, type: { type: 'string', required: false, + visibility: 'user-only', description: 'Search type: neural, keyword, auto or magic (default: auto)', }, apiKey: { type: 'string', required: true, - requiredForToolCall: true, + visibility: 'user-only', description: 'Exa AI API Key', }, }, diff --git a/apps/sim/tools/file/parser.ts b/apps/sim/tools/file/parser.ts index 98ac2860b..a673f0c80 100644 --- a/apps/sim/tools/file/parser.ts +++ b/apps/sim/tools/file/parser.ts @@ -19,11 +19,13 @@ export const fileParserTool: ToolConfig = { filePath: { type: 'string', required: true, + visibility: 'user-only', description: 'Path to the file(s). Can be a single path, URL, or an array of paths.', }, fileType: { type: 'string', required: false, + visibility: 'hidden', description: 'Type of file to parse (auto-detected if not specified)', }, }, diff --git a/apps/sim/tools/firecrawl/scrape.ts b/apps/sim/tools/firecrawl/scrape.ts index 760ce4e4c..04392b5a0 100644 --- a/apps/sim/tools/firecrawl/scrape.ts +++ b/apps/sim/tools/firecrawl/scrape.ts @@ -9,23 +9,24 @@ export const scrapeTool: ToolConfig = { version: '1.0.0', params: { - apiKey: { - type: 'string', - required: true, - requiredForToolCall: true, - description: 'Firecrawl API key', - }, url: { type: 'string', required: true, - optionalToolInput: true, + visibility: 'user-or-llm', description: 'The URL to scrape content from', }, scrapeOptions: { type: 'json', required: false, + visibility: 'hidden', description: 'Options for content scraping', }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Firecrawl API key', + }, }, request: { diff --git a/apps/sim/tools/firecrawl/search.ts b/apps/sim/tools/firecrawl/search.ts index 55646cf4a..a00aeefb3 100644 --- a/apps/sim/tools/firecrawl/search.ts +++ b/apps/sim/tools/firecrawl/search.ts @@ -8,17 +8,18 @@ export const searchTool: ToolConfig = { version: '1.0.0', params: { - apiKey: { - type: 'string', - required: true, - requiredForToolCall: true, - description: 'Firecrawl API key', - }, query: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'The search query to use', }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Firecrawl API key', + }, }, request: { diff --git a/apps/sim/tools/function/execute.ts b/apps/sim/tools/function/execute.ts index 60ad05597..8b405e0b8 100644 --- a/apps/sim/tools/function/execute.ts +++ b/apps/sim/tools/function/execute.ts @@ -14,29 +14,34 @@ export const functionExecuteTool: ToolConfig owner: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Repository owner', }, repo: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Repository name', }, - pullNumber: { - type: 'number', - required: true, - description: 'Pull request number', - }, body: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Comment content', }, + pullNumber: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Pull request number', + }, path: { type: 'string', required: false, + visibility: 'user-or-llm', description: 'File path for review comment', }, position: { type: 'number', required: false, + visibility: 'hidden', description: 'Line number for review comment', }, - apiKey: { - type: 'string', - required: true, - requiredForToolCall: true, - description: 'GitHub API token', - }, commentType: { type: 'string', required: false, + visibility: 'user-or-llm', description: 'Type of comment (pr_comment or file_comment)', }, line: { type: 'number', required: false, + visibility: 'hidden', description: 'Line number for review comment', }, side: { type: 'string', required: false, + visibility: 'hidden', description: 'Side of the diff (LEFT or RIGHT)', default: 'RIGHT', }, commitId: { type: 'string', required: false, + visibility: 'hidden', description: 'The SHA of the commit to comment on', }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, }, request: { diff --git a/apps/sim/tools/github/latest_commit.ts b/apps/sim/tools/github/latest_commit.ts index 96a01e343..356f39405 100644 --- a/apps/sim/tools/github/latest_commit.ts +++ b/apps/sim/tools/github/latest_commit.ts @@ -11,22 +11,25 @@ export const latestCommitTool: ToolConfig { - let tester: ToolTester - - // Mock PR response data - const mockPRResponse = { - number: 42, - title: 'Test PR Title', - body: 'Test PR description with details', - state: 'open', - html_url: 'https://github.com/testuser/testrepo/pull/42', - diff_url: 'https://github.com/testuser/testrepo/pull/42.diff', - created_at: '2023-01-01T00:00:00Z', - updated_at: '2023-01-02T00:00:00Z', - base: { - repo: { - name: 'testrepo', - owner: { - login: 'testuser', - }, - }, - }, - } - - // Mock PR diff data - const _mockPRDiff = `diff --git a/file.txt b/file.txt -index 1234567..abcdefg 100644 ---- a/file.txt -+++ b/file.txt -@@ -1,3 +1,4 @@ - Line 1 --Line 2 -+Line 2 modified -+Line 3 added - Line 4` - - // Mock PR files data - const mockPRFiles = [ - { - filename: 'file.txt', - additions: 2, - deletions: 1, - changes: 3, - patch: '@@ -1,3 +1,4 @@\n Line 1\n-Line 2\n+Line 2 modified\n+Line 3 added\n Line 4', - blob_url: 'https://github.com/testuser/testrepo/blob/abc123/file.txt', - raw_url: 'https://github.com/testuser/testrepo/raw/abc123/file.txt', - status: 'modified', - }, - ] - - // Expected content for the PR - const expectedContent = `PR #42: "Test PR Title" (open) - Created: 2023-01-01T00:00:00Z, Updated: 2023-01-02T00:00:00Z -Description: Test PR description with details -Files changed: 1 -URL: https://github.com/testuser/testrepo/pull/42` - - let originalTransformResponse: any - - beforeEach(() => { - tester = new ToolTester(prTool) - - // Mock the internal transformResponse method to avoid actual API calls - originalTransformResponse = prTool.transformResponse - prTool.transformResponse = async () => { - return { - success: true, - output: { - content: expectedContent, - metadata: { - number: 42, - title: 'Test PR Title', - state: 'open', - html_url: 'https://github.com/testuser/testrepo/pull/42', - diff_url: 'https://github.com/testuser/testrepo/pull/42.diff', - created_at: '2023-01-01T00:00:00Z', - updated_at: '2023-01-02T00:00:00Z', - files: mockPRFiles.map((file) => ({ - filename: file.filename, - additions: file.additions, - deletions: file.deletions, - changes: file.changes, - patch: file.patch, - blob_url: file.blob_url, - raw_url: file.raw_url, - status: file.status, - })), - }, - }, - } - } - }) - - afterEach(() => { - // Restore the original transformResponse if it exists - if (originalTransformResponse) { - prTool.transformResponse = originalTransformResponse - } - tester.cleanup() - vi.resetAllMocks() - }) - - describe('URL Construction', () => { - test('should construct correct GitHub PR API URL', () => { - const params = { - owner: 'testuser', - repo: 'testrepo', - pullNumber: 42, - apiKey: 'test-token', - } - - expect(tester.getRequestUrl(params)).toBe( - 'https://api.github.com/repos/testuser/testrepo/pulls/42' - ) - }) - }) - - describe('Headers Construction', () => { - test('should include correct headers for GitHub API', () => { - const params = { - owner: 'testuser', - repo: 'testrepo', - pullNumber: 42, - apiKey: 'test-token', - } - - const headers = tester.getRequestHeaders(params) - expect(headers.Authorization).toBe('Bearer test-token') - expect(headers.Accept).toBe('application/vnd.github.v3+json') - }) - }) - - describe('Data Transformation', () => { - test('should fetch and transform PR data including diff and files', async () => { - // Setup mock response for initial PR data - tester.setup(mockPRResponse) - - // Execute the tool - const result = await tester.execute({ - owner: 'testuser', - repo: 'testrepo', - pullNumber: 42, - apiKey: 'test-token', - }) - - // Check the result - expect(result.success).toBe(true) - - // Verify content is present and correct - expect(result.output.content).toBe(expectedContent) - - // Verify PR basic info in metadata - expect(result.output.metadata.number).toBe(42) - expect(result.output.metadata.title).toBe('Test PR Title') - expect(result.output.metadata.state).toBe('open') - - // Verify files were fetched and transformed - expect(result.output.metadata.files).toHaveLength(1) - expect(result.output.metadata.files?.[0].filename).toBe('file.txt') - expect(result.output.metadata.files?.[0].additions).toBe(2) - expect(result.output.metadata.files?.[0].deletions).toBe(1) - expect(result.output.metadata.files?.[0].status).toBe('modified') - }) - }) - - describe('Error Handling', () => { - test('should handle PR not found errors', async () => { - // Setup 404 error response - tester.setup({ message: 'Not Found' }, { ok: false, status: 404 }) - - // Execute the tool - const result = await tester.execute({ - owner: 'testuser', - repo: 'testrepo', - pullNumber: 9999, // non-existent PR - apiKey: 'test-token', - }) - - // Check error handling - expect(result.success).toBe(false) - expect(result.error).toBeDefined() - }) - - test('should handle network errors', async () => { - // Setup network error - tester.setupError('Network error') - - // Execute the tool - const result = await tester.execute({ - owner: 'testuser', - repo: 'testrepo', - pullNumber: 42, - apiKey: 'test-token', - }) - - // Check error handling - expect(result.success).toBe(false) - expect(result.error).toBeDefined() - }) - }) -}) diff --git a/apps/sim/tools/github/pr.ts b/apps/sim/tools/github/pr.ts index 4fba2dd27..62b23f800 100644 --- a/apps/sim/tools/github/pr.ts +++ b/apps/sim/tools/github/pr.ts @@ -11,22 +11,25 @@ export const prTool: ToolConfig = { owner: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Repository owner', }, repo: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Repository name', }, pullNumber: { type: 'number', required: true, + visibility: 'user-or-llm', description: 'Pull request number', }, apiKey: { type: 'string', required: true, - requiredForToolCall: true, + visibility: 'user-only', description: 'GitHub API token', }, }, diff --git a/apps/sim/tools/github/repo_info.test.ts b/apps/sim/tools/github/repo_info.test.ts deleted file mode 100644 index 9361c543d..000000000 --- a/apps/sim/tools/github/repo_info.test.ts +++ /dev/null @@ -1,241 +0,0 @@ -/** - * @vitest-environment jsdom - * - * GitHub Repository Info Tool Unit Tests - * - * This file contains unit tests for the GitHub Repository Info tool, - * which is used to fetch metadata about GitHub repositories. - */ -import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' -import { mockGitHubResponses } from '../__test-utils__/mock-data' -import { ToolTester } from '../__test-utils__/test-tools' -import { repoInfoTool } from './repo_info' - -describe('GitHub Repository Info Tool', () => { - let tester: ToolTester - - // Expected repository content string - const expectedContent = `Repository: test-repo -Description: A test repository -Language: TypeScript -Stars: 15 -Forks: 3 -Open Issues: 5 -URL: https://github.com/testuser/test-repo` - - beforeEach(() => { - tester = new ToolTester(repoInfoTool) - - // Add HTML URL to the mock response for our content string - mockGitHubResponses.repoInfo.html_url = 'https://github.com/testuser/test-repo' - }) - - afterEach(() => { - tester.cleanup() - vi.resetAllMocks() - }) - - describe('URL Construction', () => { - test('should construct correct GitHub API URL', () => { - const params = { - owner: 'testuser', - repo: 'testrepo', - apiKey: 'test-token', - } - - expect(tester.getRequestUrl(params)).toBe('https://api.github.com/repos/testuser/testrepo') - }) - }) - - describe('Headers Construction', () => { - test('should include authorization header when apiKey is provided', () => { - const params = { - owner: 'testuser', - repo: 'testrepo', - apiKey: 'test-token', - } - - const headers = tester.getRequestHeaders(params) - expect(headers.Authorization).toBe('Bearer test-token') - expect(headers.Accept).toBe('application/vnd.github+json') - expect(headers['X-GitHub-Api-Version']).toBe('2022-11-28') - }) - }) - - describe('Data Transformation', () => { - test('should transform repository data correctly', async () => { - // Mock the transformResponse method - const originalTransformResponse = repoInfoTool.transformResponse - repoInfoTool.transformResponse = async () => { - return { - success: true, - output: { - content: expectedContent, - metadata: { - name: 'test-repo', - description: 'A test repository', - stars: 15, - forks: 3, - openIssues: 5, - language: 'TypeScript', - }, - }, - } - } - - // Setup mock response - tester.setup(mockGitHubResponses.repoInfo) - - // Execute the tool - const result = await tester.execute({ - owner: 'testuser', - repo: 'testrepo', - apiKey: 'test-token', - }) - - // Check the result - expect(result.success).toBe(true) - expect(result.output.content).toBe(expectedContent) - expect(result.output.metadata).toEqual({ - name: 'test-repo', - description: 'A test repository', - stars: 15, - forks: 3, - openIssues: 5, - language: 'TypeScript', - }) - - // Restore original - if (originalTransformResponse) { - repoInfoTool.transformResponse = originalTransformResponse - } - }) - - test('should handle missing description and language', async () => { - // Create a modified response with missing fields - const modifiedResponse = { - ...mockGitHubResponses.repoInfo, - description: null, - language: null, - html_url: 'https://github.com/testuser/test-repo', - } - - // Updated expected content with missing fields - const modifiedContent = `Repository: test-repo -Description: No description -Language: Not specified -Stars: 15 -Forks: 3 -Open Issues: 5 -URL: https://github.com/testuser/test-repo` - - // Mock the transformResponse method - const originalTransformResponse = repoInfoTool.transformResponse - repoInfoTool.transformResponse = async () => { - return { - success: true, - output: { - content: modifiedContent, - metadata: { - name: 'test-repo', - description: '', - stars: 15, - forks: 3, - openIssues: 5, - language: 'Not specified', - }, - }, - } - } - - tester.setup(modifiedResponse) - - // Execute the tool - const result = await tester.execute({ - owner: 'testuser', - repo: 'testrepo', - apiKey: 'test-token', - }) - - // Check the result - expect(result.success).toBe(true) - expect(result.output.content).toBe(modifiedContent) - expect(result.output.metadata.description).toBe('') - expect(result.output.metadata.language).toBe('Not specified') - - // Restore original - if (originalTransformResponse) { - repoInfoTool.transformResponse = originalTransformResponse - } - }) - }) - - describe('Error Handling', () => { - test('should handle repository not found errors', async () => { - // Setup 404 error response - tester.setup({ message: 'Not Found' }, { ok: false, status: 404 }) - - // Mock the transformError function to return the specific error message we're testing for - const originalTransformError = repoInfoTool.transformError - repoInfoTool.transformError = () => - 'Repository not found. Please check the owner and repository name.' - - // Execute the tool - const result = await tester.execute({ - owner: 'nonexistent', - repo: 'nonexistent', - apiKey: 'test-token', - }) - - // Check error handling - expect(result.success).toBe(false) - expect(result.error).toBe('Repository not found. Please check the owner and repository name.') - - // Restore original - if (originalTransformError) { - repoInfoTool.transformError = originalTransformError - } - }) - - test('should handle authentication errors', async () => { - // Setup 401 error response - tester.setup({ message: 'Bad credentials' }, { ok: false, status: 401 }) - - // Mock the transformError function to return the specific error message we're testing for - const originalTransformError = repoInfoTool.transformError - repoInfoTool.transformError = () => 'Authentication failed. Please check your GitHub token.' - - // Execute the tool - const result = await tester.execute({ - owner: 'testuser', - repo: 'testrepo', - apiKey: 'invalid-token', - }) - - // Check error handling - expect(result.success).toBe(false) - expect(result.error).toBe('Authentication failed. Please check your GitHub token.') - - // Restore original - if (originalTransformError) { - repoInfoTool.transformError = originalTransformError - } - }) - - test('should handle network errors', async () => { - // Setup network error - tester.setupError('Network error') - - // Execute the tool - const result = await tester.execute({ - owner: 'testuser', - repo: 'testrepo', - apiKey: 'test-token', - }) - - // Check error handling - expect(result.success).toBe(false) - expect(result.error).toBeDefined() - }) - }) -}) diff --git a/apps/sim/tools/github/repo_info.ts b/apps/sim/tools/github/repo_info.ts index e1093c083..af8e5c4cd 100644 --- a/apps/sim/tools/github/repo_info.ts +++ b/apps/sim/tools/github/repo_info.ts @@ -12,17 +12,19 @@ export const repoInfoTool: ToolConfig = { owner: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Repository owner (user or organization)', }, repo: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Repository name', }, apiKey: { type: 'string', required: true, - requiredForToolCall: true, + visibility: 'user-only', description: 'GitHub Personal Access Token', }, }, diff --git a/apps/sim/tools/gmail/draft.ts b/apps/sim/tools/gmail/draft.ts index 36d38899c..e16117fb3 100644 --- a/apps/sim/tools/gmail/draft.ts +++ b/apps/sim/tools/gmail/draft.ts @@ -19,21 +19,25 @@ export const gmailDraftTool: ToolConfig = { accessToken: { type: 'string', required: true, + visibility: 'hidden', description: 'Access token for Gmail API', }, to: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Recipient email address', }, subject: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Email subject', }, body: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Email body content', }, }, diff --git a/apps/sim/tools/gmail/read.test.ts b/apps/sim/tools/gmail/read.test.ts deleted file mode 100644 index 4fe3040be..000000000 --- a/apps/sim/tools/gmail/read.test.ts +++ /dev/null @@ -1,409 +0,0 @@ -/** - * @vitest-environment jsdom - * - * Gmail Read Tool Unit Tests - * - * This file contains unit tests for the Gmail Read tool, which is used - * to fetch emails from Gmail via the Gmail API. - */ -import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' -import { mockGmailResponses } from '../__test-utils__/mock-data' -import { mockOAuthTokenRequest, ToolTester } from '../__test-utils__/test-tools' -import { gmailReadTool } from './read' - -describe('Gmail Read Tool', () => { - let tester: ToolTester - let cleanupOAuth: () => void - - beforeEach(() => { - tester = new ToolTester(gmailReadTool) - // Mock OAuth token request - cleanupOAuth = mockOAuthTokenRequest('gmail-access-token-123') - // Set base URL environment variable - process.env.NEXT_PUBLIC_APP_URL = 'http://localhost:3000' - }) - - afterEach(() => { - tester.cleanup() - cleanupOAuth() - vi.resetAllMocks() - process.env.NEXT_PUBLIC_APP_URL = undefined - }) - - describe('URL Construction', () => { - test('should construct URL for reading a specific message', () => { - const params = { - accessToken: 'test-token', - messageId: 'msg123', - } - - expect(tester.getRequestUrl(params)).toBe( - 'https://gmail.googleapis.com/gmail/v1/users/me/messages/msg123?format=full' - ) - }) - - test('should construct URL for listing messages from inbox by default', () => { - const params = { - accessToken: 'test-token', - } - - const url = tester.getRequestUrl(params) - expect(url).toContain('https://gmail.googleapis.com/gmail/v1/users/me/messages') - expect(url).toContain('in:inbox') - expect(url).toContain('maxResults=1') - }) - - test('should construct URL for listing messages from specific folder', () => { - const params = { - accessToken: 'test-token', - folder: 'SENT', - } - - const url = tester.getRequestUrl(params) - expect(url).toContain('in:sent') - }) - - test('should construct URL with unread filter when specified', () => { - const params = { - accessToken: 'test-token', - unreadOnly: true, - } - - const url = tester.getRequestUrl(params) - expect(url).toContain('is:unread') - }) - - test('should respect maxResults parameter', () => { - const params = { - accessToken: 'test-token', - maxResults: 5, - } - - const url = tester.getRequestUrl(params) - expect(url).toContain('maxResults=5') - }) - - test('should limit maxResults to 10', () => { - const params = { - accessToken: 'test-token', - maxResults: 20, // Should be limited to 10 - } - - const url = tester.getRequestUrl(params) - expect(url).toContain('maxResults=10') - }) - }) - - describe('Authentication', () => { - test('should include access token in headers', () => { - const params = { - accessToken: 'test-access-token', - messageId: 'msg123', - } - - const headers = tester.getRequestHeaders(params) - expect(headers.Authorization).toBe('Bearer test-access-token') - expect(headers['Content-Type']).toBe('application/json') - }) - - test('should use OAuth credential when provided', async () => { - // Setup initial response for message list - tester.setup(mockGmailResponses.messageList) - - // Then setup response for the first message - const originalFetch = global.fetch - global.fetch = Object.assign( - vi.fn().mockImplementation((url, options) => { - // Check if it's a token request - if (url.toString().includes('/api/auth/oauth/token')) { - return Promise.resolve({ - ok: true, - status: 200, - json: () => Promise.resolve({ accessToken: 'gmail-access-token-123' }), - }) - } - - // For message list endpoint - if (url.toString().includes('users/me/messages') && !url.toString().includes('msg1')) { - return Promise.resolve({ - ok: true, - status: 200, - json: () => Promise.resolve(mockGmailResponses.messageList), - headers: { - get: () => 'application/json', - forEach: () => {}, - }, - }) - } - - // For specific message endpoint - if (url.toString().includes('msg1')) { - return Promise.resolve({ - ok: true, - status: 200, - json: () => Promise.resolve(mockGmailResponses.singleMessage), - headers: { - get: () => 'application/json', - forEach: () => {}, - }, - }) - } - - return originalFetch(url, options) - }), - { preconnect: vi.fn() } - ) as typeof fetch - - // Execute with credential instead of access token - await tester.execute({ - credential: 'gmail-credential-id', - }) - - // There's a mismatch in how the mocks are set up - // The test setup makes only one fetch call in reality - // This is okay for this test - we just want to test the credential flow - expect(global.fetch).toHaveBeenCalled() - - // Restore original fetch - global.fetch = originalFetch - }) - }) - - describe('Message Fetching', () => { - test('should fetch a specific message by ID', async () => { - // Setup mock response for single message - tester.setup(mockGmailResponses.singleMessage) - - // Execute the tool - const result = await tester.execute({ - accessToken: 'test-token', - messageId: 'msg1', - }) - - // Check the result - expect(result.success).toBe(true) - expect(result.output.content).toBeDefined() - expect(result.output.metadata).toEqual( - expect.objectContaining({ - id: 'msg1', - threadId: 'thread1', - subject: 'Test Email Subject', - from: 'sender@example.com', - to: 'recipient@example.com', - }) - ) - }) - - test('should fetch the first message from inbox by default', async () => { - // Need to mock multiple sequential responses - const originalFetch = global.fetch - - // First setup response for message list - global.fetch = Object.assign( - vi - .fn() - .mockImplementationOnce((url, options) => { - return Promise.resolve({ - ok: true, - status: 200, - json: () => Promise.resolve(mockGmailResponses.messageList), - headers: { - get: () => 'application/json', - forEach: () => {}, - }, - }) - }) - .mockImplementationOnce((url, options) => { - // For the second request (first message) - return Promise.resolve({ - ok: true, - status: 200, - json: () => Promise.resolve(mockGmailResponses.singleMessage), - headers: { - get: () => 'application/json', - forEach: () => {}, - }, - }) - }), - { preconnect: vi.fn() } - ) as typeof fetch - - // Execute the tool - const result = await tester.execute({ - accessToken: 'test-token', - }) - - // Restore original fetch - global.fetch = originalFetch - - // Check the result - expect(result.success).toBe(true) - expect(result.output.content).toBeDefined() - expect(result.output.metadata).toEqual({ - results: [], - }) - }) - - test('should handle empty inbox', async () => { - // Setup mock response for empty list - tester.setup(mockGmailResponses.emptyList) - - // Execute the tool - const result = await tester.execute({ - accessToken: 'test-token', - }) - - // Check the result - expect(result.success).toBe(true) - expect(result.output.content).toContain('No messages found') - expect(result.output.metadata.results).toEqual([]) - }) - - test('should fetch multiple messages when maxResults > 1', async () => { - // Need a completely controlled mock for this test - const originalFetch = global.fetch - - // Directly mock the transformResponse instead of trying to set up complex fetch chains - const origTransformResponse = tester.tool.transformResponse - tester.tool.transformResponse = async () => ({ - success: true, - output: { - content: 'Found 3 messages in your inbox', - metadata: { - results: [ - { id: 'msg1', threadId: 'thread1', subject: 'Email 1' }, - { id: 'msg2', threadId: 'thread2', subject: 'Email 2' }, - { id: 'msg3', threadId: 'thread3', subject: 'Email 3' }, - ], - }, - }, - }) - - // Execute the tool with maxResults = 3 - const result = await tester.execute({ - accessToken: 'test-token', - maxResults: 3, - }) - - // Restore original implementation - tester.tool.transformResponse = origTransformResponse - global.fetch = originalFetch - - // Check the result - expect(result.success).toBe(true) - expect(result.output.content).toContain('Found 3 messages') - expect(result.output.metadata.results).toHaveLength(3) - }) - }) - - describe('Error Handling', () => { - test('should handle invalid access token errors', async () => { - // Setup error response - tester.setup( - { error: { message: 'invalid authentication credentials' } }, - { ok: false, status: 401 } - ) - - // Execute the tool - const result = await tester.execute({ - accessToken: 'invalid-token', - messageId: 'msg1', - }) - - // Check error handling - expect(result.success).toBe(false) - expect(result.error).toBeDefined() - }) - - test('should handle quota exceeded errors', async () => { - // Setup error response - tester.setup( - { error: { message: 'quota exceeded for quota metric' } }, - { ok: false, status: 429 } - ) - - // Execute the tool - const result = await tester.execute({ - accessToken: 'test-token', - messageId: 'msg1', - }) - - // Check error handling - expect(result.success).toBe(false) - expect(result.error).toBeDefined() - }) - - test('should handle message not found errors', async () => { - // Setup error response - tester.setup({ error: { message: 'Resource not found' } }, { ok: false, status: 404 }) - - // Execute the tool - const result = await tester.execute({ - accessToken: 'test-token', - messageId: 'non-existent-msg', - }) - - // Check error handling - expect(result.success).toBe(false) - expect(result.error).toBeDefined() - }) - }) - - describe('Content Extraction', () => { - test('should extract plain text content from message', async () => { - // Setup successful response - tester.setup(mockGmailResponses.singleMessage) - - // Execute the tool - const result = await tester.execute({ - accessToken: 'test-token', - messageId: 'msg1', - }) - - // Check content extraction - expect(result.success).toBe(true) - expect(result.output.content).toBe('This is the plain text content of the email') - }) - - test('should handle message with missing body', async () => { - // Create a modified message with no body data - const modifiedMessage = JSON.parse(JSON.stringify(mockGmailResponses.singleMessage)) - modifiedMessage.payload.parts[0].body.data = undefined - modifiedMessage.payload.parts[1].body.data = undefined - - // Setup the modified response - tester.setup(modifiedMessage) - - // Execute the tool - const result = await tester.execute({ - accessToken: 'test-token', - messageId: 'msg1', - }) - - // Check content extraction fallback - expect(result.success).toBe(true) - expect(result.output.content).toBe('No content found in email') - }) - - test('should extract headers correctly', async () => { - // Setup successful response - tester.setup(mockGmailResponses.singleMessage) - - // Execute the tool - const result = await tester.execute({ - accessToken: 'test-token', - messageId: 'msg1', - }) - - // Check headers extraction - expect(result.output.metadata).toEqual( - expect.objectContaining({ - from: 'sender@example.com', - to: 'recipient@example.com', - subject: 'Test Email Subject', - date: 'Mon, 15 Mar 2025 10:30:00 -0800', - }) - ) - }) - }) -}) diff --git a/apps/sim/tools/gmail/read.ts b/apps/sim/tools/gmail/read.ts index 4a89d86d2..c7c2a2bdb 100644 --- a/apps/sim/tools/gmail/read.ts +++ b/apps/sim/tools/gmail/read.ts @@ -12,36 +12,38 @@ export const gmailReadTool: ToolConfig = { oauth: { required: true, provider: 'google-email', - additionalScopes: [ - // 'https://www.googleapis.com/auth/gmail.readonly', - 'https://www.googleapis.com/auth/gmail.labels', - ], + additionalScopes: ['https://www.googleapis.com/auth/gmail.labels'], }, params: { accessToken: { type: 'string', required: true, + visibility: 'hidden', description: 'Access token for Gmail API', }, messageId: { type: 'string', required: false, + visibility: 'user-only', description: 'ID of the message to read', }, folder: { type: 'string', required: false, + visibility: 'user-or-llm', description: 'Folder/label to read emails from', }, unreadOnly: { type: 'boolean', required: false, + visibility: 'user-only', description: 'Only retrieve unread messages', }, maxResults: { type: 'number', required: false, + visibility: 'user-only', description: 'Maximum number of messages to retrieve (default: 1, max: 10)', }, }, diff --git a/apps/sim/tools/gmail/search.ts b/apps/sim/tools/gmail/search.ts index ed130f95a..0bad1c391 100644 --- a/apps/sim/tools/gmail/search.ts +++ b/apps/sim/tools/gmail/search.ts @@ -12,26 +12,26 @@ export const gmailSearchTool: ToolConfig = oauth: { required: true, provider: 'google-email', - additionalScopes: [ - // 'https://www.googleapis.com/auth/gmail.readonly', - 'https://www.googleapis.com/auth/gmail.labels', - ], + additionalScopes: ['https://www.googleapis.com/auth/gmail.labels'], }, params: { accessToken: { type: 'string', required: true, + visibility: 'hidden', description: 'Access token for Gmail API', }, query: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Search query for emails', }, maxResults: { type: 'number', required: false, + visibility: 'user-only', description: 'Maximum number of results to return', }, }, diff --git a/apps/sim/tools/gmail/send.ts b/apps/sim/tools/gmail/send.ts index da50f58e9..498f82d62 100644 --- a/apps/sim/tools/gmail/send.ts +++ b/apps/sim/tools/gmail/send.ts @@ -19,21 +19,25 @@ export const gmailSendTool: ToolConfig = { accessToken: { type: 'string', required: true, + visibility: 'hidden', description: 'Access token for Gmail API', }, to: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Recipient email address', }, subject: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Email subject', }, body: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Email body content', }, }, diff --git a/apps/sim/tools/google/search.ts b/apps/sim/tools/google/search.ts index 5433be3f6..772cf25e6 100644 --- a/apps/sim/tools/google/search.ts +++ b/apps/sim/tools/google/search.ts @@ -11,25 +11,27 @@ export const searchTool: ToolConfig = query: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'The search query to execute', }, - apiKey: { - type: 'string', - required: true, - description: 'Google API key', - requiredForToolCall: true, - }, searchEngineId: { type: 'string', required: true, + visibility: 'user-only', description: 'Custom Search Engine ID', - requiredForToolCall: true, }, num: { type: 'string', // Treated as string for compatibility with tool interfaces required: false, + visibility: 'user-only', description: 'Number of results to return (default: 10, max: 10)', }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Google API key', + }, }, request: { diff --git a/apps/sim/tools/google_calendar/create.ts b/apps/sim/tools/google_calendar/create.ts index efb737540..b36eff211 100644 --- a/apps/sim/tools/google_calendar/create.ts +++ b/apps/sim/tools/google_calendar/create.ts @@ -23,51 +23,61 @@ export const createTool: ToolConfig accessToken: { type: 'string', required: true, + visibility: 'hidden', description: 'The access token for the Google Docs API', }, - documentId: { type: 'string', required: true, description: 'The ID of the document to read' }, + documentId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the document to read', + }, }, request: { url: (params) => { diff --git a/apps/sim/tools/google_docs/write.ts b/apps/sim/tools/google_docs/write.ts index 192d5afdb..0adfca4e1 100644 --- a/apps/sim/tools/google_docs/write.ts +++ b/apps/sim/tools/google_docs/write.ts @@ -15,6 +15,7 @@ export const writeTool: ToolConfig { - const metadata = { + const metadata: { + name: string | undefined + mimeType: string + parents?: string[] + } = { name: params.fileName, mimeType: 'application/vnd.google-apps.folder', - ...(params.folderId ? { parents: [params.folderId] } : {}), } - if (params.folderSelector) { - metadata.parents = [params.folderSelector] + // Add parent folder if specified (prefer folderSelector over folderId) + const parentFolderId = params.folderSelector || params.folderId + if (parentFolderId) { + metadata.parents = [parentFolderId] } return metadata diff --git a/apps/sim/tools/google_drive/get_content.ts b/apps/sim/tools/google_drive/get_content.ts index 541c576a7..21d5cbec3 100644 --- a/apps/sim/tools/google_drive/get_content.ts +++ b/apps/sim/tools/google_drive/get_content.ts @@ -20,16 +20,19 @@ export const getContentTool: ToolConfig { - const metadata = { + const metadata: { + name: string | undefined + mimeType: string + parents?: string[] + } = { name: params.fileName, // Important: Always include the filename in metadata mimeType: params.mimeType || 'text/plain', - ...(params.folderId && params.folderId.trim() !== '' ? { parents: [params.folderId] } : {}), } - if (params.folderSelector) { - metadata.parents = [params.folderSelector] + // Add parent folder if specified (prefer folderSelector over folderId) + const parentFolderId = params.folderSelector || params.folderId + if (parentFolderId && parentFolderId.trim() !== '') { + metadata.parents = [parentFolderId] } return metadata diff --git a/apps/sim/tools/google_sheets/append.ts b/apps/sim/tools/google_sheets/append.ts index 117e708f0..532f3d376 100644 --- a/apps/sim/tools/google_sheets/append.ts +++ b/apps/sim/tools/google_sheets/append.ts @@ -15,28 +15,43 @@ export const appendTool: ToolConfig { diff --git a/apps/sim/tools/google_sheets/update.ts b/apps/sim/tools/google_sheets/update.ts index 98a399839..798c600fd 100644 --- a/apps/sim/tools/google_sheets/update.ts +++ b/apps/sim/tools/google_sheets/update.ts @@ -15,23 +15,37 @@ export const updateTool: ToolConfig = { - id: 'guesty_guest', - name: 'Guesty Guest', - description: 'Search for guests in Guesty by phone number', - version: '1.0.0', - - params: { - apiKey: { - type: 'string', - required: true, - requiredForToolCall: true, - description: 'Your Guesty API token', - }, - phoneNumber: { - type: 'string', - required: true, - description: 'The phone number to search for', - }, - }, - - request: { - url: 'https://open-api.guesty.com/v1/guests', - method: 'GET', - headers: (params: GuestyGuestParams) => ({ - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.apiKey}`, - }), - body: (params: GuestyGuestParams) => ({ - filters: { - phone: params.phoneNumber, - }, - fields: 'fullName,email,phone,address,city,country', - }), - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - - if (!response.ok) { - throw new Error(data.message || 'Failed to search for guests in Guesty') - } - - return { - success: true, - output: { - guests: data.results.map((guest: any) => ({ - id: guest.id, - fullName: guest.fullName || 'N/A', - email: guest.email || 'N/A', - phone: guest.phone || 'N/A', - address: guest.address || 'N/A', - city: guest.city || 'N/A', - country: guest.country || 'N/A', - })), - }, - } - }, - - transformError: (error: any) => { - const message = error.message || 'Failed to search for guests in Guesty' - return message - }, -} diff --git a/apps/sim/tools/guesty/index.ts b/apps/sim/tools/guesty/index.ts deleted file mode 100644 index ed3e953e3..000000000 --- a/apps/sim/tools/guesty/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { guestyGuestTool } from './guest' -import { guestyReservationTool } from './reservation' - -export { guestyGuestTool, guestyReservationTool } diff --git a/apps/sim/tools/guesty/reservation.ts b/apps/sim/tools/guesty/reservation.ts deleted file mode 100644 index 2e03e7c4f..000000000 --- a/apps/sim/tools/guesty/reservation.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { ToolConfig } from '../types' -import type { GuestyReservationParams, GuestyReservationResponse } from './types' - -export const guestyReservationTool: ToolConfig = - { - id: 'guesty_reservation', - name: 'Guesty Reservation', - description: 'Fetch reservation details from Guesty by reservation ID', - version: '1.0.0', - - params: { - apiKey: { - type: 'string', - required: true, - requiredForToolCall: true, - description: 'Your Guesty API token', - }, - reservationId: { - type: 'string', - required: true, - description: 'The ID of the reservation to fetch', - }, - }, - - request: { - url: (params: GuestyReservationParams) => - `https://open-api.guesty.com/v1/reservations/${params.reservationId}`, - method: 'GET', - headers: (params: GuestyReservationParams) => ({ - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.apiKey}`, - }), - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - - if (!response.ok) { - throw new Error(data.message || 'Failed to fetch reservation from Guesty') - } - - return { - success: true, - output: { - id: data.id, - guest: { - fullName: data.guest?.fullName || 'N/A', - email: data.guest?.email || 'N/A', - phone: data.guest?.phone || 'N/A', - }, - checkIn: data.checkIn || 'N/A', - checkOut: data.checkOut || 'N/A', - status: data.status || 'N/A', - listing: { - id: data.listing?.id || 'N/A', - title: data.listing?.title || 'N/A', - }, - money: { - totalPaid: data.money?.totalPaid || 0, - currency: data.money?.currency || 'USD', - }, - }, - } - }, - - transformError: (error: any) => { - const message = error.message || 'Failed to fetch reservation from Guesty' - return message - }, - } diff --git a/apps/sim/tools/guesty/types.ts b/apps/sim/tools/guesty/types.ts deleted file mode 100644 index 70f632224..000000000 --- a/apps/sim/tools/guesty/types.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { ToolResponse } from '../types' - -export interface GuestyGuestParams { - apiKey: string - phoneNumber: string -} - -export interface GuestyGuestResponse extends ToolResponse { - output: { - guests: Array<{ - id: string - fullName: string - email: string - phone: string - address: string - city: string - country: string - }> - } -} - -export interface GuestyReservationParams { - apiKey: string - reservationId: string -} - -export interface GuestyReservationResponse extends ToolResponse { - output: { - id: string - guest: { - fullName: string - email: string - phone: string - } - checkIn: string - checkOut: string - status: string - listing: { - id: string - title: string - } - money: { - totalPaid: number - currency: string - } - } -} diff --git a/apps/sim/tools/hubspot/contacts.ts b/apps/sim/tools/hubspot/contacts.ts deleted file mode 100644 index a155bd343..000000000 --- a/apps/sim/tools/hubspot/contacts.ts +++ /dev/null @@ -1,137 +0,0 @@ -import type { ToolConfig, ToolResponse } from '../types' - -export interface ContactsParams { - apiKey: string - action: 'create' | 'update' | 'search' | 'delete' - id?: string - email?: string - firstName?: string - lastName?: string - phone?: string - company?: string - properties?: Record - limit?: number - after?: string - data: Record -} - -export interface ContactsResponse extends ToolResponse { - output: { - contacts: any[] - totalResults?: number - pagination?: { - hasMore: boolean - offset: number - } - } -} - -export const contactsTool: ToolConfig = { - id: 'hubspot_contacts', - name: 'HubSpot Contacts', - description: - 'Manage HubSpot CRM contacts with full CRUD operations support. Handle contact properties, pagination, and custom fields while maintaining data consistency across the CRM.', - version: '1.0.0', - - params: { - apiKey: { - type: 'string', - required: true, - requiredForToolCall: true, - description: 'HubSpot API key', - }, - email: { - type: 'string', - required: true, - description: 'Contact email address', - }, - firstName: { - type: 'string', - description: 'Contact first name', - }, - lastName: { - type: 'string', - description: 'Contact last name', - }, - phone: { - type: 'string', - description: 'Contact phone number', - }, - company: { - type: 'string', - description: 'Contact company name', - }, - id: { - type: 'string', - description: 'Contact ID (required for updates)', - }, - properties: { - type: 'object', - description: 'Additional contact properties', - }, - limit: { - type: 'number', - default: 100, - description: 'Number of records to return', - }, - after: { - type: 'string', - description: 'Pagination cursor', - }, - }, - - request: { - url: (params) => { - const baseUrl = 'https://api.hubapi.com/crm/v3/objects/contacts' - if (params.id) { - return `${baseUrl}/${params.id}` - } - return baseUrl - }, - method: 'POST', - headers: (params) => ({ - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.apiKey}`, - }), - body: (params) => { - const properties = { - email: params.email, - ...(params.firstName && { firstname: params.firstName }), - ...(params.lastName && { lastname: params.lastName }), - ...(params.phone && { phone: params.phone }), - ...(params.company && { company: params.company }), - ...params.properties, - } - - if (params.id) { - // Update existing contact - return { properties } - } - - // Create new contact or search - return { - properties, - ...(params.limit && { limit: params.limit }), - ...(params.after && { after: params.after }), - } - }, - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - return { - success: true, - output: { - contacts: data.results || [data], - totalResults: data.total, - pagination: data.paging, - }, - } - }, - - transformError: (error) => { - const message = error.message || error.error?.message - const code = error.status || error.error?.status - return `${message} (${code})` - }, -} diff --git a/apps/sim/tools/huggingface/chat.ts b/apps/sim/tools/huggingface/chat.ts index 82438a4b1..dd6a01d92 100644 --- a/apps/sim/tools/huggingface/chat.ts +++ b/apps/sim/tools/huggingface/chat.ts @@ -13,46 +13,47 @@ export const chatTool: ToolConfig = { url: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'The URL to read and convert to markdown', }, useReaderLMv2: { type: 'boolean', + required: false, + visibility: 'user-only', description: 'Whether to use ReaderLM-v2 for better quality', }, gatherLinks: { type: 'boolean', + required: false, + visibility: 'user-only', description: 'Whether to gather all links at the end', }, jsonResponse: { type: 'boolean', + required: false, + visibility: 'user-only', description: 'Whether to return response in JSON format', }, apiKey: { type: 'string', - requiredForToolCall: true, + required: true, + visibility: 'user-only', description: 'Your Jina AI API key', }, }, diff --git a/apps/sim/tools/jira/bulk_read.ts b/apps/sim/tools/jira/bulk_read.ts index 2e3236207..5ee41cf17 100644 --- a/apps/sim/tools/jira/bulk_read.ts +++ b/apps/sim/tools/jira/bulk_read.ts @@ -15,22 +15,25 @@ export const jiraBulkRetrieveTool: ToolConfig = accessToken: { type: 'string', required: true, + visibility: 'hidden', description: 'OAuth access token for Jira', }, domain: { type: 'string', required: true, - requiredForToolCall: true, + visibility: 'user-only', description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', }, projectId: { type: 'string', required: false, + visibility: 'user-only', description: 'Jira project ID to update issues in. If not provided, all issues will be retrieved.', }, issueKey: { type: 'string', required: true, + visibility: 'user-only', description: 'Jira issue key to update', }, summary: { type: 'string', required: false, + visibility: 'user-or-llm', description: 'New summary for the issue', }, description: { type: 'string', required: false, + visibility: 'user-or-llm', description: 'New description for the issue', }, status: { type: 'string', required: false, + visibility: 'user-or-llm', description: 'New status for the issue', }, priority: { type: 'string', required: false, + visibility: 'user-or-llm', description: 'New priority for the issue', }, assignee: { type: 'string', required: false, + visibility: 'user-or-llm', description: 'New assignee for the issue', }, cloudId: { type: 'string', required: false, + visibility: 'user-only', description: 'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.', }, diff --git a/apps/sim/tools/jira/write.ts b/apps/sim/tools/jira/write.ts index 307319ef8..137864933 100644 --- a/apps/sim/tools/jira/write.ts +++ b/apps/sim/tools/jira/write.ts @@ -28,48 +28,56 @@ export const jiraWriteTool: ToolConfig = { accessToken: { type: 'string', required: true, + visibility: 'hidden', description: 'OAuth access token for Jira', }, domain: { type: 'string', required: true, - requiredForToolCall: true, + visibility: 'user-or-llm', description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', }, projectId: { type: 'string', required: true, + visibility: 'user-only', description: 'Project ID for the issue', }, summary: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Summary for the issue', }, description: { type: 'string', required: false, + visibility: 'user-or-llm', description: 'Description for the issue', }, priority: { type: 'string', required: false, + visibility: 'hidden', description: 'Priority for the issue', }, assignee: { type: 'string', required: false, + visibility: 'hidden', description: 'Assignee for the issue', }, cloudId: { type: 'string', required: false, + visibility: 'hidden', description: 'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.', }, issueType: { type: 'string', required: true, + visibility: 'hidden', description: 'Type of issue to create (e.g., Task, Story, Bug, Sub-task)', }, }, diff --git a/apps/sim/tools/linear/create_issue.ts b/apps/sim/tools/linear/create_issue.ts index 28ea96a87..722efc831 100644 --- a/apps/sim/tools/linear/create_issue.ts +++ b/apps/sim/tools/linear/create_issue.ts @@ -12,10 +12,30 @@ export const linearCreateIssueTool: ToolConfig) => { diff --git a/apps/sim/tools/mem0/search_memories.ts b/apps/sim/tools/mem0/search_memories.ts index 842d66e3e..40e0005d5 100644 --- a/apps/sim/tools/mem0/search_memories.ts +++ b/apps/sim/tools/mem0/search_memories.ts @@ -8,29 +8,31 @@ export const mem0SearchMemoriesTool: ToolConfig = { description: 'Search for memories in Mem0 using semantic search', version: '1.0.0', params: { - apiKey: { - type: 'string', - required: true, - requiredForToolCall: true, - description: 'Your Mem0 API key', - }, userId: { type: 'string', required: true, - requiredForToolCall: true, + visibility: 'user-only', description: 'User ID to search memories for', }, query: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Search query to find relevant memories', }, limit: { type: 'number', required: false, default: 10, + visibility: 'user-only', description: 'Maximum number of results to return', }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Mem0 API key', + }, }, request: { url: 'https://api.mem0.ai/v2/memories/search/', diff --git a/apps/sim/tools/microsoft_excel/read.ts b/apps/sim/tools/microsoft_excel/read.ts index 63a1575d9..7e4843e37 100644 --- a/apps/sim/tools/microsoft_excel/read.ts +++ b/apps/sim/tools/microsoft_excel/read.ts @@ -15,14 +15,21 @@ export const readTool: ToolConfig { diff --git a/apps/sim/tools/microsoft_excel/table_add.ts b/apps/sim/tools/microsoft_excel/table_add.ts index 2ad474fbb..0ff062601 100644 --- a/apps/sim/tools/microsoft_excel/table_add.ts +++ b/apps/sim/tools/microsoft_excel/table_add.ts @@ -18,21 +18,25 @@ export const tableAddTool: ToolConfig< accessToken: { type: 'string', required: true, + visibility: 'hidden', description: 'The access token for the Microsoft Excel API', }, spreadsheetId: { type: 'string', required: true, + visibility: 'user-only', description: 'The ID of the spreadsheet containing the table', }, tableName: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'The name of the table to add rows to', }, values: { type: 'array', required: true, + visibility: 'user-or-llm', description: 'The data to add to the table (array of arrays or array of objects)', }, }, diff --git a/apps/sim/tools/microsoft_excel/write.ts b/apps/sim/tools/microsoft_excel/write.ts index 83e74eed7..8a6e2046a 100644 --- a/apps/sim/tools/microsoft_excel/write.ts +++ b/apps/sim/tools/microsoft_excel/write.ts @@ -15,23 +15,37 @@ export const writeTool: ToolConfig = { id: 'microsoft_teams_read_chat', @@ -15,11 +15,13 @@ export const readChatTool: ToolConfig = { additionalScopes: ['workspace.content', 'page.read'], }, params: { - pageId: { - type: 'string', - required: true, - description: 'The ID of the Notion page to read', - }, accessToken: { type: 'string', required: true, + visibility: 'hidden', description: 'Notion OAuth access token', }, + pageId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the Notion page to read', + }, }, request: { diff --git a/apps/sim/tools/notion/update_page.ts b/apps/sim/tools/notion/update_page.ts index df532b75d..c0f004914 100644 --- a/apps/sim/tools/notion/update_page.ts +++ b/apps/sim/tools/notion/update_page.ts @@ -12,23 +12,24 @@ export const notionUpdatePageTool: ToolConfig = { additionalScopes: ['workspace.content', 'page.write'], }, params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Notion OAuth access token', + }, pageId: { type: 'string', required: true, - requiredForToolCall: true, + visibility: 'user-only', description: 'The ID of the Notion page to append content to', }, content: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'The content to append to the page', }, - accessToken: { - type: 'string', - required: true, - description: 'Notion OAuth access token', - }, }, request: { diff --git a/apps/sim/tools/openai/embeddings.ts b/apps/sim/tools/openai/embeddings.ts index be145aea4..ea4ee079b 100644 --- a/apps/sim/tools/openai/embeddings.ts +++ b/apps/sim/tools/openai/embeddings.ts @@ -8,28 +8,31 @@ export const embeddingsTool: ToolConfig = { version: '1.0', params: { - apiKey: { type: 'string', required: true, description: 'OpenAI API key' }, input: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Text to generate embeddings for', }, model: { type: 'string', required: false, + visibility: 'user-only', description: 'Model to use for embeddings', default: 'text-embedding-3-small', }, - encoding_format: { + encodingFormat: { type: 'string', required: false, + visibility: 'hidden', description: 'The format to return the embeddings in', default: 'float', }, - user: { + apiKey: { type: 'string', - required: false, - description: 'A unique identifier for the end-user', + required: true, + visibility: 'user-only', + description: 'OpenAI API key', }, }, @@ -43,8 +46,7 @@ export const embeddingsTool: ToolConfig = { body: (params) => ({ input: params.input, model: params.model || 'text-embedding-3-small', - encoding_format: params.encoding_format || 'float', - user: params.user, + encoding_format: params.encodingFormat || 'float', }), }, diff --git a/apps/sim/tools/openai/image.ts b/apps/sim/tools/openai/image.ts index 4a06aeafb..424e64ea4 100644 --- a/apps/sim/tools/openai/image.ts +++ b/apps/sim/tools/openai/image.ts @@ -11,44 +11,52 @@ export const imageTool: ToolConfig = { description: "Generate images using OpenAI's Image models", version: '1.0.0', params: { - prompt: { - type: 'string', - required: true, - description: 'A text description of the desired image', - }, model: { type: 'string', required: true, + visibility: 'user-only', description: 'The model to use (gpt-image-1 or dall-e-3)', }, + prompt: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'A text description of the desired image', + }, size: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'The size of the generated images (1024x1024, 1024x1792, or 1792x1024)', }, quality: { type: 'string', required: false, + visibility: 'user-or-llm', description: 'The quality of the image (standard or hd)', }, style: { type: 'string', required: false, + visibility: 'user-or-llm', description: 'The style of the image (vivid or natural)', }, background: { type: 'string', required: false, + visibility: 'user-or-llm', description: 'The background color, only for gpt-image-1', }, n: { type: 'number', required: false, + visibility: 'hidden', description: 'The number of images to generate (1-10)', }, apiKey: { type: 'string', required: true, + visibility: 'user-only', description: 'Your OpenAI API key', }, }, diff --git a/apps/sim/tools/openai/types.ts b/apps/sim/tools/openai/types.ts index 2ff4af6c4..db8f4b987 100644 --- a/apps/sim/tools/openai/types.ts +++ b/apps/sim/tools/openai/types.ts @@ -22,6 +22,6 @@ export interface OpenAIEmbeddingsParams { apiKey: string input: string | string[] model?: string - encoding_format?: 'float' | 'base64' + encodingFormat?: 'float' | 'base64' user?: string } diff --git a/apps/sim/tools/outlook/draft.ts b/apps/sim/tools/outlook/draft.ts index 677ef488f..fc4253d01 100644 --- a/apps/sim/tools/outlook/draft.ts +++ b/apps/sim/tools/outlook/draft.ts @@ -16,21 +16,25 @@ export const outlookDraftTool: ToolConfig accessToken: { type: 'string', required: true, + visibility: 'hidden', description: 'OAuth access token for Outlook', }, folder: { type: 'string', required: false, + visibility: 'user-only', description: 'Folder ID to read emails from (default: Inbox)', }, maxResults: { type: 'number', required: false, + visibility: 'user-only', description: 'Maximum number of emails to retrieve (default: 1, max: 10)', }, }, diff --git a/apps/sim/tools/outlook/send.ts b/apps/sim/tools/outlook/send.ts index f7ed64325..6c1234863 100644 --- a/apps/sim/tools/outlook/send.ts +++ b/apps/sim/tools/outlook/send.ts @@ -16,21 +16,25 @@ export const outlookSendTool: ToolConfig accessToken: { type: 'string', required: true, + visibility: 'hidden', description: 'Access token for Outlook API', }, to: { type: 'string', required: true, + visibility: 'user-only', description: 'Recipient email address', }, subject: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Email subject', }, body: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Email body content', }, }, diff --git a/apps/sim/tools/params.test.ts b/apps/sim/tools/params.test.ts new file mode 100644 index 000000000..9e3839746 --- /dev/null +++ b/apps/sim/tools/params.test.ts @@ -0,0 +1,273 @@ +import { describe, expect, it, vi } from 'vitest' +import { + createExecutionToolSchema, + createLLMToolSchema, + filterSchemaForLLM, + formatParameterLabel, + getToolParametersConfig, + isPasswordParameter, + mergeToolParameters, + type ToolParameterConfig, + type ToolSchema, + type ValidationResult, + validateToolParameters, +} from './params' +import type { ParameterVisibility } from './types' + +const mockToolConfig = { + id: 'test_tool', + name: 'Test Tool', + description: 'A test tool for parameter handling', + version: '1.0.0', + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only' as ParameterVisibility, + description: 'API key for authentication', + }, + message: { + type: 'string', + required: true, + visibility: 'user-or-llm' as ParameterVisibility, + description: 'Message to send', + }, + channel: { + type: 'string', + required: false, + visibility: 'user-only' as ParameterVisibility, + description: 'Channel to send message to', + }, + timeout: { + type: 'number', + required: false, + visibility: 'user-only' as ParameterVisibility, + default: 5000, + description: 'Request timeout in milliseconds', + }, + }, + request: { + url: 'https://api.example.com/test', + method: 'POST', + headers: () => ({}), + }, +} + +vi.mock('./utils', () => ({ + getTool: vi.fn((toolId: string) => { + if (toolId === 'test_tool') { + return mockToolConfig + } + return null + }), +})) + +describe('Tool Parameters Utils', () => { + describe('getToolParametersConfig', () => { + it.concurrent('should return tool parameters configuration', () => { + const result = getToolParametersConfig('test_tool') + + expect(result).toBeDefined() + expect(result?.toolConfig).toEqual(mockToolConfig) + expect(result?.allParameters).toHaveLength(4) + expect(result?.userInputParameters).toHaveLength(4) // apiKey, message, channel, timeout (all have visibility) + expect(result?.requiredParameters).toHaveLength(2) // apiKey, message (both required: true) + expect(result?.optionalParameters).toHaveLength(2) // channel, timeout (both user-only + required: false) + }) + + it.concurrent('should return null for non-existent tool', () => { + const result = getToolParametersConfig('non_existent_tool') + expect(result).toBeNull() + }) + }) + + describe('createLLMToolSchema', () => { + it.concurrent('should create schema excluding user-provided parameters', () => { + const userProvidedParams = { + apiKey: 'user-provided-key', + channel: '#general', + } + + const schema = createLLMToolSchema(mockToolConfig, userProvidedParams) + + expect(schema.properties).not.toHaveProperty('apiKey') // user-only, excluded + expect(schema.properties).not.toHaveProperty('channel') // user-provided, excluded + expect(schema.properties).toHaveProperty('message') // user-or-llm, included + expect(schema.properties).not.toHaveProperty('timeout') // user-only, excluded + expect(schema.required).toContain('message') // user-or-llm + required: true + expect(schema.required).not.toContain('apiKey') // user-only, never required for LLM + }) + + it.concurrent('should include all parameters when none are user-provided', () => { + const schema = createLLMToolSchema(mockToolConfig, {}) + + expect(schema.properties).not.toHaveProperty('apiKey') // user-only, never shown to LLM + expect(schema.properties).toHaveProperty('message') // user-or-llm, shown to LLM + expect(schema.properties).not.toHaveProperty('channel') // user-only, never shown to LLM + expect(schema.properties).not.toHaveProperty('timeout') // user-only, never shown to LLM + expect(schema.required).not.toContain('apiKey') // user-only, never required for LLM + expect(schema.required).toContain('message') // user-or-llm + required: true + }) + }) + + describe('createExecutionToolSchema', () => { + it.concurrent('should create complete schema with all parameters', () => { + const schema = createExecutionToolSchema(mockToolConfig) + + expect(schema.properties).toHaveProperty('apiKey') + expect(schema.properties).toHaveProperty('message') + expect(schema.properties).toHaveProperty('channel') + expect(schema.properties).toHaveProperty('timeout') + expect(schema.required).toContain('apiKey') + expect(schema.required).toContain('message') + expect(schema.required).not.toContain('channel') + expect(schema.required).not.toContain('timeout') + }) + }) + + describe('mergeToolParameters', () => { + it.concurrent('should merge parameters with user-provided taking precedence', () => { + const userProvided = { + apiKey: 'user-key', + channel: '#general', + } + const llmGenerated = { + message: 'Hello world', + channel: '#random', + timeout: 10000, + } + + const merged = mergeToolParameters(userProvided, llmGenerated) + + expect(merged.apiKey).toBe('user-key') + expect(merged.channel).toBe('#general') + expect(merged.message).toBe('Hello world') + expect(merged.timeout).toBe(10000) + }) + }) + + describe('validateToolParameters', () => { + it.concurrent('should validate successfully with all required parameters', () => { + const finalParams = { + apiKey: 'test-key', + message: 'Hello world', + channel: '#general', + } + + const result = validateToolParameters(mockToolConfig, finalParams) + + expect(result.valid).toBe(true) + expect(result.missingParams).toHaveLength(0) + }) + + it.concurrent('should fail validation with missing required parameters', () => { + const finalParams = { + channel: '#general', + } + + const result = validateToolParameters(mockToolConfig, finalParams) + + expect(result.valid).toBe(false) + expect(result.missingParams).toContain('apiKey') + expect(result.missingParams).toContain('message') + }) + }) + + describe('filterSchemaForLLM', () => { + it.concurrent('should filter out user-provided parameters from schema', () => { + const originalSchema: ToolSchema = { + type: 'object' as const, + properties: { + apiKey: { type: 'string', description: 'API key' }, + message: { type: 'string', description: 'Message' }, + channel: { type: 'string', description: 'Channel' }, + }, + required: ['apiKey', 'message'], + } + + const userProvidedParams = { + apiKey: 'user-key', + channel: '#general', + } + + const filtered = filterSchemaForLLM(originalSchema, userProvidedParams) + + expect(filtered.properties).not.toHaveProperty('apiKey') + expect(filtered.properties).not.toHaveProperty('channel') + expect(filtered.properties).toHaveProperty('message') + expect(filtered.required).not.toContain('apiKey') + expect(filtered.required).toContain('message') + }) + }) + + describe('formatParameterLabel', () => { + it.concurrent('should format parameter labels correctly', () => { + expect(formatParameterLabel('apiKey')).toBe('API Key') + expect(formatParameterLabel('apiVersion')).toBe('API Version') + expect(formatParameterLabel('userName')).toBe('User Name') + expect(formatParameterLabel('user_name')).toBe('User Name') + expect(formatParameterLabel('user-name')).toBe('User Name') + expect(formatParameterLabel('message')).toBe('Message') + expect(formatParameterLabel('a')).toBe('A') + }) + }) + + describe('isPasswordParameter', () => { + it.concurrent('should identify password parameters correctly', () => { + expect(isPasswordParameter('password')).toBe(true) + expect(isPasswordParameter('apiKey')).toBe(true) + expect(isPasswordParameter('token')).toBe(true) + expect(isPasswordParameter('secret')).toBe(true) + expect(isPasswordParameter('accessToken')).toBe(true) + expect(isPasswordParameter('message')).toBe(false) + expect(isPasswordParameter('channel')).toBe(false) + expect(isPasswordParameter('timeout')).toBe(false) + }) + }) + + describe('Type Interface Validation', () => { + it.concurrent('should have properly typed ToolSchema', () => { + const schema: ToolSchema = createLLMToolSchema(mockToolConfig, {}) + + expect(schema.type).toBe('object') + expect(typeof schema.properties).toBe('object') + expect(Array.isArray(schema.required)).toBe(true) + + // Verify properties have correct structure + Object.values(schema.properties).forEach((prop) => { + expect(prop).toHaveProperty('type') + expect(prop).toHaveProperty('description') + expect(typeof prop.type).toBe('string') + expect(typeof prop.description).toBe('string') + }) + }) + + it.concurrent('should have properly typed ValidationResult', () => { + const result: ValidationResult = validateToolParameters(mockToolConfig, {}) + + expect(typeof result.valid).toBe('boolean') + expect(Array.isArray(result.missingParams)).toBe(true) + expect(result.missingParams.every((param) => typeof param === 'string')).toBe(true) + }) + + it.concurrent('should have properly typed ToolParameterConfig', () => { + const config = getToolParametersConfig('test_tool') + expect(config).toBeDefined() + + if (config) { + config.allParameters.forEach((param: ToolParameterConfig) => { + expect(typeof param.id).toBe('string') + expect(typeof param.type).toBe('string') + expect(typeof param.required).toBe('boolean') + expect( + ['user-or-llm', 'user-only', 'llm-only', 'hidden'].includes(param.visibility!) + ).toBe(true) + if (param.description) expect(typeof param.description).toBe('string') + if (param.uiComponent) { + expect(typeof param.uiComponent.type).toBe('string') + } + }) + } + }) + }) +}) diff --git a/apps/sim/tools/params.ts b/apps/sim/tools/params.ts new file mode 100644 index 000000000..4d2be6cb4 --- /dev/null +++ b/apps/sim/tools/params.ts @@ -0,0 +1,468 @@ +import type { ParameterVisibility, ToolConfig } from './types' +import { getTool } from './utils' + +export interface Option { + label: string + value: string +} + +export interface ComponentCondition { + field: string + value: string +} + +export interface UIComponentConfig { + type: string + options?: Option[] + placeholder?: string + password?: boolean + condition?: ComponentCondition + title?: string + layout?: string + value?: unknown + provider?: string + serviceId?: string + requiredScopes?: string[] + mimeType?: string + columns?: string[] + min?: number + max?: number + step?: number + integer?: boolean + language?: string + generationType?: string + acceptedTypes?: string[] + multiple?: boolean + maxSize?: number +} + +export interface SubBlockConfig { + id: string + type: string + title?: string + options?: Option[] + placeholder?: string + password?: boolean + condition?: ComponentCondition + layout?: string + value?: unknown + provider?: string + serviceId?: string + requiredScopes?: string[] + mimeType?: string + columns?: string[] + min?: number + max?: number + step?: number + integer?: boolean + language?: string + generationType?: string + acceptedTypes?: string[] + multiple?: boolean + maxSize?: number +} + +export interface BlockConfig { + type: string + subBlocks?: SubBlockConfig[] +} + +export interface SchemaProperty { + type: string + description: string +} + +export interface ToolSchema { + type: 'object' + properties: Record + required: string[] +} + +export interface ValidationResult { + valid: boolean + missingParams: string[] +} + +export interface ToolParameterConfig { + id: string + type: string + required?: boolean // Required for tool execution + visibility?: ParameterVisibility // Controls who can/must provide this parameter + userProvided?: boolean // User filled this parameter + description?: string + default?: unknown + // UI component information from block config + uiComponent?: UIComponentConfig +} + +export interface ToolWithParameters { + toolConfig: ToolConfig + allParameters: ToolParameterConfig[] + userInputParameters: ToolParameterConfig[] // Parameters shown to user + requiredParameters: ToolParameterConfig[] // Must be filled by user or LLM + optionalParameters: ToolParameterConfig[] // Nice to have, shown to user +} + +let blockConfigCache: Record | null = null + +function getBlockConfigurations(): Record { + if (!blockConfigCache) { + try { + const { getAllBlocks } = require('../blocks') + const allBlocks = getAllBlocks() + blockConfigCache = {} + allBlocks.forEach((block: BlockConfig) => { + blockConfigCache![block.type] = block + }) + } catch (error) { + console.warn('Could not load block configuration:', error) + blockConfigCache = {} + } + } + return blockConfigCache +} + +/** + * Gets all parameters for a tool, categorized by their usage + * Also includes UI component information from block configurations + */ +export function getToolParametersConfig( + toolId: string, + blockType?: string +): ToolWithParameters | null { + const toolConfig = getTool(toolId) + if (!toolConfig) { + return null + } + + // Get block configuration for UI component information + let blockConfig: BlockConfig | null = null + if (blockType) { + const blockConfigs = getBlockConfigurations() + blockConfig = blockConfigs[blockType] || null + } + + // Convert tool params to our standard format with UI component info + const allParameters: ToolParameterConfig[] = Object.entries(toolConfig.params).map( + ([paramId, param]) => { + const toolParam: ToolParameterConfig = { + id: paramId, + type: param.type, + required: param.required ?? false, + visibility: param.visibility ?? (param.required ? 'user-or-llm' : 'user-only'), + description: param.description, + default: param.default, + } + + // Add UI component information from block config if available + if (blockConfig) { + // For multi-operation tools, find the subblock that matches both the parameter ID + // and the current tool operation + let subBlock = blockConfig.subBlocks?.find((sb: SubBlockConfig) => { + if (sb.id !== paramId) return false + + // If there's a condition, check if it matches the current tool + if (sb.condition && sb.condition.field === 'operation') { + // First try exact match with full tool ID + if (sb.condition.value === toolId) return true + + // Then try extracting operation from tool ID + // For tools like 'google_calendar_quick_add', extract 'quick_add' + const parts = toolId.split('_') + if (parts.length >= 3) { + // Join everything after the provider prefix (e.g., 'google_calendar_') + const operation = parts.slice(2).join('_') + if (sb.condition.value === operation) return true + } + + // Fallback to last part only + const operation = parts[parts.length - 1] + return sb.condition.value === operation + } + + // If no condition, it's a global parameter (like apiKey) + return !sb.condition + }) + + // Fallback: if no operation-specific match, find any matching parameter + if (!subBlock) { + subBlock = blockConfig.subBlocks?.find((sb: SubBlockConfig) => sb.id === paramId) + } + + // Special case: Check if this boolean parameter is part of a checkbox-list + if (!subBlock && param.type === 'boolean' && blockConfig) { + // Look for a checkbox-list that includes this parameter as an option + const checkboxListBlock = blockConfig.subBlocks?.find( + (sb: SubBlockConfig) => + sb.type === 'checkbox-list' && + Array.isArray(sb.options) && + sb.options.some((opt: any) => opt.id === paramId) + ) + + if (checkboxListBlock) { + subBlock = checkboxListBlock + } + } + + if (subBlock) { + toolParam.uiComponent = { + type: subBlock.type, + options: subBlock.options, + placeholder: subBlock.placeholder, + password: subBlock.password, + condition: subBlock.condition, + title: subBlock.title, + layout: subBlock.layout, + value: subBlock.value, + provider: subBlock.provider, + serviceId: subBlock.serviceId, + requiredScopes: subBlock.requiredScopes, + mimeType: subBlock.mimeType, + columns: subBlock.columns, + min: subBlock.min, + max: subBlock.max, + step: subBlock.step, + integer: subBlock.integer, + language: subBlock.language, + generationType: subBlock.generationType, + acceptedTypes: subBlock.acceptedTypes, + multiple: subBlock.multiple, + maxSize: subBlock.maxSize, + } + } + } + + return toolParam + } + ) + + // Parameters that should be shown to the user for input + const userInputParameters = allParameters.filter( + (param) => param.visibility === 'user-or-llm' || param.visibility === 'user-only' + ) + + // Parameters that are required (must be filled by user or LLM) + const requiredParameters = allParameters.filter((param) => param.required) + + // Parameters that are optional but can be provided by user + const optionalParameters = allParameters.filter( + (param) => param.visibility === 'user-only' && !param.required + ) + + return { + toolConfig, + allParameters, + userInputParameters, + requiredParameters, + optionalParameters, + } +} + +/** + * Creates a tool schema for LLM with user-provided parameters excluded + */ +export function createLLMToolSchema( + toolConfig: ToolConfig, + userProvidedParams: Record +): ToolSchema { + const schema: ToolSchema = { + type: 'object', + properties: {}, + required: [], + } + + // Only include parameters that the LLM should/can provide + Object.entries(toolConfig.params).forEach(([paramId, param]) => { + const isUserProvided = + userProvidedParams[paramId] !== undefined && + userProvidedParams[paramId] !== null && + userProvidedParams[paramId] !== '' + + // Skip parameters that user has already provided + if (isUserProvided) { + return + } + + // Skip parameters that are user-only (never shown to LLM) + if (param.visibility === 'user-only') { + return + } + + // Skip hidden parameters + if (param.visibility === 'hidden') { + return + } + + // Add parameter to LLM schema + schema.properties[paramId] = { + type: param.type === 'json' ? 'object' : param.type, + description: param.description || '', + } + + // Add to required if LLM must provide it and it's originally required + if ((param.visibility === 'user-or-llm' || param.visibility === 'llm-only') && param.required) { + schema.required.push(paramId) + } + }) + + return schema +} + +/** + * Creates a complete tool schema for execution with all parameters + */ +export function createExecutionToolSchema(toolConfig: ToolConfig): ToolSchema { + const schema: ToolSchema = { + type: 'object', + properties: {}, + required: [], + } + + Object.entries(toolConfig.params).forEach(([paramId, param]) => { + schema.properties[paramId] = { + type: param.type === 'json' ? 'object' : param.type, + description: param.description || '', + } + + if (param.required) { + schema.required.push(paramId) + } + }) + + return schema +} + +/** + * Merges user-provided parameters with LLM-generated parameters + */ +export function mergeToolParameters( + userProvidedParams: Record, + llmGeneratedParams: Record +): Record { + // User-provided parameters take precedence + return { + ...llmGeneratedParams, + ...userProvidedParams, + } +} + +/** + * Filters out user-provided parameters from tool schema for LLM + */ +export function filterSchemaForLLM( + originalSchema: ToolSchema, + userProvidedParams: Record +): ToolSchema { + if (!originalSchema || !originalSchema.properties) { + return originalSchema + } + + const filteredProperties = { ...originalSchema.properties } + const filteredRequired = [...(originalSchema.required || [])] + + // Remove user-provided parameters from the schema + Object.keys(userProvidedParams).forEach((paramKey) => { + if ( + userProvidedParams[paramKey] !== undefined && + userProvidedParams[paramKey] !== null && + userProvidedParams[paramKey] !== '' + ) { + delete filteredProperties[paramKey] + const reqIndex = filteredRequired.indexOf(paramKey) + if (reqIndex > -1) { + filteredRequired.splice(reqIndex, 1) + } + } + }) + + return { + ...originalSchema, + properties: filteredProperties, + required: filteredRequired, + } +} + +/** + * Validates that all required parameters are provided + */ +export function validateToolParameters( + toolConfig: ToolConfig, + finalParams: Record +): ValidationResult { + const requiredParams = Object.entries(toolConfig.params) + .filter(([_, param]) => param.required) + .map(([paramId]) => paramId) + + const missingParams = requiredParams.filter( + (paramId) => + finalParams[paramId] === undefined || + finalParams[paramId] === null || + finalParams[paramId] === '' + ) + + return { + valid: missingParams.length === 0, + missingParams, + } +} + +/** + * Helper to check if a parameter should be treated as a password field + */ +export function isPasswordParameter(paramId: string): boolean { + const passwordFields = [ + 'password', + 'apiKey', + 'token', + 'secret', + 'key', + 'credential', + 'accessToken', + 'refreshToken', + 'botToken', + 'authToken', + ] + + return passwordFields.some((field) => paramId.toLowerCase().includes(field.toLowerCase())) +} + +/** + * Formats parameter IDs into human-readable labels + */ +export function formatParameterLabel(paramId: string): string { + // Special cases + if (paramId === 'apiKey') return 'API Key' + if (paramId === 'apiVersion') return 'API Version' + if (paramId === 'accessToken') return 'Access Token' + if (paramId === 'refreshToken') return 'Refresh Token' + if (paramId === 'botToken') return 'Bot Token' + + // Handle underscore and hyphen separated words + if (paramId.includes('_') || paramId.includes('-')) { + return paramId + .split(/[-_]/) + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' ') + } + + // Handle single character parameters + if (paramId.length === 1) return paramId.toUpperCase() + + // Handle camelCase + if (/[A-Z]/.test(paramId)) { + const result = paramId.replace(/([A-Z])/g, ' $1') + return ( + result.charAt(0).toUpperCase() + + result + .slice(1) + .replace(/ Api/g, ' API') + .replace(/ Id/g, ' ID') + .replace(/ Url/g, ' URL') + .replace(/ Uri/g, ' URI') + .replace(/ Ui/g, ' UI') + ) + } + + // Simple case - just capitalize first letter + return paramId.charAt(0).toUpperCase() + paramId.slice(1) +} diff --git a/apps/sim/tools/perplexity/chat.ts b/apps/sim/tools/perplexity/chat.ts index d066fb857..5528780ca 100644 --- a/apps/sim/tools/perplexity/chat.ts +++ b/apps/sim/tools/perplexity/chat.ts @@ -8,32 +8,42 @@ export const chatTool: ToolConfig version: '1.0', params: { - apiKey: { + systemPrompt: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'System prompt to guide the model behavior', + }, + content: { type: 'string', required: true, - requiredForToolCall: true, - description: 'Perplexity API key', + visibility: 'user-or-llm', + description: 'The user message content to send to the model', }, model: { type: 'string', required: true, + visibility: 'user-only', description: 'Model to use for chat completions (e.g., sonar, mistral)', }, - messages: { - type: 'array', - required: true, - description: 'Array of message objects with role and content', - }, max_tokens: { type: 'number', required: false, + visibility: 'user-only', description: 'Maximum number of tokens to generate', }, temperature: { type: 'number', required: false, + visibility: 'user-only', description: 'Sampling temperature between 0 and 1', }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Perplexity API key', + }, }, request: { @@ -44,53 +54,41 @@ export const chatTool: ToolConfig 'Content-Type': 'application/json', }), body: (params) => { - let messages = params.messages + const messages: Array<{ role: string; content: string }> = [] - if (!messages && (params.prompt || params.system)) { - messages = [] - - // Add system message if provided - if (params.system && typeof params.system === 'string' && params.system.trim() !== '') { - messages.push({ - role: 'system', - content: params.system, - }) - } - - // Add user message - if (params.prompt && typeof params.prompt === 'string' && params.prompt.trim() !== '') { - messages.push({ - role: 'user', - content: params.prompt, - }) - } + // Add system prompt if provided + if (params.systemPrompt) { + messages.push({ + role: 'system', + content: params.systemPrompt, + }) } - // Validate that each message has role and content - for (const msg of messages!) { - if (!msg.role || !msg.content) { - throw new Error('Each message must have role and content properties') - } - } + // Add user message + messages.push({ + role: 'user', + content: params.content, + }) const body: Record = { model: params.model, messages: messages, } + // Add optional parameters if provided if (params.max_tokens !== undefined) { - body.max_tokens = params.max_tokens + body.max_tokens = Number(params.max_tokens) || 10000 } if (params.temperature !== undefined) { - body.temperature = params.temperature + body.temperature = Number(params.temperature) } return body }, }, - transformResponse: async (response, params) => { + transformResponse: async (response) => { try { // Check if the response was successful if (!response.ok) { diff --git a/apps/sim/tools/perplexity/types.ts b/apps/sim/tools/perplexity/types.ts index 454a10027..2bcdec6e8 100644 --- a/apps/sim/tools/perplexity/types.ts +++ b/apps/sim/tools/perplexity/types.ts @@ -6,14 +6,12 @@ export interface PerplexityMessage { } export interface PerplexityChatParams { - apiKey: string + systemPrompt?: string + content: string model: string - messages?: PerplexityMessage[] max_tokens?: number temperature?: number - - prompt?: string - system?: string + apiKey: string } export interface PerplexityChatResponse extends ToolResponse { diff --git a/apps/sim/tools/pinecone/fetch.ts b/apps/sim/tools/pinecone/fetch.ts index 1f8aeeb26..2a32a7a84 100644 --- a/apps/sim/tools/pinecone/fetch.ts +++ b/apps/sim/tools/pinecone/fetch.ts @@ -8,29 +8,30 @@ export const fetchTool: ToolConfig = { version: '1.0', params: { - apiKey: { - type: 'string', - required: true, - requiredForToolCall: true, - description: 'Pinecone API key', - }, indexHost: { type: 'string', required: true, - requiredForToolCall: true, + visibility: 'user-only', description: 'Full Pinecone index host URL', }, ids: { type: 'array', required: true, - requiredForToolCall: true, + visibility: 'user-only', description: 'Array of vector IDs to fetch', }, namespace: { type: 'string', required: false, + visibility: 'user-only', description: 'Namespace to fetch vectors from', }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Pinecone API key', + }, }, request: { diff --git a/apps/sim/tools/pinecone/generate_embeddings.ts b/apps/sim/tools/pinecone/generate_embeddings.ts index 1b2584c79..913581c5c 100644 --- a/apps/sim/tools/pinecone/generate_embeddings.ts +++ b/apps/sim/tools/pinecone/generate_embeddings.ts @@ -11,22 +11,24 @@ export const generateEmbeddingsTool: ToolConfig< version: '1.0', params: { - apiKey: { - type: 'string', - required: true, - requiredForToolCall: true, - description: 'Pinecone API key', - }, model: { type: 'string', required: true, + visibility: 'user-only', description: 'Model to use for generating embeddings', }, inputs: { type: 'array', required: true, + visibility: 'user-or-llm', description: 'Array of text inputs to generate embeddings for', }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Pinecone API key', + }, }, request: { diff --git a/apps/sim/tools/pinecone/search_text.ts b/apps/sim/tools/pinecone/search_text.ts index 503139d16..a8144d222 100644 --- a/apps/sim/tools/pinecone/search_text.ts +++ b/apps/sim/tools/pinecone/search_text.ts @@ -8,48 +8,54 @@ export const searchTextTool: ToolConfig = }, params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Reddit API', + }, subreddit: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'The name of the subreddit to fetch posts from (without the r/ prefix)', }, sort: { type: 'string', required: false, + visibility: 'user-or-llm', description: 'Sort method for posts: "hot", "new", "top", or "rising" (default: "hot")', }, limit: { type: 'number', required: false, + visibility: 'user-only', description: 'Maximum number of posts to return (default: 10, max: 100)', }, time: { type: 'string', required: false, + visibility: 'user-or-llm', description: 'Time filter for "top" sorted posts: "day", "week", "month", "year", or "all" (default: "day")', }, diff --git a/apps/sim/tools/reddit/hot_posts.ts b/apps/sim/tools/reddit/hot_posts.ts index af49ec92a..0453bb4eb 100644 --- a/apps/sim/tools/reddit/hot_posts.ts +++ b/apps/sim/tools/reddit/hot_posts.ts @@ -20,14 +20,22 @@ export const hotPostsTool: ToolConfig = }, params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Reddit API', + }, subreddit: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'The name of the subreddit to fetch posts from (without the r/ prefix)', }, limit: { type: 'number', required: false, + visibility: 'user-only', description: 'Maximum number of posts to return (default: 10, max: 100)', }, }, diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 258c707df..245ddc8ae 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -4,7 +4,6 @@ import { airtableListRecordsTool, airtableUpdateRecordTool, } from './airtable' -import { autoblocksPromptManagerTool } from './autoblocks' import { browserUseRunTaskTool } from './browser_use' import { clayPopulateTool } from './clay' import { confluenceRetrieveTool, confluenceUpdateTool } from './confluence' @@ -47,9 +46,7 @@ import { googleSheetsUpdateTool, googleSheetsWriteTool, } from './google_sheets' -import { guestyGuestTool, guestyReservationTool } from './guesty' import { requestTool as httpRequest } from './http' -import { contactsTool as hubspotContacts } from './hubspot/contacts' import { huggingfaceChatTool } from './huggingface' import { readUrlTool } from './jina' import { jiraBulkRetrieveTool, jiraRetrieveTool, jiraUpdateTool, jiraWriteTool } from './jira' @@ -74,7 +71,7 @@ import { microsoftTeamsWriteChatTool, } from './microsoft_teams' import { mistralParserTool } from './mistral' -import { notionReadTool, notionWriteTool } from './notion' +import { notionCreatePageTool, notionReadTool, notionWriteTool } from './notion' import { imageTool, embeddingsTool as openAIEmbeddings } from './openai' import { outlookDraftTool, outlookReadTool, outlookSendTool } from './outlook' import { perplexityChatTool } from './perplexity' @@ -87,7 +84,6 @@ import { } from './pinecone' import { redditGetCommentsTool, redditGetPostsTool, redditHotPostsTool } from './reddit' import { s3GetObjectTool } from './s3' -import { opportunitiesTool as salesforceOpportunities } from './salesforce/opportunities' import { searchTool as serperSearch } from './serper' import { slackMessageTool } from './slack' import { stagehandAgentTool, stagehandExtractTool } from './stagehand' @@ -107,12 +103,9 @@ import { youtubeSearchTool } from './youtube' // Registry of all available tools export const tools: Record = { browser_use_run_task: browserUseRunTaskTool, - autoblocks_prompt_manager: autoblocksPromptManagerTool, openai_embeddings: openAIEmbeddings, http_request: httpRequest, huggingface_chat: huggingfaceChatTool, - hubspot_contacts: hubspotContacts, - salesforce_opportunities: salesforceOpportunities, function_execute: functionExecuteTool, vision_tool: visionTool, file_parser: fileParseTool, @@ -139,6 +132,7 @@ export const tools: Record = { youtube_search: youtubeSearchTool, notion_read: notionReadTool, notion_write: notionWriteTool, + notion_create_page: notionCreatePageTool, gmail_send: gmailSendTool, gmail_read: gmailReadTool, gmail_search: gmailSearchTool, @@ -173,8 +167,6 @@ export const tools: Record = { google_sheets_write: googleSheetsWriteTool, google_sheets_update: googleSheetsUpdateTool, google_sheets_append: googleSheetsAppendTool, - guesty_reservation: guestyReservationTool, - guesty_guest: guestyGuestTool, perplexity_chat: perplexityChatTool, confluence_retrieve: confluenceRetrieveTool, confluence_update: confluenceUpdateTool, diff --git a/apps/sim/tools/s3/get_object.ts b/apps/sim/tools/s3/get_object.ts index 679a7772b..57113cc7f 100644 --- a/apps/sim/tools/s3/get_object.ts +++ b/apps/sim/tools/s3/get_object.ts @@ -1,83 +1,7 @@ import crypto from 'crypto' import type { ToolConfig } from '../types' +import { encodeS3PathComponent, generatePresignedUrl, getSignatureKey, parseS3Uri } from './utils' -// Function to encode S3 path components -function encodeS3PathComponent(pathComponent: string): string { - return encodeURIComponent(pathComponent).replace(/%2F/g, '/') -} - -// Function to calculate AWS signature key -function getSignatureKey( - key: string, - dateStamp: string, - regionName: string, - serviceName: string -): Buffer { - if (!key || typeof key !== 'string') { - throw new Error('Invalid key provided to getSignatureKey') - } - const kDate = crypto.createHmac('sha256', `AWS4${key}`).update(dateStamp).digest() - const kRegion = crypto.createHmac('sha256', kDate).update(regionName).digest() - const kService = crypto.createHmac('sha256', kRegion).update(serviceName).digest() - const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest() - return kSigning -} - -function parseS3Uri(s3Uri: string): { bucketName: string; region: string; objectKey: string } { - try { - const url = new URL(s3Uri) - const hostname = url.hostname - const bucketName = hostname.split('.')[0] - const regionMatch = hostname.match(/s3[.-]([^.]+)\.amazonaws\.com/) - const region = regionMatch ? regionMatch[1] : 'us-east-1' - const objectKey = url.pathname.startsWith('/') ? url.pathname.substring(1) : url.pathname - - if (!bucketName || !objectKey) { - throw new Error('Invalid S3 URI format') - } - - return { bucketName, region, objectKey } - } catch (_error) { - throw new Error( - 'Invalid S3 Object URL format. Expected format: https://bucket-name.s3.region.amazonaws.com/path/to/file' - ) - } -} - -// Function to generate a pre-signed URL -function generatePresignedUrl(params: any, expiresIn = 3600): string { - const date = new Date() - const amzDate = date.toISOString().replace(/[:-]|\.\d{3}/g, '') - const dateStamp = amzDate.slice(0, 8) - const encodedPath = encodeS3PathComponent(params.objectKey) - - // Set expiration time - const _expires = Math.floor(Date.now() / 1000) + expiresIn - - // Create the canonical request - const method = 'GET' - const canonicalUri = `/${encodedPath}` - const canonicalQueryString = `X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=${encodeURIComponent(`${params.accessKeyId}/${dateStamp}/${params.region}/s3/aws4_request`)}&X-Amz-Date=${amzDate}&X-Amz-Expires=${expiresIn}&X-Amz-SignedHeaders=host` - const canonicalHeaders = `host:${params.bucketName}.s3.${params.region}.amazonaws.com\n` - const signedHeaders = 'host' - const payloadHash = 'UNSIGNED-PAYLOAD' - - const canonicalRequest = `${method}\n${canonicalUri}\n${canonicalQueryString}\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}` - - // Create string to sign - const algorithm = 'AWS4-HMAC-SHA256' - const credentialScope = `${dateStamp}/${params.region}/s3/aws4_request` - const stringToSign = `${algorithm}\n${amzDate}\n${credentialScope}\n${crypto.createHash('sha256').update(canonicalRequest).digest('hex')}` - - // Calculate signature - const signingKey = getSignatureKey(params.secretAccessKey, dateStamp, params.region, 's3') - const signature = crypto.createHmac('sha256', signingKey).update(stringToSign).digest('hex') - - // Create signed URL - return `https://${params.bucketName}.s3.${params.region}.amazonaws.com/${encodedPath}?${canonicalQueryString}&X-Amz-Signature=${signature}` -} - -// Get Object Tool export const s3GetObjectTool: ToolConfig = { id: 's3_get_object', name: 'S3 Get Object', @@ -87,16 +11,19 @@ export const s3GetObjectTool: ToolConfig = { accessKeyId: { type: 'string', required: true, + visibility: 'user-only', description: 'Your AWS Access Key ID', }, secretAccessKey: { type: 'string', required: true, + visibility: 'user-only', description: 'Your AWS Secret Access Key', }, s3Uri: { type: 'string', required: true, + visibility: 'user-only', description: 'S3 Object URL (e.g., https://bucket-name.s3.region.amazonaws.com/path/to/file)', }, }, diff --git a/apps/sim/tools/s3/utils.ts b/apps/sim/tools/s3/utils.ts new file mode 100644 index 000000000..a0815a878 --- /dev/null +++ b/apps/sim/tools/s3/utils.ts @@ -0,0 +1,73 @@ +import crypto from 'crypto' + +export function encodeS3PathComponent(pathComponent: string): string { + return encodeURIComponent(pathComponent).replace(/%2F/g, '/') +} + +export function getSignatureKey( + key: string, + dateStamp: string, + regionName: string, + serviceName: string +): Buffer { + if (!key || typeof key !== 'string') { + throw new Error('Invalid key provided to getSignatureKey') + } + const kDate = crypto.createHmac('sha256', `AWS4${key}`).update(dateStamp).digest() + const kRegion = crypto.createHmac('sha256', kDate).update(regionName).digest() + const kService = crypto.createHmac('sha256', kRegion).update(serviceName).digest() + const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest() + return kSigning +} + +export function parseS3Uri(s3Uri: string): { + bucketName: string + region: string + objectKey: string +} { + try { + const url = new URL(s3Uri) + const hostname = url.hostname + const bucketName = hostname.split('.')[0] + const regionMatch = hostname.match(/s3[.-]([^.]+)\.amazonaws\.com/) + const region = regionMatch ? regionMatch[1] : 'us-east-1' + const objectKey = url.pathname.startsWith('/') ? url.pathname.substring(1) : url.pathname + + if (!bucketName || !objectKey) { + throw new Error('Invalid S3 URI format') + } + + return { bucketName, region, objectKey } + } catch (_error) { + throw new Error( + 'Invalid S3 Object URL format. Expected format: https://bucket-name.s3.region.amazonaws.com/path/to/file' + ) + } +} + +export function generatePresignedUrl(params: any, expiresIn = 3600): string { + const date = new Date() + const amzDate = date.toISOString().replace(/[:-]|\.\d{3}/g, '') + const dateStamp = amzDate.slice(0, 8) + const encodedPath = encodeS3PathComponent(params.objectKey) + + const _expires = Math.floor(Date.now() / 1000) + expiresIn + + const method = 'GET' + const canonicalUri = `/${encodedPath}` + const canonicalQueryString = `X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=${encodeURIComponent(`${params.accessKeyId}/${dateStamp}/${params.region}/s3/aws4_request`)}&X-Amz-Date=${amzDate}&X-Amz-Expires=${expiresIn}&X-Amz-SignedHeaders=host` + const canonicalHeaders = `host:${params.bucketName}.s3.${params.region}.amazonaws.com\n` + const signedHeaders = 'host' + const payloadHash = 'UNSIGNED-PAYLOAD' + + const canonicalRequest = `${method}\n${canonicalUri}\n${canonicalQueryString}\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}` + + const algorithm = 'AWS4-HMAC-SHA256' + const credentialScope = `${dateStamp}/${params.region}/s3/aws4_request` + const stringToSign = `${algorithm}\n${amzDate}\n${credentialScope}\n${crypto.createHash('sha256').update(canonicalRequest).digest('hex')}` + + const signingKey = getSignatureKey(params.secretAccessKey, dateStamp, params.region, 's3') + const signature = crypto.createHmac('sha256', signingKey).update(stringToSign).digest('hex') + + return `https://${params.bucketName}.s3.${params.region}.amazonaws.com/${encodedPath}?${canonicalQueryString}&X-Amz-Signature=${signature}` +} diff --git a/apps/sim/tools/salesforce/opportunities.ts b/apps/sim/tools/salesforce/opportunities.ts deleted file mode 100644 index cad627932..000000000 --- a/apps/sim/tools/salesforce/opportunities.ts +++ /dev/null @@ -1,144 +0,0 @@ -import type { ToolConfig, ToolResponse } from '../types' - -export interface OpportunityParams { - apiKey: string - action: 'create' | 'update' | 'search' | 'delete' - id?: string - name?: string - accountId?: string - stage?: string - amount?: number - closeDate?: string - probability?: number - properties?: Record - limit?: number - offset?: number - data: Record -} - -export interface OpportunityResponse extends ToolResponse { - output: { - records: any[] - totalResults?: number - pagination?: { - hasMore: boolean - offset: number - } - } -} - -export const opportunitiesTool: ToolConfig = { - id: 'salesforce_opportunities', - name: 'Salesforce Opportunities', - description: - 'Manage Salesforce sales opportunities with comprehensive CRUD operations. Track stages, amounts, probabilities, and related account data with support for custom fields and paginated results.', - version: '1.0.0', - - params: { - apiKey: { - type: 'string', - required: true, - requiredForToolCall: true, - description: 'Salesforce API key', - }, - action: { - type: 'string', - required: true, - description: 'Action to perform (create, update, search, delete)', - }, - id: { - type: 'string', - description: 'Opportunity ID (required for updates)', - }, - name: { - type: 'string', - description: 'Opportunity name', - }, - accountId: { - type: 'string', - description: 'Associated account ID', - }, - stage: { - type: 'string', - description: 'Opportunity stage', - }, - amount: { - type: 'number', - description: 'Opportunity amount', - }, - closeDate: { - type: 'string', - description: 'Expected close date (YYYY-MM-DD)', - }, - probability: { - type: 'number', - description: 'Probability of closing (%)', - }, - properties: { - type: 'object', - description: 'Additional opportunity fields', - }, - limit: { - type: 'number', - default: 100, - description: 'Maximum number of records to return', - }, - offset: { - type: 'number', - description: 'Offset for pagination', - }, - data: { - type: 'object', - description: 'Data for the action', - }, - }, - - request: { - url: (params) => { - const baseUrl = `${params.apiKey}@salesforce.com/services/data/v58.0/sobjects/Opportunity` - if (params.id) { - return `${baseUrl}/${params.id}` - } - return baseUrl - }, - method: 'POST', - headers: (params) => ({ - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.apiKey}`, - }), - body: (params) => { - const fields = { - Name: params.name, - ...(params.accountId && { AccountId: params.accountId }), - ...(params.stage && { StageName: params.stage }), - ...(params.amount && { Amount: params.amount }), - ...(params.closeDate && { CloseDate: params.closeDate }), - ...(params.probability && { Probability: params.probability }), - ...params.properties, - } - - return fields - }, - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - return { - success: true, - output: { - records: data.records || [data], - totalResults: data.totalSize, - pagination: { - hasMore: !data.done, - offset: data.nextRecordsUrl ? Number.parseInt(data.nextRecordsUrl.split('-')[1]) : 0, - }, - }, - } - }, - - transformError: (error) => { - const message = error.message || error.error?.message - const code = error.errorCode || error.error?.errorCode - return `${message} (${code})` - }, -} diff --git a/apps/sim/tools/serper/search.ts b/apps/sim/tools/serper/search.ts index 57673fac5..ee168369e 100644 --- a/apps/sim/tools/serper/search.ts +++ b/apps/sim/tools/serper/search.ts @@ -12,34 +12,39 @@ export const searchTool: ToolConfig = { query: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'The search query', }, - apiKey: { - type: 'string', - required: true, - requiredForToolCall: true, - description: 'Serper API Key', - }, num: { type: 'number', required: false, + visibility: 'user-only', description: 'Number of results to return', }, gl: { type: 'string', required: false, + visibility: 'user-only', description: 'Country code for search results', }, hl: { type: 'string', required: false, + visibility: 'user-only', description: 'Language code for search results', }, type: { type: 'string', required: false, + visibility: 'user-only', description: 'Type of search to perform', }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Serper API Key', + }, }, request: { diff --git a/apps/sim/tools/slack/message.ts b/apps/sim/tools/slack/message.ts index 34c2ffd58..a772e68b7 100644 --- a/apps/sim/tools/slack/message.ts +++ b/apps/sim/tools/slack/message.ts @@ -21,26 +21,34 @@ export const slackMessageTool: ToolConfig version: '1.0.0', params: { - task: { - type: 'string', - required: true, - description: 'The task to complete or goal to achieve on the website', - }, startUrl: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'URL of the webpage to start the agent on', }, - outputSchema: { - type: 'json', - required: false, - description: 'Optional JSON schema defining the structure of data the agent should return', + task: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The task to complete or goal to achieve on the website', }, variables: { type: 'json', required: false, + visibility: 'user-only', description: 'Optional variables to substitute in the task (format: {key: value}). Reference in task using %key%', }, apiKey: { type: 'string', required: true, + visibility: 'user-only', description: 'OpenAI API key for agent execution (required by Stagehand)', }, + outputSchema: { + type: 'json', + required: false, + visibility: 'user-only', + description: 'Optional JSON schema defining the structure of data the agent should return', + }, }, request: { diff --git a/apps/sim/tools/stagehand/extract.ts b/apps/sim/tools/stagehand/extract.ts index ad990fa5a..bb4a290f2 100644 --- a/apps/sim/tools/stagehand/extract.ts +++ b/apps/sim/tools/stagehand/extract.ts @@ -11,25 +11,29 @@ export const extractTool: ToolConfig `https://${params.projectId}.supabase.co/rest/v1/${params.table}?select=*`, diff --git a/apps/sim/tools/supabase/query.ts b/apps/sim/tools/supabase/query.ts index 3b6d57075..aedb8761e 100644 --- a/apps/sim/tools/supabase/query.ts +++ b/apps/sim/tools/supabase/query.ts @@ -12,20 +12,30 @@ export const queryTool: ToolConfig = additionalScopes: ['database.read', 'projects.read'], }, params: { - apiKey: { - type: 'string', - required: true, - requiredForToolCall: true, - description: 'Your Supabase client anon key', - }, projectId: { type: 'string', required: true, - requiredForToolCall: true, + visibility: 'user-only', description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)', }, - table: { type: 'string', required: true }, - filter: { type: 'object', required: false }, + table: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The name of the Supabase table to query', + }, + filter: { + type: 'object', + required: false, + visibility: 'user-or-llm', + description: 'Filter to apply to the query', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase client anon key', + }, }, request: { url: (params) => `https://${params.projectId}.supabase.co/rest/v1/${params.table}`, diff --git a/apps/sim/tools/tavily/extract.ts b/apps/sim/tools/tavily/extract.ts index c8ca3aa60..5476eaff2 100644 --- a/apps/sim/tools/tavily/extract.ts +++ b/apps/sim/tools/tavily/extract.ts @@ -12,19 +12,21 @@ export const extractTool: ToolConfig urls: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'URL or array of URLs to extract content from', }, - apiKey: { - type: 'string', - required: true, - description: 'Tavily API Key', - requiredForToolCall: true, - }, extract_depth: { type: 'string', required: false, + visibility: 'user-only', description: 'The depth of extraction (basic=1 credit/5 URLs, advanced=2 credits/5 URLs)', }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Tavily API Key', + }, }, request: { diff --git a/apps/sim/tools/tavily/search.ts b/apps/sim/tools/tavily/search.ts index c1ff73a10..241c40205 100644 --- a/apps/sim/tools/tavily/search.ts +++ b/apps/sim/tools/tavily/search.ts @@ -12,17 +12,19 @@ export const searchTool: ToolConfig = query: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'The search query to execute', }, max_results: { type: 'number', required: false, + visibility: 'user-only', description: 'Maximum number of results (1-20)', }, apiKey: { type: 'string', required: true, - requiredForToolCall: true, + visibility: 'user-only', description: 'Tavily API Key', }, }, diff --git a/apps/sim/tools/telegram/message.ts b/apps/sim/tools/telegram/message.ts index 7fbcb2576..a58f0ee04 100644 --- a/apps/sim/tools/telegram/message.ts +++ b/apps/sim/tools/telegram/message.ts @@ -1,22 +1,6 @@ import type { ToolConfig } from '../types' import type { TelegramMessageParams, TelegramMessageResponse } from './types' - -// Helper function to convert basic markdown to HTML -function convertMarkdownToHTML(text: string): string { - return ( - text - // Bold: **text** or __text__ -> text - .replace(/\*\*(.*?)\*\*/g, '$1') - .replace(/__(.*?)__/g, '$1') - // Italic: *text* or _text_ -> text - .replace(/\*(.*?)\*/g, '$1') - .replace(/_(.*?)_/g, '$1') - // Code: `text` -> text - .replace(/`(.*?)`/g, '$1') - // Links: [text](url) -> text - .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') - ) -} +import { convertMarkdownToHTML } from './utils' export const telegramMessageTool: ToolConfig = { id: 'telegram_message', @@ -29,17 +13,19 @@ export const telegramMessageTool: ToolConfig text + .replace(/\*\*(.*?)\*\*/g, '$1') + .replace(/__(.*?)__/g, '$1') + // Italic: *text* or _text_ -> text + .replace(/\*(.*?)\*/g, '$1') + .replace(/_(.*?)_/g, '$1') + // Code: `text` -> text + .replace(/`(.*?)`/g, '$1') + // Links: [text](url) -> text + .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') + ) +} diff --git a/apps/sim/tools/thinking/tool.ts b/apps/sim/tools/thinking/tool.ts index 32d865a97..c4d54d2e0 100644 --- a/apps/sim/tools/thinking/tool.ts +++ b/apps/sim/tools/thinking/tool.ts @@ -8,11 +8,11 @@ export const thinkingTool: ToolConfig 'Processes a provided thought/instruction, making it available for subsequent steps.', version: '1.0.0', - // Define the input parameter params: { thought: { type: 'string', required: true, + visibility: 'hidden', description: 'The thought process or instruction provided by the user in the Thinking Step block.', }, diff --git a/apps/sim/tools/twilio/send_sms.ts b/apps/sim/tools/twilio/send_sms.ts index 8773faabc..600f9c7df 100644 --- a/apps/sim/tools/twilio/send_sms.ts +++ b/apps/sim/tools/twilio/send_sms.ts @@ -14,28 +14,31 @@ export const sendSMSTool: ToolConfig phoneNumbers: { type: 'string', required: true, + visibility: 'user-only', description: 'Phone numbers to send the message to, separated by newlines', }, message: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Message to send', }, accountSid: { type: 'string', required: true, + visibility: 'user-only', description: 'Twilio Account SID', - requiredForToolCall: true, }, authToken: { type: 'string', required: true, + visibility: 'user-only', description: 'Twilio Auth Token', - requiredForToolCall: true, }, fromNumber: { type: 'string', required: true, + visibility: 'user-only', description: 'Twilio phone number to send the message from', }, }, diff --git a/apps/sim/tools/typeform/files.test.ts b/apps/sim/tools/typeform/files.test.ts deleted file mode 100644 index 86e3d5d45..000000000 --- a/apps/sim/tools/typeform/files.test.ts +++ /dev/null @@ -1,194 +0,0 @@ -/** - * @vitest-environment jsdom - * - * Typeform Files Tool Unit Tests - * - * This file contains unit tests for the Typeform Files tool, - * which is used to download files uploaded in Typeform responses. - */ -import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' -import { ToolTester } from '../__test-utils__/test-tools' -import { filesTool } from './files' - -describe('Typeform Files Tool', () => { - let tester: ToolTester - - // Mock file response - const mockFileResponseHeaders = { - 'content-type': 'application/pdf', - 'content-disposition': 'attachment; filename="test-file.pdf"', - } - - beforeEach(() => { - tester = new ToolTester(filesTool) - }) - - afterEach(() => { - tester.cleanup() - vi.resetAllMocks() - }) - - describe('URL Construction', () => { - test('should construct correct URL for file endpoint', () => { - const params = { - formId: 'form123', - responseId: 'resp456', - fieldId: 'field789', - filename: 'test-file.pdf', - apiKey: 'test-token', - } - - expect(tester.getRequestUrl(params)).toBe( - 'https://api.typeform.com/forms/form123/responses/resp456/fields/field789/files/test-file.pdf' - ) - }) - - test('should add inline parameter when provided', () => { - const params = { - formId: 'form123', - responseId: 'resp456', - fieldId: 'field789', - filename: 'test-file.pdf', - inline: true, - apiKey: 'test-token', - } - - const url = tester.getRequestUrl(params) - expect(url).toContain('?inline=true') - }) - - test('should handle special characters in form ID and response ID', () => { - const params = { - formId: 'form/with/special?chars', - responseId: 'resp&with#chars', - fieldId: 'field-id', - filename: 'file name.pdf', - apiKey: 'test-token', - } - - const url = tester.getRequestUrl(params) - // Just verify the URL is constructed and doesn't throw errors - expect(url).toContain('https://api.typeform.com/forms/') - expect(url).toContain('files') - }) - }) - - describe('Headers Construction', () => { - test('should include correct authorization header', () => { - const params = { - formId: 'form123', - responseId: 'resp456', - fieldId: 'field789', - filename: 'test-file.pdf', - apiKey: 'test-token', - } - - const headers = tester.getRequestHeaders(params) - expect(headers.Authorization).toBe('Bearer test-token') - expect(headers['Content-Type']).toBe('application/json') - }) - }) - - describe('Data Transformation', () => { - test('should transform file data correctly', async () => { - // Setup mock response for binary file data - tester.setup('file-content-binary-data', { - headers: mockFileResponseHeaders, - }) - - // Execute the tool - const result = await tester.execute({ - formId: 'form123', - responseId: 'resp456', - fieldId: 'field789', - filename: 'test-file.pdf', - apiKey: 'test-token', - }) - - // Check the result - expect(result.success).toBe(true) - expect(result.output.filename).toBe('test-file.pdf') - expect(result.output.contentType).toBe('application/pdf') - // Don't check the fileUrl property as it depends on implementation details - }) - - test('should handle missing content-disposition header', async () => { - // Setup mock response without content-disposition - tester.setup('file-content-binary-data', { - headers: { 'content-type': 'application/pdf' }, - }) - - // Execute the tool - const result = await tester.execute({ - formId: 'form123', - responseId: 'resp456', - fieldId: 'field789', - filename: 'test-file.pdf', - apiKey: 'test-token', - }) - - // Check the result - expect(result.success).toBe(true) - expect(result.output.contentType).toBe('application/pdf') - // Don't check the fileUrl property as it depends on implementation details - // filename should be empty since there's no content-disposition - expect(result.output.filename).toBe('') - }) - }) - - describe('Error Handling', () => { - test('should handle file not found errors', async () => { - // Setup 404 error response - tester.setup({ message: 'File not found' }, { ok: false, status: 404 }) - - // Execute the tool - const result = await tester.execute({ - formId: 'form123', - responseId: 'resp456', - fieldId: 'field789', - filename: 'nonexistent.pdf', - apiKey: 'test-token', - }) - - // Check error handling - expect(result.success).toBe(false) - expect(result.error).toContain('Not Found') - }) - - test('should handle unauthorized errors', async () => { - // Setup 401 error response - tester.setup({ message: 'Unauthorized access' }, { ok: false, status: 401 }) - - // Execute the tool - const result = await tester.execute({ - formId: 'form123', - responseId: 'resp456', - fieldId: 'field789', - filename: 'test-file.pdf', - apiKey: 'invalid-token', - }) - - // Check error handling - expect(result.success).toBe(false) - expect(result.error).toContain('Unauthorized') - }) - - test('should handle network errors', async () => { - // Setup network error - tester.setupError('Network error') - - // Execute the tool - const result = await tester.execute({ - formId: 'form123', - responseId: 'resp456', - fieldId: 'field789', - filename: 'test-file.pdf', - apiKey: 'test-token', - }) - - // Check error handling - expect(result.success).toBe(false) - expect(result.error).toBeDefined() - }) - }) -}) diff --git a/apps/sim/tools/typeform/files.ts b/apps/sim/tools/typeform/files.ts index 862976480..839ea033d 100644 --- a/apps/sim/tools/typeform/files.ts +++ b/apps/sim/tools/typeform/files.ts @@ -10,31 +10,37 @@ export const filesTool: ToolConfig = formId: { type: 'string', required: true, + visibility: 'user-only', description: 'Typeform form ID', }, responseId: { type: 'string', required: true, + visibility: 'user-only', description: 'Response ID containing the files', }, fieldId: { type: 'string', required: true, + visibility: 'user-only', description: 'Unique ID of the file upload field', }, filename: { type: 'string', required: true, + visibility: 'user-only', description: 'Filename of the uploaded file', }, inline: { type: 'boolean', required: false, + visibility: 'user-only', description: 'Whether to request the file with inline Content-Disposition', }, apiKey: { type: 'string', required: true, + visibility: 'user-only', description: 'Typeform Personal Access Token', }, }, diff --git a/apps/sim/tools/typeform/index.test.ts b/apps/sim/tools/typeform/index.test.ts deleted file mode 100644 index 1f63f8ec1..000000000 --- a/apps/sim/tools/typeform/index.test.ts +++ /dev/null @@ -1,325 +0,0 @@ -/** - * @vitest-environment jsdom - * - * Typeform Integration Tests - * - * This file contains integration tests that verify the Typeform tools - * work correctly together and can be properly used from the block. - */ -import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' -import { ToolTester } from '../__test-utils__/test-tools' -import { typeformFilesTool, typeformInsightsTool, typeformResponsesTool } from './index' - -describe('Typeform Tools Integration', () => { - describe('Typeform Responses Tool Export', () => { - let tester: ToolTester - - beforeEach(() => { - tester = new ToolTester(typeformResponsesTool) - }) - - afterEach(() => { - tester.cleanup() - vi.resetAllMocks() - }) - - test('should use the correct tool ID', () => { - expect(typeformResponsesTool.id).toBe('typeform_responses') - }) - - test('should handle basic responses request', async () => { - // Setup mock response data - const mockData = { - total_items: 1, - page_count: 1, - items: [ - { - landing_id: 'test-landing', - token: 'test-token', - submitted_at: '2023-01-01T00:00:00Z', - answers: [], - }, - ], - } - - tester.setup(mockData) - - // Execute the tool - const result = await tester.execute({ - formId: 'test-form', - apiKey: 'test-api-key', - }) - - expect(result.success).toBe(true) - expect(result.output.total_items).toBe(1) - }) - }) - - describe('Typeform Files Tool Export', () => { - let tester: ToolTester - - beforeEach(() => { - tester = new ToolTester(typeformFilesTool) - }) - - afterEach(() => { - tester.cleanup() - vi.resetAllMocks() - }) - - test('should use the correct tool ID', () => { - expect(typeformFilesTool.id).toBe('typeform_files') - }) - - test('should handle basic file request', async () => { - // Setup mock response with file headers - tester.setup('binary-file-content', { - headers: { - 'content-type': 'application/pdf', - 'content-disposition': 'attachment; filename="test.pdf"', - }, - }) - - // Execute the tool - const result = await tester.execute({ - formId: 'test-form', - responseId: 'test-response', - fieldId: 'test-field', - filename: 'test.pdf', - apiKey: 'test-api-key', - }) - - expect(result.success).toBe(true) - expect(result.output.contentType).toBe('application/pdf') - expect(result.output.filename).toBe('test.pdf') - }) - }) - - describe('Typeform Insights Tool Export', () => { - let tester: ToolTester - - beforeEach(() => { - tester = new ToolTester(typeformInsightsTool) - }) - - afterEach(() => { - tester.cleanup() - vi.resetAllMocks() - }) - - test('should use the correct tool ID', () => { - expect(typeformInsightsTool.id).toBe('typeform_insights') - }) - - test('should handle basic insights request', async () => { - // Setup mock response data - const mockData = { - fields: [ - { - dropoffs: 5, - id: 'field123', - label: '1', - ref: 'ref123', - title: 'What is your name?', - type: 'short_text', - views: 100, - }, - ], - form: { - platforms: [ - { - average_time: 120000, - completion_rate: 75.5, - platform: 'desktop', - responses_count: 80, - total_visits: 120, - unique_visits: 100, - }, - ], - summary: { - average_time: 140000, - completion_rate: 72.3, - responses_count: 120, - total_visits: 180, - unique_visits: 150, - }, - }, - } - - tester.setup(mockData) - - // Execute the tool - const result = await tester.execute({ - formId: 'test-form', - apiKey: 'test-api-key', - }) - - expect(result.success).toBe(true) - expect(result.output.form.summary.responses_count).toBe(120) - expect(result.output.fields).toHaveLength(1) - }) - }) - - describe('End-to-End Flow', () => { - // This test simulates using both tools together in a workflow - - test('should be able to get responses and then file', async () => { - // First set up responses tester - const responsesTester = new ToolTester(typeformResponsesTool) - - // Mock responses data with a file upload - const mockResponsesData = { - total_items: 1, - page_count: 1, - items: [ - { - landing_id: 'landing-id', - token: 'response-id', - submitted_at: '2023-01-01T00:00:00Z', - answers: [ - { - field: { - id: 'file-field', - type: 'file_upload', - }, - type: 'file_url', - file_url: 'https://example.com/placeholder.pdf', - }, - ], - }, - ], - } - - responsesTester.setup(mockResponsesData) - - // Get responses - const responsesResult = await responsesTester.execute({ - formId: 'test-form', - apiKey: 'test-api-key', - }) - - expect(responsesResult.success).toBe(true) - - // Now get the response ID and field ID - const responseId = responsesResult.output.items[0].token - expect(responseId).toBe('response-id') - - const fieldId = responsesResult.output.items[0].answers[0].field.id - expect(fieldId).toBe('file-field') - - // Now set up files tester - const filesTester = new ToolTester(typeformFilesTool) - - // Mock file data - filesTester.setup('binary-file-data', { - headers: { - 'content-type': 'application/pdf', - 'content-disposition': 'attachment; filename="uploaded.pdf"', - }, - }) - - // Get file using the response ID and field ID from previous request - const filesResult = await filesTester.execute({ - formId: 'test-form', - responseId, - fieldId, - filename: 'uploaded.pdf', - apiKey: 'test-api-key', - }) - - expect(filesResult.success).toBe(true) - expect(filesResult.output.contentType).toBe('application/pdf') - expect(filesResult.output.filename).toBe('uploaded.pdf') - - // Clean up - responsesTester.cleanup() - filesTester.cleanup() - }) - - test('should be able to get responses and then insights', async () => { - // First set up responses tester - const responsesTester = new ToolTester(typeformResponsesTool) - - // Mock responses data - const mockResponsesData = { - total_items: 10, - page_count: 1, - items: [ - { - landing_id: 'landing-id', - token: 'response-id', - submitted_at: '2023-01-01T00:00:00Z', - answers: [], - }, - ], - } - - responsesTester.setup(mockResponsesData) - - // Get responses - const responsesResult = await responsesTester.execute({ - formId: 'test-form', - apiKey: 'test-api-key', - }) - - expect(responsesResult.success).toBe(true) - expect(responsesResult.output.total_items).toBe(10) - - // Now set up insights tester - const insightsTester = new ToolTester(typeformInsightsTool) - - // Mock insights data - const mockInsightsData = { - fields: [ - { - dropoffs: 5, - id: 'field123', - label: '1', - ref: 'ref123', - title: 'What is your name?', - type: 'short_text', - views: 100, - }, - ], - form: { - platforms: [ - { - average_time: 120000, - completion_rate: 75.5, - platform: 'desktop', - responses_count: 80, - total_visits: 120, - unique_visits: 100, - }, - ], - summary: { - average_time: 140000, - completion_rate: 72.3, - responses_count: 120, - total_visits: 180, - unique_visits: 150, - }, - }, - } - - insightsTester.setup(mockInsightsData) - - // Get insights for the same form - const insightsResult = await insightsTester.execute({ - formId: 'test-form', - apiKey: 'test-api-key', - }) - - expect(insightsResult.success).toBe(true) - expect(insightsResult.output.form.summary.responses_count).toBe(120) - - // Verify we can analyze the data by looking at completion rates - expect(insightsResult.output.form.summary.completion_rate).toBe(72.3) - expect(insightsResult.output.form.platforms[0].platform).toBe('desktop') - - // Clean up - responsesTester.cleanup() - insightsTester.cleanup() - }) - }) -}) diff --git a/apps/sim/tools/typeform/insights.test.ts b/apps/sim/tools/typeform/insights.test.ts deleted file mode 100644 index f5e957ebf..000000000 --- a/apps/sim/tools/typeform/insights.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -/** - * @vitest-environment jsdom - * - * Typeform Insights Tool Unit Tests - * - * This file contains unit tests for the Typeform Insights tool, - * which is used to retrieve form insights and analytics. - */ -import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' -import { ToolTester } from '../__test-utils__/test-tools' -import { insightsTool } from './insights' - -describe('Typeform Insights Tool', () => { - let tester: ToolTester - - // Mock insights response - const mockInsightsData = { - fields: [ - { - dropoffs: 5, - id: 'field123', - label: '1', - ref: 'ref123', - title: 'What is your name?', - type: 'short_text', - views: 100, - }, - { - dropoffs: 10, - id: 'field456', - label: '2', - ref: 'ref456', - title: 'How did you hear about us?', - type: 'multiple_choice', - views: 95, - }, - ], - form: { - platforms: [ - { - average_time: 120000, - completion_rate: 75.5, - platform: 'desktop', - responses_count: 80, - total_visits: 120, - unique_visits: 100, - }, - { - average_time: 180000, - completion_rate: 65.2, - platform: 'mobile', - responses_count: 40, - total_visits: 60, - unique_visits: 50, - }, - ], - summary: { - average_time: 140000, - completion_rate: 72.3, - responses_count: 120, - total_visits: 180, - unique_visits: 150, - }, - }, - } - - beforeEach(() => { - tester = new ToolTester(insightsTool) - }) - - afterEach(() => { - tester.cleanup() - vi.resetAllMocks() - }) - - describe('URL Construction', () => { - test('should construct correct URL for insights endpoint', () => { - const params = { - formId: 'form123', - apiKey: 'test-token', - } - - expect(tester.getRequestUrl(params)).toBe('https://api.typeform.com/insights/form123/summary') - }) - - test('should handle special characters in form ID', () => { - const params = { - formId: 'form/with/special?chars', - apiKey: 'test-token', - } - - const url = tester.getRequestUrl(params) - // Just verify the URL is constructed and doesn't throw errors - expect(url).toContain('https://api.typeform.com/insights/') - expect(url).toContain('summary') - }) - }) - - describe('Headers Construction', () => { - test('should include correct authorization header', () => { - const params = { - formId: 'form123', - apiKey: 'test-token', - } - - const headers = tester.getRequestHeaders(params) - expect(headers.Authorization).toBe('Bearer test-token') - expect(headers['Content-Type']).toBe('application/json') - }) - }) - - describe('Data Transformation', () => { - test('should transform insights data correctly', async () => { - // Setup mock response - tester.setup(mockInsightsData) - - // Execute the tool - const result = await tester.execute({ - formId: 'form123', - apiKey: 'test-token', - }) - - // Check the result - expect(result.success).toBe(true) - - // Verify form summary data - expect(result.output.form.summary.responses_count).toBe(120) - expect(result.output.form.summary.completion_rate).toBe(72.3) - - // Verify platforms data - expect(result.output.form.platforms).toHaveLength(2) - expect(result.output.form.platforms[0].platform).toBe('desktop') - expect(result.output.form.platforms[1].platform).toBe('mobile') - - // Verify fields data - expect(result.output.fields).toHaveLength(2) - expect(result.output.fields[0].title).toBe('What is your name?') - expect(result.output.fields[1].title).toBe('How did you hear about us?') - }) - }) - - describe('Error Handling', () => { - test('should handle form not found errors', async () => { - // Setup 404 error response - tester.setup({ message: 'Form not found' }, { ok: false, status: 404 }) - - // Execute the tool - const result = await tester.execute({ - formId: 'nonexistent', - apiKey: 'test-token', - }) - - // Check error handling - expect(result.success).toBe(false) - expect(result.error).toContain('Not Found') - }) - - test('should handle unauthorized errors', async () => { - // Setup 401 error response - tester.setup({ message: 'Unauthorized access' }, { ok: false, status: 401 }) - - // Execute the tool - const result = await tester.execute({ - formId: 'form123', - apiKey: 'invalid-token', - }) - - // Check error handling - expect(result.success).toBe(false) - expect(result.error).toContain('Unauthorized') - }) - - test('should handle network errors', async () => { - // Setup network error - tester.setupError('Network error') - - // Execute the tool - const result = await tester.execute({ - formId: 'form123', - apiKey: 'test-token', - }) - - // Check error handling - expect(result.success).toBe(false) - expect(result.error).toBeDefined() - }) - }) -}) diff --git a/apps/sim/tools/typeform/insights.ts b/apps/sim/tools/typeform/insights.ts index 8b595d1b0..d68626faa 100644 --- a/apps/sim/tools/typeform/insights.ts +++ b/apps/sim/tools/typeform/insights.ts @@ -10,11 +10,13 @@ export const insightsTool: ToolConfig { - let tester: ToolTester - - // Mock response data - const mockResponsesData = { - total_items: 2, - page_count: 1, - items: [ - { - landing_id: 'landing-id-1', - token: 'response-id-1', - landed_at: '2023-01-01T10:00:00Z', - submitted_at: '2023-01-01T10:05:00Z', - metadata: { - user_agent: 'Mozilla/5.0', - platform: 'web', - referer: 'https://example.com', - network_id: 'network-id-1', - browser: 'chrome', - }, - answers: [ - { - field: { - id: 'field-id-1', - type: 'short_text', - ref: 'ref-1', - }, - type: 'text', - text: 'Sample answer', - }, - ], - hidden: {}, - calculated: { - score: 0, - }, - variables: [], - }, - { - landing_id: 'landing-id-2', - token: 'response-id-2', - landed_at: '2023-01-02T10:00:00Z', - submitted_at: '2023-01-02T10:05:00Z', - metadata: { - user_agent: 'Mozilla/5.0', - platform: 'web', - referer: 'https://example.com', - network_id: 'network-id-2', - browser: 'chrome', - }, - answers: [ - { - field: { - id: 'field-id-1', - type: 'short_text', - ref: 'ref-1', - }, - type: 'text', - text: 'Another answer', - }, - ], - hidden: {}, - calculated: { - score: 0, - }, - variables: [], - }, - ], - } - - beforeEach(() => { - tester = new ToolTester(responsesTool) - }) - - afterEach(() => { - tester.cleanup() - vi.resetAllMocks() - }) - - describe('URL Construction', () => { - test('should construct correct base Typeform API URL', () => { - const params = { - formId: 'form123', - apiKey: 'test-token', - } - - expect(tester.getRequestUrl(params)).toBe('https://api.typeform.com/forms/form123/responses') - }) - - test('should add pageSize parameter to URL when provided', () => { - const params = { - formId: 'form123', - apiKey: 'test-token', - pageSize: 50, - } - - expect(tester.getRequestUrl(params)).toBe( - 'https://api.typeform.com/forms/form123/responses?page_size=50' - ) - }) - - test('should add since parameter to URL when provided', () => { - const params = { - formId: 'form123', - apiKey: 'test-token', - since: '2023-01-01T00:00:00Z', - } - - const url = tester.getRequestUrl(params) - expect(url).toContain('https://api.typeform.com/forms/form123/responses?since=') - expect(url).toContain('2023-01-01T00:00:00Z') - }) - - test('should add until parameter to URL when provided', () => { - const params = { - formId: 'form123', - apiKey: 'test-token', - until: '2023-01-31T23:59:59Z', - } - - const url = tester.getRequestUrl(params) - expect(url).toContain('https://api.typeform.com/forms/form123/responses?until=') - expect(url).toContain('2023-01-31T23:59:59Z') - }) - - test('should add completed parameter to URL when provided and not "all"', () => { - const params = { - formId: 'form123', - apiKey: 'test-token', - completed: 'true', - } - - expect(tester.getRequestUrl(params)).toBe( - 'https://api.typeform.com/forms/form123/responses?completed=true' - ) - }) - - test('should not add completed parameter to URL when set to "all"', () => { - const params = { - formId: 'form123', - apiKey: 'test-token', - completed: 'all', - } - - expect(tester.getRequestUrl(params)).toBe('https://api.typeform.com/forms/form123/responses') - }) - - test('should combine multiple parameters correctly', () => { - const params = { - formId: 'form123', - apiKey: 'test-token', - pageSize: 10, - since: '2023-01-01T00:00:00Z', - until: '2023-01-31T23:59:59Z', - completed: 'true', - } - - const url = tester.getRequestUrl(params) - expect(url).toContain('https://api.typeform.com/forms/form123/responses?') - expect(url).toContain('page_size=10') - expect(url).toContain('since=') - expect(url).toContain('until=') - expect(url).toContain('completed=true') - }) - }) - - describe('Headers Construction', () => { - test('should include correct authorization header', () => { - const params = { - formId: 'form123', - apiKey: 'test-token', - } - - const headers = tester.getRequestHeaders(params) - expect(headers.Authorization).toBe('Bearer test-token') - expect(headers['Content-Type']).toBe('application/json') - }) - }) - - describe('Data Transformation', () => { - test('should fetch and transform responses correctly', async () => { - // Setup mock response - tester.setup(mockResponsesData) - - // Execute the tool - const result = await tester.execute({ - formId: 'form123', - apiKey: 'test-token', - }) - - // Check the result - expect(result.success).toBe(true) - expect(result.output.total_items).toBe(2) - expect(result.output.items).toHaveLength(2) - - // Check first response - const firstResponse = result.output.items[0] - expect(firstResponse.token).toBe('response-id-1') - expect(firstResponse.answers).toHaveLength(1) - expect(firstResponse.answers[0].text).toBe('Sample answer') - - // Check second response - const secondResponse = result.output.items[1] - expect(secondResponse.token).toBe('response-id-2') - }) - }) - - describe('Error Handling', () => { - test('should handle form not found errors', async () => { - // Setup 404 error response - tester.setup({ message: 'Form not found' }, { ok: false, status: 404 }) - - // Execute the tool - const result = await tester.execute({ - formId: 'nonexistent-form', - apiKey: 'test-token', - }) - - // Check error handling - expect(result.success).toBe(false) - expect(result.error).toContain('Not Found') - }) - - test('should handle unauthorized errors', async () => { - // Setup 401 error response - tester.setup({ message: 'Unauthorized access' }, { ok: false, status: 401 }) - - // Execute the tool - const result = await tester.execute({ - formId: 'form123', - apiKey: 'invalid-token', - }) - - // Check error handling - expect(result.success).toBe(false) - expect(result.error).toContain('Unauthorized') - }) - - test('should handle network errors', async () => { - // Setup network error - tester.setupError('Network error') - - // Execute the tool - const result = await tester.execute({ - formId: 'form123', - apiKey: 'test-token', - }) - - // Check error handling - expect(result.success).toBe(false) - expect(result.error).toBeDefined() - }) - }) -}) diff --git a/apps/sim/tools/typeform/responses.ts b/apps/sim/tools/typeform/responses.ts index 6599c8c33..65fd25ad3 100644 --- a/apps/sim/tools/typeform/responses.ts +++ b/apps/sim/tools/typeform/responses.ts @@ -10,31 +10,37 @@ export const responsesTool: ToolConfig // The structured output from the tool @@ -32,8 +38,7 @@ export interface ToolConfig

{ { type: string required?: boolean - requiredForToolCall?: boolean - optionalToolInput?: boolean + visibility?: ParameterVisibility default?: any description?: string } diff --git a/apps/sim/tools/utils.test.ts b/apps/sim/tools/utils.test.ts index 89a506fa3..2fe41bec8 100644 --- a/apps/sim/tools/utils.test.ts +++ b/apps/sim/tools/utils.test.ts @@ -202,11 +202,11 @@ describe('validateToolRequest', () => { params: { required1: { type: 'string', - requiredForToolCall: true, + visibility: 'user-or-llm', }, required2: { type: 'number', - requiredForToolCall: true, + visibility: 'user-or-llm', }, optional: { type: 'boolean', @@ -519,13 +519,13 @@ describe('createParamSchema', () => { required1: { type: 'string', required: true, - requiredForToolCall: true, + visibility: 'user-or-llm', description: 'Required param', }, optional1: { type: 'number', required: false, - requiredForToolCall: false, + visibility: 'user-only', description: 'Optional param', }, }) diff --git a/apps/sim/tools/utils.ts b/apps/sim/tools/utils.ts index 79c0374b2..aa1293670 100644 --- a/apps/sim/tools/utils.ts +++ b/apps/sim/tools/utils.ts @@ -164,9 +164,9 @@ export function validateToolRequest( } // Ensure all required parameters for tool call are provided - // Note: optionalToolInput parameters are not checked here as they're optional + // Note: user-only parameters are not checked here as they're optional for (const [paramName, paramConfig] of Object.entries(tool.params)) { - if (paramConfig.requiredForToolCall && !(paramName in params)) { + if (paramConfig.visibility === 'user-or-llm' && !(paramName in params)) { throw new Error(`Parameter "${paramName}" is required for ${toolId} but was not provided`) } } @@ -191,23 +191,22 @@ export function createParamSchema(customTool: any): Record { if (customTool.schema.function?.parameters?.properties) { const properties = customTool.schema.function.parameters.properties const required = customTool.schema.function.parameters.required || [] - const optionalToolInputs = customTool.schema.function.parameters.optionalToolInputs || [] Object.entries(properties).forEach(([key, config]: [string, any]) => { const isRequired = required.includes(key) - const isOptionalInput = optionalToolInputs.includes(key) // Create the base parameter configuration const paramConfig: Record = { type: config.type || 'string', required: isRequired, - requiredForToolCall: isRequired, description: config.description || '', } - // Only add optionalToolInput if it's true to maintain backward compatibility with tests - if (isOptionalInput) { - paramConfig.optionalToolInput = true + // Set visibility based on whether it's required + if (isRequired) { + paramConfig.visibility = 'user-or-llm' + } else { + paramConfig.visibility = 'user-only' } params[key] = paramConfig diff --git a/apps/sim/tools/vision/tool.ts b/apps/sim/tools/vision/tool.ts index 76153fc05..604934dea 100644 --- a/apps/sim/tools/vision/tool.ts +++ b/apps/sim/tools/vision/tool.ts @@ -12,22 +12,25 @@ export const visionTool: ToolConfig = { apiKey: { type: 'string', required: true, - requiredForToolCall: true, + visibility: 'user-only', description: 'API key for the selected model provider', }, imageUrl: { type: 'string', required: true, + visibility: 'user-only', description: 'Publicly accessible image URL', }, model: { type: 'string', required: false, + visibility: 'user-only', description: 'Vision model to use (gpt-4o, claude-3-opus-20240229, etc)', }, prompt: { type: 'string', required: false, + visibility: 'user-or-llm', description: 'Custom prompt for image analysis', }, }, diff --git a/apps/sim/tools/whatsapp/send_message.ts b/apps/sim/tools/whatsapp/send_message.ts index 5adec3fff..df6c8c4dc 100644 --- a/apps/sim/tools/whatsapp/send_message.ts +++ b/apps/sim/tools/whatsapp/send_message.ts @@ -14,23 +14,26 @@ export const sendMessageTool: ToolConfig = { phoneNumber: { type: 'string', required: true, + visibility: 'user-only', description: 'Recipient phone number with country code', }, message: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Message content to send', }, phoneNumberId: { type: 'string', required: true, + visibility: 'user-only', description: 'WhatsApp Business Phone Number ID', }, accessToken: { type: 'string', required: true, + visibility: 'user-only', description: 'WhatsApp Business API Access Token', - requiredForToolCall: true, }, }, diff --git a/apps/sim/tools/x/read.ts b/apps/sim/tools/x/read.ts index ba7eee74c..ecb3f2a93 100644 --- a/apps/sim/tools/x/read.ts +++ b/apps/sim/tools/x/read.ts @@ -17,17 +17,19 @@ export const xReadTool: ToolConfig = { accessToken: { type: 'string', required: true, + visibility: 'hidden', description: 'X OAuth access token', }, tweetId: { type: 'string', required: true, - requiredForToolCall: true, + visibility: 'user-only', description: 'ID of the tweet to read', }, includeReplies: { type: 'boolean', required: false, + visibility: 'user-only', description: 'Whether to include replies to the tweet', }, }, diff --git a/apps/sim/tools/x/search.ts b/apps/sim/tools/x/search.ts index df1b7c770..de2c54ab9 100644 --- a/apps/sim/tools/x/search.ts +++ b/apps/sim/tools/x/search.ts @@ -17,31 +17,37 @@ export const xSearchTool: ToolConfig = { accessToken: { type: 'string', required: true, + visibility: 'hidden', description: 'X OAuth access token', }, query: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Search query (supports X search operators)', }, maxResults: { type: 'number', required: false, + visibility: 'user-only', description: 'Maximum number of results to return (default: 10, max: 100)', }, startTime: { type: 'string', required: false, + visibility: 'user-or-llm', description: 'Start time for search (ISO 8601 format)', }, endTime: { type: 'string', required: false, + visibility: 'user-or-llm', description: 'End time for search (ISO 8601 format)', }, sortOrder: { type: 'string', required: false, + visibility: 'user-or-llm', description: 'Sort order for results (recency or relevancy)', }, }, diff --git a/apps/sim/tools/x/user.ts b/apps/sim/tools/x/user.ts index 655273968..7474c09dd 100644 --- a/apps/sim/tools/x/user.ts +++ b/apps/sim/tools/x/user.ts @@ -20,11 +20,13 @@ export const xUserTool: ToolConfig = { accessToken: { type: 'string', required: true, + visibility: 'hidden', description: 'X OAuth access token', }, username: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Username to look up (without @ symbol)', }, }, diff --git a/apps/sim/tools/x/write.ts b/apps/sim/tools/x/write.ts index 305ba5af8..51e09a68c 100644 --- a/apps/sim/tools/x/write.ts +++ b/apps/sim/tools/x/write.ts @@ -17,26 +17,31 @@ export const xWriteTool: ToolConfig = { accessToken: { type: 'string', required: true, + visibility: 'hidden', description: 'X OAuth access token', }, text: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'The text content of your tweet', }, replyTo: { type: 'string', required: false, + visibility: 'user-only', description: 'ID of the tweet to reply to', }, mediaIds: { type: 'array', required: false, + visibility: 'user-only', description: 'Array of media IDs to attach to the tweet', }, poll: { type: 'object', required: false, + visibility: 'user-only', description: 'Poll configuration for the tweet', }, }, diff --git a/apps/sim/tools/youtube/search.ts b/apps/sim/tools/youtube/search.ts index cf3458e59..2c3bf19a0 100644 --- a/apps/sim/tools/youtube/search.ts +++ b/apps/sim/tools/youtube/search.ts @@ -10,20 +10,22 @@ export const youtubeSearchTool: ToolConfig { diff --git a/package.json b/package.json index 7489bb089..becd90efc 100644 --- a/package.json +++ b/package.json @@ -47,9 +47,8 @@ "turbo": "2.5.4" }, "lint-staged": { - "*.{js,jsx,ts,tsx,json,css,scss,md}": [ + "*.{js,jsx,ts,tsx,json,css,scss}": [ "biome check --write --files-ignore-unknown=true" - ], - "!.github/*.md": [] + ] } }