![Crea un agente con funzionalità di chiamata di funzioni OpenAI | di Tianyi Li | Marzo 2024
| Intelligenza-Artificiale Crea un agente con funzionalità di chiamata di funzioni OpenAI | di Tianyi Li | Marzo 2024
| Intelligenza-Artificiale](https://intelligenza-artificiale.eu/wp-content/uploads/2024/04/Crea-un-agente-con-funzionalita-di-chiamata-di-funzioni-OpenAI.png)
Prerequisiti:
Chiave API OpenAI: Puoi ottenerlo dalla piattaforma OpenAI.
Passaggio 1: prepararsi a chiamare la modella:
Per avviare una conversazione, inizia con un messaggio di sistema e una richiesta utente per l'attività:
- Creare un
messages
array per tenere traccia della cronologia delle conversazioni. - Includere un messaggio di sistema nel file
messages
array per stabilire il ruolo e il contesto dell'assistente. - Accogli gli utenti con un messaggio di saluto e chiedi loro di specificare la loro attività.
- Aggiungi il prompt dell'utente al file
messages
vettore.
const messages: ChatCompletionMessageParam() = ();console.log(StaticPromptMap.welcome);
messages.push(SystemPromptMap.context);
const userPrompt = await createUserMessage();
messages.push(userPrompt);
Come mia preferenza personale, tutti i suggerimenti sono memorizzati negli oggetti della mappa per un facile accesso e modifica. Fare riferimento ai seguenti frammenti di codice per tutte le istruzioni utilizzate nell'applicazione. Sentiti libero di adottare o modificare questo approccio come preferisci.
- StaticPromptMap: messaggi statici utilizzati durante la conversazione.
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;
- UserPromptMap: messaggi utente generati in base all'input dell'utente.
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?",
}),
};
- SystemPromptMap: messaggi di sistema generati in base al contesto di 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.",
},
};
- FunzionePromptMap: messaggi di funzione che sono fondamentalmente i valori restituiti dalle funzioni.
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,
}),
};
Passaggio 2: definire gli strumenti
Come menzionato prima, tools
sono essenzialmente le descrizioni delle funzioni che il modello può richiamare. In questo caso definiamo quattro strumenti per soddisfare le esigenze dell’assistente di viaggio in fattoria:
get_farms
: recupera un elenco di destinazioni farm in base alla posizione dell'utente.get_activities_per_farm
: Fornisce informazioni dettagliate sulle attività disponibili in una fattoria specifica.book_activity
: Facilita la prenotazione di un'attività selezionata.file_complaint
: Offre un processo semplice per presentare reclami.
Il seguente frammento di codice mostra come vengono definiti questi strumenti:
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,
}));
Comprendere le descrizioni delle funzioni
Le descrizioni delle funzioni richiedono i seguenti tasti:
name
: Identifica la funzione.description
: Fornisce un riepilogo di ciò che fa la funzione.parameters
: Definisce i parametri della funzione, inclusi i lorotype
,description
e se lo sonorequired
.type
: specifica il tipo di parametro, in genere un oggetto.properties
: descrive dettagliatamente ogni parametro, incluso il tipo e la descrizione.required
: Elenca i parametri essenziali per il funzionamento della funzione.
Aggiunta di una nuova funzione
Per introdurre una nuova funzione procedere come segue:
- Estendi DescriptionFunctionName con una nuova enumerazione, ad esempio
DoNewThings
. - Definire un tipo Props per i parametri, ad esempio,
DoNewThingsProps
. - Inserisci una nuova voce nel file
functionDescriptionsMap
oggetto. - Implementa la nuova funzione nella directory delle funzioni, nominandola dopo il valore enum.
Passaggio 3: chiama il modello con i messaggi e gli strumenti
Una volta impostati i messaggi e gli strumenti, siamo pronti per chiamare il modello utilizzandoli.
È importante notare che a partire da marzo 2024 la chiamata di funzione è supportata solo da gpt-3.5-turbo-0125
E gpt-4-turbo-preview
Modelli.
Implementazione del codice:
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
Opzioni
IL tool_choice
L'opzione controlla l'approccio del modello alle chiamate di funzione:
Specific Function
: Per specificare una funzione particolare, impostaretool_choice
ad un oggetto contype: "function"
e includere il nome e i dettagli della funzione. Ad esempio,tool_choice: { type: "function", function: { name: "get_farms"}}
dice al modello di chiamare theget_farms
funzione indipendentemente dal contesto. Anche un semplice messaggio utente come “Ciao”. attiverà questa chiamata di funzione.No Function
: Per fare in modo che il modello generi una risposta senza chiamate di funzione, utilizzaretool_choice: "none"
. Questa opzione richiede al modello di fare affidamento esclusivamente sui messaggi di input per generare la sua risposta.Automatic Selection
: L'impostazione predefinita,tool_choice: "auto"
lascia che il modello decida autonomamente se e quale funzione chiamare, in base al contesto della conversazione. Questa flessibilità è vantaggiosa per il processo decisionale dinamico relativo alle chiamate di funzione.
Passaggio 4: gestione delle risposte del modello
Le risposte del modello rientrano in due categorie principali, con la possibilità di errori che richiedono un messaggio di fallback:
- Richiesta di chiamata di funzione: Il modello indica il desiderio di chiamare una o più funzioni. Questo è il vero potenziale della chiamata di funzione. Il modello seleziona in modo intelligente quali funzioni eseguire in base al contesto e alle query dell'utente. Ad esempio, se l'utente chiede consigli sull'azienda agricola, il modello potrebbe suggerire di chiamare il
get_farms
funzione.
Ma non si ferma qui, il modello analizza anche l'input dell'utente per determinare se contiene le informazioni necessarie (arguments
) per la chiamata di funzione. In caso contrario, il modello richiederebbe all'utente i dettagli mancanti.
Una volta raccolte tutte le informazioni richieste (arguments
), il modello restituisce un oggetto JSON che descrive in dettaglio il nome della funzione e gli argomenti. Questa risposta strutturata può essere tradotta facilmente in un oggetto JavaScript all'interno della nostra applicazione, consentendoci di invocare la funzione specificata senza problemi, garantendo così un'esperienza utente fluida.
Inoltre, il modello può scegliere di richiamare più funzioni, simultaneamente o in sequenza, ciascuna delle quali richiede dettagli specifici. Gestirlo all'interno dell'applicazione è fondamentale per un funzionamento regolare.
Esempio di risposta del modello:
{
"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. Risposta in testo semplice: Il modello fornisce una risposta testuale diretta. Questo è l'output standard a cui siamo abituati dai modelli di intelligenza artificiale, che offre risposte semplici alle domande degli utenti. Per queste risposte è sufficiente restituire semplicemente il contenuto del testo.
Esempio di risposta del modello:
{
"role": "assistant",
"content": {
"text": "I can help you with that. What is your location?"
}
}
La distinzione fondamentale è la presenza di a tool_calls
chiave per function calls
. Se tool_calls
è presente, il modello richiede di eseguire una funzione; in caso contrario, fornisce una risposta testuale semplice.
Per elaborare queste risposte, considera il seguente approccio in base al tipo di risposta:
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");
}
});
}
IL arguments
estratti dalle chiamate di funzione vengono poi utilizzati per eseguire le funzioni effettive nell'applicazione, mentre il contenuto testuale aiuta a portare avanti la conversazione.
Di seguito è riportato un blocco if-else illustrando come si svolge questo processo:
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,
})
);
}
}
Passaggio 5: eseguire la funzione e richiamare nuovamente il modello
Quando il modello richiede una chiamata di funzione, eseguiamo quella funzione nella nostra applicazione e quindi aggiorniamo il modello con i nuovi messaggi. Ciò mantiene il modello informato sul risultato della funzione, permettendogli di dare una risposta pertinente all'utente.
Mantenere la corretta sequenza di esecuzioni delle funzioni è fondamentale, soprattutto quando il modello sceglie di eseguire più funzioni in sequenza per completare un'attività. Usare un for
ciclo invece di Promise.all
preserva l'ordine di esecuzione, essenziale per un flusso di lavoro di successo. Tuttavia, se le funzioni sono indipendenti e possono essere eseguite in parallelo, prendi in considerazione ottimizzazioni personalizzate per migliorare le prestazioni.
Ecco come eseguire la funzione:
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);
}
Ed ecco come aggiornare l'array dei messaggi con la risposta della funzione:
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,
})
);
}
Esempio delle funzioni che possono essere richiamate:
// 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"),
},
...
),
});
}
Esempio del tool
messaggio con risposta della funzione:
{
"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"
)
},
...
)
}
}
Passaggio 6: riepilogare i risultati per l'utente
Dopo aver eseguito le funzioni e aggiornato l'array di messaggi, ricolleghiamo il modello con questi messaggi aggiornati per informare l'utente sui risultati. Ciò implica invocare ripetutamente il file inizia chat funzione tramite un loop.
Per evitare un loop infinito, è fondamentale monitorare gli input dell'utente che segnalano la fine della conversazione, come “Arrivederci” o “Fine”, garantendo che il loop termini in modo appropriato.
Implementazione del codice:
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);
}
Fonte: towardsdatascience.com