From e73bcc0aaf216098fb03b565bd82700356b508c0 Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Wed, 3 Jul 2024 13:34:16 -0400 Subject: [PATCH 1/7] fix: ensure SDK server is running on shutdown and restart Signed-off-by: Donnie Adams --- src/gptscript.ts | 1452 +++++++++++++++++++++++----------------------- 1 file changed, 730 insertions(+), 722 deletions(-) diff --git a/src/gptscript.ts b/src/gptscript.ts index da5a738..9a2928c 100644 --- a/src/gptscript.ts +++ b/src/gptscript.ts @@ -5,612 +5,620 @@ import {fileURLToPath} from "url" import net from "net" export interface GlobalOpts { - APIKey?: string - BaseURL?: string - DefaultModel?: string - Env?: string[] + APIKey?: string + BaseURL?: string + DefaultModel?: string + Env?: string[] } function globalOptsToEnv(env: NodeJS.ProcessEnv, opts?: GlobalOpts) { - if (!opts) { - return - } - - if (opts.APIKey) { - env["OPENAI_API_KEY"] = opts.APIKey - } - if (opts.BaseURL) { - env["OPENAI_BASE_URL"] = opts.BaseURL - } - if (opts.DefaultModel) { - env["GPTSCRIPT_SDKSERVER_DEFAULT_MODEL"] = opts.DefaultModel - } + if (!opts) { + return + } + + if (opts.APIKey) { + env["OPENAI_API_KEY"] = opts.APIKey + } + if (opts.BaseURL) { + env["OPENAI_BASE_URL"] = opts.BaseURL + } + if (opts.DefaultModel) { + env["GPTSCRIPT_SDKSERVER_DEFAULT_MODEL"] = opts.DefaultModel + } } export interface RunOpts { - input?: string - disableCache?: boolean - quiet?: boolean - chdir?: string - subTool?: string - workspace?: string - chatState?: string - confirm?: boolean - prompt?: boolean - credentialOverrides?: string[] - env?: string[] - - APIKey?: string - BaseURL?: string - DefaultModel?: string + input?: string + disableCache?: boolean + quiet?: boolean + chdir?: string + subTool?: string + workspace?: string + chatState?: string + confirm?: boolean + prompt?: boolean + credentialOverrides?: string[] + env?: string[] + + APIKey?: string + BaseURL?: string + DefaultModel?: string } export enum RunEventType { - Event = "event", - RunStart = "runStart", - RunFinish = "runFinish", - CallStart = "callStart", - CallChat = "callChat", - CallSubCalls = "callSubCalls", - CallProgress = "callProgress", - CallConfirm = "callConfirm", - CallContinue = "callContinue", - CallFinish = "callFinish", - - Prompt = "prompt" + Event = "event", + RunStart = "runStart", + RunFinish = "runFinish", + CallStart = "callStart", + CallChat = "callChat", + CallSubCalls = "callSubCalls", + CallProgress = "callProgress", + CallConfirm = "callConfirm", + CallContinue = "callContinue", + CallFinish = "callFinish", + + Prompt = "prompt" } export class GPTScript { - private static serverURL: string = "" - private static serverProcess: child_process.ChildProcess - private static instanceCount: number = 0 - - - private ready: boolean - - constructor(opts?: GlobalOpts) { - this.ready = false - GPTScript.instanceCount++ - if (!GPTScript.serverURL) { - GPTScript.serverURL = "http://" + (process.env.GPTSCRIPT_URL || "127.0.0.1:0") - } - if (GPTScript.instanceCount === 1 && process.env.GPTSCRIPT_DISABLE_SERVER !== "true") { - const u = new URL(GPTScript.serverURL) - if (u.port === "0") { - const srv = net.createServer() - const s = srv.listen(0, () => { - GPTScript.serverURL = "http://" + u.hostname + ":" + String((s.address() as net.AddressInfo).port) - srv.close() - - let env = process.env - if (opts && opts.Env) { - env = {} - for (const v of opts.Env) { - const equalIndex = v.indexOf("=") - if (equalIndex === -1) { - env[v] = "" - } else { - env[v.substring(0, equalIndex)] = v.substring(equalIndex + 1) - } - } - } - - globalOptsToEnv(env, opts) - - GPTScript.serverProcess = child_process.spawn(getCmdPath(), ["sys.sdkserver", "--listen-address", GPTScript.serverURL.replace("http://", "")], { - env: env, - stdio: ["pipe"] - }) - - process.on("exit", (code) => { - GPTScript.serverProcess.stdin?.end() - GPTScript.serverProcess.kill(code) - }) - }) - } - } - } - - close(): void { - GPTScript.instanceCount-- - if (GPTScript.instanceCount === 0 && GPTScript.serverProcess) { - GPTScript.serverProcess.kill("SIGTERM") - GPTScript.serverProcess.stdin?.end() - } - } - - listTools(): Promise { - return this.runBasicCommand("list-tools") - } - - listModels(): Promise { - return this.runBasicCommand("list-models") - } - - version(): Promise { - return this.runBasicCommand("version") - } - - async runBasicCommand(cmd: string): Promise { - if (!this.ready) { - this.ready = await this.testGPTScriptURL(20) - } - const r = new RunSubcommand(cmd, "", {}, GPTScript.serverURL) - r.requestNoStream(null) - return r.text() - } - - /** - * Runs a tool with the specified name and options. - * - * @param {string} toolName - The name of the tool to run. Can be a file path, URL, or GitHub URL. - * @param {RunOpts} [opts={}] - The options for running the tool. - * @return {Run} The Run object representing the running tool. - */ - async run(toolName: string, opts: RunOpts = {}): Promise { - if (!this.ready) { - this.ready = await this.testGPTScriptURL(20) - } - return (new Run("run", toolName, opts, GPTScript.serverURL)).nextChat(opts.input) - } - - /** - * Evaluates the given tool and returns a Run object. - * - * @param {ToolDef | ToolDef[]} tool - The tool to be evaluated. Can be a single ToolDef object or an array of ToolDef objects. - * @param {RunOpts} [opts={}] - Optional options for the evaluation. - * @return {Run} The Run object representing the evaluation. - */ - async evaluate(tool: ToolDef | ToolDef[], opts: RunOpts = {}): Promise { - if (!this.ready) { - this.ready = await this.testGPTScriptURL(20) - } - - return (new Run("evaluate", tool, opts, GPTScript.serverURL)).nextChat(opts.input) - } - - async parse(fileName: string): Promise { - if (!this.ready) { - this.ready = await this.testGPTScriptURL(20) - } - const r: Run = new RunSubcommand("parse", fileName, {}, GPTScript.serverURL) - r.request({file: fileName}) - return parseBlocksFromNodes((await r.json()).nodes) - } - - async parseTool(toolContent: string): Promise { - if (!this.ready) { - this.ready = await this.testGPTScriptURL(20) - } - const r: Run = new RunSubcommand("parse", "", {}, GPTScript.serverURL) - r.request({content: toolContent}) - return parseBlocksFromNodes((await r.json()).nodes) - } - - async stringify(blocks: Block[]): Promise { - if (!this.ready) { - this.ready = await this.testGPTScriptURL(20) - } - const nodes: any[] = [] - - for (const block of blocks) { - if (block.type === "tool") { - nodes.push({ - toolNode: { - tool: block - } - }) - } else if (block.type === "text") { - nodes.push({ - textNode: { - text: "!" + (block.format || "text") + "\n" + block.content - } - }) - } - } - - const r: Run = new RunSubcommand("fmt", "", {}, GPTScript.serverURL) - r.request({nodes: nodes}) - return r.text() - } - - async confirm(response: AuthResponse): Promise { - if (!this.ready) { - this.ready = await this.testGPTScriptURL(20) - } - const resp = await fetch(`${GPTScript.serverURL}/confirm/${response.id}`, { - method: "POST", - body: JSON.stringify(response) - }) - - if (resp.status < 200 || resp.status >= 400) { - throw new Error(`Failed to confirm ${response.id}: ${await resp.text()}`) - } - } - - async promptResponse(response: PromptResponse): Promise { - if (!this.ready) { - this.ready = await this.testGPTScriptURL(20) - } - const resp = await fetch(`${GPTScript.serverURL}/prompt-response/${response.id}`, { - method: "POST", - body: JSON.stringify(response.responses) - }) - - if (resp.status < 200 || resp.status >= 400) { - throw new Error(`Failed to respond to prompt ${response.id}: ${await resp.text()}`) - } - } - - private async testGPTScriptURL(count: number): Promise { - try { - await fetch(`${GPTScript.serverURL}/healthz`) - return true - } catch { - if (count === 0) { - throw new Error("Failed to wait for gptscript to be ready") - } - await new Promise(r => setTimeout(r, 500)) - return this.testGPTScriptURL(count - 1) - } - } + private static serverURL: string = "" + private static serverProcess: child_process.ChildProcess + private static instanceCount: number = 0 + + + private ready: boolean + + constructor(opts?: GlobalOpts) { + this.ready = false + GPTScript.instanceCount++ + if (!GPTScript.serverURL) { + GPTScript.serverURL = "http://" + (process.env.GPTSCRIPT_URL || "127.0.0.1:0") + } + if (GPTScript.instanceCount === 1 && process.env.GPTSCRIPT_DISABLE_SERVER !== "true") { + let env = process.env + if (opts && opts.Env) { + env = {} + for (const v of opts.Env) { + const equalIndex = v.indexOf("=") + if (equalIndex === -1) { + env[v] = "" + } else { + env[v.substring(0, equalIndex)] = v.substring(equalIndex + 1) + } + } + } + + globalOptsToEnv(env, opts) + process.on("exit", (code) => { + if (GPTScript.serverProcess) { + GPTScript.serverProcess.stdin?.end() + GPTScript.serverProcess.kill(code) + } + }) + + const u = new URL(GPTScript.serverURL) + if (u.port === "0") { + const srv = net.createServer() + const s = srv.listen(0, () => { + GPTScript.serverURL = "http://" + u.hostname + ":" + String((s.address() as net.AddressInfo).port) + srv.close() + + GPTScript.startGPTScriptProcess(env) + }) + } else { + GPTScript.startGPTScriptProcess(env) + } + } + } + + private static startGPTScriptProcess(env: NodeJS.ProcessEnv) { + GPTScript.serverProcess = child_process.spawn(getCmdPath(), ["sys.sdkserver", "--listen-address", GPTScript.serverURL.replace("http://", "")], { + env: env, + stdio: ["pipe"] + }) + } + + close(): void { + GPTScript.instanceCount-- + if (GPTScript.instanceCount === 0 && GPTScript.serverProcess) { + GPTScript.serverURL = "http://" + (process.env.GPTSCRIPT_URL || "127.0.0.1:0") + GPTScript.serverProcess.kill("SIGTERM") + GPTScript.serverProcess.stdin?.end() + } + } + + listTools(): Promise { + return this.runBasicCommand("list-tools") + } + + listModels(): Promise { + return this.runBasicCommand("list-models") + } + + version(): Promise { + return this.runBasicCommand("version") + } + + async runBasicCommand(cmd: string): Promise { + if (!this.ready) { + this.ready = await this.testGPTScriptURL(20) + } + const r = new RunSubcommand(cmd, "", {}, GPTScript.serverURL) + r.requestNoStream(null) + return r.text() + } + + /** + * Runs a tool with the specified name and options. + * + * @param {string} toolName - The name of the tool to run. Can be a file path, URL, or GitHub URL. + * @param {RunOpts} [opts={}] - The options for running the tool. + * @return {Run} The Run object representing the running tool. + */ + async run(toolName: string, opts: RunOpts = {}): Promise { + if (!this.ready) { + this.ready = await this.testGPTScriptURL(20) + } + return (new Run("run", toolName, opts, GPTScript.serverURL)).nextChat(opts.input) + } + + /** + * Evaluates the given tool and returns a Run object. + * + * @param {ToolDef | ToolDef[]} tool - The tool to be evaluated. Can be a single ToolDef object or an array of ToolDef objects. + * @param {RunOpts} [opts={}] - Optional options for the evaluation. + * @return {Run} The Run object representing the evaluation. + */ + async evaluate(tool: ToolDef | ToolDef[], opts: RunOpts = {}): Promise { + if (!this.ready) { + this.ready = await this.testGPTScriptURL(20) + } + + return (new Run("evaluate", tool, opts, GPTScript.serverURL)).nextChat(opts.input) + } + + async parse(fileName: string): Promise { + if (!this.ready) { + this.ready = await this.testGPTScriptURL(20) + } + const r: Run = new RunSubcommand("parse", fileName, {}, GPTScript.serverURL) + r.request({file: fileName}) + return parseBlocksFromNodes((await r.json()).nodes) + } + + async parseTool(toolContent: string): Promise { + if (!this.ready) { + this.ready = await this.testGPTScriptURL(20) + } + const r: Run = new RunSubcommand("parse", "", {}, GPTScript.serverURL) + r.request({content: toolContent}) + return parseBlocksFromNodes((await r.json()).nodes) + } + + async stringify(blocks: Block[]): Promise { + if (!this.ready) { + this.ready = await this.testGPTScriptURL(20) + } + const nodes: any[] = [] + + for (const block of blocks) { + if (block.type === "tool") { + nodes.push({ + toolNode: { + tool: block + } + }) + } else if (block.type === "text") { + nodes.push({ + textNode: { + text: "!" + (block.format || "text") + "\n" + block.content + } + }) + } + } + + const r: Run = new RunSubcommand("fmt", "", {}, GPTScript.serverURL) + r.request({nodes: nodes}) + return r.text() + } + + async confirm(response: AuthResponse): Promise { + if (!this.ready) { + this.ready = await this.testGPTScriptURL(20) + } + const resp = await fetch(`${GPTScript.serverURL}/confirm/${response.id}`, { + method: "POST", + body: JSON.stringify(response) + }) + + if (resp.status < 200 || resp.status >= 400) { + throw new Error(`Failed to confirm ${response.id}: ${await resp.text()}`) + } + } + + async promptResponse(response: PromptResponse): Promise { + if (!this.ready) { + this.ready = await this.testGPTScriptURL(20) + } + const resp = await fetch(`${GPTScript.serverURL}/prompt-response/${response.id}`, { + method: "POST", + body: JSON.stringify(response.responses) + }) + + if (resp.status < 200 || resp.status >= 400) { + throw new Error(`Failed to respond to prompt ${response.id}: ${await resp.text()}`) + } + } + + private async testGPTScriptURL(count: number): Promise { + try { + await fetch(`${GPTScript.serverURL}/healthz`) + return true + } catch { + if (count === 0) { + throw new Error("Failed to wait for gptscript to be ready") + } + await new Promise(r => setTimeout(r, 500)) + return this.testGPTScriptURL(count - 1) + } + } } export class Run { - public readonly id: string - public readonly opts: RunOpts - public readonly tools?: ToolDef | ToolDef[] | string - public state: RunState = RunState.Creating - public calls: Record = {} - public err: string = "" - - protected stdout?: string - - private readonly gptscriptURL?: string - private readonly requestPath: string = "" - private promise?: Promise - private req?: http.ClientRequest - private stderr?: string - private callbacks: Record void)[]> = {} - private chatState?: string - private parentCallId: string = "" - private prg?: Program - private respondingToolId?: string - - constructor(subCommand: string, tools: ToolDef | ToolDef[] | string, opts: RunOpts, gptscriptURL?: string) { - this.id = randomId("run-") - this.requestPath = subCommand - this.opts = opts - this.tools = tools - - this.gptscriptURL = gptscriptURL - } - - nextChat(input: string = ""): Run { - if (this.state !== RunState.Continue && this.state !== RunState.Creating && this.state !== RunState.Error) { - throw (new Error(`Run must in creating, continue or error state, not ${this.state}`)) - } - - let run = this - if (run.state !== RunState.Creating) { - run = new (this.constructor as any)(this.requestPath, this.tools, this.opts, this.gptscriptURL) - } - - if (this.chatState && this.state === RunState.Continue) { - // Only update the chat state if the previous run didn't error. - // The chat state on opts will be the chat state for the last successful run. - this.opts.chatState = this.chatState - } - run.opts.input = input - if (Array.isArray(this.tools)) { - run.request({toolDefs: this.tools, ...this.opts}) - } else if (typeof this.tools === "string") { - run.request({file: this.tools, ...this.opts}) - } else { - // In this last case, this.tools is a single ToolDef. - run.request({toolDefs: [this.tools], ...this.opts}) - } - - return run - } - - processStdout(data: string | object): string { - if (typeof data === "string") { - if (data.trim() === "") { - return "" - } - - try { - data = JSON.parse(data) - } catch (e) { - return data as string - } - } - - const out = data as ChatState - if (out.done === undefined || !out.done) { - this.chatState = JSON.stringify(out.state) - this.state = RunState.Continue - this.respondingToolId = out.toolId - } else { - this.state = RunState.Finished - this.chatState = undefined - } - - return "" - } - - request(tool: any) { - if (!this.gptscriptURL) { - throw new Error("request() requires gptscriptURL to be set") - } - const options = this.requestOptions(this.gptscriptURL, this.requestPath, tool) - options.headers = {"Transfer-Encoding": "chunked", ...options.headers} as any - - this.promise = new Promise(async (resolve, reject) => { - let frag = "" - this.req = http.request(options, (res: http.IncomingMessage) => { - this.state = RunState.Running - res.on("data", (chunk: any) => { - for (let line of (frag + chunk.toString()).split("\n")) { - const c = line.replace(/^(data: )/, "").trim() - if (!c) { - continue - } - - if (c === "[DONE]") { - return - } - - let e: any - try { - e = JSON.parse(c) - } catch { - frag = c - return - } - - if (e.stderr) { - this.stderr = (this.stderr || "") + (typeof e.stderr === "string" ? e.stderr : JSON.stringify(e.stderr)) - frag = "" - } else if (e.stdout) { - frag = this.processStdout(e.stdout) - } else { - frag = this.emitEvent(c) - } - } - }) - - res.on("end", () => { - if (this.state === RunState.Running || this.state === RunState.Finished || this.state === RunState.Continue) { - if (this.stdout) { - if (this.state !== RunState.Continue) { - this.state = RunState.Finished - } - resolve(this.stdout) - } else { - this.state = RunState.Error - reject(this.stderr) - } - } else if (this.state === RunState.Error) { - reject(this.err) - } - }) - - res.on("aborted", () => { - if (this.state !== RunState.Finished && this.state !== RunState.Error) { - this.state = RunState.Error - this.err = "Run has been aborted" - reject(this.err) - } - }) - - res.on("error", (error: Error) => { - if (this.state !== RunState.Error) { - this.state = RunState.Error - this.err = error.message || "" - } - reject(this.err) - }) - }) - - this.req.on("error", (error: Error) => { - if (this.state !== RunState.Error) { - this.state = RunState.Error - this.err = error.message || "" - } - reject(this.err) - }) - - this.req.write(JSON.stringify({...tool, ...this.opts})) - this.req.end() - }) - } - - requestNoStream(tool: any) { - if (!this.gptscriptURL) { - throw new Error("request() requires gptscriptURL to be set") - } - - const options = this.requestOptions(this.gptscriptURL, this.requestPath, tool) as any - if (tool) { - options.body = {...tool, ...this.opts} - } - const req = new Request(this.gptscriptURL + "/" + this.requestPath, options) - - this.promise = new Promise(async (resolve, reject) => { - fetch(req).then(resp => resp.json()).then(res => resolve(res.stdout)).catch(e => { - reject(e) - }) - }) - } - - requestOptions(gptscriptURL: string, path: string, tool: any) { - let method = "GET" - if (tool) { - method = "POST" - } - - const url = new URL(gptscriptURL) - - return { - hostname: url.hostname, - port: url.port || 80, - protocol: url.protocol || "http:", - path: "/" + path, - method: method, - headers: { - "Content-Type": "application/json" - }, - } - } - - public on(event: RunEventType.RunStart | RunEventType.RunFinish, listener: (data: RunFrame) => void): this; - public on(event: RunEventType.CallStart | RunEventType.CallProgress | RunEventType.CallContinue | RunEventType.CallChat | RunEventType.CallConfirm | RunEventType.CallFinish, listener: (data: CallFrame) => void): this; - public on(event: RunEventType.Prompt, listener: (data: PromptFrame) => void): this; - public on(event: RunEventType.Event, listener: (data: Frame) => void): this; - public on(event: RunEventType, listener: (data: any) => void): this { - if (!this.callbacks[event]) { - this.callbacks[event] = [] - } - - this.callbacks[event].push(listener) - - return this - } - - public text(): Promise { - if (this.err) { - throw new Error(this.err) - } - - if (!this.promise) { - throw new Error("Run not started") - } - - return this.promise - } - - public async json(): Promise { - return JSON.parse(await this.text()) - } - - public currentChatState(): string | undefined { - return this.chatState - } - - public parentCallFrame(): CallFrame | undefined { - if (this.parentCallId) { - return this.calls[this.parentCallId] - } - - return undefined - } - - public program(): Program | undefined { - return this.prg - } - - public respondingTool(): Tool | undefined { - return this.respondingToolId ? this.prg?.toolSet[this.respondingToolId] : undefined - } - - public close(): void { - if (this.req) { - this.req.destroy() - return - } - throw new Error("Run not started") - } - - private emitEvent(data: string): string { - for (let event of data.split("\n")) { - event = event.trim() - - if (!event) { - continue - } - let f: Frame - try { - const obj = JSON.parse(event) - if (obj.run) { - f = obj.run as Frame - } else if (obj.call) { - f = obj.call as Frame - } else if (obj.prompt) { - f = obj.prompt as Frame - } else { - return event - } - } catch (error) { - return event - } - - if (!this.state) { - 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 - this.prg = f.program - } else if (f.type === RunEventType.RunFinish) { - if (f.error) { - this.state = RunState.Error - this.err = f.error || "" - } else { - this.state = RunState.Finished - this.stdout = f.output || "" - } - } else if ((f.type as string).startsWith("call")) { - f = f as CallFrame - if (!f.parentID && this.parentCallId === "") { - this.parentCallId = f.id - } - this.calls[f.id] = f - } - - this.emit(RunEventType.Event, f) - this.emit(f.type, f) - } - - return "" - } - - private emit(event: RunEventType, data: any) { - for (const cb of this.callbacks[event] || []) { - cb(data) - } - } + public readonly id: string + public readonly opts: RunOpts + public readonly tools?: ToolDef | ToolDef[] | string + public state: RunState = RunState.Creating + public calls: Record = {} + public err: string = "" + + protected stdout?: string + + private readonly gptscriptURL?: string + private readonly requestPath: string = "" + private promise?: Promise + private req?: http.ClientRequest + private stderr?: string + private callbacks: Record void)[]> = {} + private chatState?: string + private parentCallId: string = "" + private prg?: Program + private respondingToolId?: string + + constructor(subCommand: string, tools: ToolDef | ToolDef[] | string, opts: RunOpts, gptscriptURL?: string) { + this.id = randomId("run-") + this.requestPath = subCommand + this.opts = opts + this.tools = tools + + this.gptscriptURL = gptscriptURL + } + + nextChat(input: string = ""): Run { + if (this.state !== RunState.Continue && this.state !== RunState.Creating && this.state !== RunState.Error) { + throw (new Error(`Run must in creating, continue or error state, not ${this.state}`)) + } + + let run = this + if (run.state !== RunState.Creating) { + run = new (this.constructor as any)(this.requestPath, this.tools, this.opts, this.gptscriptURL) + } + + if (this.chatState && this.state === RunState.Continue) { + // Only update the chat state if the previous run didn't error. + // The chat state on opts will be the chat state for the last successful run. + this.opts.chatState = this.chatState + } + run.opts.input = input + if (Array.isArray(this.tools)) { + run.request({toolDefs: this.tools, ...this.opts}) + } else if (typeof this.tools === "string") { + run.request({file: this.tools, ...this.opts}) + } else { + // In this last case, this.tools is a single ToolDef. + run.request({toolDefs: [this.tools], ...this.opts}) + } + + return run + } + + processStdout(data: string | object): string { + if (typeof data === "string") { + if (data.trim() === "") { + return "" + } + + try { + data = JSON.parse(data) + } catch (e) { + return data as string + } + } + + const out = data as ChatState + if (out.done === undefined || !out.done) { + this.chatState = JSON.stringify(out.state) + this.state = RunState.Continue + this.respondingToolId = out.toolId + } else { + this.state = RunState.Finished + this.chatState = undefined + } + + return "" + } + + request(tool: any) { + if (!this.gptscriptURL) { + throw new Error("request() requires gptscriptURL to be set") + } + const options = this.requestOptions(this.gptscriptURL, this.requestPath, tool) + options.headers = {"Transfer-Encoding": "chunked", ...options.headers} as any + + this.promise = new Promise(async (resolve, reject) => { + let frag = "" + this.req = http.request(options, (res: http.IncomingMessage) => { + this.state = RunState.Running + res.on("data", (chunk: any) => { + for (let line of (frag + chunk.toString()).split("\n")) { + const c = line.replace(/^(data: )/, "").trim() + if (!c) { + continue + } + + if (c === "[DONE]") { + return + } + + let e: any + try { + e = JSON.parse(c) + } catch { + frag = c + return + } + + if (e.stderr) { + this.stderr = (this.stderr || "") + (typeof e.stderr === "string" ? e.stderr : JSON.stringify(e.stderr)) + frag = "" + } else if (e.stdout) { + frag = this.processStdout(e.stdout) + } else { + frag = this.emitEvent(c) + } + } + }) + + res.on("end", () => { + if (this.state === RunState.Running || this.state === RunState.Finished || this.state === RunState.Continue) { + if (this.stdout) { + if (this.state !== RunState.Continue) { + this.state = RunState.Finished + } + resolve(this.stdout) + } else { + this.state = RunState.Error + reject(this.stderr) + } + } else if (this.state === RunState.Error) { + reject(this.err) + } + }) + + res.on("aborted", () => { + if (this.state !== RunState.Finished && this.state !== RunState.Error) { + this.state = RunState.Error + this.err = "Run has been aborted" + reject(this.err) + } + }) + + res.on("error", (error: Error) => { + if (this.state !== RunState.Error) { + this.state = RunState.Error + this.err = error.message || "" + } + reject(this.err) + }) + }) + + this.req.on("error", (error: Error) => { + if (this.state !== RunState.Error) { + this.state = RunState.Error + this.err = error.message || "" + } + reject(this.err) + }) + + this.req.write(JSON.stringify({...tool, ...this.opts})) + this.req.end() + }) + } + + requestNoStream(tool: any) { + if (!this.gptscriptURL) { + throw new Error("request() requires gptscriptURL to be set") + } + + const options = this.requestOptions(this.gptscriptURL, this.requestPath, tool) as any + if (tool) { + options.body = {...tool, ...this.opts} + } + const req = new Request(this.gptscriptURL + "/" + this.requestPath, options) + + this.promise = new Promise(async (resolve, reject) => { + fetch(req).then(resp => resp.json()).then(res => resolve(res.stdout)).catch(e => { + reject(e) + }) + }) + } + + requestOptions(gptscriptURL: string, path: string, tool: any) { + let method = "GET" + if (tool) { + method = "POST" + } + + const url = new URL(gptscriptURL) + + return { + hostname: url.hostname, + port: url.port || 80, + protocol: url.protocol || "http:", + path: "/" + path, + method: method, + headers: { + "Content-Type": "application/json" + }, + } + } + + public on(event: RunEventType.RunStart | RunEventType.RunFinish, listener: (data: RunFrame) => void): this; + public on(event: RunEventType.CallStart | RunEventType.CallProgress | RunEventType.CallContinue | RunEventType.CallChat | RunEventType.CallConfirm | RunEventType.CallFinish, listener: (data: CallFrame) => void): this; + public on(event: RunEventType.Prompt, listener: (data: PromptFrame) => void): this; + public on(event: RunEventType.Event, listener: (data: Frame) => void): this; + public on(event: RunEventType, listener: (data: any) => void): this { + if (!this.callbacks[event]) { + this.callbacks[event] = [] + } + + this.callbacks[event].push(listener) + + return this + } + + public text(): Promise { + if (this.err) { + throw new Error(this.err) + } + + if (!this.promise) { + throw new Error("Run not started") + } + + return this.promise + } + + public async json(): Promise { + return JSON.parse(await this.text()) + } + + public currentChatState(): string | undefined { + return this.chatState + } + + public parentCallFrame(): CallFrame | undefined { + if (this.parentCallId) { + return this.calls[this.parentCallId] + } + + return undefined + } + + public program(): Program | undefined { + return this.prg + } + + public respondingTool(): Tool | undefined { + return this.respondingToolId ? this.prg?.toolSet[this.respondingToolId] : undefined + } + + public close(): void { + if (this.req) { + this.req.destroy() + return + } + throw new Error("Run not started") + } + + private emitEvent(data: string): string { + for (let event of data.split("\n")) { + event = event.trim() + + if (!event) { + continue + } + let f: Frame + try { + const obj = JSON.parse(event) + if (obj.run) { + f = obj.run as Frame + } else if (obj.call) { + f = obj.call as Frame + } else if (obj.prompt) { + f = obj.prompt as Frame + } else { + return event + } + } catch (error) { + return event + } + + if (!this.state) { + 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 + this.prg = f.program + } else if (f.type === RunEventType.RunFinish) { + if (f.error) { + this.state = RunState.Error + this.err = f.error || "" + } else { + this.state = RunState.Finished + this.stdout = f.output || "" + } + } else if ((f.type as string).startsWith("call")) { + f = f as CallFrame + if (!f.parentID && this.parentCallId === "") { + this.parentCallId = f.id + } + this.calls[f.id] = f + } + + this.emit(RunEventType.Event, f) + this.emit(f.type, f) + } + + return "" + } + + private emit(event: RunEventType, data: any) { + for (const cb of this.callbacks[event] || []) { + cb(data) + } + } } class RunSubcommand extends Run { - constructor(subCommand: string, tool: ToolDef | ToolDef[] | string, opts: RunOpts, gptscriptURL?: string) { - super(subCommand, tool, opts, gptscriptURL) - } - - processStdout(data: string | object): string { - if (typeof data === "string") { - this.stdout = (this.stdout || "") + data - } else { - this.stdout = JSON.stringify(data) - } - - return "" - } + constructor(subCommand: string, tool: ToolDef | ToolDef[] | string, opts: RunOpts, gptscriptURL?: string) { + super(subCommand, tool, opts, gptscriptURL) + } + + processStdout(data: string | object): string { + if (typeof data === "string") { + this.stdout = (this.stdout || "") + data + } else { + this.stdout = JSON.stringify(data) + } + + return "" + } } interface ChatState { - state: string - done: boolean - content: string - toolId: string + state: string + done: boolean + content: string + toolId: string } export type Arguments = string | Record @@ -618,218 +626,218 @@ export type Arguments = string | Record export const ArgumentSchemaType = "object" as const export interface ArgumentSchema { - type: typeof ArgumentSchemaType - properties?: Record - required?: string[] + type: typeof ArgumentSchemaType + properties?: Record + required?: string[] } export interface Program { - name: string - toolSet: Record - openAPICache: Record + name: string + toolSet: Record + openAPICache: Record } export const PropertyType = "string" as const export interface Property { - type: typeof PropertyType - description: string - default?: string + type: typeof PropertyType + description: string + default?: string } export interface Repo { - VCS: string - Root: string - Path: string - Name: string - Revision: string + VCS: string + Root: string + Path: string + Name: string + Revision: string } export interface ToolDef { - name?: string - description?: string - maxTokens?: number - modelName?: string - modelProvider?: boolean - jsonResponse?: boolean - temperature?: number - cache?: boolean - chat?: boolean - internalPrompt?: boolean - arguments?: ArgumentSchema - tools?: string[] - globalTools?: string[] - globalModelName?: string - context?: string[] - exportContext?: string[] - export?: string[] - agents?: string[] - credentials?: string[] - instructions?: string + name?: string + description?: string + maxTokens?: number + modelName?: string + modelProvider?: boolean + jsonResponse?: boolean + temperature?: number + cache?: boolean + chat?: boolean + internalPrompt?: boolean + arguments?: ArgumentSchema + tools?: string[] + globalTools?: string[] + globalModelName?: string + context?: string[] + exportContext?: string[] + export?: string[] + agents?: string[] + credentials?: string[] + instructions?: string } export interface ToolReference { - named: string - reference: string - arg: string - toolID: string + named: string + reference: string + arg: string + toolID: string } export const ToolType = "tool" as const export interface Tool extends ToolDef { - id: string - type: typeof ToolType - toolMapping?: Record - localTools?: Record - source?: SourceRef - workingDir?: string + id: string + type: typeof ToolType + toolMapping?: Record + localTools?: Record + source?: SourceRef + workingDir?: string } export interface SourceRef { - location: string - lineNo: number - repo?: Repo + location: string + lineNo: number + repo?: Repo } export const TextType = "text" as const export interface Text { - id: string - type: typeof TextType - format: string - content: string + id: string + type: typeof TextType + format: string + content: string } export type Block = Tool | Text export enum RunState { - Creating = "creating", - Running = "running", - Continue = "continue", - Finished = "finished", - Error = "error" + Creating = "creating", + Running = "running", + Continue = "continue", + Finished = "finished", + Error = "error" } export enum ToolCategory { - ProviderToolCategory = "provider", - CredentialToolCategory = "credential", - ContextToolCategory = "context", - InputToolCategory = "input", - OutputToolCategory = "output", - NoCategory = "" + ProviderToolCategory = "provider", + CredentialToolCategory = "credential", + ContextToolCategory = "context", + InputToolCategory = "input", + OutputToolCategory = "output", + NoCategory = "" } export interface RunFrame { - id: string - type: RunEventType.RunStart | RunEventType.RunFinish - program: Program - input: string - output: string - error: string - start: string - end: string - state: RunState - chatState: any + id: string + type: RunEventType.RunStart | RunEventType.RunFinish + program: Program + input: string + output: string + error: string + start: string + end: string + state: RunState + chatState: any } export interface Call { - toolID: string - input?: string + toolID: string + input?: string } export interface Output { - content?: string - subCalls: Record + content?: string + subCalls: Record } export interface InputContext { - toolID: string - content: string + toolID: string + content: string } export interface Usage { - promptTokens: number - completionTokens: number - totalTokens: number + promptTokens: number + completionTokens: number + totalTokens: number } export interface CallFrame { - id: string - tool?: Tool - agentGroup?: ToolReference[] - currentAgent?: ToolReference - displayText?: string - inputContext: InputContext[] - toolCategory?: ToolCategory - toolName: string - parentID?: string - type: RunEventType.CallStart | RunEventType.CallChat | RunEventType.CallConfirm | RunEventType.CallContinue | RunEventType.CallSubCalls | RunEventType.CallProgress | RunEventType.CallFinish - start: string - end: string - input: Arguments - output: Output[] - error?: string - usage: Usage - llmRequest?: any - llmResponse?: any + id: string + tool?: Tool + agentGroup?: ToolReference[] + currentAgent?: ToolReference + displayText?: string + inputContext: InputContext[] + toolCategory?: ToolCategory + toolName: string + parentID?: string + type: RunEventType.CallStart | RunEventType.CallChat | RunEventType.CallConfirm | RunEventType.CallContinue | RunEventType.CallSubCalls | RunEventType.CallProgress | RunEventType.CallFinish + start: string + end: string + input: Arguments + output: Output[] + error?: string + usage: Usage + llmRequest?: any + llmResponse?: any } export interface PromptFrame { - id: string - type: RunEventType.Prompt - time: string - message: string - fields: string[] - sensitive: boolean + id: string + type: RunEventType.Prompt + time: string + message: string + fields: string[] + sensitive: boolean } export type Frame = RunFrame | CallFrame | PromptFrame export interface AuthResponse { - id: string - accept: boolean - message?: string + id: string + accept: boolean + message?: string } export interface PromptResponse { - id: string - responses: Record + id: string + responses: Record } function getCmdPath(): string { - if (process.env.GPTSCRIPT_BIN) { - return process.env.GPTSCRIPT_BIN - } + if (process.env.GPTSCRIPT_BIN) { + return process.env.GPTSCRIPT_BIN + } - return path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "bin", "gptscript" + (process.platform === "win32" ? ".exe" : "")) + return path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "bin", "gptscript" + (process.platform === "win32" ? ".exe" : "")) } function parseBlocksFromNodes(nodes: any[]): Block[] { - const blocks: Block[] = [] - for (const node of nodes) { - if (node.toolNode) { - if (!node.toolNode.tool.id) { - node.toolNode.tool.id = randomId("tool-") - } - blocks.push({ - type: "tool", - ...node.toolNode.tool, - } as Tool) - } - if (node.textNode) { - const format = node.textNode.text.substring(1, node.textNode.text.indexOf("\n")).trim() || "text" - blocks.push({ - id: randomId("text-"), - type: "text", - format: format, - content: node.textNode.text.substring(node.textNode.text.indexOf("\n") + 1).trim(), - } as Text) - } - } - return blocks + const blocks: Block[] = [] + for (const node of nodes) { + if (node.toolNode) { + if (!node.toolNode.tool.id) { + node.toolNode.tool.id = randomId("tool-") + } + blocks.push({ + type: "tool", + ...node.toolNode.tool, + } as Tool) + } + if (node.textNode) { + const format = node.textNode.text.substring(1, node.textNode.text.indexOf("\n")).trim() || "text" + blocks.push({ + id: randomId("text-"), + type: "text", + format: format, + content: node.textNode.text.substring(node.textNode.text.indexOf("\n") + 1).trim(), + } as Text) + } + } + return blocks } function randomId(prefix: string): string { - return prefix + Math.random().toString(36).substring(2, 12) + return prefix + Math.random().toString(36).substring(2, 12) } From ace6d2742ea0e5ea6bd35e938727c26f234d7f88 Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Tue, 16 Jul 2024 14:52:06 -0400 Subject: [PATCH 2/7] chore: add location to run opts Signed-off-by: Donnie Adams --- src/gptscript.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gptscript.ts b/src/gptscript.ts index 9a2928c..c3fcf2b 100644 --- a/src/gptscript.ts +++ b/src/gptscript.ts @@ -38,6 +38,7 @@ export interface RunOpts { confirm?: boolean prompt?: boolean credentialOverrides?: string[] + location?: string env?: string[] APIKey?: string From eed3a4a802a63327d947b3c8167b6ede35b694f9 Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Mon, 22 Jul 2024 11:21:28 -0400 Subject: [PATCH 3/7] fix: use separate gptscript for cred override on windows --- .../fixtures/credential-override-windows.gpt | 5 + tests/gptscript.test.ts | 1053 +++++++++-------- 2 files changed, 533 insertions(+), 525 deletions(-) create mode 100644 tests/fixtures/credential-override-windows.gpt diff --git a/tests/fixtures/credential-override-windows.gpt b/tests/fixtures/credential-override-windows.gpt new file mode 100644 index 0000000..7e5764c --- /dev/null +++ b/tests/fixtures/credential-override-windows.gpt @@ -0,0 +1,5 @@ +credentials: github.com/gptscript-ai/credential as test.ts.credential_override with TEST_CRED as env + +#!/usr/bin/env powershell.exe + +echo "$env:TEST_CRED" diff --git a/tests/gptscript.test.ts b/tests/gptscript.test.ts index 799b623..2920c6c 100644 --- a/tests/gptscript.test.ts +++ b/tests/gptscript.test.ts @@ -7,529 +7,532 @@ let g: gptscript.GPTScript const __dirname = path.dirname(fileURLToPath(import.meta.url)) describe("gptscript module", () => { - beforeAll(async () => { - if (!process.env.OPENAI_API_KEY && !process.env.GPTSCRIPT_URL) { - throw new Error("neither OPENAI_API_KEY nor GPTSCRIPT_URL is set") - } - - g = new gptscript.GPTScript({APIKey: process.env.OPENAI_API_KEY}) - }) - afterAll(() => { - g.close() - }) - - test("creating an closing another instance should work", async () => { - const other = new gptscript.GPTScript() - await other.version() - other.close() - }) - - test("listTools returns available tools", async () => { - const tools = await g.listTools() - expect(tools).toBeDefined() - }) - - test("listModels returns a list of models", async () => { - // Similar structure to listTools - let models = await g.listModels() - expect(models).toBeDefined() - }) - - test("version returns a gptscript version", async () => { - // Similar structure to listTools - let version = await g.version() - expect(version).toContain("gptscript version") - }) - - test("evaluate executes a prompt correctly", async () => { - const t = { - instructions: "who was the president of the united states in 1928?" - } - - const run = await g.evaluate(t) - expect(run).toBeDefined() - expect(await run.text()).toContain("Calvin Coolidge") - }) - - test("evaluate executes and streams a prompt correctly", async () => { - let out = "" - let err = undefined - const t = { - instructions: "who was the president of the united states in 1928?" - } - const opts = { - disableCache: true, - } - - const run = await g.evaluate(t, opts) - run.on(gptscript.RunEventType.CallProgress, (data: gptscript.CallFrame) => { - for (let output of data.output) out += `system: ${output.content}` - }) - - let callFinished = false - run.on(gptscript.RunEventType.CallFinish, (data: gptscript.CallFrame) => { - if (data.type == RunEventType.CallFinish) { - expect(callFinished).toBe(false) - callFinished = true - } - }) - - await run.text() - err = run.err - - expect(out).toContain("Calvin Coolidge") - expect(err).toEqual("") - expect(run.parentCallFrame()).toBeTruthy() - }) - - test("evaluate executes a prompt correctly with context", async () => { - let out = "" - let err = undefined - const t = { - instructions: "who was the president of the united states in 1928?", - context: [path.join(__dirname, "fixtures", "acorn-labs-context.gpt")] - } - - const run = await g.evaluate(t, {disableCache: true}) - out = await run.text() - err = run.err - - expect(out).toContain("Acorn Labs") - expect(err).toEqual("") - }) - - test("should execute test.gpt correctly", async () => { - const testGptPath = path.join(__dirname, "fixtures", "test.gpt") - - const result = await (await g.run(testGptPath)).text() - expect(result).toBeDefined() - expect(result).toContain("Calvin Coolidge") - }) - - test("should override credentials correctly", async () => { - const testGptPath = path.join(__dirname, "fixtures", "credential-override.gpt") - - const result = await (await g.run(testGptPath, { - disableCache: true, - credentialOverrides: ['test.ts.credential_override:TEST_CRED=foo'], - })).text() - - expect(result).toBeDefined() - expect(result).toContain("foo") - }) - - test("run executes and stream a file correctly", async () => { - let out = "" - let err = undefined - const testGptPath = path.join(__dirname, "fixtures", "test.gpt") - const opts = { - disableCache: true, - } - - const run = await g.run(testGptPath, opts) - run.on(gptscript.RunEventType.CallProgress, data => { - for (let output of data.output) out += `system: ${output.content}` - }) - await run.text() - err = run.err - - expect(out).toContain("Calvin Coolidge") - expect(err).toEqual("") - }) - - test("run executes and streams a file with global tools correctly", async () => { - let out = "" - let err = undefined - const testGptPath = path.join(__dirname, "fixtures", "global-tools.gpt") - const opts = { - disableCache: true, - } - - const run = await g.run(testGptPath, opts) - run.on(gptscript.RunEventType.CallProgress, data => { - for (let output of data.output) out += `system: ${output.content}` - }) - await run.text() - err = run.err - - expect(out).toContain("Hello!") - expect(err).toEqual("") - }, 15000) - - test("aborting a run is reported correctly", async () => { - let errMessage = "" - let err = undefined - const testGptPath = path.join(__dirname, "fixtures", "test.gpt") - const opts = { - disableCache: true, - } - - try { - const run = await g.run(testGptPath, opts) - run.on(gptscript.RunEventType.CallProgress, data => { - run.close() - }) - await run.text() - err = run.err - } catch (error: any) { - errMessage = error - } - - expect(errMessage).toContain("aborted") - expect(err).toBeUndefined() - }) - - - describe("evaluate with multiple tools", () => { - test("multiple tools", async () => { - const t0 = { - tools: ["ask"], - instructions: "Only use the ask tool to ask who was the president of the united states in 1928?" - } - const t1 = { - name: "ask", - description: "This tool is used to ask a question", - arguments: { - type: ArgumentSchemaType, - properties: { - question: { - type: PropertyType, - description: "The question to ask", - } - } - }, - instructions: "${question}" - } - - const response = await (await g.evaluate([t0, t1])).text() - expect(response).toBeDefined() - expect(response).toContain("Calvin Coolidge") - }, 30000) - - test("with sub tool", async () => { - const t0 = { - tools: ["ask"], - instructions: "Only use the ask tool to ask who was the president of the united states in 1928?" - } - const t1 = { - name: "other", - instructions: "Who was the president of the united states in 1986?" - } - const t2 = { - name: "ask", - description: "This tool is used to ask a question", - arguments: { - type: "object", - question: "The question to ask" - }, - instructions: "${question}" - } - - const response = await (await g.evaluate([t0, t1, t2], {subTool: "other"})).text() - expect(response).toBeDefined() - expect(response).toContain("Ronald Reagan") - }, 30000) - }) - - test("parse file", async () => { - const response = await g.parse(path.join(__dirname, "fixtures", "test.gpt")) - expect(response).toBeDefined() - expect(response).toHaveLength(1) - expect((response[0] as gptscript.Tool).instructions).toEqual("who was the president in 1928?") - }, 30000) - - test("parse string tool", async () => { - const tool = "How much wood would a woodchuck chuck if a woodchuck could chuck wood?" - const response = await g.parseTool(tool) - expect(response).toBeDefined() - expect(response).toHaveLength(1) - expect((response[0] as gptscript.Tool).instructions).toEqual(tool) - }, 30000) - - test("parse string tool with text node", async () => { - const tool = "How much wood would a woodchuck chuck if a woodchuck could chuck wood?\n---\n!markdown\nThis is a text node" - const response = await g.parseTool(tool) - expect(response).toBeDefined() - expect(response).toHaveLength(2) - expect((response[0] as gptscript.Tool).instructions).toEqual("How much wood would a woodchuck chuck if a woodchuck could chuck wood?") - expect((response[1] as gptscript.Text).content).toEqual("This is a text node") - }, 30000) - - test("parse string tool global tools", async () => { - const tool = "Global Tools: acorn, do-work\nHow much wood would a woodchuck chuck if a woodchuck could chuck wood?" - const response = await g.parseTool(tool) - expect(response).toBeDefined() - expect(response).toHaveLength(1) - expect((response[0] as gptscript.Tool).instructions).toEqual("How much wood would a woodchuck chuck if a woodchuck could chuck wood?") - expect((response[0] as gptscript.Tool).globalTools).toEqual(["acorn", "do-work"]) - }, 30000) - - test("parse string tool first line shebang", async () => { - const tool = "\n#!/usr/bin/env python\nHow much wood would a woodchuck chuck if a woodchuck could chuck wood?" - const response = await g.parseTool(tool) - expect(response).toBeDefined() - expect(response).toHaveLength(1) - expect((response[0] as gptscript.Tool).instructions).toEqual("#!/usr/bin/env python\nHow much wood would a woodchuck chuck if a woodchuck could chuck wood?") - }, 30000) - - test("format tool", async () => { - const tool = { - id: "my-tool", - type: ToolType, - tools: ["sys.write", "sys.read"], - instructions: "This is a test", - arguments: { - type: ArgumentSchemaType, - properties: { - text: { - type: PropertyType, - description: "The text to write" - } - } - } - } - - const response = await g.stringify([tool]) - expect(response).toBeDefined() - expect(response).toContain("Tools: sys.write, sys.read") - expect(response).toContain("This is a test") - expect(response).toContain("Parameter: text: The text to write") - }) - - test("exec tool with chat", async () => { - let err = undefined - const t = { - chat: true, - instructions: "You are a chat bot. Don't finish the conversation until I say 'bye'.", - tools: ["sys.chat.finish"] - } - const opts = { - disableCache: true, - } - let run = await g.evaluate(t, opts) - - const inputs = [ - "List the three largest states in the United States by area.", - "What is the capital of the third one?", - "What timezone is the first one in?" - ] - - const expectedOutputs = [ - "California", - "Sacramento", - "Alaska Time Zone" - ] - - await run.text() - for (let i: number = 0; i < inputs.length; i++) { - run = run.nextChat(inputs[i]) - err = run.err - - if (err) { - break - } - - expect(await run.text()).toContain(expectedOutputs[i]) - expect(run.state).toEqual(gptscript.RunState.Continue) - } - - run = run.nextChat("bye") - await run.text() - - expect(run.state).toEqual(gptscript.RunState.Finished) - expect(err).toEqual("") - }, 60000) - - test("exec file with chat", async () => { - let err = undefined - const opts = { - disableCache: true - } - let run = await g.run(path.join(__dirname, "fixtures", "chat.gpt"), opts) - - const inputs = [ - "List the 3 largest of the Great Lakes by volume.", - "What is the volume of the second one in cubic miles?", - "What is the total area of the third one in square miles?" - ] - - const expectedOutputs = [ - "Lake Superior", - "Lake Michigan", - "Lake Huron" - ] - - await run.text() - for (let i: number = 0; i < inputs.length; i++) { - run = run.nextChat(inputs[i]) - err = run.err - - if (err) { - break - } - - expect(await run.text()).toContain(expectedOutputs[i]) - expect(run.state).toEqual(gptscript.RunState.Continue) - } - - run = run.nextChat("bye") - await run.text() - - expect(run.state).toEqual(gptscript.RunState.Finished) - expect(err).toEqual("") - }, 60000) - - test("nextChat on file providing chat state", async () => { - let run = await g.run(path.join(__dirname, "fixtures", "chat.gpt"), {disableCache: true}) - - run = run.nextChat("List the 3 largest of the Great Lakes by volume.") - expect(await run.text()).toContain("Lake Superior") - expect(run.err).toEqual("") - expect(run.state).toEqual(gptscript.RunState.Continue) - - run = await g.run(path.join(__dirname, "fixtures", "chat.gpt"), { - disableCache: true, - input: "What is the total area of the third one in square miles?", - chatState: run.currentChatState() - }) - - expect(await run.text()).toContain("Lake Huron") - expect(run.err).toEqual("") - expect(run.state).toEqual(gptscript.RunState.Continue) - }, 10000) - - test("nextChat on tool providing chat state", async () => { - const t = { - chat: true, - instructions: "You are a chat bot. Don't finish the conversation until I say 'bye'.", - tools: ["sys.chat.finish"] - } - let run = await g.evaluate(t, {disableCache: true}) - - run = run.nextChat("List the three largest states in the United States by area.") - expect(await run.text()).toContain("California") - expect(run.err).toEqual("") - expect(run.state).toEqual(gptscript.RunState.Continue) - - run = await g.evaluate(t, { - disableCache: true, - input: "What is the capital of the second one?", - chatState: run.currentChatState() - }) - - expect(await run.text()).toContain("Austin") - expect(run.err).toEqual("") - expect(run.state).toEqual(gptscript.RunState.Continue) - }, 10000) - - test("confirm", async () => { - const t = { - instructions: "List the files in the current working directory.", - tools: ["sys.exec"] - } - - const commands = [`"ls"`, `"dir"`] - let confirmCallCount = 0 - const run = await g.evaluate(t, {confirm: true}) - run.on(gptscript.RunEventType.CallConfirm, async (data: gptscript.CallFrame) => { - // On Windows, ls is not always a command. The LLM will try to run dir in this case. Allow both. - expect(data.input).toContain(commands[confirmCallCount]) - confirmCallCount++ - await g.confirm({id: data.id, accept: true}) - }) - - expect(await run.text()).toContain("README.md") - expect(run.err).toEqual("") - expect(confirmCallCount > 0).toBeTruthy() - }) - - test("do not confirm", async () => { - let confirmFound = false - const t = { - instructions: "List the files in the current directory as '.'. If that doesn't work print the word FAIL.", - tools: ["sys.exec"] - } - const run = await g.evaluate(t, {confirm: true}) - run.on(gptscript.RunEventType.CallConfirm, async (data: gptscript.CallFrame) => { - expect(data.input).toContain(`"ls"`) - confirmFound = true - await g.confirm({id: data.id, accept: false, message: "I will not allow it!"}) - }) - - expect(await run.text()).toContain("FAIL") - expect(run.err).toEqual("") - expect(confirmFound).toBeTruthy() - }) - - test("prompt", 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 g.evaluate(t, {prompt: true}) - run.on(gptscript.RunEventType.Prompt, async (data: gptscript.PromptFrame) => { - expect(data.message).toContain("first name") - expect(data.fields.length).toEqual(1) - expect(data.fields[0]).toEqual("first name") - expect(data.sensitive).toBeFalsy() - - promptFound = true - await g.promptResponse({id: data.id, responses: {[data.fields[0]]: "Clicky"}}) - }) - - expect(await run.text()).toContain("Clicky") - 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 g.evaluate(t) - 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() - }) - - test("retry failed run", async () => { - let shebang = `#!/bin/bash\nexit \${EXIT_CODE}` - if (process.platform == "win32") { - shebang = "#!/usr/bin/env powershell.exe\n$e = $env:EXIT_CODE;\nif ($e) { Exit 1; }" - } - const t = { - instructions: "say hello", - context: ["my-context"] - } as gptscript.ToolDef - const contextTool = { - name: "my-context", - instructions: `${shebang}\nexit \${EXIT_CODE}` - } as gptscript.ToolDef - - let run = await g.evaluate([t, contextTool], {disableCache: true, env: ["EXIT_CODE=1"]}) - try { - await run.text() - } catch { - } - - expect(run.err).not.toEqual("") - - run.opts.env = [] - run = run.nextChat() - - await run.text() - - expect(run.err).toEqual("") - }) + beforeAll(async () => { + if (!process.env.OPENAI_API_KEY && !process.env.GPTSCRIPT_URL) { + throw new Error("neither OPENAI_API_KEY nor GPTSCRIPT_URL is set") + } + + g = new gptscript.GPTScript({APIKey: process.env.OPENAI_API_KEY}) + }) + afterAll(() => { + g.close() + }) + + test("creating an closing another instance should work", async () => { + const other = new gptscript.GPTScript() + await other.version() + other.close() + }) + + test("listTools returns available tools", async () => { + const tools = await g.listTools() + expect(tools).toBeDefined() + }) + + test("listModels returns a list of models", async () => { + // Similar structure to listTools + let models = await g.listModels() + expect(models).toBeDefined() + }) + + test("version returns a gptscript version", async () => { + // Similar structure to listTools + let version = await g.version() + expect(version).toContain("gptscript version") + }) + + test("evaluate executes a prompt correctly", async () => { + const t = { + instructions: "who was the president of the united states in 1928?" + } + + const run = await g.evaluate(t) + expect(run).toBeDefined() + expect(await run.text()).toContain("Calvin Coolidge") + }) + + test("evaluate executes and streams a prompt correctly", async () => { + let out = "" + let err = undefined + const t = { + instructions: "who was the president of the united states in 1928?" + } + const opts = { + disableCache: true, + } + + const run = await g.evaluate(t, opts) + run.on(gptscript.RunEventType.CallProgress, (data: gptscript.CallFrame) => { + for (let output of data.output) out += `system: ${output.content}` + }) + + let callFinished = false + run.on(gptscript.RunEventType.CallFinish, (data: gptscript.CallFrame) => { + if (data.type == RunEventType.CallFinish) { + expect(callFinished).toBe(false) + callFinished = true + } + }) + + await run.text() + err = run.err + + expect(out).toContain("Calvin Coolidge") + expect(err).toEqual("") + expect(run.parentCallFrame()).toBeTruthy() + }) + + test("evaluate executes a prompt correctly with context", async () => { + let out = "" + let err = undefined + const t = { + instructions: "who was the president of the united states in 1928?", + context: [path.join(__dirname, "fixtures", "acorn-labs-context.gpt")] + } + + const run = await g.evaluate(t, {disableCache: true}) + out = await run.text() + err = run.err + + expect(out).toContain("Acorn Labs") + expect(err).toEqual("") + }) + + test("should execute test.gpt correctly", async () => { + const testGptPath = path.join(__dirname, "fixtures", "test.gpt") + + const result = await (await g.run(testGptPath)).text() + expect(result).toBeDefined() + expect(result).toContain("Calvin Coolidge") + }) + + test("should override credentials correctly", async () => { + let testGptPath = path.join(__dirname, "fixtures", "credential-override.gpt") + if (process.platform === "win32") { + testGptPath = path.join(__dirname, "fixtures", "credential-override-windows.gpt") + } + + const result = await (await g.run(testGptPath, { + disableCache: true, + credentialOverrides: ["test.ts.credential_override:TEST_CRED=foo"], + })).text() + + expect(result).toBeDefined() + expect(result).toContain("foo") + }) + + test("run executes and stream a file correctly", async () => { + let out = "" + let err = undefined + const testGptPath = path.join(__dirname, "fixtures", "test.gpt") + const opts = { + disableCache: true, + } + + const run = await g.run(testGptPath, opts) + run.on(gptscript.RunEventType.CallProgress, data => { + for (let output of data.output) out += `system: ${output.content}` + }) + await run.text() + err = run.err + + expect(out).toContain("Calvin Coolidge") + expect(err).toEqual("") + }) + + test("run executes and streams a file with global tools correctly", async () => { + let out = "" + let err = undefined + const testGptPath = path.join(__dirname, "fixtures", "global-tools.gpt") + const opts = { + disableCache: true, + } + + const run = await g.run(testGptPath, opts) + run.on(gptscript.RunEventType.CallProgress, data => { + for (let output of data.output) out += `system: ${output.content}` + }) + await run.text() + err = run.err + + expect(out).toContain("Hello!") + expect(err).toEqual("") + }, 15000) + + test("aborting a run is reported correctly", async () => { + let errMessage = "" + let err = undefined + const testGptPath = path.join(__dirname, "fixtures", "test.gpt") + const opts = { + disableCache: true, + } + + try { + const run = await g.run(testGptPath, opts) + run.on(gptscript.RunEventType.CallProgress, data => { + run.close() + }) + await run.text() + err = run.err + } catch (error: any) { + errMessage = error + } + + expect(errMessage).toContain("aborted") + expect(err).toBeUndefined() + }) + + + describe("evaluate with multiple tools", () => { + test("multiple tools", async () => { + const t0 = { + tools: ["ask"], + instructions: "Only use the ask tool to ask who was the president of the united states in 1928?" + } + const t1 = { + name: "ask", + description: "This tool is used to ask a question", + arguments: { + type: ArgumentSchemaType, + properties: { + question: { + type: PropertyType, + description: "The question to ask", + } + } + }, + instructions: "${question}" + } + + const response = await (await g.evaluate([t0, t1])).text() + expect(response).toBeDefined() + expect(response).toContain("Calvin Coolidge") + }, 30000) + + test("with sub tool", async () => { + const t0 = { + tools: ["ask"], + instructions: "Only use the ask tool to ask who was the president of the united states in 1928?" + } + const t1 = { + name: "other", + instructions: "Who was the president of the united states in 1986?" + } + const t2 = { + name: "ask", + description: "This tool is used to ask a question", + arguments: { + type: "object", + question: "The question to ask" + }, + instructions: "${question}" + } + + const response = await (await g.evaluate([t0, t1, t2], {subTool: "other"})).text() + expect(response).toBeDefined() + expect(response).toContain("Ronald Reagan") + }, 30000) + }) + + test("parse file", async () => { + const response = await g.parse(path.join(__dirname, "fixtures", "test.gpt")) + expect(response).toBeDefined() + expect(response).toHaveLength(1) + expect((response[0] as gptscript.Tool).instructions).toEqual("who was the president in 1928?") + }, 30000) + + test("parse string tool", async () => { + const tool = "How much wood would a woodchuck chuck if a woodchuck could chuck wood?" + const response = await g.parseTool(tool) + expect(response).toBeDefined() + expect(response).toHaveLength(1) + expect((response[0] as gptscript.Tool).instructions).toEqual(tool) + }, 30000) + + test("parse string tool with text node", async () => { + const tool = "How much wood would a woodchuck chuck if a woodchuck could chuck wood?\n---\n!markdown\nThis is a text node" + const response = await g.parseTool(tool) + expect(response).toBeDefined() + expect(response).toHaveLength(2) + expect((response[0] as gptscript.Tool).instructions).toEqual("How much wood would a woodchuck chuck if a woodchuck could chuck wood?") + expect((response[1] as gptscript.Text).content).toEqual("This is a text node") + }, 30000) + + test("parse string tool global tools", async () => { + const tool = "Global Tools: acorn, do-work\nHow much wood would a woodchuck chuck if a woodchuck could chuck wood?" + const response = await g.parseTool(tool) + expect(response).toBeDefined() + expect(response).toHaveLength(1) + expect((response[0] as gptscript.Tool).instructions).toEqual("How much wood would a woodchuck chuck if a woodchuck could chuck wood?") + expect((response[0] as gptscript.Tool).globalTools).toEqual(["acorn", "do-work"]) + }, 30000) + + test("parse string tool first line shebang", async () => { + const tool = "\n#!/usr/bin/env python\nHow much wood would a woodchuck chuck if a woodchuck could chuck wood?" + const response = await g.parseTool(tool) + expect(response).toBeDefined() + expect(response).toHaveLength(1) + expect((response[0] as gptscript.Tool).instructions).toEqual("#!/usr/bin/env python\nHow much wood would a woodchuck chuck if a woodchuck could chuck wood?") + }, 30000) + + test("format tool", async () => { + const tool = { + id: "my-tool", + type: ToolType, + tools: ["sys.write", "sys.read"], + instructions: "This is a test", + arguments: { + type: ArgumentSchemaType, + properties: { + text: { + type: PropertyType, + description: "The text to write" + } + } + } + } + + const response = await g.stringify([tool]) + expect(response).toBeDefined() + expect(response).toContain("Tools: sys.write, sys.read") + expect(response).toContain("This is a test") + expect(response).toContain("Parameter: text: The text to write") + }) + + test("exec tool with chat", async () => { + let err = undefined + const t = { + chat: true, + instructions: "You are a chat bot. Don't finish the conversation until I say 'bye'.", + tools: ["sys.chat.finish"] + } + const opts = { + disableCache: true, + } + let run = await g.evaluate(t, opts) + + const inputs = [ + "List the three largest states in the United States by area.", + "What is the capital of the third one?", + "What timezone is the first one in?" + ] + + const expectedOutputs = [ + "California", + "Sacramento", + "Alaska Time Zone" + ] + + await run.text() + for (let i: number = 0; i < inputs.length; i++) { + run = run.nextChat(inputs[i]) + err = run.err + + if (err) { + break + } + + expect(await run.text()).toContain(expectedOutputs[i]) + expect(run.state).toEqual(gptscript.RunState.Continue) + } + + run = run.nextChat("bye") + await run.text() + + expect(run.state).toEqual(gptscript.RunState.Finished) + expect(err).toEqual("") + }, 60000) + + test("exec file with chat", async () => { + let err = undefined + const opts = { + disableCache: true + } + let run = await g.run(path.join(__dirname, "fixtures", "chat.gpt"), opts) + + const inputs = [ + "List the 3 largest of the Great Lakes by volume.", + "What is the volume of the second one in cubic miles?", + "What is the total area of the third one in square miles?" + ] + + const expectedOutputs = [ + "Lake Superior", + "Lake Michigan", + "Lake Huron" + ] + + await run.text() + for (let i: number = 0; i < inputs.length; i++) { + run = run.nextChat(inputs[i]) + err = run.err + + if (err) { + break + } + + expect(await run.text()).toContain(expectedOutputs[i]) + expect(run.state).toEqual(gptscript.RunState.Continue) + } + + run = run.nextChat("bye") + await run.text() + + expect(run.state).toEqual(gptscript.RunState.Finished) + expect(err).toEqual("") + }, 60000) + + test("nextChat on file providing chat state", async () => { + let run = await g.run(path.join(__dirname, "fixtures", "chat.gpt"), {disableCache: true}) + + run = run.nextChat("List the 3 largest of the Great Lakes by volume.") + expect(await run.text()).toContain("Lake Superior") + expect(run.err).toEqual("") + expect(run.state).toEqual(gptscript.RunState.Continue) + + run = await g.run(path.join(__dirname, "fixtures", "chat.gpt"), { + disableCache: true, + input: "What is the total area of the third one in square miles?", + chatState: run.currentChatState() + }) + + expect(await run.text()).toContain("Lake Huron") + expect(run.err).toEqual("") + expect(run.state).toEqual(gptscript.RunState.Continue) + }, 10000) + + test("nextChat on tool providing chat state", async () => { + const t = { + chat: true, + instructions: "You are a chat bot. Don't finish the conversation until I say 'bye'.", + tools: ["sys.chat.finish"] + } + let run = await g.evaluate(t, {disableCache: true}) + + run = run.nextChat("List the three largest states in the United States by area.") + expect(await run.text()).toContain("California") + expect(run.err).toEqual("") + expect(run.state).toEqual(gptscript.RunState.Continue) + + run = await g.evaluate(t, { + disableCache: true, + input: "What is the capital of the second one?", + chatState: run.currentChatState() + }) + + expect(await run.text()).toContain("Austin") + expect(run.err).toEqual("") + expect(run.state).toEqual(gptscript.RunState.Continue) + }, 10000) + + test("confirm", async () => { + const t = { + instructions: "List the files in the current working directory.", + tools: ["sys.exec"] + } + + const commands = [`"ls"`, `"dir"`] + let confirmCallCount = 0 + const run = await g.evaluate(t, {confirm: true}) + run.on(gptscript.RunEventType.CallConfirm, async (data: gptscript.CallFrame) => { + // On Windows, ls is not always a command. The LLM will try to run dir in this case. Allow both. + expect(data.input).toContain(commands[confirmCallCount]) + confirmCallCount++ + await g.confirm({id: data.id, accept: true}) + }) + + expect(await run.text()).toContain("README.md") + expect(run.err).toEqual("") + expect(confirmCallCount > 0).toBeTruthy() + }) + + test("do not confirm", async () => { + let confirmFound = false + const t = { + instructions: "List the files in the current directory as '.'. If that doesn't work print the word FAIL.", + tools: ["sys.exec"] + } + const run = await g.evaluate(t, {confirm: true}) + run.on(gptscript.RunEventType.CallConfirm, async (data: gptscript.CallFrame) => { + expect(data.input).toContain(`"ls"`) + confirmFound = true + await g.confirm({id: data.id, accept: false, message: "I will not allow it!"}) + }) + + expect(await run.text()).toContain("FAIL") + expect(run.err).toEqual("") + expect(confirmFound).toBeTruthy() + }) + + test("prompt", 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 g.evaluate(t, {prompt: true}) + run.on(gptscript.RunEventType.Prompt, async (data: gptscript.PromptFrame) => { + expect(data.message).toContain("first name") + expect(data.fields.length).toEqual(1) + expect(data.fields[0]).toEqual("first name") + expect(data.sensitive).toBeFalsy() + + promptFound = true + await g.promptResponse({id: data.id, responses: {[data.fields[0]]: "Clicky"}}) + }) + + expect(await run.text()).toContain("Clicky") + 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 g.evaluate(t) + 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() + }) + + test("retry failed run", async () => { + let shebang = `#!/bin/bash\nexit \${EXIT_CODE}` + if (process.platform == "win32") { + shebang = "#!/usr/bin/env powershell.exe\n$e = $env:EXIT_CODE;\nif ($e) { Exit 1; }" + } + const t = { + instructions: "say hello", + context: ["my-context"] + } as gptscript.ToolDef + const contextTool = { + name: "my-context", + instructions: `${shebang}\nexit \${EXIT_CODE}` + } as gptscript.ToolDef + + let run = await g.evaluate([t, contextTool], {disableCache: true, env: ["EXIT_CODE=1"]}) + try { + await run.text() + } catch { + } + + expect(run.err).not.toEqual("") + + run.opts.env = [] + run = run.nextChat() + + await run.text() + + expect(run.err).toEqual("") + }) }) From 86178ae196c0c48e6d2c81eaf1c7444704995807 Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Mon, 22 Jul 2024 11:23:43 -0400 Subject: [PATCH 4/7] feat: use new sdk server launch process Signed-off-by: Donnie Adams --- src/gptscript.ts | 52 +++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/src/gptscript.ts b/src/gptscript.ts index c3fcf2b..655b10f 100644 --- a/src/gptscript.ts +++ b/src/gptscript.ts @@ -2,7 +2,6 @@ import http from "http" import path from "path" import child_process from "child_process" import {fileURLToPath} from "url" -import net from "net" export interface GlobalOpts { APIKey?: string @@ -97,26 +96,22 @@ export class GPTScript { } }) - const u = new URL(GPTScript.serverURL) - if (u.port === "0") { - const srv = net.createServer() - const s = srv.listen(0, () => { - GPTScript.serverURL = "http://" + u.hostname + ":" + String((s.address() as net.AddressInfo).port) - srv.close() + GPTScript.serverProcess = child_process.spawn(getCmdPath(), ["sys.sdkserver", "--listen-address", GPTScript.serverURL.replace("http://", "")], { + env: env, + stdio: ["pipe", "ignore", "pipe"] + }) - GPTScript.startGPTScriptProcess(env) - }) - } else { - GPTScript.startGPTScriptProcess(env) - } - } - } + GPTScript.serverProcess.stderr?.on("data", (data) => { + let url = data.toString().trim() + if (url.includes("=")) { + url = url.substring(url.indexOf("=") + 1) + } - private static startGPTScriptProcess(env: NodeJS.ProcessEnv) { - GPTScript.serverProcess = child_process.spawn(getCmdPath(), ["sys.sdkserver", "--listen-address", GPTScript.serverURL.replace("http://", "")], { - env: env, - stdio: ["pipe"] - }) + GPTScript.serverURL = `http://${url}` + + GPTScript.serverProcess.stderr?.removeAllListeners() + }) + } } close(): void { @@ -252,16 +247,19 @@ export class GPTScript { } private async testGPTScriptURL(count: number): Promise { - try { - await fetch(`${GPTScript.serverURL}/healthz`) - return true - } catch { - if (count === 0) { - throw new Error("Failed to wait for gptscript to be ready") + while (count > 0) { + try { + await fetch(`${GPTScript.serverURL}/healthz`) + return true + } catch { + if (count === 0) { + } + await new Promise(r => setTimeout(r, 500)) + count-- } - await new Promise(r => setTimeout(r, 500)) - return this.testGPTScriptURL(count - 1) } + + throw new Error("Failed to wait for gptscript to be ready") } } From 6bb5fe0141faa127c8288ba19555bf2041b7c51e Mon Sep 17 00:00:00 2001 From: Nick Hale <4175918+njhale@users.noreply.github.com> Date: Fri, 26 Jul 2024 03:31:19 -0400 Subject: [PATCH 5/7] enhance: cleanup zip/tar files after extracting gptscript binary Signed-off-by: Nick Hale <4175918+njhale@users.noreply.github.com> --- package-lock.json | 18 +++++++++++------- scripts/install-binary.js | 16 +++++++++------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2cf7cfe..7987d16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3911,12 +3911,13 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -5067,10 +5068,11 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -5799,6 +5801,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -10014,6 +10017,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, diff --git a/scripts/install-binary.js b/scripts/install-binary.js index 85325fa..918f661 100644 --- a/scripts/install-binary.js +++ b/scripts/install-binary.js @@ -17,14 +17,18 @@ async function downloadAndExtract(url, saveDirectory) { return new Promise((resolve, reject) => { dlh.on('end', () => { + const downloadedFilePath = path.join(dlh.getDownloadPath()); if (url.endsWith('.zip')) { - const zip = new AdmZip(path.join(dlh.getDownloadPath())); + const zip = new AdmZip(downloadedFilePath); zip.extractAllTo(saveDirectory, true); + fs.unlinkSync(downloadedFilePath); } else if (url.endsWith('.tar.gz')) { tar.x({ - file: path.join(dlh.getDownloadPath()), + file: downloadedFilePath, cwd: saveDirectory, - }); + }).then(() => { + fs.unlinkSync(downloadedFilePath); // Delete the tar.gz file after extraction + }).catch((error) => reject(error)); } resolve(); }); @@ -121,10 +125,8 @@ async function needToInstall() { console.log(`Downloading and extracting gptscript binary from ${url}...`); try { - downloadAndExtract(url, outputDir) + await downloadAndExtract(url, outputDir); } catch (error) { - console.error('Error downloading and extracting:', error) + console.error('Error downloading and extracting:', error); } })(); - - From f002ae37f668dc9c3b922e1b8f6b507d2f00fb62 Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Sat, 27 Jul 2024 11:46:53 -0400 Subject: [PATCH 6/7] feat: add force sequential option Signed-off-by: Donnie Adams --- src/gptscript.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gptscript.ts b/src/gptscript.ts index 655b10f..3143350 100644 --- a/src/gptscript.ts +++ b/src/gptscript.ts @@ -39,6 +39,7 @@ export interface RunOpts { credentialOverrides?: string[] location?: string env?: string[] + forceSequential?: boolean APIKey?: string BaseURL?: string From a41859fd78612f0088c40b564b0faac823487eb6 Mon Sep 17 00:00:00 2001 From: acorn-io-bot Date: Tue, 30 Jul 2024 12:42:15 +0000 Subject: [PATCH 7/7] Automated GPTScript Version Update --- package-lock.json | 4 ++-- package.json | 2 +- scripts/install-binary.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7987d16..e1c33d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@gptscript-ai/gptscript", - "version": "v0.9.2", + "version": "v0.9.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@gptscript-ai/gptscript", - "version": "v0.9.2", + "version": "v0.9.3", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index cabd52c..c19a6e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@gptscript-ai/gptscript", - "version": "v0.9.2", + "version": "v0.9.3", "description": "Run gptscript in node.js", "source": "src/gptscript.ts", "main": "dist/gptscript.js", diff --git a/scripts/install-binary.js b/scripts/install-binary.js index 918f661..28705ef 100644 --- a/scripts/install-binary.js +++ b/scripts/install-binary.js @@ -72,7 +72,7 @@ if (process.platform === 'win32') { const gptscript_info = { name: "gptscript", url: "https://github.com/gptscript-ai/gptscript/releases/download/", - version: "v0.9.2" + version: "v0.9.3" } const pltfm = {