Skip to content

Commit

Permalink
Add Run Options. Fixed #7.
Browse files Browse the repository at this point in the history
  • Loading branch information
metaskills committed May 28, 2024
1 parent a7eb0d4 commit 1803158
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 11 deletions.
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
16 changes: 12 additions & 4 deletions src/experts/assistant.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
7 changes: 3 additions & 4 deletions src/experts/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
53 changes: 53 additions & 0 deletions test/fixtures/toolboxAssistant.js
Original file line number Diff line number Diff line change
@@ -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 };
17 changes: 17 additions & 0 deletions test/uat/runOptions.test.js
Original file line number Diff line number Diff line change
@@ -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/);
});

0 comments on commit 1803158

Please sign in to comment.