diff --git a/packages/visual-editor/src/a2/a2-registry.ts b/packages/visual-editor/src/a2/a2-registry.ts index 07c5dec7fc2..8fb9a955faa 100644 --- a/packages/visual-editor/src/a2/a2-registry.ts +++ b/packages/visual-editor/src/a2/a2-registry.ts @@ -26,6 +26,15 @@ import searchEnterpriseInvoke, { import codeExecutionInvoke, { describe as codeExecutionDescribe, } from "./tools/code-execution.js"; +import userInputInvoke, { + describe as userInputDescribe, +} from "./a2/user-input.js"; +import renderOutputsInvoke, { + describe as renderOutputsDescribe, +} from "./a2/render-outputs.js"; +import generateInvoke, { + describe as generateDescribe, +} from "./generate/main.js"; export { A2_COMPONENTS, A2_TOOLS }; @@ -123,6 +132,12 @@ const A2_TOOLS: [string, StaticTool][] = [ * Static registry of A2 components that appear in the component picker. * These are the step types available in the editor controls. */ + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type ComponentInvoke = (...args: any[]) => Promise; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type ComponentDescribe = (...args: any[]) => Promise; + type A2Component = { url: string; title: string; @@ -130,17 +145,27 @@ type A2Component = { icon: string; order: number; category: "input" | "generate" | "output"; + invoke: ComponentInvoke; + describe: ComponentDescribe; + /** + * Optional module URL alias. If present, requests for this component's URL + * should be redirected to this module URL for proper context wiring. + */ + moduleUrl?: string; }; const A2_COMPONENTS: A2Component[] = [ { url: "embed://a2/a2.bgl.json#21ee02e7-83fa-49d0-964c-0cab10eafc2c", + moduleUrl: "embed://a2/a2.bgl.json#module:user-input", title: "User Input", description: "Allows asking user for input that could be then used in next steps", icon: "ask-user", order: 1, category: "input", + invoke: userInputInvoke, + describe: userInputDescribe, }, { url: "embed://a2/generate.bgl.json#module:main", @@ -149,6 +174,8 @@ const A2_COMPONENTS: A2Component[] = [ icon: "generative", order: 1, category: "generate", + invoke: generateInvoke, + describe: generateDescribe, }, { url: "embed://a2/a2.bgl.json#module:render-outputs", @@ -157,5 +184,7 @@ const A2_COMPONENTS: A2Component[] = [ icon: "responsive_layout", order: 100, category: "output", + invoke: renderOutputsInvoke, + describe: renderOutputsDescribe, }, ]; diff --git a/packages/visual-editor/src/a2/a2.ts b/packages/visual-editor/src/a2/a2.ts deleted file mode 100644 index 0e96a0d7562..00000000000 --- a/packages/visual-editor/src/a2/a2.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { exports as a2Exports } from "./a2/index.js"; -import { exports as agentExports } from "./agent/index.js"; -import { exports as audioGeneratorExports } from "./audio-generator/index.js"; -import { exports as deepResearchExports } from "./deep-research/index.js"; -import { exports as generateExports } from "./generate/index.js"; -import { exports as generateTextExports } from "./generate-text/index.js"; -import { exports as goOverListExports } from "./go-over-list/index.js"; -import { exports as googleDriveExports } from "./google-drive/index.js"; -import { exports as musicGeneratorExports } from "./music-generator/index.js"; -import { exports as toolsExports } from "./tools/index.js"; -import { exports as videoGeneratorExports } from "./video-generator/index.js"; -import { exports as autonameExports } from "./autoname/index.js"; - -export const a2 = { - a2: a2Exports, - agent: agentExports, - "audio-generator": audioGeneratorExports, - autoname: autonameExports, - generate: generateExports, - "generate-text": generateTextExports, - "go-over-list": goOverListExports, - "google-drive": googleDriveExports, - tools: toolsExports, - "video-generator": videoGeneratorExports, - "music-generator": musicGeneratorExports, - "deep-research": deepResearchExports, -}; diff --git a/packages/visual-editor/src/a2/a2/bgl.json b/packages/visual-editor/src/a2/a2/bgl.json index 2a5aec596bc..c2b2dfdbf87 100644 --- a/packages/visual-editor/src/a2/a2/bgl.json +++ b/packages/visual-editor/src/a2/a2/bgl.json @@ -52,129 +52,11 @@ "#module:image-editor", "#module:render-outputs", "#module:audio-generator", - "#21ee02e7-83fa-49d0-964c-0cab10eafc2c", + "#module:user-input", "#module:combine-outputs", "#module:make-code" ], - "graphs": { - "21ee02e7-83fa-49d0-964c-0cab10eafc2c": { - "title": "Ask User", - "description": "A block of text as input or output", - "version": "0.0.1", - "nodes": [ - { - "type": "input", - "id": "input", - "metadata": { - "visual": { - "x": 580.0000000000005, - "y": -539.9999999999994, - "collapsed": "expanded", - "outputHeight": 44 - }, - "title": "Waiting for user input", - "logLevel": "info" - }, - "configuration": {} - }, - { - "type": "output", - "id": "output", - "configuration": { - "schema": { - "properties": { - "context": { - "type": "array", - "title": "Context", - "items": { - "type": "object", - "behavior": ["llm-content"] - }, - "default": "null" - } - }, - "type": "object", - "required": [] - } - }, - "metadata": { - "visual": { - "x": 1240.0000000000005, - "y": -399.99999999999943, - "collapsed": "expanded", - "outputHeight": 44 - } - } - }, - { - "id": "board-64b2c3a8", - "type": "#module:text-entry", - "metadata": { - "visual": { - "x": 225.9030760391795, - "y": -646.8568148490385, - "collapsed": "expanded", - "outputHeight": 44 - }, - "title": "text-entry" - } - }, - { - "id": "board-95a57400", - "type": "#module:text-main", - "metadata": { - "visual": { - "x": 900, - "y": -459.99999999999943, - "collapsed": "expanded", - "outputHeight": 44 - }, - "title": "text-main" - } - } - ], - "edges": [ - { - "from": "board-64b2c3a8", - "out": "toInput", - "to": "input", - "in": "schema" - }, - { - "from": "board-64b2c3a8", - "out": "toMain", - "to": "board-95a57400", - "in": "request" - }, - { - "from": "board-95a57400", - "to": "output", - "out": "context", - "in": "context" - }, - { - "from": "board-64b2c3a8", - "to": "board-95a57400", - "out": "context", - "in": "context" - }, - { - "from": "input", - "to": "board-95a57400", - "out": "request", - "in": "request" - } - ], - "metadata": { - "visual": { - "minimized": false - }, - "tags": [], - "describer": "module:text-entry", - "icon": "text" - } - } - }, + "graphs": {}, "assets": { "@@thumbnail": { "metadata": { diff --git a/packages/visual-editor/src/a2/a2/index.ts b/packages/visual-editor/src/a2/a2/index.ts index 75b5e7f2242..33491960bc7 100644 --- a/packages/visual-editor/src/a2/a2/index.ts +++ b/packages/visual-editor/src/a2/a2/index.ts @@ -22,8 +22,7 @@ import * as a2Researcher from "./researcher.js"; import * as a2Settings from "./settings.js"; import * as a2StepExecutor from "./step-executor.js"; import * as a2Template from "./template.js"; -import * as a2TextEntry from "./text-entry.js"; -import * as a2TextMain from "./text-main.js"; +import * as a2UserInput from "./user-input.js"; import * as a2ToolManager from "./tool-manager.js"; import * as a2Utils from "./utils.js"; @@ -49,8 +48,7 @@ export const exports = { settings: a2Settings, "step-executor": a2StepExecutor, template: a2Template, - "text-entry": a2TextEntry, - "text-main": a2TextMain, + "user-input": a2UserInput, "tool-manager": a2ToolManager, utils: a2Utils, }; diff --git a/packages/visual-editor/src/a2/a2/text-main.ts b/packages/visual-editor/src/a2/a2/text-main.ts deleted file mode 100644 index af5e52e83db..00000000000 --- a/packages/visual-editor/src/a2/a2/text-main.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * @fileoverview Calls Gemini agent loop. - */ - -import { LLMContent, Outcome, Schema } from "@breadboard-ai/types"; -import { err } from "./utils.js"; - -export { invoke as default, describe }; - -type TextMainInputs = { - context: LLMContent | "nothing"; - request?: LLMContent; -}; - -type TextMainOutputs = { - context: LLMContent[]; -}; - -async function invoke({ - context, - request, -}: TextMainInputs): Promise> { - if (context == "nothing") { - if (!request) { - return err(`No text supplied.`); - } - return { context: [request] }; - } - return { context: [context] }; -} - -async function describe() { - return { - inputSchema: { - type: "object", - properties: { - context: { - type: "object", - title: "Context in", - }, - request: { - type: "object", - title: "Data From Input", - }, - }, - } satisfies Schema, - outputSchema: { - type: "object", - properties: { - context: { - type: "array", - items: { type: "object", behavior: ["llm-content"] }, - title: "Context out", - }, - }, - } satisfies Schema, - }; -} diff --git a/packages/visual-editor/src/a2/a2/text-entry.ts b/packages/visual-editor/src/a2/a2/user-input.ts similarity index 75% rename from packages/visual-editor/src/a2/a2/text-entry.ts rename to packages/visual-editor/src/a2/a2/user-input.ts index f7893a9efe0..5bd0217d537 100644 --- a/packages/visual-editor/src/a2/a2/text-entry.ts +++ b/packages/visual-editor/src/a2/a2/user-input.ts @@ -1,5 +1,7 @@ /** * @fileoverview Allows asking user for input that could be then used in next steps. + * Consolidates text-entry.ts and text-main.ts into a single module using + * direct caps.input/caps.output calls. */ import { BehaviorSchema, @@ -9,10 +11,11 @@ import { Schema, SchemaEnumValue, } from "@breadboard-ai/types"; +import { ok } from "@breadboard-ai/utils"; import { type Params } from "./common.js"; import { report } from "./output.js"; import { Template } from "./template.js"; -import { defaultLLMContent, llm, ok, toText } from "./utils.js"; +import { defaultLLMContent, llm, toText } from "./utils.js"; export { invoke as default, describe }; @@ -27,42 +30,15 @@ const MODALITY: readonly string[] = [ type Modality = (typeof MODALITY)[number]; -type TextInputs = { +type UserInputInputs = { description?: LLMContent; "p-modality"?: Modality; "p-required"?: boolean; } & Params; -type TextOutputs = - | { - toInput: Schema; - context: "nothing"; - } - | { - toMain: string; - context: LLMContent; - }; - -function toInput( - title: string, - modality: Modality | undefined, - required: boolean | undefined -) { - const requiredBehavior: BehaviorSchema[] = required ? ["hint-required"] : []; - const toInput: Schema = { - type: "object", - properties: { - request: { - type: "object", - title, - behavior: ["transient", "llm-content", ...requiredBehavior], - examples: [defaultLLMContent()], - format: computeIcon(modality), - }, - }, - }; - return toInput; -} +type UserInputOutputs = { + context: LLMContent[]; +}; const ICONS: Record = { Any: "asterisk", @@ -105,9 +81,9 @@ async function invoke( "p-modality": modality, "p-required": required, ...params - }: TextInputs, + }: UserInputInputs, caps: Capabilities -): Promise> { +): Promise> { const template = new Template(caps, description); let details = llm`Please provide input`.asContent(); if (description) { @@ -117,6 +93,8 @@ async function invoke( } details = substituting; } + + // Report to console await report(caps, { actor: "User Input", category: "Requesting Input", @@ -125,12 +103,54 @@ async function invoke( icon: "input", chat: true, }); + const title = toText(details); - return { context: "nothing", toInput: toInput(title, modality, required) }; + const requiredBehavior: BehaviorSchema[] = required ? ["hint-required"] : []; + + // Output the prompt to display to user + await caps.output({ + schema: { + type: "object", + properties: { + message: { + type: "object", + behavior: ["llm-content"], + title, + }, + }, + }, + message: details, + }); + + // Request input from user + const response = (await caps.input({ + schema: { + type: "object", + properties: { + request: { + type: "object", + title, + behavior: ["transient", "llm-content", ...requiredBehavior], + examples: [defaultLLMContent()], + format: computeIcon(modality), + }, + }, + }, + })) as Outcome<{ request?: LLMContent }>; + + if (!ok(response)) { + return response; + } + + // Return context with the user's response + if (!response.request) { + return { context: [] }; + } + return { context: [response.request] }; } type DescribeInputs = { - inputs: TextInputs; + inputs: UserInputInputs; }; async function describe( diff --git a/packages/visual-editor/src/a2/runnable-module-factory.ts b/packages/visual-editor/src/a2/runnable-module-factory.ts index 28e1643f53f..da5a6562615 100644 --- a/packages/visual-editor/src/a2/runnable-module-factory.ts +++ b/packages/visual-editor/src/a2/runnable-module-factory.ts @@ -30,7 +30,7 @@ import { err, filterUndefined, ok } from "@breadboard-ai/utils"; import { OpalShellHostProtocol } from "@breadboard-ai/types/opal-shell-protocol.js"; import { urlComponentsFromString } from "../engine/loader/loader.js"; import { McpClientManager } from "../mcp/index.js"; -import { a2 } from "./a2.js"; +import { A2_COMPONENTS } from "./a2-registry.js"; import { type ConsentController } from "../sca/controller/subcontrollers/global/global.js"; import { AgentContext } from "./agent/agent-context.js"; @@ -39,6 +39,32 @@ export { createA2ModuleFactory }; const URL_PREFIX = "embed://a2/"; const URL_SUFFIX = ".bgl.json"; +/** + * Lookup a component's invoke or describe function by URL. + * Returns undefined if the URL doesn't match a registered component. + * Handles both direct URLs and module: prefixed URLs. + */ +function lookupComponent( + url: string, + method: "invoke" | "describe" +): unknown | undefined { + // Try direct match first (against both url and moduleUrl) + let component = A2_COMPONENTS.find( + (c) => c.url === url || c.moduleUrl === url + ); + + // If no match, try with module: prefix (for module-based components) + if (!component && url.includes("#") && !url.includes("#module:")) { + const moduleUrl = url.replace("#", "#module:"); + component = A2_COMPONENTS.find( + (c) => c.url === moduleUrl || c.moduleUrl === moduleUrl + ); + } + + if (!component) return undefined; + return method === "invoke" ? component.invoke : component.describe; +} + export type A2ModuleFactoryArgs = { mcpClientManager: McpClientManager; fetchWithCreds: typeof globalThis.fetch; @@ -190,10 +216,9 @@ class A2Module implements RunnableModule { ) {} getModule(name: string, method: "invoke" | "describe"): unknown | undefined { - const exp = method === "invoke" ? "default" : "describe"; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const module = (a2 as any)[this.dir]?.[name]?.[exp]; - return module; + // Static component registry lookup + const url = `${URL_PREFIX}${this.dir}${URL_SUFFIX}#${name}`; + return lookupComponent(url, method); } async invoke( diff --git a/packages/visual-editor/src/engine/runtime/handler.ts b/packages/visual-editor/src/engine/runtime/handler.ts index e89a2c12607..c73a4e8745a 100644 --- a/packages/visual-editor/src/engine/runtime/handler.ts +++ b/packages/visual-editor/src/engine/runtime/handler.ts @@ -19,6 +19,7 @@ import type { import { graphUrlLike } from "@breadboard-ai/utils"; import { GraphBasedNodeHandler } from "./graph-based-node-handler.js"; import { getGraphUrl } from "../loader/loader.js"; +import { A2_COMPONENTS } from "../../a2/a2-registry.js"; // TODO: Deduplicate. function contextFromMutableGraph(mutable: MutableGraph): NodeHandlerContext { @@ -133,6 +134,15 @@ export async function getGraphHandler( if (is3pModule(type) && !allow3PModules) { return undefined; } + + // Check if this is a registered A2 component with a URL alias + // If so, redirect to the moduleUrl so it goes through the normal loader + // with proper context wiring (capabilities, args, etc.) + const component = A2_COMPONENTS.find((c) => c.url === type); + if (component?.moduleUrl) { + type = component.moduleUrl; + } + const nodeTypeUrl = graphUrlLike(type) ? getGraphUrl(type, context) : undefined;