diff --git a/.github/workflows/pull_request.yaml b/.github/workflows/pull_request.yaml index e4b61eb..6ec42cc 100644 --- a/.github/workflows/pull_request.yaml +++ b/.github/workflows/pull_request.yaml @@ -38,3 +38,4 @@ jobs: git_ref: ${{ github.event.pull_request.head.sha }} secrets: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} diff --git a/.github/workflows/push_main.yaml b/.github/workflows/push_main.yaml index c7db5c9..8e7b2cb 100644 --- a/.github/workflows/push_main.yaml +++ b/.github/workflows/push_main.yaml @@ -13,3 +13,4 @@ jobs: git_ref: '' secrets: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index dfd7770..64f3a71 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -9,6 +9,8 @@ on: secrets: OPENAI_API_KEY: required: true + ANTHROPIC_API_KEY: + required: true jobs: test-linux: @@ -31,6 +33,7 @@ jobs: env: GPTSCRIPT_BIN: ./gptscriptexe OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} NODE_GPTSCRIPT_SKIP_INSTALL_BINARY: true run: npm test @@ -47,15 +50,12 @@ jobs: - name: Install gptscript run: | curl https://get.gptscript.ai/releases/default_windows_amd64_v1/gptscript.exe -o gptscript.exe - - name: Create config file - run: | - echo '{"credsStore":"file"}' > config - name: Install dependencies run: npm install - name: Run Tests env: GPTSCRIPT_BIN: .\gptscript.exe - GPTSCRIPT_CONFIG_FILE: .\config OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} NODE_GPTSCRIPT_SKIP_INSTALL_BINARY: true run: npm test diff --git a/package-lock.json b/package-lock.json index 45674ae..af8c116 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@gptscript-ai/gptscript", - "version": "v0.9.5-rc3", + "version": "v0.9.5-rc4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@gptscript-ai/gptscript", - "version": "v0.9.5-rc3", + "version": "v0.9.5-rc4", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 5e7e2cb..300cf69 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@gptscript-ai/gptscript", - "version": "v0.9.5-rc3", + "version": "v0.9.5-rc4", "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 3988e9e..9e04cfd 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.5-rc3" + version: "v0.9.5-rc4" } const pltfm = { diff --git a/src/gptscript.ts b/src/gptscript.ts index 393752c..c47e56a 100644 --- a/src/gptscript.ts +++ b/src/gptscript.ts @@ -73,8 +73,10 @@ export class GPTScript { private ready: boolean + private opts: GlobalOpts constructor(opts?: GlobalOpts) { + this.opts = opts || {} this.ready = false GPTScript.instanceCount++ if (!GPTScript.serverURL) { @@ -82,9 +84,9 @@ export class GPTScript { } if (GPTScript.instanceCount === 1 && process.env.GPTSCRIPT_DISABLE_SERVER !== "true") { let env = process.env - if (opts && opts.Env) { + if (this.opts.Env) { env = {} - for (const v of opts.Env) { + for (const v of this.opts.Env) { const equalIndex = v.indexOf("=") if (equalIndex === -1) { env[v] = "" @@ -94,7 +96,7 @@ export class GPTScript { } } - globalOptsToEnv(env, opts) + globalOptsToEnv(env, this.opts) process.on("exit", (code) => { if (GPTScript.serverProcess) { GPTScript.serverProcess.stdin?.end() @@ -133,20 +135,30 @@ export class GPTScript { return this.runBasicCommand("list-tools") } - listModels(): Promise { - return this.runBasicCommand("list-models") + listModels(providers?: string[], credentialOverrides?: string[]): Promise { + if (this.opts.DefaultModelProvider) { + if (!providers) { + providers = [] + } + providers.push(this.opts.DefaultModelProvider) + } + return this.runBasicCommand("list-models", { + "providers": providers, + "env": this.opts.Env, + "credentialOverrides": credentialOverrides + }) } version(): Promise { return this.runBasicCommand("version") } - async runBasicCommand(cmd: string): Promise { + async runBasicCommand(cmd: string, body?: any): Promise { if (!this.ready) { this.ready = await this.testGPTScriptURL(20) } const r = new RunSubcommand(cmd, "", {}, GPTScript.serverURL) - r.requestNoStream(null) + r.requestNoStream(body) return r.text() } @@ -161,7 +173,8 @@ export class GPTScript { if (!this.ready) { this.ready = await this.testGPTScriptURL(20) } - return (new Run("run", toolName, opts, GPTScript.serverURL)).nextChat(opts.input) + + return (new Run("run", toolName, {...this.opts, ...opts}, GPTScript.serverURL)).nextChat(opts.input) } /** @@ -176,7 +189,7 @@ export class GPTScript { this.ready = await this.testGPTScriptURL(20) } - return (new Run("evaluate", tool, opts, GPTScript.serverURL)).nextChat(opts.input) + return (new Run("evaluate", tool, {...this.opts, ...opts}, GPTScript.serverURL)).nextChat(opts.input) } async parse(fileName: string, disableCache?: boolean): Promise { @@ -265,7 +278,7 @@ export class GPTScript { disableCache?: boolean, subTool?: string ): Promise { - return this._load({ file: fileName, disableCache, subTool }); + return this._load({file: fileName, disableCache, subTool}) } /** @@ -281,7 +294,7 @@ export class GPTScript { disableCache?: boolean, subTool?: string ): Promise { - return this._load({ content, disableCache, subTool }); + return this._load({content, disableCache, subTool}) } /** @@ -297,7 +310,7 @@ export class GPTScript { disableCache?: boolean, subTool?: string ): Promise { - return this._load({ toolDefs, disableCache, subTool }); + return this._load({toolDefs, disableCache, subTool}) } /** @@ -308,12 +321,12 @@ export class GPTScript { */ private async _load(payload: any): Promise { if (!this.ready) { - this.ready = await this.testGPTScriptURL(20); + this.ready = await this.testGPTScriptURL(20) } - const r: Run = new RunSubcommand("load", payload.toolDefs || [], {}, GPTScript.serverURL); + const r: Run = new RunSubcommand("load", payload.toolDefs || [], {}, GPTScript.serverURL) - r.request(payload); - return (await r.json()) as LoadResponse; + r.request(payload) + return (await r.json()) as LoadResponse } private async testGPTScriptURL(count: number): Promise { @@ -511,12 +524,16 @@ export class Run { const options = this.requestOptions(this.gptscriptURL, this.requestPath, tool) as any if (tool) { - options.body = {...tool, ...this.opts} + options.body = JSON.stringify({...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 => { + fetch(req).then(resp => { + return resp.json() + }).then(res => { + resolve(res.stdout) + }).catch(e => { reject(e) }) }) @@ -861,6 +878,7 @@ export interface PromptFrame { message: string fields: string[] sensitive: boolean + metadata: Record } export type Frame = RunFrame | CallFrame | PromptFrame diff --git a/tests/gptscript.test.ts b/tests/gptscript.test.ts index cdac726..1352840 100644 --- a/tests/gptscript.test.ts +++ b/tests/gptscript.test.ts @@ -3,6 +3,7 @@ import {ArgumentSchemaType, getEnv, PropertyType, RunEventType, ToolType} from " import path from "path" import {fileURLToPath} from "url" +let gFirst: gptscript.GPTScript let g: gptscript.GPTScript const __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -12,9 +13,13 @@ describe("gptscript module", () => { throw new Error("neither OPENAI_API_KEY nor GPTSCRIPT_URL is set") } + // Start an initial GPTScript instance. + // This one doesn't have any options, but it's there to ensure that using another instance works as expected in all cases. + gFirst = new gptscript.GPTScript() g = new gptscript.GPTScript({APIKey: process.env.OPENAI_API_KEY}) }) afterAll(() => { + gFirst.close() g.close() }) @@ -35,6 +40,39 @@ describe("gptscript module", () => { expect(models).toBeDefined() }) + test("listModels with providers returns a list of models from that provider", async () => { + if (!process.env.ANTHROPIC_API_KEY) { + return + } + + let models = await g.listModels(["github.com/gptscript-ai/claude3-anthropic-provider"], ["github.com/gptscript-ai/claude3-anthropic-provider/credential:ANTHROPIC_API_KEY"]) + expect(models).toBeDefined() + for (let model of models.split("\n")) { + expect(model).toBeDefined() + expect(model.startsWith("claude-3-")).toBe(true) + expect(model.endsWith("from github.com/gptscript-ai/claude3-anthropic-provider")).toBe(true) + } + }, 60000) + + test("listModels with default provider returns a list of models from that provider", async () => { + if (!process.env.ANTHROPIC_API_KEY) { + return + } + + const newg = new gptscript.GPTScript({DefaultModelProvider: "github.com/gptscript-ai/claude3-anthropic-provider"}) + try { + let models = await newg.listModels(undefined, ["github.com/gptscript-ai/claude3-anthropic-provider/credential:ANTHROPIC_API_KEY"]) + expect(models).toBeDefined() + for (let model of models.split("\n")) { + expect(model).toBeDefined() + expect(model.startsWith("claude-3-")).toBe(true) + expect(model.endsWith("from github.com/gptscript-ai/claude3-anthropic-provider")).toBe(true) + } + } finally { + newg.close() + } + }, 15000) + test("version returns a gptscript version", async () => { // Similar structure to listTools let version = await g.version() @@ -507,6 +545,27 @@ describe("gptscript module", () => { expect(promptFound).toBeTruthy() }) + test("prompt with metadata", async () => { + let promptFound = false + const run = await g.run("sys.prompt", { + prompt: true, + input: "{\"fields\":\"first name\",\"metadata\":{\"key\":\"value\"}}" + }) + run.on(gptscript.RunEventType.Prompt, async (data: gptscript.PromptFrame) => { + expect(data.fields.length).toEqual(1) + expect(data.fields[0]).toEqual("first name") + expect(data.metadata).toEqual({key: "value"}) + 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 = {