From cfe89f0282dba045f98173f4ffa8c00d1e0a66c0 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Wed, 15 Nov 2023 11:31:41 -0800 Subject: [PATCH] [syntax] added support for memoized (nee constant) wires --- .../docs/graphs/new/accumulating-context.md | 24 +++ .../docs/graphs/new/custom-inline-action.md | 4 +- .../graphs/new/accumulating-context.json | 141 ++++++++++++++++++ .../graphs/new/custom-inline-action.json | 21 ++- seeds/breadboard-web/public/local-boards.json | 2 +- ...accumulator.ts => accumulating-context.ts} | 31 ++-- .../src/boards/new/custom-inline-action.ts | 12 +- seeds/breadboard-web/src/new/lib.ts | 136 +++++++++++------ 8 files changed, 298 insertions(+), 73 deletions(-) create mode 100644 seeds/breadboard-web/docs/graphs/new/accumulating-context.md create mode 100644 seeds/breadboard-web/public/graphs/new/accumulating-context.json rename seeds/breadboard-web/src/boards/new/{accumulator.ts => accumulating-context.ts} (63%) diff --git a/seeds/breadboard-web/docs/graphs/new/accumulating-context.md b/seeds/breadboard-web/docs/graphs/new/accumulating-context.md new file mode 100644 index 00000000..ba7d1f90 --- /dev/null +++ b/seeds/breadboard-web/docs/graphs/new/accumulating-context.md @@ -0,0 +1,24 @@ +## accumulating-context.ts + +```mermaid +%%{init: 'themeVariables': { 'fontFamily': 'Fira Code, monospace' }}%% +graph TD; +userRequest[/"input
id='userRequest'"/]:::input -- "text->question" --> assistant["promptTemplate
id='assistant'"] +userRequest[/"input
id='userRequest'"/]:::input -- "text->user" --> append0["append
id='append-0'"] +start(("passthrough
id='start'")):::passthrough --> userRequest[/"input
id='userRequest'"/]:::input +output3{{"output
id='output-3'"}}:::output --> userRequest[/"input
id='userRequest'"/]:::input +assistant["promptTemplate
id='assistant'"] -- "prompt->text" --> generateText2["generateText
id='generateText-2'"] +append0["append
id='append-0'"] -- "accumulator->accumulator" --> append0["append
id='append-0'"] +append0["append
id='append-0'"] -- "accumulator->context" --> assistant["promptTemplate
id='assistant'"] +generateText2["generateText
id='generateText-2'"] -- "completion->accumulator" --> append0["append
id='append-0'"] +generateText2["generateText
id='generateText-2'"] -- "completion->text" --> output3{{"output
id='output-3'"}}:::output +secrets1("secrets
id='secrets-1'"):::secrets -- "PALM_KEY->PALM_KEY" --o generateText2["generateText
id='generateText-2'"] +classDef default stroke:#ffab40,fill:#fff2ccff,color:#000 +classDef input stroke:#3c78d8,fill:#c9daf8ff,color:#000 +classDef output stroke:#38761d,fill:#b6d7a8ff,color:#000 +classDef passthrough stroke:#a64d79,fill:#ead1dcff,color:#000 +classDef slot stroke:#a64d79,fill:#ead1dcff,color:#000 +classDef config stroke:#a64d79,fill:#ead1dcff,color:#000 +classDef secrets stroke:#db4437,fill:#f4cccc,color:#000 +classDef slotted stroke:#a64d79 +``` \ No newline at end of file diff --git a/seeds/breadboard-web/docs/graphs/new/custom-inline-action.md b/seeds/breadboard-web/docs/graphs/new/custom-inline-action.md index 21390cec..c379b972 100644 --- a/seeds/breadboard-web/docs/graphs/new/custom-inline-action.md +++ b/seeds/breadboard-web/docs/graphs/new/custom-inline-action.md @@ -3,8 +3,8 @@ ```mermaid %%{init: 'themeVariables': { 'fontFamily': 'Fira Code, monospace' }}%% graph TD; -fn7["runJavascript
id='fn-7'"] -- all --> output6{{"output
id='output-6'"}}:::output -input5[/"input
id='input-5'"/]:::input -- all --> fn7["runJavascript
id='fn-7'"] +fn89["fn-8
id='fn-8-9'"] -- all --> output7{{"output
id='output-7'"}}:::output +input6[/"input
id='input-6'"/]:::input -- all --> fn89["fn-8
id='fn-8-9'"] classDef default stroke:#ffab40,fill:#fff2ccff,color:#000 classDef input stroke:#3c78d8,fill:#c9daf8ff,color:#000 classDef output stroke:#38761d,fill:#b6d7a8ff,color:#000 diff --git a/seeds/breadboard-web/public/graphs/new/accumulating-context.json b/seeds/breadboard-web/public/graphs/new/accumulating-context.json new file mode 100644 index 00000000..cb5d8803 --- /dev/null +++ b/seeds/breadboard-web/public/graphs/new/accumulating-context.json @@ -0,0 +1,141 @@ +{ + "title": "New: Accumulating context", + "edges": [ + { + "from": "userRequest", + "to": "assistant", + "out": "text", + "in": "question" + }, + { + "from": "userRequest", + "to": "append-0", + "out": "text", + "in": "user" + }, + { + "from": "start", + "to": "userRequest", + "out": "", + "in": "" + }, + { + "from": "output-3", + "to": "userRequest", + "out": "", + "in": "" + }, + { + "from": "assistant", + "to": "generateText-2", + "out": "prompt", + "in": "text" + }, + { + "from": "append-0", + "to": "append-0", + "out": "accumulator", + "in": "accumulator" + }, + { + "from": "append-0", + "to": "assistant", + "out": "accumulator", + "in": "context" + }, + { + "from": "generateText-2", + "to": "append-0", + "out": "completion", + "in": "accumulator" + }, + { + "from": "generateText-2", + "to": "output-3", + "out": "completion", + "in": "text" + }, + { + "from": "secrets-1", + "to": "generateText-2", + "out": "PALM_KEY", + "in": "PALM_KEY", + "constant": true + } + ], + "nodes": [ + { + "id": "userRequest", + "type": "input", + "configuration": { + "schema": { + "type": "object", + "properties": { + "text": { + "type": "string", + "title": "User", + "description": "Type here to chat with the assistant" + } + }, + "required": [ + "text" + ] + } + } + }, + { + "id": "start", + "type": "passthrough", + "configuration": {} + }, + { + "id": "output-3", + "type": "output", + "configuration": { + "schema": { + "type": "object", + "properties": { + "text": { + "type": "string", + "title": "Assistant", + "description": "Assistant's response in the conversation with the user" + } + }, + "required": [ + "text" + ] + } + } + }, + { + "id": "assistant", + "type": "promptTemplate", + "configuration": { + "template": "This is a conversation between a friendly assistant and their user. You are the assistant and your job is to try to be helpful, empathetic, and fun.\n{{context}}\n\n== Current Conversation\nuser: {{question}}\nassistant:", + "context": "" + } + }, + { + "id": "append-0", + "type": "append", + "configuration": { + "accumulator": "\n== Conversation History" + } + }, + { + "id": "generateText-2", + "type": "generateText", + "configuration": {} + }, + { + "id": "secrets-1", + "type": "secrets", + "configuration": { + "keys": [ + "PALM_KEY" + ] + } + } + ], + "graphs": {} +} \ No newline at end of file diff --git a/seeds/breadboard-web/public/graphs/new/custom-inline-action.json b/seeds/breadboard-web/public/graphs/new/custom-inline-action.json index 545c0d46..98f1c4ac 100644 --- a/seeds/breadboard-web/public/graphs/new/custom-inline-action.json +++ b/seeds/breadboard-web/public/graphs/new/custom-inline-action.json @@ -2,34 +2,31 @@ "title": "New: Custom inline action", "edges": [ { - "from": "fn-7", - "to": "output-6", + "from": "fn-8-9", + "to": "output-7", "out": "*", "in": "*" }, { - "from": "input-5", - "to": "fn-7", + "from": "input-6", + "to": "fn-8-9", "out": "*", "in": "*" } ], "nodes": [ { - "id": "output-6", + "id": "output-7", "type": "output", "configuration": {} }, { - "id": "fn-7", - "type": "runJavascript", - "configuration": { - "code": "async function fn_7(inputs2) {const{a,b}=await inputs2;return{result:(a||0)+(b||0)}}", - "name": "fn_7" - } + "id": "fn-8-9", + "type": "fn-8", + "configuration": {} }, { - "id": "input-5", + "id": "input-6", "type": "input", "configuration": {} } diff --git a/seeds/breadboard-web/public/local-boards.json b/seeds/breadboard-web/public/local-boards.json index 7809d867..e1cc5d3c 100644 --- a/seeds/breadboard-web/public/local-boards.json +++ b/seeds/breadboard-web/public/local-boards.json @@ -5,7 +5,7 @@ }, { "title": "New: Accumulating context", - "url": "/graphs/new/accumulator.json" + "url": "/graphs/new/accumulating-context.json" }, { "title": "New: Custom inline action", diff --git a/seeds/breadboard-web/src/boards/new/accumulator.ts b/seeds/breadboard-web/src/boards/new/accumulating-context.ts similarity index 63% rename from seeds/breadboard-web/src/boards/new/accumulator.ts rename to seeds/breadboard-web/src/boards/new/accumulating-context.ts index df280323..090b7424 100644 --- a/seeds/breadboard-web/src/boards/new/accumulator.ts +++ b/seeds/breadboard-web/src/boards/new/accumulating-context.ts @@ -21,7 +21,7 @@ const input = base.input({ }, }); -core.passthrough({ $id: "start" }).to(input); +core.passthrough({ $id: "start" }).as({}).to(input); const prompt = llm.promptTemplate({ template: @@ -33,24 +33,37 @@ const prompt = llm.promptTemplate({ const conversationMemory = llm.append({ accumulator: "\n== Conversation History", - $id: "conversationMemory", user: input.text, }); -conversationMemory.accumulator.to(conversationMemory); +conversationMemory.in(conversationMemory.accumulator); +prompt.in({ context: conversationMemory.accumulator }); +// conversationMemory.accumulator.to(prompt.context); ??? +// conversationMemory.accumulator.as("context").to(prompt); const response = llm.generateText({ text: prompt.prompt, - PALM_KEY: llm.secrets({ keys: ["PALM_KEY"] }).PALM_KEY, + PALM_KEY: llm.secrets({ keys: ["PALM_KEY"] }).PALM_KEY.memoize(), }); - conversationMemory.in({ accumulator: response.completion }); +// response.completion.to(conversationMemory.accumulator); -prompt.in({ context: conversationMemory.accumulator }); +const output = base.output({ + text: response.completion, -const output = base.output({ text: response.completion }); + schema: { + type: "object", + properties: { + text: { + type: "string", + title: "Assistant", + description: "Assistant's response in the conversation with the user", + }, + }, + required: ["text"], + }, +}); -// TODO: Don't send data, just "->" -output.to(input); +output.as({}).to(input); export const graph = input; // Any node would work here. diff --git a/seeds/breadboard-web/src/boards/new/custom-inline-action.ts b/seeds/breadboard-web/src/boards/new/custom-inline-action.ts index 5242d5ec..9a8d2a91 100644 --- a/seeds/breadboard-web/src/boards/new/custom-inline-action.ts +++ b/seeds/breadboard-web/src/boards/new/custom-inline-action.ts @@ -4,13 +4,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { action, flow } from "../../new/lib.js"; +import { action } from "../../new/lib.js"; export const graph = action((inputs) => { - return flow(async (inputs) => { - const { a, b } = await inputs; - return { result: ((a as number) || 0) + ((b as number) || 0) }; - }, inputs); + return action<{ a: number; b: number }, { result: number }>( + async (inputs) => { + const { a, b } = await inputs; + return { result: (a || 0) + (b || 0) }; + } + )(inputs); }); export const example = { a: 1, b: 2 }; diff --git a/seeds/breadboard-web/src/new/lib.ts b/seeds/breadboard-web/src/new/lib.ts index fe108a12..4708b018 100644 --- a/seeds/breadboard-web/src/new/lib.ts +++ b/seeds/breadboard-web/src/new/lib.ts @@ -146,6 +146,7 @@ export interface EdgeImpl< to: NodeImpl; out: string; in: string; + constant?: boolean; } let nodeIdCounter = 0; @@ -205,7 +206,7 @@ class NodeImpl< type: string; outgoing: EdgeImpl[] = []; incoming: EdgeImpl[] = []; - configuration: InputValues = {}; + configuration: Partial = {}; #handler: NodeHandler; @@ -213,7 +214,8 @@ class NodeImpl< #resolve?: (value: O | PromiseLike) => void; #reject?: (reason?: unknown) => void; - #inputs: InputValues; + #inputs: Partial; + #constants: Partial = {}; #receivedFrom: NodeImpl[] = []; #outputs?: O; @@ -240,15 +242,19 @@ class NodeImpl< let id: string | undefined = undefined; if (config instanceof NodeImpl) { - this.addInputAsNode(config.unProxy()); + this.addInputsFromNode(config.unProxy()); } else if (isValue(config)) { - this.addInputAsNode(...(config as Value).asNodeInput()); + this.addInputsFromNode(...(config as Value).asNodeInput()); } else { const { $id, ...rest } = config as Partial> & { $id?: string; }; id = $id; this.addInputsAsValues(rest as InputsMaybeAsValues); + + // Treat incoming constants as configuration + this.configuration = { ...this.configuration, ...this.#constants }; + this.#constants = {}; } this.#inputs = { ...this.configuration }; @@ -265,51 +271,75 @@ class NodeImpl< }); } - addInputsAsConstants(values: InputValues) { - this.configuration = { ...this.configuration, ...values }; - } - addInputsAsValues(values: InputsMaybeAsValues) { // Split into constants and nodes const constants: Partial = {}; - const nodes: [NodeImpl, KeyMap][] = []; + const nodes: [NodeImpl, KeyMap, boolean][] = []; Object.entries(values).forEach(([key, value]) => { if (isValue(value)) { nodes.push((value as Value).as(key).asNodeInput()); } else if (value instanceof NodeImpl) { - nodes.push([value.unProxy(), { [key]: key }]); + nodes.push([value.unProxy(), { [key]: key }, false]); } else { constants[key] = value; } }); - this.addInputsAsConstants(constants); - nodes.forEach((node) => this.addInputAsNode(...node)); + this.#constants = { ...this.#constants, ...constants }; + nodes.forEach((node) => this.addInputsFromNode(...node)); } // Add inputs from another node as edges - addInputAsNode(from: NodeImpl, keymap: KeyMap = { "*": "*" }) { - Object.entries(keymap).forEach(([fromKey, toKey]) => { - // "*-" means "all outputs from " and comes from using a node in a - // spread, e.g. newNode({ ...node, $id: "id" } - if (fromKey.startsWith("*-")) fromKey = toKey = "*"; + addInputsFromNode( + from: NodeImpl, + keymap: KeyMap = { "*": "*" }, + constant?: boolean + ) { + const keyPairs = Object.entries(keymap); + if (keyPairs.length === 0) { + // Add an empty edge: Just control flow, no data moving. const edge: EdgeImpl = { to: this as unknown as NodeImpl, from, - out: fromKey, - in: toKey, + out: "", + in: "", }; - this.incoming.push(edge); from.outgoing.push(edge); - }); + } else + keyPairs.forEach(([fromKey, toKey]) => { + // "*-" means "all outputs from " and comes from using a node in a + // spread, e.g. newNode({ ...node, $id: "id" } + if (fromKey.startsWith("*-")) fromKey = toKey = "*"; + + const edge: EdgeImpl = { + to: this as unknown as NodeImpl, + from, + out: fromKey, + in: toKey, + }; + + if (constant) edge.constant = true; + + this.incoming.push(edge); + from.outgoing.push(edge); + }); } - receiveInputs(inputs: Partial, from: NodeImpl) { - this.#inputs = { ...this.#inputs, ...inputs }; - this.#receivedFrom.push(from); + receiveInputs(edge: EdgeImpl, inputs: InputValues) { + const data = + edge.out === "*" + ? inputs + : edge.out === "" + ? {} + : { [edge.in]: inputs[edge.out] }; + + if (edge.constant) this.#constants = { ...this.#constants, ...data }; + + this.#inputs = { ...this.#inputs, ...data }; + this.#receivedFrom.push(edge.from); } /** @@ -335,7 +365,10 @@ class NodeImpl< .map((edge) => edge.from) ); - const presentKeys = new Set(Object.keys(this.#inputs)); + const presentKeys = new Set([ + ...Object.keys(this.#inputs), + ...Object.keys(this.#constants), + ]); const presentNodes = new Set(this.#receivedFrom); return ( @@ -366,7 +399,7 @@ class NodeImpl< this.#outputs = result; - this.#inputs = { ...this.configuration }; + this.#inputs = { ...this.configuration, ...this.#constants }; this.#receivedFrom = []; return result; @@ -379,7 +412,7 @@ class NodeImpl< } async serialize(metadata?: GraphMetadata) { - return this.#runner.serialize(this, metadata); + return this.#runner.serialize(this as unknown as NodeImpl, metadata); } async serializeNode(): Promise<[NodeDescriptor, GraphDescriptor?]> { @@ -410,11 +443,11 @@ class NodeImpl< // otherwise connect the returned node's outputs to the output node. if (result.unProxy().type === "output") return runner.serialize(result as unknown as NodeImpl); - outputNode.addInputAsNode(result.unProxy()); + outputNode.addInputsFromNode(result.unProxy()); } else if (isValue(result)) { // Wire up the value to the output node const value = isValue(result) as Value; - outputNode.addInputAsNode(...value.asNodeInput()); + outputNode.addInputsFromNode(...value.asNodeInput()); } else { // Otherwise wire up all keys of the returned object to the output. let output = await result; @@ -428,14 +461,16 @@ class NodeImpl< // Refactor to merge with similar code in constructor Object.keys(output).forEach((key) => isValue(output[key]) - ? outputNode.addInputAsNode( + ? outputNode.addInputsFromNode( ...(output[key] as Value).as(key).asNodeInput() ) : output[key] instanceof NodeImpl - ? outputNode.addInputAsNode(output[key] as NodeImpl, { + ? outputNode.addInputsFromNode(output[key] as NodeImpl, { [key]: key, }) - : (outputNode.configuration[key] = output[key]) + : (outputNode.configuration[key as keyof O] = output[ + key + ] as (typeof outputNode.configuration)[keyof O]) ); } return runner.serialize(outputNode as unknown as NodeImpl); @@ -615,7 +650,7 @@ class NodeImpl< // TODO: Ideally we would look at the schema here and use * only if // the output is open ended and/or not all fields are present all the time. - toNode.addInputAsNode(this as unknown as NodeImpl, { "*": "*" }); + toNode.addInputsFromNode(this as unknown as NodeImpl, { "*": "*" }); return (toNode as NodeImpl).asProxy(); } @@ -628,10 +663,10 @@ class NodeImpl< ) { if (inputs instanceof NodeImpl) { const node = inputs as NodeImpl; - this.addInputAsNode(node); + this.addInputsFromNode(node); } else if (isValue(inputs)) { const value = inputs as Value; - this.addInputAsNode(...value.asNodeInput()); + this.addInputsFromNode(...value.asNodeInput()); } else { const values = inputs as InputsMaybeAsValues; this.addInputsAsValues(values); @@ -676,16 +711,19 @@ class Value #node: NodeImpl>; #runner: Runner; #keymap: KeyMap; + #constant: boolean; constructor( node: NodeImpl>, runner: Runner, - keymap: string | KeyMap + keymap: string | KeyMap, + constant = false ) { this.#node = node; this.#runner = runner; this.#keymap = typeof keymap === "string" ? { [keymap]: keymap } : keymap; (this as unknown as { [key: symbol]: Value })[IsValueSymbol] = this; + this.#constant = constant; } then( @@ -707,11 +745,13 @@ class Value asNodeInput(): [ NodeImpl, - { [key: string]: string } + { [key: string]: string }, + constant: boolean ] { return [ this.#node.unProxy() as NodeImpl, this.#keymap, + this.#constant, ]; } @@ -734,11 +774,16 @@ class Value config as OutputValue & ToC ); - toNode.addInputAsNode(this.#node as unknown as NodeImpl, this.#keymap); + toNode.addInputsFromNode( + this.#node as unknown as NodeImpl, + this.#keymap, + this.#constant + ); return (toNode as NodeImpl & ToC, ToO>).asProxy(); } + // TODO: Double check this, as it's acting on output types, not input types. in(inputs: NodeImpl | InputValues) { if (inputs instanceof NodeImpl || isValue(inputs)) { let invertedMap = Object.fromEntries( @@ -747,9 +792,9 @@ class Value const asValue = isValue(inputs); if (asValue) { invertedMap = asValue.#remapKeys(invertedMap); - this.#node.addInputAsNode(asValue.#node, invertedMap); + this.#node.addInputsFromNode(asValue.#node, invertedMap); } else { - this.#node.addInputAsNode(inputs as NodeImpl, invertedMap); + this.#node.addInputsFromNode(inputs as NodeImpl, invertedMap); } } else { this.#node.addInputsAsValues(inputs); @@ -766,7 +811,11 @@ class Value newMap = this.#remapKeys(newKey); } - return new Value(this.#node, this.#runner, newMap); + return new Value(this.#node, this.#runner, newMap, this.#constant); + } + + memoize() { + return new Value(this.#node, this.#runner, this.#keymap, true); } #remapKeys(newKeys: KeyMap) { @@ -846,9 +895,7 @@ export class Runner { // Distribute data to outgoing edges node.outgoing.forEach((edge) => { - const data = - edge.out === "*" ? result : { [edge.in]: result[edge.out] }; - edge.to.receiveInputs(data, node); + edge.to.receiveInputs(edge, result); // If it's ready to run, add it to the queue if (edge.to.hasAllRequiredInputs()) queue.push(edge.to); @@ -878,6 +925,7 @@ export class Runner { to: edge.to.id, out: edge.out, in: edge.in, + ...(edge.constant ? { constant: true } : {}), })) );