Home Machine Learning Create an Agent with OpenAI Operate Calling Capabilities | by Tianyi Li | Mar, 2024

Create an Agent with OpenAI Operate Calling Capabilities | by Tianyi Li | Mar, 2024

0
Create an Agent with OpenAI Operate Calling Capabilities | by Tianyi Li | Mar, 2024

[ad_1]

Pre-requisites:

OpenAI API key: You’ll be able to receive this from the OpenAI platform.

Step 1: Put together to name the mannequin:

To provoke a dialog, start with a system message and a consumer immediate for the duty:

  • Create a messages array to maintain observe of the dialog historical past.
  • Embrace a system message within the messages array to to determine the assistant’s function and context.
  • Welcome the customers with a greeting message and immediate them to specify their job.
  • Add the consumer immediate to the messages array.
const messages: ChatCompletionMessageParam[] = [];

console.log(StaticPromptMap.welcome);

messages.push(SystemPromptMap.context);

const userPrompt = await createUserMessage();
messages.push(userPrompt);

As my private desire, all of the prompts are saved in map objects for simple entry and modification. Please discuss with the next code snippets for all of the prompts used within the utility. Be at liberty to undertake or modify this strategy as fits you.

  • StaticPromptMap: Static messages which might be used all through the dialog.
export const StaticPromptMap = {
welcome:
"Welcome to the farm assistant! What can I assist you with at present? You'll be able to ask me what I can do.",
fallback: "I am sorry, I do not perceive.",
finish: "I hope I used to be in a position that can assist you. Goodbye!",
} as const;
  • UserPromptMap: Person messages which might be generated based mostly on consumer enter.
import { ChatCompletionUserMessageParam } from "openai/sources/index.mjs";

kind UserPromptMapKey = "job";
kind UserPromptMapValue = (
userInput?: string
) => ChatCompletionUserMessageParam;
export const UserPromptMap: File<UserPromptMapKey, UserPromptMapValue> = {
job: (userInput) => (),
};

  • SystemPromptMap: System messages which might be generated based mostly on system context.
import { ChatCompletionSystemMessageParam } from "openai/sources/index.mjs";

kind SystemPromptMapKey = "context";
export const SystemPromptMap: File<
SystemPromptMapKey,
ChatCompletionSystemMessageParam
> = {
context: {
function: "system",
content material:
"You're an farm go to assistant. You're upbeat and pleasant. You introduce your self when first saying `Howdy!`. In the event you determine to name a operate, you need to retrieve the required fields for the operate from the consumer. Your reply ought to be as exact as attainable. If in case you have not but retrieve the required fields of the operate fully, you don't reply the query and inform the consumer you wouldn't have sufficient info.",
},
};

  • FunctionPromptMap: Operate messages which might be principally the return values of the capabilities.
import { ChatCompletionToolMessageParam } from "openai/sources/index.mjs";

kind FunctionPromptMapKey = "function_response";
kind FunctionPromptMapValue = (
args: Omit<ChatCompletionToolMessageParam, "function">
) => ChatCompletionToolMessageParam;
export const FunctionPromptMap: File<
FunctionPromptMapKey,
FunctionPromptMapValue
> = {
function_response: ({ tool_call_id, content material }) => ({
function: "instrument",
tool_call_id,
content material,
}),
};

Step 2: Outline the instruments

As talked about earlier, instruments are basically the descriptions of capabilities that the mannequin can name. On this case, we outline 4 instruments to fulfill the necessities of the farm journey assistant agent:

  1. get_farms: Retrieves a listing of farm locations based mostly on consumer’s location.
  2. get_activities_per_farm: Gives detailed info on actions out there at a particular farm.
  3. book_activity: Facilitates the reserving of a specific exercise.
  4. file_complaint: Presents a simple course of for submitting complaints.

The next code snippet demonstrates how these instruments are outlined:

