Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Major Change to Tool Patterns v.1.3.0 #17

Merged
merged 3 commits into from
Jun 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

See this http://keepachangelog.com link for information on how we want this documented formatted.

## v1.3.0

### Fixed

Now `parentsTools` can now have multiple functions present. This should have worked all along but was overlooked. See changes around `MyTool.toolName` below.

### Changed

No documented usage of `MyTool.toolName`. It is still used internally for a Tool's thread meta. The function is still available for use, but it is not recommended.

> [!CAUTION]
> It is critical that your tool's function name be unique across its parent's entire set of tool names.
## v1.2.0

### Changed
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ class EchoTool extends Tool {
{
type: "function",
function: {
name: EchoTool.toolName,
name: "echo",
description: description,
parameters: {
type: "object",
Expand All @@ -170,7 +170,7 @@ class EchoTool extends Tool {
```

> [!CAUTION]
> It is critical that your tool's function name use the `toolName` getter. Experts.js converts this to a snake_case string and uses the name to find the the right tool and call it.
> It is critical that your tool's function name be unique across its parent's entire set of tool names.

As such, Tool class names are important and help OpenAI's models decide which tool to call. So pick a good name for your tool class. For example, `ProductsOpenSearchTool` will be `products_open_search` and clearly helps the model infer along with the tool's description what role it performs.

Expand Down Expand Up @@ -282,7 +282,7 @@ class CarpenterAssistant extends Assistant {
run_options: {
tool_choice: {
type: "function",
function: { name: MyTool.toolName },
function: { name: "my_tool_name" },
},
},
});
Expand All @@ -296,7 +296,7 @@ Alternatively, you can pass an options object to the `ask` method to be used for
```javascript
await assistant.ask("...", "thread_abc123", {
run: {
tool_choice: { type: "function", function: { name: MyTool.toolName } },
tool_choice: { type: "function", function: { name: "my_tool_name" } },
additional_instructions: "...",
additional_messages: [...],
},
Expand Down
2 changes: 2 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

* Add a buffered output tool (append, prepend) to support bespoke user interfaces.
12 changes: 6 additions & 6 deletions package-lock.json

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

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "experts",
"version": "1.2.0",
"version": "1.3.0",
"description": "An opinionated panel of experts implementation using OpenAI's Assistants API",
"type": "module",
"main": "./src/index.js",
Expand Down Expand Up @@ -33,6 +33,6 @@
},
"dependencies": {
"eventemitter2": "^6.4.9",
"openai": "^4.47.1"
"openai": "^4.52.0"
}
}
4 changes: 2 additions & 2 deletions src/experts/assistant.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class Assistant {
this.temperature =
options.temperature !== undefined ? options.temperature : 1.0;
this.top_p = options.top_p !== undefined ? options.top_p : 1.0;
this.experts = {};
this.experts = [];
this.tools = options.tools || [];
this.tool_resources = options.tool_resources || {};
this._metadata = options.metadata;
Expand Down Expand Up @@ -104,7 +104,7 @@ class Assistant {

addAssistantTool(toolClass) {
const assistantTool = new toolClass();
this.experts[assistantTool.toolName] = assistantTool;
this.experts.push(assistantTool);
if (assistantTool.isParentsTools) {
for (const tool of assistantTool.parentsTools) {
this.tools.push(tool);
Expand Down
19 changes: 18 additions & 1 deletion src/experts/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ class Run {
debug("🪚 " + JSON.stringify(toolCall));
if (toolCall.type === "function") {
const toolOutput = { tool_call_id: toolCall.id };
const toolCaller = this.assistant.experts[toolCall.function.name];
const toolCaller = this.#findExpertByToolName(toolCall.function.name);
if (toolCaller && typeof toolCaller.ask === "function") {
const output = await toolCaller.ask(
toolCall.function.arguments,
Expand Down Expand Up @@ -123,6 +123,23 @@ class Run {
});
}
}

#findExpertByToolName(functionName) {
let toolCaller;
this.assistant.experts.forEach((expert) => {
if (expert.isParentsTools) {
expert.parentsTools.forEach((parentTool) => {
if (
parentTool.type === "function" &&
parentTool.function.name === functionName
) {
toolCaller = expert;
}
});
}
});
return toolCaller;
}
}

export { Run };
4 changes: 4 additions & 0 deletions test/fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ export { ProductsAssistant } from "./fixtures/productsAssistant.js";
export { NoLLMToolAssistant } from "./fixtures/noLLMToolAssistant.js";
export { OutputsAssistant } from "./fixtures/outputsAssistant.js";
export { EventedAssistant } from "./fixtures/eventedAssistant.js";
export {
AccountsAssistant,
AccountsTool,
} from "./fixtures/accountsAssistant.js";
72 changes: 72 additions & 0 deletions test/fixtures/accountsAssistant.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Tool } from "../../src/experts/tool.js";
import { Assistant } from "../../src/experts/assistant.js";

class AccountsTool extends Tool {
constructor() {
super({
llm: false,
parentsTools: [
{
type: "function",
function: {
name: "accounts_find_by_id",
description: "Find an account by ID",
parameters: {
type: "object",
properties: { id: { type: "integer" } },
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "accounts_find_by_email",
description: "Find an account by Email",
parameters: {
type: "object",
properties: { email: { type: "string" } },
required: ["email"],
},
},
},
],
});
}

async ask(message) {
const params = JSON.parse(message);
if (params.id) {
return this.#findByID(params.id);
} else if (params.email) {
return this.#findByEmail(params.email);
}
}

#findByID(id) {
return JSON.stringify({
id: id,
email: "[email protected]",
name: "Jordan Whitaker",
});
}

#findByEmail(email) {
return JSON.stringify({
id: 838282,
email: email,
name: "Elena Prescott",
});
}
}

class AccountsAssistant extends Assistant {
constructor() {
super({
instructions: "Routes messages to the right tool.",
});
this.addAssistantTool(AccountsTool);
}
}

export { AccountsAssistant, AccountsTool };
2 changes: 1 addition & 1 deletion test/fixtures/echoTool.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class EchoTool extends Tool {
{
type: "function",
function: {
name: EchoTool.toolName,
name: "echo",
description: "Echo",
parameters: {
type: "object",
Expand Down
4 changes: 2 additions & 2 deletions test/fixtures/noLLMToolAssistant.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class AnswerTool extends Tool {
{
type: "function",
function: {
name: AnswerTool.toolName,
name: "answer",
description: "Answers to messages.",
parameters: {
type: "object",
Expand All @@ -32,7 +32,7 @@ class NoLLMToolAssistant extends Assistant {
super({
name: helperName("NoLLMToolAssistant"),
description: "Answers to messages.",
instructions: `You must route /tool messages in full to your '${AnswerTool.toolName}' tool. Never respond without first using that tool. Never! Ex: When asked what color the sky is, use the '${AnswerTool.toolName}' tool first.`,
instructions: `You must route /tool messages in full to your 'answer' tool. Never respond without first using that tool. Never! Ex: When asked what color the sky is, use the 'answer' tool first.`,
});
this.addAssistantTool(AnswerTool);
}
Expand Down
8 changes: 4 additions & 4 deletions test/fixtures/outputsAssistant.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class AnswerTwoTool extends Tool {
{
type: "function",
function: {
name: AnswerTwoTool.toolName,
name: "answer_two",
description: "Answers to messages.",
parameters: {
type: "object",
Expand All @@ -32,14 +32,14 @@ class AnswerOneTool extends Tool {
super({
name: helperName("AnswerOneTool"),
description: "Answers to messages.",
instructions: `You must route the message in full to your '${AnswerTwoTool.toolName}' tool. Never respond without first using that tool. Never! Ex: When asked what is my favorite food, use the '${AnswerTwoTool.toolName}' tool first. Lastly, tespond only with the single word 'Success' to the question.`,
instructions: `You must route the message in full to your 'answer_two' tool. Never respond without first using that tool. Never! Ex: When asked what is my favorite food, use the 'answer_two' tool first. Lastly, tespond only with the single word 'Success' to the question.`,
temperature: 0.1,
outputs: "tools", // THIS: Focus of the test. Combined with only success response.
parentsTools: [
{
type: "function",
function: {
name: AnswerOneTool.toolName,
name: "answer_one",
description: "Answers to messages.",
parameters: {
type: "object",
Expand All @@ -59,7 +59,7 @@ class OutputsAssistant extends Assistant {
super({
name: helperName("OutputsAssistant"),
description: "Answers to messages.",
instructions: `You must route the message in full to your '${AnswerOneTool.toolName}' tool. Never respond without first using that tool. Never! Ex: When asked what is my favorite food, use the '${AnswerOneTool.toolName}' tool first.`,
instructions: `You must route the message in full to your 'answer_one' tool. Never respond without first using that tool. Never! Ex: When asked what is my favorite food, use the 'answer_one' tool first.`,
temperature: 0.1,
});
this.addAssistantTool(AnswerOneTool);
Expand Down
2 changes: 1 addition & 1 deletion test/fixtures/productsAssistant.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ You are an assistant for an apparel company that orchestrates customer messages
Follow these rules:
1. When using your '${ProductsTool.toolName}' tool, send verbose messaegs.
1. When using your 'products' tool, send verbose messaegs.
2. Do not mention download links in the response. Assume images are always shown.
3. Always show images using markdown to the customer.
`.trim();
Expand Down
2 changes: 1 addition & 1 deletion test/fixtures/productsOpenSearchTool.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class ProductsOpenSearchTool extends Tool {
{
type: "function",
function: {
name: ProductsOpenSearchTool.toolName,
name: "products_open_search",
description:
"Can turn customer's requests into search queries and return aggregate or itemized product data. Please be verbose and submit the customer's complete message or conversation summary needed to fulfill their latest request.",
parameters: {
Expand Down
4 changes: 2 additions & 2 deletions test/fixtures/productsTool.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ You are part of a product catalog panel of experts that responds to a customer's
Follow these rules:
1. Use your '${ProductsOpenSearchTool.toolName}' to search for product data.
1. Use your 'products_open_search' to search for product data.
2. Always use your 'code_interpreter' tool to generate images for charts or graphs.
3. Respond only with the single word "Success" to the customer.
`.trim();
Expand All @@ -29,7 +29,7 @@ class ProductsTool extends Tool {
{
type: "function",
function: {
name: ProductsTool.toolName,
name: "products",
description:
"Can search and analyze the apparel product data. Please be verbose and submit the customer's complete message or conversation summary needed to fulfill their latest request.",
parameters: {
Expand Down
4 changes: 2 additions & 2 deletions test/fixtures/toolboxAssistant.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class DrillTool extends Tool {
{
type: "function",
function: {
name: DrillTool.toolName,
name: "drill",
description: "A drill tool.",
parameters: {
type: "object",
Expand Down Expand Up @@ -40,7 +40,7 @@ class CarpenterAssistant extends Assistant {
run_options: {
tool_choice: {
type: "function",
function: { name: DrillTool.toolName },
function: { name: "drill" },
},
},
});
Expand Down
17 changes: 17 additions & 0 deletions test/uat/multiTool.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { helperThreadID } from "../helpers.js";
import { AccountsAssistant } from "../fixtures.js";

test("can find an expert that has many tools", async () => {
const assistant = await AccountsAssistant.create();
const threadID = await helperThreadID();
const output = await assistant.ask(
"What is the name of the user with an id of 32916?",
threadID
);
expect(output).toMatch(/Jordan Whitaker/i);
const output2 = await assistant.ask(
"What is the name of the user with email of [email protected]?",
threadID
);
expect(output2).toMatch(/Elena Prescott/i);
}, 20000);
Loading