From 993fafe3ebbb912953ed14c17f4bce42d405ce78 Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Fri, 31 May 2024 17:11:28 -0400 Subject: [PATCH] fix: make prompt explicit A caller must now use `prompt: true` to enable prompting of the user. This explicit opt-in ensures a caller knows they are required to handle the prompt event. If a prompt event occurs when a caller has not explicitly opted-in, then the run fails with an error. --- README.md | 39 +++++++++++++++++++++++++++++++++++++++ src/gptscript.ts | 22 +++++++++++++++++----- tests/gptscript.test.ts | 22 +++++++++++++++++++++- 3 files changed, 77 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 71f8ccb..29fb3e0 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ None of the options is required, and the defaults will reduce the number of call - `workspace`: Directory to use for the workspace, if specified it will not be deleted on exit - `chatState`: The chat state to continue, or null to start a new chat and return the state - `confirm`: Prompt before running potentially dangerous commands +- `prompt`: Allow scripts to prompt the user for input ## Functions @@ -227,6 +228,44 @@ async function streamExecFileWithEvents() { } ``` +### Prompt + +A gptscript may need to prompt the user for information like credentials. A user should listen for +the `RunEventType.Prompt`. Note that if `prompt: true` is not set in the options, then an error will occur if a +gptscript attempts to prompt the user. + +```javascript +const gptscript = require('@gptscript-ai/gptscript'); + +const opts = { + disableCache: true, + input: "--testin how high is that there mouse?", + confirm: true +}; + +async function streamExecFileWithEvents() { + const client = new gptscript.Client(); + try { + const run = await client.run('./test.gpt', opts); + + run.on(gptscript.RunEventType.Prompt, async (data: gptscript.PromptFrame) => { + // data will have the information for what the gptscript is prompting. + + await client.promptResponse({ + id: data.id, + // response is a map of fields to values + responses: {[data.fields[0]]: "Some Value"} + }) + }); + + await run.text(); + } catch (e) { + console.error(e); + } + client.close(); +} +``` + ### Chat support For tools that support chat, you can use the `nextChat` method on the run object to continue the chat. This method takes diff --git a/src/gptscript.ts b/src/gptscript.ts index 37bdc97..2b15145 100644 --- a/src/gptscript.ts +++ b/src/gptscript.ts @@ -12,6 +12,7 @@ export interface RunOpts { workspace?: string chatState?: string confirm?: boolean + prompt?: boolean } export enum RunEventType { @@ -344,7 +345,7 @@ export class Run { }) res.on("aborted", () => { - if (this.state !== RunState.Finished) { + if (this.state !== RunState.Finished && this.state !== RunState.Error) { this.state = RunState.Error this.err = "Run has been aborted" reject(this.err) @@ -352,15 +353,19 @@ export class Run { }) res.on("error", (error: Error) => { - this.state = RunState.Error - this.err = error.message || "" + if (this.state !== RunState.Error) { + this.state = RunState.Error + this.err = error.message || "" + } reject(this.err) }) }) this.req.on("error", (error: Error) => { - this.state = RunState.Error - this.err = error.message || "" + if (this.state !== RunState.Error) { + this.state = RunState.Error + this.err = error.message || "" + } reject(this.err) }) @@ -434,6 +439,13 @@ export class Run { this.state = RunState.Creating } + if (f.type === RunEventType.Prompt && !this.opts.prompt) { + this.state = RunState.Error + this.err = `prompt occurred when prompt was not allowed: Message: ${f.message}\nFields: ${f.fields}\nSensitive: ${f.sensitive}` + this.close() + return "" + } + if (f.type === RunEventType.RunStart) { this.state = RunState.Running } else if (f.type === RunEventType.RunFinish) { diff --git a/tests/gptscript.test.ts b/tests/gptscript.test.ts index a27da88..27cb1f8 100644 --- a/tests/gptscript.test.ts +++ b/tests/gptscript.test.ts @@ -435,7 +435,7 @@ describe("gptscript module", () => { instructions: "Use the sys.prompt user to ask the user for 'first name' which is not sensitive. After you get their first name, say hello.", tools: ["sys.prompt"] } - const run = await client.evaluate(t as any) + const run = await client.evaluate(t as any, {prompt: true}) run.on(gptscript.RunEventType.Prompt, async (data: gptscript.PromptFrame) => { expect(data.message).toContain("first name") expect(data.fields.length).toEqual(1) @@ -450,4 +450,24 @@ describe("gptscript module", () => { expect(run.err).toEqual("") expect(promptFound).toBeTruthy() }) + + test("prompt without prompt allowed should fail", async () => { + let promptFound = false + const t = { + instructions: "Use the sys.prompt user to ask the user for 'first name' which is not sensitive. After you get their first name, say hello.", + tools: ["sys.prompt"] + } + const run = await client.evaluate(t as any) + run.on(gptscript.RunEventType.Prompt, async (data: gptscript.PromptFrame) => { + promptFound = true + }) + + try { + await run.text() + } catch (e) { + expect(e).toContain("prompt occurred") + } + expect(run.err).toContain("prompt occurred") + expect(promptFound).toBeFalsy() + }) })