import {
ChatCompletionTool,
FunctionDefinition,
} from "openai/sources/index.mjs";
import {
ConvertTypeNameStringLiteralToType,
JsonAcceptable,
} from "../utils/type-utils.js";

// An enum to outline the names of the capabilities. This can be shared between the operate descriptions and the precise capabilities
export enum DescribedFunctionName {
FileComplaint = "file_complaint",
getFarms = "get_farms",
getActivitiesPerFarm = "get_activities_per_farm",
bookActivity = "book_activity",
}
// It is a utility kind to slender down the `parameters` kind within the `FunctionDefinition`.
// It pairs with the key phrase `satisfies` to make sure that the properties of parameters are appropriately outlined.
// It is a workaround because the default kind of `parameters` in `FunctionDefinition` is `kind FunctionParameters = File<string, unknown>` which is overly broad.
kind FunctionParametersNarrowed<
T extends File<string, PropBase<JsonAcceptable>>
> = {
kind: JsonAcceptable; // principally all the categories that JSON can settle for
properties: T;
required: (keyof T)[];
};
// It is a base kind for every property of the parameters
kind PropBase<T extends JsonAcceptable = "string"> = {
kind: T;
description: string;
};
// This utility kind transforms parameter property string literals into usable sorts for operate parameters.
// Instance: { e mail: { kind: "string" } } -> { e mail: string }
export kind ConvertedFunctionParamProps<
Props extends File<string, PropBase<JsonAcceptable>>
> = {
[K in keyof Props]: ConvertTypeNameStringLiteralToType<Props[K]["type"]>;
};
// Outline the parameters for every operate
export kind FileComplaintProps = {
identify: PropBase;
e mail: PropBase;
textual content: PropBase;
};
export kind GetFarmsProps = {
location: PropBase;
};
export kind GetActivitiesPerFarmProps = {
farm_name: PropBase;
};
export kind BookActivityProps = {
farm_name: PropBase;
activity_name: PropBase;
datetime: PropBase;
identify: PropBase;
e mail: PropBase;
number_of_people: PropBase<"quantity">;
};
// Outline the operate descriptions
const functionDescriptionsMap: File<
DescribedFunctionName,
FunctionDefinition
> = {
[DescribedFunctionName.FileComplaint]: {
identify: DescribedFunctionName.FileComplaint,
description: "File a criticism as a buyer",
parameters: {
kind: "object",
properties: {
identify: {
kind: "string",
description: "The identify of the consumer, e.g. John Doe",
},
e mail: {
kind: "string",
description: "The e-mail handle of the consumer, e.g. john@doe.com",
},
textual content: {
kind: "string",
description: "Description of problem",
},
},
required: ["name", "email", "text"],
} satisfies FunctionParametersNarrowed<FileComplaintProps>,
},
[DescribedFunctionName.getFarms]: {
identify: DescribedFunctionName.getFarms,
description: "Get the data of farms based mostly on the placement",
parameters: {
kind: "object",
properties: {
location: {
kind: "string",
description: "The situation of the farm, e.g. Melbourne VIC",
},
},
required: ["location"],
} satisfies FunctionParametersNarrowed<GetFarmsProps>,
},
[DescribedFunctionName.getActivitiesPerFarm]: {
identify: DescribedFunctionName.getActivitiesPerFarm,
description: "Get the actions out there on a farm",
parameters: {
kind: "object",
properties: {
farm_name: {
kind: "string",
description: "The identify of the farm, e.g. Collingwood Youngsters's Farm",
},
},
required: ["farm_name"],
} satisfies FunctionParametersNarrowed<GetActivitiesPerFarmProps>,
},
[DescribedFunctionName.bookActivity]: {
identify: DescribedFunctionName.bookActivity,
description: "E book an exercise on a farm",
parameters: {
kind: "object",
properties: {
farm_name: {
kind: "string",
description: "The identify of the farm, e.g. Collingwood Youngsters's Farm",
},
activity_name: {
kind: "string",
description: "The identify of the exercise, e.g. Goat Feeding",
},
datetime: {
kind: "string",
description: "The date and time of the exercise",
},
identify: {
kind: "string",
description: "The identify of the consumer",
},
e mail: {
kind: "string",
description: "The e-mail handle of the consumer",
},
number_of_people: {
kind: "quantity",
description: "The variety of folks attending the exercise",
},
},
required: [
"farm_name",
"activity_name",
"datetime",
"name",
"email",
"number_of_people",
],
} satisfies FunctionParametersNarrowed<BookActivityProps>,
},
};
// Format the operate descriptions into instruments and export them
export const instruments = Object.values(
functionDescriptionsMap
).map<ChatCompletionTool>((description) => ({
kind: "operate",
operate: description,
}));

