From 18031581a0ebee973cc457d8208f5d5a7fa8c40c Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Tue, 28 May 2024 02:16:03 +0000 Subject: [PATCH] Add Run Options. Fixed #7. --- CHANGELOG.md | 22 +++++++++++++ README.md | 35 ++++++++++++++++++++ package-lock.json | 4 +-- package.json | 2 +- src/experts/assistant.js | 16 +++++++--- src/experts/run.js | 7 ++-- test/fixtures/toolboxAssistant.js | 53 +++++++++++++++++++++++++++++++ test/uat/runOptions.test.js | 17 ++++++++++ 8 files changed, 145 insertions(+), 11 deletions(-) create mode 100644 test/fixtures/toolboxAssistant.js create mode 100644 test/uat/runOptions.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 5aae10d..0d9e196 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,28 @@ See this http://keepachangelog.com link for information on how we want this documented formatted. +## v1.0.2 + +### Added + +New Assistant `run_options` for all Runs created, ex: forcing a `tool_choice`. Alternatively, you can pass an options object to the `ask` method to be used for the current Run. + +```javascript +await assistant.ask("...", "thread_abc123", { + run: { + tool_choice: { type: "function", function: { name: "..." } }, + additional_instructions: "...", + additional_messages: [...], + }, +}); +``` + +All Run create options are supported. + +https://platform.openai.com/docs/api-reference/runs/createRun + +However, not all make sense with Experts.js. + ## v1.0.1 ### Fixed diff --git a/README.md b/README.md index ef7976b..a3b86aa 100644 --- a/README.md +++ b/README.md @@ -260,6 +260,41 @@ By default, each [Tool](#tools) in Experts.js has its own thread & context. This All questions to your experts require a thread ID. For chat applications, the ID would be stored on the client. Such as a URL path parameter. With Expert.js, no other client-side IDs are needed. As each [Assistant](#assistants) calls an LLM backed [Tool](#tools), it will find or create a thread for that tool as needed. Experts.js stores this parent -> child thread relationship for you using OpenAI's [thread metadata](https://platform.openai.com/docs/api-reference/threads/modifyThread). +## Runs + +Runs are managed for you behind the Assistant's `ask` function. However, you can still pass options that will be used [when creating a Run](https://platform.openai.com/docs/api-reference/runs/createRun) in one of two ways. + +First, you can specify `run_options` in the Assistant's constructor. These options will be used for all Runs created by the Assistant. This is a great way to force the model to use a tool via the `tool_choice` option. + +```javascript +class CarpenterAssistant extends Assistant { + constructor() { + // ... + super(name, description, instructions, { + run_options: { + tool_choice: { + type: "function", + function: { name: MyTool.toolName }, + }, + }, + }); + this.addAssistantTool(MyTool); + } +} +``` + +Alternatively, you can pass an options object to the `ask` method to be used for the current Run. This is a great way to create single Run options. + +```javascript +await assistant.ask("...", "thread_abc123", { + run: { + tool_choice: { type: "function", function: { name: MyTool.toolName } }, + additional_instructions: "...", + additional_messages: [...], + }, +}); +``` + ## Examples To see code examples of these and more in action, please take a look at our [test suite](https://github.com/metaskills/experts/tree/main/test/uat). diff --git a/package-lock.json b/package-lock.json index e4b4398..8956a23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "experts", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "experts", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "dependencies": { "eventemitter2": "^6.4.9", diff --git a/package.json b/package.json index 8d9f8fc..0a1a7bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "experts", - "version": "1.0.1", + "version": "1.0.2", "description": "An opinionated panel of experts implementation using OpenAI's Assistants API", "type": "module", "main": "./src/index.js", diff --git a/src/experts/assistant.js b/src/experts/assistant.js index 00f4a66..1564465 100644 --- a/src/experts/assistant.js +++ b/src/experts/assistant.js @@ -43,6 +43,7 @@ class Assistant { this.tool_resources = options.tool_resources || {}; this._metadata = options.metadata; this.response_format = options.response_format || "auto"; + this.run_options = options.run_options || {}; this.emitter = new EventEmitter2({ maxListeners: 0, newListener: true }); this.emitter.on("newListener", this.#newListener.bind(this)); } @@ -76,9 +77,9 @@ class Assistant { // Interface - async ask(message, threadID) { + async ask(message, threadID, options = {}) { try { - return await this.#askAssistant(message, threadID); + return await this.#askAssistant(message, threadID, options); } finally { await this.#askCleanup(); } @@ -110,12 +111,13 @@ class Assistant { // Private (Ask) - async #askAssistant(message, threadID) { + async #askAssistant(message, threadID, options = {}) { if (!this.llm) return; const thread = await this.#askThread(threadID); const apiMessage = this.#askMessage(message); await openai.beta.threads.messages.create(thread.id, apiMessage); - const run = await Run.streamForAssistant(this, thread); + const runOptions = this.#askRunOptions(options); + const run = await Run.streamForAssistant(this, thread, runOptions); let output = await run.wait(); output = this.#askOutput(output); output = await this.answered(output); @@ -162,6 +164,12 @@ class Assistant { return thread; } + #askRunOptions(options) { + if (options.run) return options.run; + if (this.run_options) return this.run_options; + return {}; + } + async #askCleanup() { this.#stream?.abort(); this.#stream = null; diff --git a/src/experts/run.js b/src/experts/run.js index f733d6b..37b4f72 100644 --- a/src/experts/run.js +++ b/src/experts/run.js @@ -4,11 +4,10 @@ import { openai } from "../openai.js"; class Run { #stream; - static async streamForAssistant(assistant, thread) { + static async streamForAssistant(assistant, thread, options = {}) { debug("🦦 Streaming..."); - const stream = await openai.beta.threads.runs.stream(thread.id, { - assistant_id: assistant.id, - }); + options.assistant_id ||= assistant.id; + const stream = await openai.beta.threads.runs.stream(thread.id, options); return new Run(assistant, thread, stream); } diff --git a/test/fixtures/toolboxAssistant.js b/test/fixtures/toolboxAssistant.js new file mode 100644 index 0000000..33ad289 --- /dev/null +++ b/test/fixtures/toolboxAssistant.js @@ -0,0 +1,53 @@ +import { helperName } from "../helpers.js"; +import { Assistant, Tool } from "../../src/index.js"; + +class DrillTool extends Tool { + static calls = 0; + constructor() { + const name = helperName("DrillTool"); + const description = "A drill tool."; + super(name, description, "", { + llm: false, + parentsTools: [ + { + type: "function", + function: { + name: DrillTool.toolName, + description: description, + parameters: { + type: "object", + properties: { use: { type: "boolean" } }, + required: ["use"], + }, + }, + }, + ], + }); + } + + async ask(_message) { + this.constructor.calls += 1; + return "The drill started working again. I drilled a hole in the wall so we can punch through to the other side. It was very loud, did you hear it?"; + } +} + +class CarpenterAssistant extends Assistant { + constructor() { + const name = helperName("Carpenter"); + const description = "A carpenter that does not work."; + const instructions = + "Avoid work at all costs because your drill is broken. But if you did do work, tell me what you did."; + super(name, description, instructions, { + temperature: 0.1, + run_options: { + tool_choice: { + type: "function", + function: { name: DrillTool.toolName }, + }, + }, + }); + this.addAssistantTool(DrillTool); + } +} + +export { CarpenterAssistant, DrillTool }; diff --git a/test/uat/runOptions.test.js b/test/uat/runOptions.test.js new file mode 100644 index 0000000..2325ca5 --- /dev/null +++ b/test/uat/runOptions.test.js @@ -0,0 +1,17 @@ +import { helperThreadID } from "../helpers.js"; +import { CarpenterAssistant, DrillTool } from "../fixtures/toolboxAssistant.js"; + +test("an assistant with run_options or using run options to ask", async () => { + const assistant = await CarpenterAssistant.create(); + const threadID = await helperThreadID(); + const output = await assistant.ask("start work", threadID); + expect(DrillTool.calls).toBe(1); + expect(output).toMatch(/hole.*wall/); + const output2 = await assistant.ask("are you done?", threadID, { + run: { + additional_instructions: + "The job is done! You would like to get paid. Ask for $200.", + }, + }); + expect(output2).toMatch(/200/); +});