diff --git a/README.md b/README.md index 8674dca..0bbdb80 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,6 @@ OPENAI_API_KEY=sk-... Now you can run the following commands: ```bash -npm install -npm run test +./bin/setup +./bin/test ``` diff --git a/TODO.md b/TODO.md index 5a00800..a83d872 100644 --- a/TODO.md +++ b/TODO.md @@ -1,4 +1,5 @@ +* Remove the Assistant#messages. Is even `Message` needed? * Create a GitHub Publish Action * Add Contributors diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..ec3ab3e --- /dev/null +++ b/bin/setup @@ -0,0 +1,4 @@ +#!/bin/sh +set -e + +npm install diff --git a/bin/test b/bin/test new file mode 100755 index 0000000..dbe0990 --- /dev/null +++ b/bin/test @@ -0,0 +1,6 @@ +#!/bin/sh +set -e + +export NODE_OPTIONS='--no-warnings --experimental-vm-modules' + +npx jest --runInBand $1 diff --git a/package.json b/package.json index eb4208c..c15aa6e 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,15 @@ "version": "0.0.1", "description": "An opinionated panel of experts implementation using OpenAI's Assistants API", "type": "module", - "main": "index.js", + "main": "./src/index.js", + "exports": { + ".": "./src/index.js", + "./thread": "./src/thread.js", + "./assistant": "./src/assistant.js", + "./tool": "./src/tool.js" + }, "scripts": { - "test": "NODE_OPTIONS='--no-warnings --experimental-vm-modules' jest" + "test": "./bin/test" }, "author": "Ken COllins", "license": "MIT", @@ -17,5 +23,9 @@ "babel-jest": "^29.7.0", "jest": "^29.7.0", "jest-environment-node": "^29.7.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/metaskills/experts" } } diff --git a/src/experts/assistant.js b/src/experts/assistant.js index 5e3b6c4..68a4760 100644 --- a/src/experts/assistant.js +++ b/src/experts/assistant.js @@ -5,8 +5,8 @@ import { Message } from "./messages.js"; import { Run } from "./run.js"; class Assistant { - static async create() { - const asst = new this(); + static async create(agentName, description, instructions, options = {}) { + const asst = new this(agentName, description, instructions, options); await asst.init(); return asst; } @@ -23,10 +23,11 @@ class Assistant { this.llm = options.llm !== undefined ? options.llm : true; if (this.llm) { this.id = options.id; - this.temperature = - options.temperature !== undefined ? options.temperature : 0.1; - this.model = options.model || "gpt-3.5-turbo"; + this.model = options.model || "gpt-4-turbo"; this.messages = []; + this.temperature = + options.temperature !== undefined ? options.temperature : 1.0; + this.top_p = options.top_p !== undefined ? options.top_p : 1.0; this.assistantsTools = {}; this.assistantsToolsOutputs = []; this.tools = []; @@ -143,6 +144,8 @@ class Assistant { name: this.agentName, description: this.description, instructions: this.instructions, + temperature: this.temperature, + top_p: this.top_p, tools: this.tools, }); debug(`💁‍♂️ Created ${this.agentName} assistant ${assistant.id}...`); diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..f91c1be --- /dev/null +++ b/src/index.js @@ -0,0 +1,3 @@ +export { Thread } from "./experts/thread.js"; +export { Assistant } from "./experts/assistant.js"; +export { Tool } from "./experts/tool.js"; diff --git a/test/experts/assistant.test.js b/test/experts/assistant.test.js new file mode 100644 index 0000000..f454c30 --- /dev/null +++ b/test/experts/assistant.test.js @@ -0,0 +1,67 @@ +import { + helperThreadID, + helperDeleteAllAssistants, + helperFindAssistant, +} from "../helpers.js"; +import { Assistant } from "../../src/experts/assistant.js"; + +const NAME = "Experts.js (Test)"; + +beforeEach(async () => { + await helperDeleteAllAssistants(); +}); + +afterEach(async () => { + await helperDeleteAllAssistants(); +}); + +test("can ask the assistant a question using a thread id", async () => { + const assistant = await Assistant.create( + NAME, + "echo", + "echo the same text back to the user" + ); + const threadID = await helperThreadID(); + const output = await assistant.ask("hello 123", threadID); + expect(output).toBe("hello 123"); +}); + +test("can override defaults such as temperature and top_p", async () => { + const assistant = await Assistant.create( + NAME, + "test-description", + "test-instructions", + { temperature: 0.5, top_p: 0.5 } + ); + expect(assistant.temperature).toBe(0.5); + expect(assistant.top_p).toBe(0.5); +}); + +test("create new assistant using name, description, and instruction defaults", async () => { + // None exists before creation. + const assistantNone = await helperFindAssistant(NAME); + expect(assistantNone).toBeUndefined(); + const assistant = await Assistant.create( + NAME, + "test-description", + "test-instructions" + ); + const backendAssistant = await helperFindAssistant(NAME); + // Assistant + expect(assistant.agentName).toBe(NAME); + expect(assistant.description).toBe("test-description"); + expect(assistant.instructions).toBe("test-instructions"); + expect(assistant.llm).toBe(true); + expect(assistant.model).toBe("gpt-4-turbo"); + expect(assistant.temperature).toBe(1.0); + expect(assistant.id).toMatch(/^asst_/); + expect(assistant.id).toBe(backendAssistant.id); + // Backend + expect(backendAssistant).toBeDefined(); + expect(backendAssistant.name).toBe(NAME); + expect(backendAssistant.description).toBe("test-description"); + expect(backendAssistant.instructions).toBe("test-instructions"); + expect(backendAssistant.model).toBe("gpt-4-turbo"); + expect(backendAssistant.temperature).toBe(1.0); + expect(backendAssistant.id).toMatch(/^asst_/); +}); diff --git a/test/experts/openai.test.js b/test/experts/openai.test.js deleted file mode 100644 index 5c71a43..0000000 --- a/test/experts/openai.test.js +++ /dev/null @@ -1,5 +0,0 @@ -import { openai } from "../../src/openai.js"; - -test("adds 1 + 2 to equal 3", () => { - expect(3).toBe(3); -}); diff --git a/test/helpers.js b/test/helpers.js new file mode 100644 index 0000000..3caf84e --- /dev/null +++ b/test/helpers.js @@ -0,0 +1,42 @@ +import { openai } from "../src/openai.js"; + +const helperThreadID = async () => { + const thread = await openai.beta.threads.create(); + return thread.id; +}; + +const helperFindAllAssistants = async () => { + const assistants = await openai.beta.assistants.list({ limit: 100 }); + return assistants.data.filter((a) => a.name?.startsWith("Experts.js")); +}; + +const helperDeleteAllAssistants = async () => { + const assistants = await helperFindAllAssistants(); + for (const assistant of assistants) { + try { + await openai.beta.assistants.del(assistant.id); + } catch (error) {} + } +}; + +const helperFindAssistant = async (name) => { + const assistant = ( + await openai.beta.assistants.list({ limit: 100 }) + ).data.find((a) => a.name === name); + return assistant; +}; + +const helperDeleteAssistant = async (name) => { + const assistant = await helperFindAssistant(name); + if (assistant !== undefined) { + await openai.beta.assistants.del(assistant.id); + } +}; + +export { + helperThreadID, + helperFindAllAssistants, + helperDeleteAllAssistants, + helperFindAssistant, + helperDeleteAssistant, +}; diff --git a/test/index.test.js b/test/index.test.js new file mode 100644 index 0000000..3ae4164 --- /dev/null +++ b/test/index.test.js @@ -0,0 +1,36 @@ +import { helperDeleteAllAssistants } from "./helpers.js"; +import { Thread, Assistant, Tool } from "../src/index.js"; + +const NAME = "Experts.js (Test)"; + +beforeEach(async () => { + await helperDeleteAllAssistants(); +}); + +afterEach(async () => { + await helperDeleteAllAssistants(); +}); + +test("can import Thread", async () => { + const thread = await Thread.create(); + expect(thread.id).toMatch(/^thread_/); +}); + +test("can import Assistant", async () => { + const assistant = await Assistant.create( + NAME, + "test-description", + "test-instructions" + ); + expect(assistant.id).toMatch(/^asst_/); +}); + +test("can import Tool", async () => { + const tool = await Tool.create( + NAME, + "test-description", + "test-instructions", + { llm: false } + ); + expect(tool.isTool).toBe(true); +}); diff --git a/test/openai.test.js b/test/openai.test.js new file mode 100644 index 0000000..2a5309a --- /dev/null +++ b/test/openai.test.js @@ -0,0 +1,7 @@ +import { openai } from "../src/openai.js"; + +test("exports an openai client", () => { + expect(openai).toBeInstanceOf(Object); + expect(openai.beta).toBeInstanceOf(Object); + expect(openai.beta.threads).toBeInstanceOf(Object); +});