Understanding Operate Descriptions

Operate descriptions require the next keys:

  • identify: Identifies the operate.
  • description: Gives a abstract of what the operate does.
  • parameters: Defines the operate’s parameters, together with their kind, description, and whether or not they’re required.
  • kind: Specifies the parameter kind, usually an object.
  • properties: Particulars every parameter, together with its kind and outline.
  • required: Lists the parameters which might be important for the operate to function.

Including a New Operate

To introduce a brand new operate, proceed as follows:

  1. Lengthen DescribedFunctionName with a brand new enum, similar to DoNewThings.
  2. Outline a Props kind for the parameters, e.g., DoNewThingsProps.
  3. Insert a brand new entry within the functionDescriptionsMap object.
  4. Implement the brand new operate within the operate listing, naming it after the enum worth.

Step 3: Name the mannequin with the messages and the instruments

With the messages and instruments arrange, we’re able to name the mannequin utilizing them.

It’s essential to notice that as of March 2024, operate calling is supported solely by the gpt-3.5-turbo-0125 and gpt-4-turbo-preview fashions.

Code implementation:

export const startChat = async (messages: ChatCompletionMessageParam[]) => {
const response = await openai.chat.completions.create({
mannequin: "gpt-3.5-turbo",
top_p: 0.95,
temperature: 0.5,
max_tokens: 1024,

messages, // The messages array we created earlier
instruments, // The operate descriptions we outlined earlier
tool_choice: "auto", // The mannequin will determine whether or not to name a operate and which operate to name
});
const { message } = response.selections[0] ?? {};
if (!message) {
throw new Error("Error: No response from the API.");
}
messages.push(message);
return processMessage(message);
};

tool_choice Choices

The tool_choice choice controls the mannequin’s strategy to operate calls:

  • Particular Operate: To specify a specific operate, set tool_choice to an object with kind: "operate" and embrace the operate’s identify and particulars. As an illustration, tool_choice: { kind: "operate", operate: { identify: "get_farms"}} tells the mannequin to name the get_farms operate whatever the context. Even a easy consumer immediate like “Hello.” will set off this operate name.
  • No Operate: To have the mannequin generate a response with none operate calls, use tool_choice: "none". This selection prompts the mannequin to rely solely on the enter messages for producing its response.
  • Computerized Choice: The default setting, tool_choice: "auto", lets the mannequin autonomously determine if and which operate to name, based mostly on the dialog’s context. This flexibility is helpful for dynamic decision-making concerning operate calls.

Step 4: Dealing with Mannequin Responses

The mannequin’s responses fall into two major classes, with a possible for errors that necessitate a fallback message:

  1. Operate Name Request: The mannequin signifies a want to name operate(s). That is the true potential of operate calling. The mannequin intelligently selects which operate(s) to execute based mostly on context and consumer queries. As an illustration, if the consumer asks for farm suggestions, the mannequin could counsel calling the get_farms operate.

But it surely doesn’t simply cease there, the mannequin additionally analyzes the consumer enter to find out if it incorporates the required info (arguments) for the operate name. If not, the mannequin would immediate the consumer for the lacking particulars.

