1s6xeclnxymabyi6yyjjveg.png

Requisitos previos:

Clave API de OpenAI: Puede obtenerlo desde la plataforma OpenAI.

Paso 1: prepárese para llamar al modelo:

Para iniciar una conversación, comience con un mensaje del sistema y un mensaje de usuario para la tarea:

  • Crear un messages matriz para realizar un seguimiento del historial de conversaciones.
  • Incluir un mensaje del sistema en el messages matriz para establecer el rol y el contexto del asistente.
  • Dé la bienvenida a los usuarios con un mensaje de saludo y solicíteles que especifiquen su tarea.
  • Agregue el mensaje de usuario al messages formación.
const messages: ChatCompletionMessageParam[] = [];

console.log(StaticPromptMap.welcome);

messages.push(SystemPromptMap.context);

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

Según mi preferencia personal, todas las indicaciones se almacenan en objetos del mapa para facilitar el acceso y la modificación. Consulte los siguientes fragmentos de código para conocer todas las indicaciones utilizadas en la aplicación. Siéntase libre de adoptar o modificar este enfoque según le convenga.

  • Mapa de aviso estático: Mensajes estáticos que se utilizan a lo largo de la conversación.
export const StaticPromptMap = {
welcome:
"Welcome to the farm assistant! What can I help you with today? You can ask me what I can do.",
fallback: "I'm sorry, I don't understand.",
end: "I hope I was able to help you. Goodbye!",
} as const;
  • Mapa de aviso de usuario: mensajes de usuario que se generan en función de la entrada del usuario.
import { ChatCompletionUserMessageParam } from "openai/resources/index.mjs";

type UserPromptMapKey = "task";
type UserPromptMapValue = (
userInput?: string
) => ChatCompletionUserMessageParam;
export const UserPromptMap: Record<UserPromptMapKey, UserPromptMapValue> = {
task: (userInput) => ({
role: "user",
content: userInput || "What can you do?",
}),
};

  • Mapa de aviso del sistema: Mensajes del sistema que se generan según el contexto del sistema.
import { ChatCompletionSystemMessageParam } from "openai/resources/index.mjs";

type SystemPromptMapKey = "context";
export const SystemPromptMap: Record<
SystemPromptMapKey,
ChatCompletionSystemMessageParam
> = {
context: {
role: "system",
content:
"You are an farm visit assistant. You are upbeat and friendly. You introduce yourself when first saying `Howdy!`. If you decide to call a function, you should retrieve the required fields for the function from the user. Your answer should be as precise as possible. If you have not yet retrieve the required fields of the function completely, you do not answer the question and inform the user you do not have enough information.",
},
};

  • FunciónPromptMap: Mensajes de función que son básicamente los valores de retorno de las funciones.
import { ChatCompletionToolMessageParam } from "openai/resources/index.mjs";

type FunctionPromptMapKey = "function_response";
type FunctionPromptMapValue = (
args: Omit<ChatCompletionToolMessageParam, "role">
) => ChatCompletionToolMessageParam;
export const FunctionPromptMap: Record<
FunctionPromptMapKey,
FunctionPromptMapValue
> = {
function_response: ({ tool_call_id, content }) => ({
role: "tool",
tool_call_id,
content,
}),
};

Paso 2: definir las herramientas

Como se mencionó anteriormente, tools son esencialmente las descripciones de funciones que el modelo puede llamar. En este caso, definimos cuatro herramientas para cumplir con los requisitos del agente asistente de viajes agrícolas:

  1. get_farms: recupera una lista de destinos de granjas según la ubicación del usuario.
  2. get_activities_per_farm: Proporciona información detallada sobre las actividades disponibles en una granja específica.
  3. book_activity: Facilita la reserva de una actividad seleccionada.
  4. file_complaint: Ofrece un proceso sencillo para presentar quejas.

El siguiente fragmento de código demuestra cómo se definen estas herramientas:

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

