From ea600078934fa354cbe5b6995cb35b33b5c9db91 Mon Sep 17 00:00:00 2001 From: Dimitri Glazkov Date: Wed, 17 Apr 2024 10:33:04 -0700 Subject: [PATCH 1/3] split runner out. --- .../team-experiments/src/breadboard/runner.ts | 64 +++++++++++++++++ .../src/ui/elements/team-job/team-job.ts | 70 ++----------------- 2 files changed, 71 insertions(+), 63 deletions(-) create mode 100644 seeds/team-experiments/src/breadboard/runner.ts diff --git a/seeds/team-experiments/src/breadboard/runner.ts b/seeds/team-experiments/src/breadboard/runner.ts new file mode 100644 index 00000000..cc21e04a --- /dev/null +++ b/seeds/team-experiments/src/breadboard/runner.ts @@ -0,0 +1,64 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { InputValues } from "@google-labs/breadboard"; +import { RunConfig, run } from "@google-labs/breadboard/harness"; +import { InputResolveRequest } from "@google-labs/breadboard/remote"; +import { RunInputRequestEvent, RunOutputProvideEvent } from "../events/events"; + +// TODO: Decide if this interaction model is better. +export class Runner extends EventTarget { + #run: ReturnType | null = null; + #pendingInput: ((data: InputResolveRequest) => Promise) | null = null; + + start(config: RunConfig) { + this.#run = run(config); + this.#pendingInput = null; + this.resume(); + } + + finished() { + return !this.#run; + } + + waitingForInputs() { + return !!this.#pendingInput; + } + + provideInputs(inputs: InputValues) { + if (!this.#pendingInput) { + return false; + } + this.#pendingInput({ inputs }); + this.#pendingInput = null; + this.resume(); + } + + async resume(): Promise { + if (!this.#run) return false; + if (this.waitingForInputs()) return true; + + for (;;) { + const result = await this.#run.next(); + if (result.done) { + this.#run = null; + return false; + } + const { type, data, reply } = result.value; + switch (type) { + case "input": { + this.#pendingInput = reply; + this.dispatchEvent(new RunInputRequestEvent(data)); + return true; + } + case "output": { + this.dispatchEvent(new RunOutputProvideEvent(data)); + break; + } + } + } + } +} diff --git a/seeds/team-experiments/src/ui/elements/team-job/team-job.ts b/seeds/team-experiments/src/ui/elements/team-job/team-job.ts index cdc8e1ff..97faa462 100644 --- a/seeds/team-experiments/src/ui/elements/team-job/team-job.ts +++ b/seeds/team-experiments/src/ui/elements/team-job/team-job.ts @@ -16,7 +16,6 @@ import { // Mock data - to replace. import { assetItems, jobDescription } from "../../../mock/assets.js"; import { activityItems } from "../../../mock/activity.js"; -import { RunConfig, run } from "@google-labs/breadboard/harness"; import { ConversationItem, ItemFormat, @@ -24,64 +23,9 @@ import { Participant, TeamListItem, } from "../../../types/types.js"; -import { InputValues } from "@google-labs/breadboard"; -import { InputResolveRequest } from "@google-labs/breadboard/remote"; import { clamp } from "../../utils/clamp.js"; import { Switcher } from "../elements.js"; - -// TODO: Decide if this interaction model is better. -class Run extends EventTarget { - #run: ReturnType | null; - #pendingInput: ((data: InputResolveRequest) => Promise) | null; - - constructor(config: RunConfig) { - super(); - this.#run = run(config); - this.#pendingInput = null; - } - - finished() { - return !this.#run; - } - - waitingForInputs() { - return !!this.#pendingInput; - } - - provideInputs(inputs: InputValues) { - if (!this.#pendingInput) { - return false; - } - this.#pendingInput({ inputs }); - this.#pendingInput = null; - this.resume(); - } - - async resume(): Promise { - if (!this.#run) return false; - if (this.waitingForInputs()) return true; - - for (;;) { - const result = await this.#run.next(); - if (result.done) { - this.#run = null; - return false; - } - const { type, data, reply } = result.value; - switch (type) { - case "input": { - this.#pendingInput = reply; - this.dispatchEvent(new RunInputRequestEvent(data)); - return true; - } - case "output": { - this.dispatchEvent(new RunOutputProvideEvent(data)); - break; - } - } - } - } -} +import { Runner } from "../../../breadboard/runner.js"; @customElement("at-team-job") export class TeamJob extends LitElement { @@ -109,17 +53,14 @@ export class TeamJob extends LitElement { } `; - #run: Run | null = null; + #run: Runner | null = null; #addConversationItem(item: ConversationItem) { this.conversation = [...this.conversation, item]; } async #startRun() { - this.#run = new Run({ - url: "/bgl/insta/mock-conversation.bgl.json", - kits: [], - }); + this.#run = new Runner(); this.#run.addEventListener(RunOutputProvideEvent.eventName, (evt) => { const e = evt as RunOutputProvideEvent; const { outputs, timestamp } = e.data; @@ -155,7 +96,10 @@ export class TeamJob extends LitElement { const schema = e.data.inputArguments.schema; this.inputValue = schema?.properties?.text.examples?.[0] || ""; }); - this.#run.resume(); + this.#run.start({ + url: "/bgl/insta/mock-conversation.bgl.json", + kits: [], + }); } connectedCallback(): void { From 973e4f91e507d2660352d79d20879e4f7e588e3a Mon Sep 17 00:00:00 2001 From: Dimitri Glazkov Date: Wed, 17 Apr 2024 10:55:17 -0700 Subject: [PATCH 2/3] Split out events. --- .../team-experiments/src/breadboard/README.md | 4 +++ .../team-experiments/src/breadboard/events.ts | 29 +++++++++++++++++++ .../team-experiments/src/breadboard/runner.ts | 10 ++----- seeds/team-experiments/src/events/events.ts | 17 ----------- .../src/ui/elements/team-job/team-job.ts | 12 ++++---- 5 files changed, 41 insertions(+), 31 deletions(-) create mode 100644 seeds/team-experiments/src/breadboard/README.md create mode 100644 seeds/team-experiments/src/breadboard/events.ts diff --git a/seeds/team-experiments/src/breadboard/README.md b/seeds/team-experiments/src/breadboard/README.md new file mode 100644 index 00000000..95ce2e20 --- /dev/null +++ b/seeds/team-experiments/src/breadboard/README.md @@ -0,0 +1,4 @@ +These files are all candidates for moving back into Breadboard or becoming +their own package that makes running boards in apps easier. + +This is why they are separated out. diff --git a/seeds/team-experiments/src/breadboard/events.ts b/seeds/team-experiments/src/breadboard/events.ts new file mode 100644 index 00000000..bc3a6e58 --- /dev/null +++ b/seeds/team-experiments/src/breadboard/events.ts @@ -0,0 +1,29 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { InputResponse, OutputResponse } from "@google-labs/breadboard"; + +const opts = { + composed: true, + bubbles: true, + cancelable: true, +}; + +export class RunInputEvent extends Event { + static readonly eventName = "input"; + + constructor(public data: InputResponse) { + super(RunInputEvent.eventName, { ...opts }); + } +} + +export class RunOutputEvent extends Event { + static readonly eventName = "output"; + + constructor(public data: OutputResponse) { + super(RunOutputEvent.eventName, { ...opts }); + } +} diff --git a/seeds/team-experiments/src/breadboard/runner.ts b/seeds/team-experiments/src/breadboard/runner.ts index cc21e04a..7337efb7 100644 --- a/seeds/team-experiments/src/breadboard/runner.ts +++ b/seeds/team-experiments/src/breadboard/runner.ts @@ -7,7 +7,7 @@ import { InputValues } from "@google-labs/breadboard"; import { RunConfig, run } from "@google-labs/breadboard/harness"; import { InputResolveRequest } from "@google-labs/breadboard/remote"; -import { RunInputRequestEvent, RunOutputProvideEvent } from "../events/events"; +import { RunInputEvent, RunOutputEvent } from "./events.js"; // TODO: Decide if this interaction model is better. export class Runner extends EventTarget { @@ -20,10 +20,6 @@ export class Runner extends EventTarget { this.resume(); } - finished() { - return !this.#run; - } - waitingForInputs() { return !!this.#pendingInput; } @@ -51,11 +47,11 @@ export class Runner extends EventTarget { switch (type) { case "input": { this.#pendingInput = reply; - this.dispatchEvent(new RunInputRequestEvent(data)); + this.dispatchEvent(new RunInputEvent(data)); return true; } case "output": { - this.dispatchEvent(new RunOutputProvideEvent(data)); + this.dispatchEvent(new RunOutputEvent(data)); break; } } diff --git a/seeds/team-experiments/src/events/events.ts b/seeds/team-experiments/src/events/events.ts index a7287d4c..d575d626 100644 --- a/seeds/team-experiments/src/events/events.ts +++ b/seeds/team-experiments/src/events/events.ts @@ -4,7 +4,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { InputResponse, OutputResponse } from "@google-labs/breadboard"; import type { ConversationInputPart, State } from "../types/types.js"; const opts = { @@ -29,22 +28,6 @@ export class ConversationItemCreateEvent extends Event { } } -export class RunInputRequestEvent extends Event { - static readonly eventName = "runinputrequest"; - - constructor(public data: InputResponse) { - super(RunInputRequestEvent.eventName, { ...opts }); - } -} - -export class RunOutputProvideEvent extends Event { - static readonly eventName = "runoutputprovide"; - - constructor(public data: OutputResponse) { - super(RunOutputProvideEvent.eventName, { ...opts }); - } -} - export class TeamSelectEvent extends Event { static readonly eventName = "teamselect"; diff --git a/seeds/team-experiments/src/ui/elements/team-job/team-job.ts b/seeds/team-experiments/src/ui/elements/team-job/team-job.ts index 97faa462..e724f9dc 100644 --- a/seeds/team-experiments/src/ui/elements/team-job/team-job.ts +++ b/seeds/team-experiments/src/ui/elements/team-job/team-job.ts @@ -8,8 +8,6 @@ import { LitElement, css, html } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { ConversationItemCreateEvent, - RunInputRequestEvent, - RunOutputProvideEvent, TeamSectionSelectEvent, } from "../../../events/events.js"; @@ -26,6 +24,7 @@ import { import { clamp } from "../../utils/clamp.js"; import { Switcher } from "../elements.js"; import { Runner } from "../../../breadboard/runner.js"; +import { RunInputEvent, RunOutputEvent } from "../../../breadboard/events.js"; @customElement("at-team-job") export class TeamJob extends LitElement { @@ -61,8 +60,8 @@ export class TeamJob extends LitElement { async #startRun() { this.#run = new Runner(); - this.#run.addEventListener(RunOutputProvideEvent.eventName, (evt) => { - const e = evt as RunOutputProvideEvent; + this.#run.addEventListener(RunOutputEvent.eventName, (evt) => { + const e = evt as RunOutputEvent; const { outputs, timestamp } = e.data; const role = "Team Lead"; if (outputs.text) { @@ -86,8 +85,8 @@ export class TeamJob extends LitElement { }); } }); - this.#run.addEventListener(RunInputRequestEvent.eventName, (evt) => { - const e = evt as RunInputRequestEvent; + this.#run.addEventListener(RunInputEvent.eventName, (evt) => { + const e = evt as RunInputEvent; // Nasty stuff. Should I use like, inspector API here? // Note, this diving into schema and the whole // this.inputValue is only needed to grab sample @@ -136,7 +135,6 @@ export class TeamJob extends LitElement { if (!this.#run) return; - if (this.#run.finished()) return; if (!this.#run.waitingForInputs()) return; this.#run.provideInputs({ text: evt.message }); From 5de4ee7f218af920ea6929233ceb563db8b0528f Mon Sep 17 00:00:00 2001 From: Dimitri Glazkov Date: Wed, 17 Apr 2024 11:44:40 -0700 Subject: [PATCH 3/3] Properly implement event handling. --- .../team-experiments/src/breadboard/events.ts | 9 ++-- .../team-experiments/src/breadboard/index.ts | 7 +++ .../team-experiments/src/breadboard/runner.ts | 9 ++-- .../team-experiments/src/breadboard/types.ts | 43 +++++++++++++++++++ .../src/ui/elements/team-job/team-job.ts | 9 ++-- 5 files changed, 63 insertions(+), 14 deletions(-) create mode 100644 seeds/team-experiments/src/breadboard/index.ts create mode 100644 seeds/team-experiments/src/breadboard/types.ts diff --git a/seeds/team-experiments/src/breadboard/events.ts b/seeds/team-experiments/src/breadboard/events.ts index bc3a6e58..255ffa44 100644 --- a/seeds/team-experiments/src/breadboard/events.ts +++ b/seeds/team-experiments/src/breadboard/events.ts @@ -5,6 +5,7 @@ */ import { InputResponse, OutputResponse } from "@google-labs/breadboard"; +import { RunInputEvent, RunOutputEvent } from "./types.js"; const opts = { composed: true, @@ -12,18 +13,18 @@ const opts = { cancelable: true, }; -export class RunInputEvent extends Event { +export class InputEvent extends Event implements RunInputEvent { static readonly eventName = "input"; constructor(public data: InputResponse) { - super(RunInputEvent.eventName, { ...opts }); + super(InputEvent.eventName, { ...opts }); } } -export class RunOutputEvent extends Event { +export class OutputEvent extends Event implements RunOutputEvent { static readonly eventName = "output"; constructor(public data: OutputResponse) { - super(RunOutputEvent.eventName, { ...opts }); + super(OutputEvent.eventName, { ...opts }); } } diff --git a/seeds/team-experiments/src/breadboard/index.ts b/seeds/team-experiments/src/breadboard/index.ts new file mode 100644 index 00000000..ae89452a --- /dev/null +++ b/seeds/team-experiments/src/breadboard/index.ts @@ -0,0 +1,7 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export { Runner } from "./runner.js"; diff --git a/seeds/team-experiments/src/breadboard/runner.ts b/seeds/team-experiments/src/breadboard/runner.ts index 7337efb7..92766562 100644 --- a/seeds/team-experiments/src/breadboard/runner.ts +++ b/seeds/team-experiments/src/breadboard/runner.ts @@ -7,10 +7,11 @@ import { InputValues } from "@google-labs/breadboard"; import { RunConfig, run } from "@google-labs/breadboard/harness"; import { InputResolveRequest } from "@google-labs/breadboard/remote"; -import { RunInputEvent, RunOutputEvent } from "./events.js"; +import { InputEvent, OutputEvent } from "./events.js"; +import { RunEventTarget } from "./types.js"; // TODO: Decide if this interaction model is better. -export class Runner extends EventTarget { +export class Runner extends (EventTarget as RunEventTarget) implements Runner { #run: ReturnType | null = null; #pendingInput: ((data: InputResolveRequest) => Promise) | null = null; @@ -47,11 +48,11 @@ export class Runner extends EventTarget { switch (type) { case "input": { this.#pendingInput = reply; - this.dispatchEvent(new RunInputEvent(data)); + this.dispatchEvent(new InputEvent(data)); return true; } case "output": { - this.dispatchEvent(new RunOutputEvent(data)); + this.dispatchEvent(new OutputEvent(data)); break; } } diff --git a/seeds/team-experiments/src/breadboard/types.ts b/seeds/team-experiments/src/breadboard/types.ts new file mode 100644 index 00000000..3a04cd64 --- /dev/null +++ b/seeds/team-experiments/src/breadboard/types.ts @@ -0,0 +1,43 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { InputResponse, OutputResponse } from "@google-labs/breadboard"; + +type RunEventMap = { + input: RunInputEvent; + output: RunOutputEvent; +}; + +export type RunInputEvent = Event & { + data: InputResponse; +}; + +export type RunOutputEvent = Event & { + data: OutputResponse; +}; + +export type TypedEventTarget = { + new (): IntermediateEventTarget; +}; + +// TODO: This needs to be a more global helper. +interface IntermediateEventTarget extends EventTarget { + addEventListener( + type: K, + callback: ( + event: EventMap[K] extends Event ? EventMap[K] : never + ) => EventMap[K] extends Event ? void : never, + options?: boolean | AddEventListenerOptions + ): void; + + addEventListener( + type: string, + callback: EventListenerOrEventListenerObject | null, + options?: EventListenerOptions | boolean + ): void; +} + +export type RunEventTarget = TypedEventTarget; diff --git a/seeds/team-experiments/src/ui/elements/team-job/team-job.ts b/seeds/team-experiments/src/ui/elements/team-job/team-job.ts index e724f9dc..a969b8a0 100644 --- a/seeds/team-experiments/src/ui/elements/team-job/team-job.ts +++ b/seeds/team-experiments/src/ui/elements/team-job/team-job.ts @@ -23,8 +23,7 @@ import { } from "../../../types/types.js"; import { clamp } from "../../utils/clamp.js"; import { Switcher } from "../elements.js"; -import { Runner } from "../../../breadboard/runner.js"; -import { RunInputEvent, RunOutputEvent } from "../../../breadboard/events.js"; +import { Runner } from "../../../breadboard/index.js"; @customElement("at-team-job") export class TeamJob extends LitElement { @@ -60,8 +59,7 @@ export class TeamJob extends LitElement { async #startRun() { this.#run = new Runner(); - this.#run.addEventListener(RunOutputEvent.eventName, (evt) => { - const e = evt as RunOutputEvent; + this.#run.addEventListener("output", (e) => { const { outputs, timestamp } = e.data; const role = "Team Lead"; if (outputs.text) { @@ -85,8 +83,7 @@ export class TeamJob extends LitElement { }); } }); - this.#run.addEventListener(RunInputEvent.eventName, (evt) => { - const e = evt as RunInputEvent; + this.#run.addEventListener("input", (e) => { // Nasty stuff. Should I use like, inspector API here? // Note, this diving into schema and the whole // this.inputValue is only needed to grab sample