As soon as it has gathered all required info (arguments), the mannequin returns a JSON object detailing the operate identify and arguments. This structured response will be effortlessly translated right into a JavaScript object inside our utility, enabling us to invoke the required operate seamlessly, thereby making certain a fluid consumer expertise.

Moreover, the mannequin can select to name a number of capabilities, both concurrently or in sequence, every requiring particular particulars. Managing this throughout the utility is essential for easy operation.

Instance of mannequin’s response:

{
"function": "assistant",
"content material": null,
"tool_calls": [
{
"id": "call_JWoPQYmdxNXdNu1wQ1iDqz2z",
"type": "function",
"function": {
"name": "get_farms", // The function name to be called
"arguments": "{"location":"Melbourne"}" // The arguments required for the function
}
}
... // multiple function calls can be present
]
}

2. Plain Textual content Response: The mannequin supplies a direct textual content response. That is the usual output we’re accustomed to from AI fashions, providing simple solutions to consumer queries. Merely returning the textual content content material suffices for these responses.

Instance of mannequin’s response:

{
"function": "assistant",
"content material": {
"textual content": "I will help you with that. What's your location?"
}
}

The important thing distinction is the presence of a tool_calls key for operate calls. If tool_calls is current, the mannequin is requesting to execute a operate; in any other case, it delivers a simple textual content response.

To course of these responses, contemplate the next strategy based mostly on the response kind:

kind ChatCompletionMessageWithToolCalls = RequiredAll<
Omit<ChatCompletionMessage, "function_call">
>;

// If the message incorporates tool_calls, it extracts the operate arguments. In any other case, it returns the content material of the message.
export operate processMessage(message: ChatCompletionMessage) {
if (isMessageHasToolCalls(message)) {
return extractFunctionArguments(message);
} else {
return message.content material;
}
}
// Test if the message has `instrument calls`
operate isMessageHasToolCalls(
message: ChatCompletionMessage
): message is ChatCompletionMessageWithToolCalls {
return isDefined(message.tool_calls) && message.tool_calls.size !== 0;
}
// Extract operate identify and arguments from the message
operate extractFunctionArguments(message: ChatCompletionMessageWithToolCalls) {
return message.tool_calls.map((toolCall) => {
if (!isDefined(toolCall.operate)) {
throw new Error("No operate discovered within the instrument name");
}
attempt {
return {
tool_call_id: toolCall.id,
function_name: toolCall.operate.identify,
arguments: JSON.parse(toolCall.operate.arguments),
};
} catch (error) {
throw new Error("Invalid JSON in operate arguments");
}
});
}

The arguments extracted from the operate calls are then used to execute the precise capabilities within the utility, whereas the textual content content material helps to hold on the dialog.

Under is an if-else block illustrating how this course of unfolds:

const end result = await startChat(messages);

if (!end result) {
// Fallback message if response is empty (e.g., community error)
return console.log(StaticPromptMap.fallback);
} else if (isNonEmptyString(end result)) {
// If the response is a string, log it and immediate the consumer for the following message
console.log(`Assistant: ${end result}`);
const userPrompt = await createUserMessage();
messages.push(userPrompt);
} else {
// If the response incorporates operate calls, execute the capabilities and name the mannequin once more with the up to date messages
for (const merchandise of end result) {
const { tool_call_id, function_name, arguments: function_arguments } = merchandise;
// Execute the operate and get the operate return
const function_return = await functionMap[
function_name as keyof typeof functionMap
](function_arguments);
// Add the operate output again to the messages with a job of "instrument", the id of the instrument name, and the operate return because the content material
messages.push(
FunctionPromptMap.function_response({
tool_call_id,
content material: function_return,
})
);
}
}

Step 5: Execute the operate and name the mannequin once more

When the mannequin requests a operate name, we execute that operate in our utility after which replace the mannequin with the brand new messages. This retains the mannequin knowledgeable in regards to the operate’s end result, permitting it to present a pertinent reply to the consumer.

