Merge pull request #264 from rowboatlabs/fix_image_fr

moved generate_image from being attached to the workflow
This commit is contained in:
Ramnique Singh 2025-09-16 17:15:22 +05:30 committed by GitHub
commit 726559de76
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 209 additions and 136 deletions

View file

@ -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<any> {
// 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,
},
},
];
}

View file

@ -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<typeof WorkflowTemplate> = {
name: 'Blank Template',
@ -11,22 +10,7 @@ const defaultTemplate: z.infer<typeof WorkflowTemplate> = {
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: [],
};

View file

@ -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') {

View file

@ -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<string, z.infer<typeof WorkflowTool>>();
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

View file

@ -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 && (
<div className="h-full overflow-y-auto">
<div className="p-2">
{tools.length > 0 ? (
<div className="space-y-1">
{/* 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<string, typeof tools>);
{(() => {
// Merge workflow tools with default library tools (unique by name)
const defaults = getDefaultTools();
const toolMap = new Map<string, z.infer<typeof WorkflowTool>>();
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) => (
<ComposioCard
key={card.slug}
card={card}
selectedEntity={selectedEntity}
onSelectTool={handleToolSelection}
onDeleteTool={onDeleteTool}
selectedRef={selectedRef}
projectConfig={projectConfig}
projectId={projectId}
workflow={workflow}
onProjectToolsUpdated={onProjectToolsUpdated}
setSelectedToolkitSlug={setSelectedToolkitSlug}
setShowToolsModal={setShowToolsModal}
/>
))}
if (toolsForDisplay.length > 0) {
return (
<div className="space-y-1">
{/* 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]) => (
<ServerCard
key={serverName}
serverName={serverName}
tools={tools}
selectedEntity={selectedEntity}
onSelectTool={handleToolSelection}
onDeleteTool={onDeleteTool}
selectedRef={selectedRef}
/>
))}
// 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<string, typeof toolsForDisplay>);
{/* Show custom tools */}
{customTools.length > 0 && (
<div className="mt-2">
{customTools.map((tool, index) => (
<div
key={`custom-tool-${index}`}
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"
return (
<>
{/* Show composio cards - ordered by status */}
{Object.values(composioTools)
.map((card) => (
<ComposioCard
key={card.slug}
card={card}
selectedEntity={selectedEntity}
onSelectTool={handleToolSelection}
onDeleteTool={onDeleteTool}
selectedRef={selectedRef}
projectConfig={projectConfig}
projectId={projectId}
workflow={workflow}
onProjectToolsUpdated={onProjectToolsUpdated}
setSelectedToolkitSlug={setSelectedToolkitSlug}
setShowToolsModal={setShowToolsModal}
/>
))}
{/* Show MCP server cards */}
{Object.entries(serverTools).map(([serverName, tools]) => (
<ServerCard
key={serverName}
serverName={serverName}
tools={tools}
selectedEntity={selectedEntity}
onSelectTool={handleToolSelection}
onDeleteTool={onDeleteTool}
selectedRef={selectedRef}
/>
))}
{/* Show custom tools, including default library tools (read-only) */}
{customTools.length > 0 && (
<div className="mt-2">
{customTools.map((tool, index) => (
<div
key={`custom-tool-${index}`}
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 ? "cursor-default" : ""
)}
onClick={() => { if (!tool.isLibrary) handleToolSelection(tool.name); }}
>
{tool.isGeminiImage ? (
<ImageIcon className="w-4 h-4 text-blue-600/70 dark:text-blue-500/70" />
) : (
<Boxes className="w-4 h-4 text-blue-600/70 dark:text-blue-500/70" />
)}
<span className={clsx(
"flex-1 text-xs whitespace-normal break-words",
// Match font styling to other tools even if read-only
"text-zinc-900 dark:text-zinc-100"
)}>{tool.name}</span>
{tool.mockTool && (
<span className="ml-2 px-1 py-0 rounded bg-purple-50 text-purple-400 dark:bg-purple-900/40 dark:text-purple-200 text-[11px] font-normal align-middle">Mocked</span>
)}
{!tool.isLibrary && (
<Tooltip content="Remove tool" size="sm" delay={500}>
<button
className="ml-1 p-1 pr-2 rounded hover:bg-red-100 dark:hover:bg-red-900 flex items-center"
onClick={e => { e.stopPropagation(); onDeleteTool(tool.name); }}
>
<Trash2 className="w-3 h-3 text-red-500" />
</button>
</Tooltip>
)}
</div>
))}
</div>
)}
onClick={() => handleToolSelection(tool.name)}
>
<Boxes className="w-4 h-4 text-blue-600/70 dark:text-blue-500/70" />
<span className="flex-1 text-xs text-zinc-900 dark:text-zinc-100 whitespace-normal break-words">{tool.name}</span>
{tool.mockTool && (
<span className="ml-2 px-1 py-0 rounded bg-purple-50 text-purple-400 dark:bg-purple-900/40 dark:text-purple-200 text-[11px] font-normal align-middle">Mocked</span>
)}
<Tooltip content="Remove tool" size="sm" delay={500}>
<button
className="ml-1 p-1 pr-2 rounded hover:bg-red-100 dark:hover:bg-red-900 flex items-center"
onClick={e => { e.stopPropagation(); onDeleteTool(tool.name); }}
>
<Trash2 className="w-3 h-3 text-red-500" />
</button>
</Tooltip>
</div>
))}
</div>
)}
</>
);
})()}
</div>
) : (
<EmptyState
entity="tools"
hasFilteredItems={false}
/>
)}
</>
);
})()}
</div>
);
}
return (
<EmptyState
entity="tools"
hasFilteredItems={false}
/>
);
})()}
</div>
</div>
)}
@ -1627,7 +1652,8 @@ const ComposioCard = ({
<button
className={clsx(
"flex-1 flex items-center gap-2 text-sm text-left bg-transparent border-none p-0 m-0",
tool.isLibrary ? "text-zinc-400 dark:text-zinc-600" : "text-zinc-900 dark:text-zinc-100"
// Use same font styling for library tools; keep disabled state only
"text-zinc-900 dark:text-zinc-100"
)}
onClick={() => onSelectTool(tool.name)}
disabled={tool.isLibrary}

View file

@ -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<string, any>();
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<string, any>();
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<string, any>();
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<string, any>();
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 }); }}

View file

@ -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<typeof Workflow>): {
...acc,
[agent.name]: agent
}), {});
const toolConfig: Record<string, z.infer<typeof WorkflowTool>> = workflow.tools.reduce((acc, tool) => ({
// Merge workflow tools with default library tools (unique by name)
const mergedTools = (() => {
const defaults = getDefaultTools();
const map = new Map<string, z.infer<typeof WorkflowTool>>();
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<string, z.infer<typeof WorkflowTool>> = mergedTools.reduce((acc, tool) => ({
...acc,
[tool.name]: tool
}), {});

View file

@ -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');