diff --git a/apps/rowboat/app/lib/default_tools.ts b/apps/rowboat/app/lib/default_tools.ts new file mode 100644 index 00000000..7a55dbbe --- /dev/null +++ b/apps/rowboat/app/lib/default_tools.ts @@ -0,0 +1,36 @@ +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 { + // 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 [ + { + 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..7ba4efc7 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx @@ -6,7 +6,7 @@ import { DataSource } from "@/src/entities/models/data-source"; import { WithStringId } from "../../../lib/types/types"; import { Dropdown, DropdownItem, DropdownTrigger, DropdownMenu } from "@heroui/react"; import { useRef, useEffect, useState } from "react"; -import { EllipsisVerticalIcon, ImportIcon, PlusIcon, Brain, Boxes, Wrench, PenLine, Library, ChevronDown, ChevronRight, ServerIcon, Component, ScrollText, GripVertical, Users, Cog, CheckCircle2, LinkIcon, UnlinkIcon, MoreVertical, Eye, Trash2, AlertTriangle, Circle, Database } from "lucide-react"; +import { EllipsisVerticalIcon, ImportIcon, PlusIcon, Brain, Boxes, Wrench, PenLine, Library, ChevronDown, ChevronRight, ServerIcon, Component, ScrollText, GripVertical, Users, Cog, CheckCircle2, LinkIcon, UnlinkIcon, MoreVertical, Eye, Trash2, AlertTriangle, Circle, Database, Image as ImageIcon } from "lucide-react"; import { Tooltip } from "@heroui/react"; import { DndContext, DragEndEvent, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'; import { SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable'; @@ -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,121 @@ 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.isGeminiImage ? ( + + ) : ( + + )} + {tool.name} + {tool.mockTool && ( + Mocked + )} + {!tool.isLibrary && ( + + + + )} +
+ ))} +
)} - onClick={() => handleToolSelection(tool.name)} - > - - {tool.name} - {tool.mockTool && ( - Mocked - )} - - - -
- ))} -
- )} - - ); - })()} -
- ) : ( - - )} + + ); + })()} +
+ ); + } + + return ( + + ); + })()}
)} @@ -1627,7 +1652,8 @@ const ComposioCard = ({