// An enum to define the names of the functions. This will be shared between the function descriptions and the actual functions
export enum DescribedFunctionName {
FileComplaint = "file_complaint",
getFarms = "get_farms",
getActivitiesPerFarm = "get_activities_per_farm",
bookActivity = "book_activity",
}
// This is a utility type to narrow down the `parameters` type in the `FunctionDefinition`.
// It pairs with the keyword `satisfies` to ensure that the properties of parameters are correctly defined.
// This is a workaround as the default type of `parameters` in `FunctionDefinition` is `type FunctionParameters = Record<string, unknown>` which is overly broad.
type FunctionParametersNarrowed<
T extends Record<string, PropBase<JsonAcceptable>>
> = {
type: JsonAcceptable; // basically all the types that JSON can accept
properties: T;
required: (keyof T)[];
};
// This is a base type for each property of the parameters
type PropBase<T extends JsonAcceptable = "string"> = {
type: T;
description: string;
};
// This utility type transforms parameter property string literals into usable types for function parameters.
// Example: { email: { type: "string" } } -> { email: string }
export type ConvertedFunctionParamProps<
Props extends Record<string, PropBase<JsonAcceptable>>
> = {
[K in keyof Props]: ConvertTypeNameStringLiteralToType<Props[K]["type"]>;
};
// Define the parameters for each function
export type FileComplaintProps = {
name: PropBase;
email: PropBase;
text: PropBase;
};
export type GetFarmsProps = {
location: PropBase;
};
export type GetActivitiesPerFarmProps = {
farm_name: PropBase;
};
export type BookActivityProps = {
farm_name: PropBase;
activity_name: PropBase;
datetime: PropBase;
name: PropBase;
email: PropBase;
number_of_people: PropBase<"number">;
};
// Define the function descriptions
const functionDescriptionsMap: Record<
DescribedFunctionName,
FunctionDefinition
> = {
[DescribedFunctionName.FileComplaint]: {
name: DescribedFunctionName.FileComplaint,
description: "File a complaint as a customer",
parameters: {
type: "object",
properties: {
name: {
type: "string",
description: "The name of the user, e.g. John Doe",
},
email: {
type: "string",
description: "The email address of the user, e.g. john@doe.com",
},
text: {
type: "string",
description: "Description of issue",
},
},
required: ["name", "email", "text"],
} satisfies FunctionParametersNarrowed<FileComplaintProps>,
},
[DescribedFunctionName.getFarms]: {
name: DescribedFunctionName.getFarms,
description: "Get the information of farms based on the location",
parameters: {
type: "object",
properties: {
location: {
type: "string",
description: "The location of the farm, e.g. Melbourne VIC",
},
},
required: ["location"],
} satisfies FunctionParametersNarrowed<GetFarmsProps>,
},
[DescribedFunctionName.getActivitiesPerFarm]: {
name: DescribedFunctionName.getActivitiesPerFarm,
description: "Get the activities available on a farm",
parameters: {
type: "object",
properties: {
farm_name: {
type: "string",
description: "The name of the farm, e.g. Collingwood Children's Farm",
},
},
required: ["farm_name"],
} satisfies FunctionParametersNarrowed<GetActivitiesPerFarmProps>,
},
[DescribedFunctionName.bookActivity]: {
name: DescribedFunctionName.bookActivity,
description: "Book an activity on a farm",
parameters: {
type: "object",
properties: {
farm_name: {
type: "string",
description: "The name of the farm, e.g. Collingwood Children's Farm",
},
activity_name: {
type: "string",
description: "The name of the activity, e.g. Goat Feeding",
},
datetime: {
type: "string",
description: "The date and time of the activity",
},
name: {
type: "string",
description: "The name of the user",
},
email: {
type: "string",
description: "The email address of the user",
},
number_of_people: {
type: "number",
description: "The number of people attending the activity",
},
},
required: [
"farm_name",
"activity_name",
"datetime",
"name",
"email",
"number_of_people",
],
} satisfies FunctionParametersNarrowed<BookActivityProps>,
},
};
// Format the function descriptions into tools and export them
export const tools = Object.values(
functionDescriptionsMap
).map<ChatCompletionTool>((description) => ({
type: "function",
function: description,
}));

Comprender las descripciones de funciones

Las descripciones de funciones requieren las siguientes claves:

  • name: Identifica la función.
  • description: proporciona un resumen de lo que hace la función.
  • parameters: Define los parámetros de la función, incluidos sus type, descriptiony si son required.
  • type: Especifica el tipo de parámetro, normalmente un objeto.
  • properties: detalla cada parámetro, incluido su tipo y descripción.
  • required: enumera los parámetros que son esenciales para que funcione la función.

Agregar una nueva función

Para introducir una nueva función, proceda de la siguiente manera:

  1. Amplíe DescribedFunctionName con una nueva enumeración, como DoNewThings.
  2. Defina un tipo de accesorios para los parámetros, por ejemplo, DoNewThingsProps.
  3. Insertar una nueva entrada en el functionDescriptionsMap objeto.
  4. Implemente la nueva función en el directorio de funciones, nombrándola después del valor de enumeración.

Paso 3: Llama al modelo con los mensajes y las herramientas.

Con los mensajes y las herramientas configurados, estamos listos para llamar al modelo usándolos.

Es importante tener en cuenta que a partir de marzo de 2024, la llamada a funciones solo es compatible con gpt-3.5-turbo-0125 y gpt-4-turbo-preview modelos.