Sustaining the proper sequence of operate executions is essential, particularly when the mannequin chooses to execute a number of capabilities in a sequence to finish a job. Utilizing a for loop as an alternative of Promise.all preserves the execution order, important for a profitable workflow. Nonetheless, if the capabilities are unbiased and will be executed in parallel, contemplate customized optimizations to boost efficiency.

Right here’s how one can execute the operate:

for (const merchandise of end result) {
const { tool_call_id, function_name, arguments: function_arguments } = merchandise;

console.log(
`Calling operate "${function_name}" with ${JSON.stringify(
function_arguments
)}`
);
// Accessible capabilities are saved in a map for simple entry
const function_return = await functionMap[
function_name as keyof typeof functionMap
](function_arguments);
}

And right here’s how one can replace the messages array with the operate response:

for (const merchandise of end result) {
const { tool_call_id, function_name, arguments: function_arguments } = merchandise;

console.log(
`Calling operate "${function_name}" with ${JSON.stringify(
function_arguments
)}`
);
const function_return = await functionMap[
function_name as keyof typeof functionMap
](function_arguments);
// Add the operate output again to the messages with a job of "instrument", the id of the instrument name, and the operate return because the content material
messages.push(
FunctionPromptMap.function_response({
tool_call_id,
content material: function_return,
})
);
}

Instance of the capabilities that may be referred to as:

// Mocking getting farms based mostly on location from a database
export async operate get_farms(
args: ConvertedFunctionParamProps<GetFarmsProps>
): Promise<string> {
const { location } = args;
return JSON.stringify({
location,
farms: [
{
name: "Farm 1",
location: "Location 1",
rating: 4.5,
products: ["product 1", "product 2"],
actions: ["activity 1", "activity 2"],
},
...
],
});
}

Instance of the instrument message with operate response:

{
"function": "instrument",
"tool_call_id": "call_JWoPQYmdxNXdNu1wQ1iDqz2z",
"content material": {
// Operate return worth
"location": "Melbourne",
"farms": [
{
"name": "Farm 1",
"location": "Location 1",
"rating": 4.5,
"products": [
"product 1",
"product 2"
],
"actions": [
"activity 1",
"activity 2"
]
},
...
]
}
}

Step 6: Summarize the outcomes again to the consumer

After operating the capabilities and updating the message array, we re-engage the mannequin with these up to date messages to temporary the consumer on the outcomes. This entails repeatedly invoking the startChat operate through a loop.

To keep away from limitless looping, it’s essential to observe for consumer inputs signaling the tip of the dialog, like “Goodbye” or “Finish,” making certain the loop terminates appropriately.

Code implementation:

const CHAT_END_SIGNALS = [
"end",
"goodbye",
...
];

export operate isChatEnding(
message: ChatCompletionMessageParam | undefined | null
) {
// If the message just isn't outlined, log a fallback message
if (!isDefined(message)) {
return console.log(StaticPromptMap.fallback);
}
// Test if the message is from the consumer
if (!isUserMessage(message)) {
return false;
}
const { content material } = message;
return CHAT_END_SIGNALS.some((sign) => {
if (typeof content material === "string") {
return includeSignal(content material, sign);
} else {
// content material has a typeof ChatCompletionContentPart, which will be both ChatCompletionContentPartText or ChatCompletionContentPartImage
// If consumer attaches a picture to the present message first, we assume they don't seem to be ending the chat
const contentPart = content material.at(0);
if (contentPart?.kind !== "textual content") {
return false;
} else {
return includeSignal(contentPart.textual content, sign);
}
}
});
}
operate isUserMessage(
message: ChatCompletionMessageParam
): message is ChatCompletionUserMessageParam {
return message.function === "consumer";
}
operate includeSignal(content material: string, sign: string) {
return content material.toLowerCase().contains(sign);
}

[ad_2]