From 48c32d37dc63b15c3ec2729c877e111fb94fddbf Mon Sep 17 00:00:00 2001 From: arkml Date: Tue, 16 Sep 2025 16:22:06 +0530 Subject: [PATCH 1/4] moved generate_image from being attached to the workflow --- apps/rowboat/app/lib/default_tools.ts | 34 +++ apps/rowboat/app/lib/project_templates.ts | 18 +- apps/rowboat/app/lib/types/workflow_types.ts | 6 +- .../[projectId]/entities/agent_config.tsx | 9 +- .../[projectId]/workflow/entity_list.tsx | 194 ++++++++++-------- .../[projectId]/workflow/workflow_editor.tsx | 37 +++- .../application/lib/agents-runtime/agents.ts | 11 +- .../projects/create-project.use-case.ts | 24 +-- 8 files changed, 199 insertions(+), 134 deletions(-) create mode 100644 apps/rowboat/app/lib/default_tools.ts diff --git a/apps/rowboat/app/lib/default_tools.ts b/apps/rowboat/app/lib/default_tools.ts new file mode 100644 index 00000000..8e9d76bd --- /dev/null +++ b/apps/rowboat/app/lib/default_tools.ts @@ -0,0 +1,34 @@ +import { z } from 'zod'; + +// Returns the list of built-in tools that should appear by default +// in the workflow editor and be usable at runtime without attaching +// them to the workflow. These are displayed as read-only library tools. +// Note: avoid importing WorkflowTool here to prevent circular deps. +// Return a structurally compatible object instead. +export function getDefaultTools(): Array { + // Always expose built-in library tools in the editor so users can + // discover them. Runtime invocation will still validate required + // environment variables and return an error if missing. + + return [ + { + name: 'Generate Image', + description: + 'Generate an image using Google Gemini given a text prompt. Returns base64-encoded image data and any text parts.', + isGeminiImage: true, + isLibrary: true, + parameters: { + type: 'object', + properties: { + prompt: { + type: 'string', + description: 'Text prompt describing the image to generate', + }, + modelName: { type: 'string', description: 'Optional Gemini model override' }, + }, + required: ['prompt'], + additionalProperties: true, + }, + }, + ]; +} diff --git a/apps/rowboat/app/lib/project_templates.ts b/apps/rowboat/app/lib/project_templates.ts index d35aa31d..276dd392 100644 --- a/apps/rowboat/app/lib/project_templates.ts +++ b/apps/rowboat/app/lib/project_templates.ts @@ -3,7 +3,6 @@ import { z } from 'zod'; // Provide a minimal default template to satisfy legacy code paths that // still reference `templates.default`. Real templates are DB-backed. -const includeGeminiImageTool = !!process.env.GOOGLE_API_KEY; const defaultTemplate: z.infer = { name: 'Blank Template', @@ -11,22 +10,7 @@ const defaultTemplate: z.infer = { startAgent: "", agents: [], prompts: [], - tools: includeGeminiImageTool ? [ - { - name: "Generate Image", - description: "Generate an image using Google Gemini given a text prompt. Returns base64-encoded image data and any text parts.", - isGeminiImage: true, - parameters: { - type: 'object', - properties: { - prompt: { type: 'string', description: 'Text prompt describing the image to generate' }, - modelName: { type: 'string', description: 'Optional Gemini model override' }, - }, - required: ['prompt'], - additionalProperties: true, - }, - }, - ] : [], + tools: [], pipelines: [], }; diff --git a/apps/rowboat/app/lib/types/workflow_types.ts b/apps/rowboat/app/lib/types/workflow_types.ts index aff24a07..90cccb69 100644 --- a/apps/rowboat/app/lib/types/workflow_types.ts +++ b/apps/rowboat/app/lib/types/workflow_types.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { getDefaultTools } from "@/app/lib/default_tools"; export const WorkflowAgent = z.object({ name: z.string(), order: z.number().int().optional(), @@ -165,7 +166,10 @@ export function sanitizeTextWithMentions( const agent = workflow.agents.find(a => a.name === entity.name); return agent && agent.type !== 'pipeline'; } else if (entity.type === 'tool') { - return workflow.tools.some(t => t.name === entity.name); + // Allow referencing workflow tools or default library tools + const inWorkflow = workflow.tools.some(t => t.name === entity.name); + const inDefaults = getDefaultTools().some(t => t.name === entity.name); + return inWorkflow || inDefaults; } else if (entity.type === 'prompt') { return workflow.prompts.some(p => p.name === entity.name); } else if (entity.type === 'pipeline') { diff --git a/apps/rowboat/app/projects/[projectId]/entities/agent_config.tsx b/apps/rowboat/app/projects/[projectId]/entities/agent_config.tsx index f3cc65a0..710447d9 100644 --- a/apps/rowboat/app/projects/[projectId]/entities/agent_config.tsx +++ b/apps/rowboat/app/projects/[projectId]/entities/agent_config.tsx @@ -16,6 +16,7 @@ import { Panel } from "@/components/common/panel-common"; import { Button as CustomButton } from "@/components/ui/button"; import clsx from "clsx"; import { InputField } from "@/app/lib/components/input-field"; +import { getDefaultTools } from "@/app/lib/default_tools"; import { USE_TRANSFER_CONTROL_OPTIONS } from "@/app/lib/feature_flags"; import { Info as InfoIcon } from "lucide-react"; import { useCopilot } from "../copilot/use-copilot"; @@ -236,7 +237,13 @@ export function AgentConfig({ const atMentions = createAtMentions({ agents: agents, prompts, - tools, + tools: (() => { + const defaults = getDefaultTools(); + const map = new Map>(); + for (const t of tools) map.set(t.name, t); + for (const t of defaults) if (!map.has(t.name)) map.set(t.name, t as any); + return Array.from(map.values()); + })(), pipelines: agent.type === "pipeline" ? [] : (workflow.pipelines || []), // Pipeline agents can't reference pipelines currentAgentName: agent.name, currentAgent: agent diff --git a/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx b/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx index 3c704029..3ea811e1 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx @@ -20,6 +20,7 @@ import { ServerLogo } from '../tools/components/MCPServersCommon'; import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@heroui/react"; import { ToolsModal } from './components/ToolsModal'; import { DataSourcesModal } from './components/DataSourcesModal'; +import { getDefaultTools } from "@/app/lib/default_tools"; import { DataSourceIcon } from '../../../lib/components/datasource-icon'; import { deleteDataSource } from '../../../actions/data-source.actions'; import { ToolkitAuthModal } from '../tools/components/ToolkitAuthModal'; @@ -939,97 +940,116 @@ export const EntityList = forwardRef< {expandedPanels.tools && (
- {tools.length > 0 ? ( -
- {/* Group tools by server */} - {(() => { - // Get custom tools (non-MCP tools) - const customTools = tools.filter(tool => !tool.isMcp && !tool.isComposio); - - // Group MCP tools by server - const serverTools = tools.reduce((acc, tool) => { - if (tool.isMcp && tool.mcpServerName) { - if (!acc[tool.mcpServerName]) { - acc[tool.mcpServerName] = []; - } - acc[tool.mcpServerName].push(tool); - } - return acc; - }, {} as Record); + {(() => { + // Merge workflow tools with default library tools (unique by name) + const defaults = getDefaultTools(); + const toolMap = new Map>(); + for (const t of tools) toolMap.set(t.name, t); + for (const t of defaults) if (!toolMap.has(t.name)) toolMap.set(t.name, t as any); + const toolsForDisplay = Array.from(toolMap.values()); - return ( - <> - {/* Show composio cards - ordered by status */} - {Object.values(composioTools) - .map((card) => ( - - ))} + if (toolsForDisplay.length > 0) { + return ( +
+ {/* Group tools by server */} + {(() => { + // Get custom tools (non-MCP, non-Composio) + const customTools = toolsForDisplay.filter(tool => !tool.isMcp && !tool.isComposio); - {/* Show MCP server cards */} - {Object.entries(serverTools).map(([serverName, tools]) => ( - - ))} + // Group MCP tools by server + const serverTools = toolsForDisplay.reduce((acc, tool) => { + if (tool.isMcp && tool.mcpServerName) { + if (!acc[tool.mcpServerName]) { + acc[tool.mcpServerName] = []; + } + acc[tool.mcpServerName].push(tool); + } + return acc; + }, {} as Record); - {/* Show custom tools */} - {customTools.length > 0 && ( -
- {customTools.map((tool, index) => ( -
+ {/* Show composio cards - ordered by status */} + {Object.values(composioTools) + .map((card) => ( + + ))} + + {/* Show MCP server cards */} + {Object.entries(serverTools).map(([serverName, tools]) => ( + + ))} + + {/* Show custom tools, including default library tools (read-only) */} + {customTools.length > 0 && ( +
+ {customTools.map((tool, index) => ( +
{ if (!tool.isLibrary) handleToolSelection(tool.name); }} + > + + {tool.name} + {tool.mockTool && ( + Mocked + )} + {!tool.isLibrary && ( + + + + )} +
+ ))} +
)} - onClick={() => handleToolSelection(tool.name)} - > - - {tool.name} - {tool.mockTool && ( - Mocked - )} - - - -
- ))} -
- )} - - ); - })()} -
- ) : ( - - )} + + ); + })()} +
+ ); + } + + return ( + + ); + })()}
)} diff --git a/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx b/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx index ec2ec1fe..476d8366 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx @@ -38,6 +38,7 @@ import { Panel } from "@/components/common/panel-common"; import { Button as CustomButton } from "@/components/ui/button"; import { InputField } from "@/app/lib/components/input-field"; +import { getDefaultTools } from "@/app/lib/default_tools"; import { VoiceSection } from "../config/components/voice"; import { TopBar } from "./components/TopBar"; @@ -2207,7 +2208,14 @@ export function WorkflowEditor({ usedAgentNames={new Set(state.present.workflow.agents.filter((agent) => agent.name !== state.present.selection!.name).map((agent) => agent.name))} usedPipelineNames={new Set((state.present.workflow.pipelines || []).map((pipeline) => pipeline.name))} agents={state.present.workflow.agents} - tools={state.present.workflow.tools} + tools={(() => { + const { tools } = state.present.workflow; + const defaults = getDefaultTools(); + const map = new Map(); + for (const t of tools) map.set(t.name, t); + for (const t of defaults) if (!map.has(t.name)) map.set(t.name, t); + return Array.from(map.values()); + })()} prompts={state.present.workflow.prompts} dataSources={dataSources} handleUpdate={(update) => { dispatchGuarded({ type: "update_agent", name: state.present.selection!.name, agent: update }); }} @@ -2235,7 +2243,14 @@ export function WorkflowEditor({ key={`overlay-${state.present.selection.name}-${configKey}`} prompt={state.present.workflow.prompts.find((prompt) => prompt.name === state.present.selection!.name)!} agents={state.present.workflow.agents} - tools={state.present.workflow.tools} + tools={(() => { + const { tools } = state.present.workflow; + const defaults = getDefaultTools(); + const map = new Map(); + for (const t of tools) map.set(t.name, t); + for (const t of defaults) if (!map.has(t.name)) map.set(t.name, t); + return Array.from(map.values()); + })()} prompts={state.present.workflow.prompts} usedPromptNames={new Set(state.present.workflow.prompts.filter((prompt) => prompt.name !== state.present.selection!.name).map((prompt) => prompt.name))} handleUpdate={(update) => { dispatchGuarded({ type: "update_prompt", name: state.present.selection!.name, prompt: update }); }} @@ -2313,7 +2328,14 @@ export function WorkflowEditor({ usedAgentNames={new Set(state.present.workflow.agents.filter((agent) => agent.name !== state.present.selection!.name).map((agent) => agent.name))} usedPipelineNames={new Set((state.present.workflow.pipelines || []).map((pipeline) => pipeline.name))} agents={state.present.workflow.agents} - tools={state.present.workflow.tools} + tools={(() => { + const { tools } = state.present.workflow; + const defaults = getDefaultTools(); + const map = new Map(); + for (const t of tools) map.set(t.name, t); + for (const t of defaults) if (!map.has(t.name)) map.set(t.name, t); + return Array.from(map.values()); + })()} prompts={state.present.workflow.prompts} dataSources={dataSources} handleUpdate={(update) => { dispatchGuarded({ type: "update_agent", name: state.present.selection!.name, agent: update }); }} @@ -2341,7 +2363,14 @@ export function WorkflowEditor({ key={`overlay2-${state.present.selection.name}-${configKey}`} prompt={state.present.workflow.prompts.find((prompt) => prompt.name === state.present.selection!.name)!} agents={state.present.workflow.agents} - tools={state.present.workflow.tools} + tools={(() => { + const { tools } = state.present.workflow; + const defaults = getDefaultTools(); + const map = new Map(); + for (const t of tools) map.set(t.name, t); + for (const t of defaults) if (!map.has(t.name)) map.set(t.name, t); + return Array.from(map.values()); + })()} prompts={state.present.workflow.prompts} usedPromptNames={new Set(state.present.workflow.prompts.filter((prompt) => prompt.name !== state.present.selection!.name).map((prompt) => prompt.name))} handleUpdate={(update) => { dispatchGuarded({ type: "update_prompt", name: state.present.selection!.name, prompt: update }); }} diff --git a/apps/rowboat/src/application/lib/agents-runtime/agents.ts b/apps/rowboat/src/application/lib/agents-runtime/agents.ts index a0c6bd34..5f3f7aa8 100644 --- a/apps/rowboat/src/application/lib/agents-runtime/agents.ts +++ b/apps/rowboat/src/application/lib/agents-runtime/agents.ts @@ -9,6 +9,7 @@ import crypto from "crypto"; // Internal dependencies import { createTools, createRagTool } from "./agent-tools"; import { ConnectedEntity, sanitizeTextWithMentions, Workflow, WorkflowAgent, WorkflowPipeline, WorkflowPrompt, WorkflowTool } from "@/app/lib/types/workflow_types"; +import { getDefaultTools } from "@/app/lib/default_tools"; import { CHILD_TRANSFER_RELATED_INSTRUCTIONS, CONVERSATION_TYPE_INSTRUCTIONS, PIPELINE_TYPE_INSTRUCTIONS, RAG_INSTRUCTIONS, TASK_TYPE_INSTRUCTIONS, VARIABLES_CONTEXT_INSTRUCTIONS } from "./agent_instructions"; import { PrefixLogger } from "@/app/lib/utils"; import { Message, AssistantMessage, AssistantMessageWithToolCalls, ToolMessage } from "@/app/lib/types/types"; @@ -361,7 +362,15 @@ function mapConfig(workflow: z.infer): { ...acc, [agent.name]: agent }), {}); - const toolConfig: Record> = workflow.tools.reduce((acc, tool) => ({ + // Merge workflow tools with default library tools (unique by name) + const mergedTools = (() => { + const defaults = getDefaultTools(); + const map = new Map>(); + for (const t of workflow.tools) map.set(t.name, t); + for (const t of defaults) if (!map.has(t.name)) map.set(t.name, t as any); + return Array.from(map.values()); + })(); + const toolConfig: Record> = mergedTools.reduce((acc, tool) => ({ ...acc, [tool.name]: tool }), {}); diff --git a/apps/rowboat/src/application/use-cases/projects/create-project.use-case.ts b/apps/rowboat/src/application/use-cases/projects/create-project.use-case.ts index d5433cb7..aa14ab29 100644 --- a/apps/rowboat/src/application/use-cases/projects/create-project.use-case.ts +++ b/apps/rowboat/src/application/use-cases/projects/create-project.use-case.ts @@ -95,29 +95,7 @@ export class CreateProjectUseCase implements ICreateProjectUseCase { } } - // Conditionally include Gemini Image tool by default if GOOGLE_API_KEY is present - const hasGoogleKey = !!process.env.GOOGLE_API_KEY; - const hasImageTool = (workflow.tools || []).some(t => t.isGeminiImage || t.name === 'Generate Image'); - if (hasGoogleKey && !hasImageTool) { - const imageTool = { - name: 'Generate Image', - description: 'Generate an image using Google Gemini given a text prompt. Returns base64-encoded image data and any text parts.', - isGeminiImage: true, - parameters: { - type: 'object' as const, - properties: { - prompt: { type: 'string', description: 'Text prompt describing the image to generate' }, - modelName: { type: 'string', description: 'Optional Gemini model override' }, - }, - required: ['prompt'], - additionalProperties: true, - }, - }; - workflow = { - ...workflow, - tools: [...(workflow.tools || []), imageTool] as any, - }; - } + // Do not auto-attach image generation tool; it is available as a default library tool in the editor/runtime // create project secret const secret = crypto.randomBytes(32).toString('hex'); From 79fc18241df663112e4bae025f6fd0ccdc9b5f80 Mon Sep 17 00:00:00 2001 From: arkml Date: Tue, 16 Sep 2025 17:02:25 +0530 Subject: [PATCH 2/4] added flag for google api key --- apps/rowboat/app/lib/default_tools.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/rowboat/app/lib/default_tools.ts b/apps/rowboat/app/lib/default_tools.ts index 8e9d76bd..7a55dbbe 100644 --- a/apps/rowboat/app/lib/default_tools.ts +++ b/apps/rowboat/app/lib/default_tools.ts @@ -6,9 +6,11 @@ import { z } from 'zod'; // Note: avoid importing WorkflowTool here to prevent circular deps. // Return a structurally compatible object instead. export function getDefaultTools(): Array { - // Always expose built-in library tools in the editor so users can - // discover them. Runtime invocation will still validate required - // environment variables and return an error if missing. + // Show built-in tools only when a public, non-secret flag is set. + // Avoids exposing real secrets in client bundles. + const hasGoogleKeyFlag = (process.env.NEXT_PUBLIC_HAS_GOOGLE_API_KEY || '').toLowerCase() === 'true'; + + if (!hasGoogleKeyFlag) return []; return [ { From d8c99ab4c394f4002992df6f9d671517e0e507a5 Mon Sep 17 00:00:00 2001 From: arkml Date: Tue, 16 Sep 2025 17:07:16 +0530 Subject: [PATCH 3/4] fixed font and transparency --- .../app/projects/[projectId]/workflow/entity_list.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx b/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx index 3ea811e1..04aec67a 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx @@ -1010,14 +1010,15 @@ export const EntityList = forwardRef< className={clsx( "flex items-center gap-2 px-3 py-2 rounded cursor-pointer hover:bg-zinc-50 dark:hover:bg-zinc-800", selectedEntity?.type === "tool" && selectedEntity.name === tool.name && "bg-indigo-50 dark:bg-indigo-950/30", - tool.isLibrary ? "opacity-80 cursor-default" : "" + tool.isLibrary ? "cursor-default" : "" )} onClick={() => { if (!tool.isLibrary) handleToolSelection(tool.name); }} > {tool.name} {tool.mockTool && ( Mocked @@ -1647,7 +1648,8 @@ const ComposioCard = ({