Implementación del código:

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

messages, // The messages array we created earlier
tools, // The function descriptions we defined earlier
tool_choice: "auto", // The model will decide whether to call a function and which function to call
});
const { message } = response.choices[0] ?? {};
if (!message) {
throw new Error("Error: No response from the API.");
}
messages.push(message);
return processMessage(message);
};

tool_choice Opciones

El tool_choice La opción controla el enfoque del modelo para las llamadas a funciones:

  • Specific Function: Para especificar una función particular, configure tool_choice a un objeto con type: "function" e incluya el nombre y los detalles de la función. Por ejemplo, tool_choice: { type: "function", function: { name: "get_farms"}} le dice al modelo que llame al get_farms funcionar independientemente del contexto. Incluso un simple mensaje de usuario como «Hola». activará esta llamada de función.
  • No Function: Para que el modelo genere una respuesta sin ninguna llamada a función, utilice tool_choice: "none". Esta opción solicita al modelo que dependa únicamente de los mensajes de entrada para generar su respuesta.
  • Automatic Selection: La configuración predeterminada, tool_choice: "auto", permite que el modelo decida de forma autónoma si llamar y qué función llamar, según el contexto de la conversación. Esta flexibilidad es beneficiosa para la toma de decisiones dinámica con respecto a las llamadas a funciones.

Paso 4: Manejo de las respuestas del modelo

Las respuestas del modelo se dividen en dos categorías principales, con potencial de errores que requieren un mensaje alternativo:

  1. Solicitud de llamada de función: El modelo indica el deseo de llamar a funciones. Este es el verdadero potencial de la llamada a funciones. El modelo selecciona de forma inteligente qué funciones ejecutar en función del contexto y las consultas del usuario. Por ejemplo, si el usuario solicita recomendaciones sobre granjas, el modelo puede sugerir llamar al get_farms función.

Pero no se queda ahí, el modelo también analiza la entrada del usuario para determinar si contiene la información necesaria (arguments) para la llamada a la función. De lo contrario, el modelo solicitaría al usuario los detalles que faltan.

Una vez que haya recopilado toda la información requerida (arguments), el modelo devuelve un objeto JSON que detalla el nombre de la función y los argumentos. Esta respuesta estructurada se puede traducir sin esfuerzo a un objeto JavaScript dentro de nuestra aplicación, lo que nos permite invocar la función especificada sin problemas, garantizando así una experiencia de usuario fluida.

Además, el modelo puede optar por llamar a múltiples funciones, ya sea simultáneamente o en secuencia, cada una de las cuales requiere detalles específicos. Gestionar esto dentro de la aplicación es crucial para un buen funcionamiento.

Ejemplo de respuesta del modelo:

{
"role": "assistant",
"content": 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. Respuesta en texto plano: El modelo proporciona una respuesta de texto directa. Este es el resultado estándar al que estamos acostumbrados de los modelos de IA, que ofrece respuestas sencillas a las consultas de los usuarios. Para estas respuestas basta con devolver el contenido del texto.

Ejemplo de respuesta del modelo:

{
"role": "assistant",
"content": {
"text": "I can help you with that. What is your location?"
}
}

La distinción clave es la presencia de una tool_calls clave para function calls. Si tool_calls está presente, el modelo solicita ejecutar una función; de lo contrario, ofrece una respuesta de texto sencilla.

Para procesar estas respuestas, considere el siguiente enfoque según el tipo de respuesta:

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

// If the message contains tool_calls, it extracts the function arguments. Otherwise, it returns the content of the message.
export function processMessage(message: ChatCompletionMessage) {
if (isMessageHasToolCalls(message)) {
return extractFunctionArguments(message);
} else {
return message.content;
}
}
// Check if the message has `tool calls`
function isMessageHasToolCalls(
message: ChatCompletionMessage
): message is ChatCompletionMessageWithToolCalls {
return isDefined(message.tool_calls) && message.tool_calls.length !== 0;
}
// Extract function name and arguments from the message
function extractFunctionArguments(message: ChatCompletionMessageWithToolCalls) {
return message.tool_calls.map((toolCall) => {
if (!isDefined(toolCall.function)) {
throw new Error("No function found in the tool call");
}
try {
return {
tool_call_id: toolCall.id,
function_name: toolCall.function.name,
arguments: JSON.parse(toolCall.function.arguments),
};
} catch (error) {
throw new Error("Invalid JSON in function arguments");
}
});
}

El arguments Las llamadas a funciones extraídas de las funciones se utilizan luego para ejecutar las funciones reales en la aplicación, mientras que el contenido del texto ayuda a continuar la conversación.

A continuación se muestra un bloque si-si no ilustrando cómo se desarrolla este proceso:

const result = await startChat(messages);

if (!result) {
// Fallback message if response is empty (e.g., network error)
return console.log(StaticPromptMap.fallback);
} else if (isNonEmptyString(result)) {
// If the response is a string, log it and prompt the user for the next message
console.log(`Assistant: ${result}`);
const userPrompt = await createUserMessage();
messages.push(userPrompt);
} else {
// If the response contains function calls, execute the functions and call the model again with the updated messages
for (const item of result) {
const { tool_call_id, function_name, arguments: function_arguments } = item;
// Execute the function and get the function return
const function_return = await functionMap[
function_name as keyof typeof functionMap
](function_arguments);
// Add the function output back to the messages with a role of "tool", the id of the tool call, and the function return as the content
messages.push(
FunctionPromptMap.function_response({
tool_call_id,
content: function_return,
})
);
}
}

Paso 5: ejecutar la función y llamar al modelo nuevamente

Cuando el modelo solicita una llamada a una función, ejecutamos esa función en nuestra aplicación y luego actualizamos el modelo con los nuevos mensajes. Esto mantiene al modelo informado sobre el resultado de la función, permitiéndole dar una respuesta pertinente al usuario.

Mantener la secuencia correcta de ejecución de funciones es crucial, especialmente cuando el modelo elige ejecutar múltiples funciones en una secuencia para completar una tarea. Usando un for bucle en lugar de Promise.all preserva el orden de ejecución, esencial para un flujo de trabajo exitoso. Sin embargo, si las funciones son independientes y se pueden ejecutar en paralelo, considere optimizaciones personalizadas para mejorar el rendimiento.

A continuación se explica cómo ejecutar la función:

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

console.log(
`Calling function "${function_name}" with ${JSON.stringify(
function_arguments
)}`
);
// Available functions are stored in a map for easy access
const function_return = await functionMap[
function_name as keyof typeof functionMap
](function_arguments);
}

Y aquí se explica cómo actualizar la matriz de mensajes con la función respuesta:

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

console.log(
`Calling function "${function_name}" with ${JSON.stringify(
function_arguments
)}`
);
const function_return = await functionMap[
function_name as keyof typeof functionMap
](function_arguments);
// Add the function output back to the messages with a role of "tool", the id of the tool call, and the function return as the content
messages.push(
FunctionPromptMap.function_response({
tool_call_id,
content: function_return,
})
);
}

Ejemplo de las funciones que se pueden llamar:

// Mocking getting farms based on location from a database
export async function 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"],
activities: ["activity 1", "activity 2"],
},
...
],
});
}

Ejemplo de la tool mensaje con respuesta de función:

{
"role": "tool",
"tool_call_id": "call_JWoPQYmdxNXdNu1wQ1iDqz2z",
"content": {
// Function return value
"location": "Melbourne",
"farms": [
{
"name": "Farm 1",
"location": "Location 1",
"rating": 4.5,
"products": [
"product 1",
"product 2"
],
"activities": [
"activity 1",
"activity 2"
]
},
...
]
}
}

Paso 6: resumir los resultados al usuario

Después de ejecutar las funciones y actualizar la matriz de mensajes, volvemos a conectar el modelo con estos mensajes actualizados para informar al usuario sobre los resultados. Esto implica invocar repetidamente el Comenzar chat funcionar a través de un bucle.

Para evitar bucles interminables, es crucial monitorear las entradas del usuario que señalan el final de la conversación, como «Adiós» o «Fin», asegurando que el bucle termine apropiadamente.

Implementación del código:

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

export function isChatEnding(
message: ChatCompletionMessageParam | undefined | null
) {
// If the message is not defined, log a fallback message
if (!isDefined(message)) {
return console.log(StaticPromptMap.fallback);
}
// Check if the message is from the user
if (!isUserMessage(message)) {
return false;
}
const { content } = message;
return CHAT_END_SIGNALS.some((signal) => {
if (typeof content === "string") {
return includeSignal(content, signal);
} else {
// content has a typeof ChatCompletionContentPart, which can be either ChatCompletionContentPartText or ChatCompletionContentPartImage
// If user attaches an image to the current message first, we assume they are not ending the chat
const contentPart = content.at(0);
if (contentPart?.type !== "text") {
return false;
} else {
return includeSignal(contentPart.text, signal);
}
}
});
}
function isUserMessage(
message: ChatCompletionMessageParam
): message is ChatCompletionUserMessageParam {
return message.role === "user";
}
function includeSignal(content: string, signal: string) {
return content.toLowerCase().includes(signal);
}