Biblioteca de clientes de Assistentes OpenAI do Azure para JavaScript – versão 1.0.0-beta.5

A biblioteca de clientes de Assistentes OpenAI do Azure para JavaScript é uma adaptação das APIs REST do OpenAI que fornece uma interface idiomática e uma integração avançada com o restante do ecossistema do SDK do Azure. Ele pode se conectar aos recursos do OpenAI do Azure ou ao ponto de extremidade de inferência não Azure OpenAI, tornando-o uma ótima opção até mesmo para o desenvolvimento não Azure OpenAI.

Links principais:

Introdução

Ambientes com suporte no momento

Pré-requisitos

Se você quiser usar um recurso do Azure OpenAI, deverá ter uma assinatura do Azure e acesso ao Azure OpenAI. Isso permitirá que você crie um recurso openAI do Azure e obtenha uma URL de conexão, bem como chaves de API. Para obter mais informações, consulte Início Rápido: Introdução à geração de texto usando o Serviço OpenAI do Azure.

Se você quiser usar a biblioteca de clientes JS dos Assistentes OpenAI do Azure para se conectar ao OpenAI não Azure, precisará de uma chave de API de uma conta de desenvolvedor em https://platform.openai.com/.

Instalar o pacote @azure/openai-assistants

Instale a biblioteca de clientes dos Assistentes OpenAI do Azure para JavaScript com npm:

npm install @azure/openai-assistants

Criar e autenticar um AssistantsClient

Para configurar um cliente para uso com o Azure OpenAI, forneça um URI de ponto de extremidade válido para um recurso do Azure OpenAI juntamente com uma credencial de chave correspondente, uma credencial de token ou uma credencial de identidade do Azure autorizada a usar o recurso OpenAI do Azure. Para configurar o cliente para se conectar ao serviço do OpenAI, forneça uma chave de API no portal do desenvolvedor do OpenAI.

Usando uma chave de API do Azure

Use o Portal do Azure para navegar até o recurso OpenAI e recuperar uma chave de API ou usar o snippet da CLI do Azure abaixo:

Nota: Às vezes, a chave de API é chamada de "chave de assinatura" ou "chave de API de assinatura".

az cognitiveservices account keys list --resource-group <your-resource-group-name> --name <your-resource-name>

Principais conceitos

Consulte a documentação "como os assistentes funcionam" do OpenAI para obter uma visão geral dos conceitos e relações usados com os assistentes. Essa visão geral segue de perto o exemplo de visão geral do OpenAI para demonstrar os conceitos básicos de criação, execução e uso de assistentes e threads.

Para começar, crie um AssistantsClient:

const assistantsClient = new AssistantsClient("<endpoint>", new AzureKeyCredential("<azure_api_key>"));

Com um cliente, um assistente pode ser criado. Um assistente é uma interface criada com finalidade para modelos OpenAI que podem chamar Ferramentas, permitindo instruções de alto nível durante todo o tempo de vida do assistente.

O código para criar um assistente:

const assistant = await assistantsClient.createAssistant({
  model: "gpt-4-1106-preview",
  name: "JS Math Tutor",
  instructions: "You are a personal math tutor. Write and run code to answer math questions.",
  tools: [{ type: "code_interpreter" }]
});

Uma sessão de conversa entre um assistente e um usuário é chamada de Thread. Os threads armazenam mensagens e manipulam automaticamente o truncamento para ajustar o conteúdo ao contexto de um modelo.

Para criar um thread:

const assistantThread = await assistantsClient.createThread();

A mensagem representa uma mensagem criada por um assistente ou um usuário. As mensagens podem incluir texto, imagens e outros arquivos. As mensagens são armazenadas como uma lista no Thread. Com um thread criado, as mensagens podem ser criadas nele:

const question = "I need to solve the equation '3x + 11 = 14'. Can you help me?";
const messageResponse = await assistantsClient.createMessage(assistantThread.id, "user", question);

Uma Execução representa uma invocação de um Assistente em um Thread. O Assistente usa sua configuração e as Mensagens do Thread para executar tarefas chamando modelos e ferramentas. Como parte de uma Execução, o Assistente acrescenta Mensagens ao Thread. Em seguida, é possível iniciar uma execução que avalia o thread em relação a um assistente:

let runResponse = await assistantsClient.createRun(assistantThread.id, {
   assistantId: assistant.id,
   instructions: "Please address the user as Jane Doe. The user has a premium account." 
});

Depois que a execução for iniciada, ela deverá ser sondada até atingir um terminal status:

do {
  await new Promise((resolve) => setTimeout(resolve, 800));
  runResponse = await assistantsClient.getRun(assistantThread.id, runResponse.id);
} while (runResponse.status === "queued" || runResponse.status === "in_progress")

Supondo que a execução tenha sido concluída com êxito, listar mensagens do thread que foi executado agora refletirá novas informações adicionadas pelo assistente:

const runMessages = await assistantsClient.listMessages(assistantThread.id);
for (const runMessageDatum of runMessages.data) {
  for (const item of runMessageDatum.content) {
    if (item.type === "text") {
      console.log(item.text.value);
    } else if (item.type === "image_file") {
      console.log(item.imageFile.fileId);
    }
  }
}

Exemplo de saída desta sequência:

2023-11-14 20:21:23 -  assistant: The solution to the equation \(3x + 11 = 14\) is \(x = 1\).
2023-11-14 20:21:18 -       user: I need to solve the equation `3x + 11 = 14`. Can you help me?

Trabalhando com arquivos para recuperação

Os arquivos podem ser carregados e referenciados por assistentes ou mensagens. Primeiro, use a API de upload generalizada com a finalidade de "assistentes" para disponibilizar uma ID de arquivo:

const filename = "<path_to_text_file>";
await fs.writeFile(filename, "The word 'apple' uses the code 442345, while the word 'banana' uses the code 673457.", "utf8");
const uint8array = await fs.readFile(filename);
const uploadAssistantFile = await assistantsClient.uploadFile(uint8array, "assistants", { filename });

Depois de carregado, a ID do arquivo pode ser fornecida a um assistente após a criação. Observe que as IDs de arquivo só serão usadas se uma ferramenta apropriada, como Interpretador de Código ou Recuperação estiver habilitada.

const fileAssistant = await assistantsClient.createAssistant({
  model: "gpt-4-1106-preview",
  name: "JS SDK Test Assistant - Retrieval",
  instructions: "You are a helpful assistant that can help fetch data from files you know about.",
  tools: [{ type: "retrieval" }],
  fileIds: [ uploadAssistantFile.id ]
});

Com uma associação de ID de arquivo e uma ferramenta com suporte habilitada, o assistente poderá consumir os dados associados ao executar threads.

Usando ferramentas de função e chamada de função paralela

Conforme descrito na documentação do OpenAI para ferramentas de assistente, ferramentas que fazem referência a recursos definidos pelo chamador como funções podem ser fornecidas a um assistente para permitir que ele resolve e desambiguar dinamicamente durante uma execução.

Aqui, descrito é uma assistente simples que "sabe como fazer", por meio de funções fornecidas pelo chamador:

  1. Obter a cidade favorita do usuário
  2. Obter um apelido para uma determinada cidade
  3. Obter o clima atual, opcionalmente com uma unidade de temperatura, em uma cidade

Para fazer isso, comece definindo as funções a serem usadas – as implementações reais aqui são apenas stubs representativos.

// Example of a function that defines no parameters
const getFavoriteCity = () => "Atlanta, GA";
const getUserFavoriteCityTool = { 
  type: "function",
  function: {
    name: "getUserFavoriteCity",
    description: "Gets the user's favorite city.",
    parameters: {
      type: "object",
      properties: {}
    }
  }
}; 

// Example of a function with a single required parameter
const getCityNickname = (city) => { 
  switch (city) { 
    case "Atlanta, GA": 
      return "The ATL"; 
    case "Seattle, WA": 
      return "The Emerald City"; 
    case "Los Angeles, CA":
      return "LA"; 
    default: 
      return "Unknown"; 
  }
};

const getCityNicknameTool = { 
  type: "function",
  function: {
    name: "getCityNickname",
    description: "Gets the nickname for a city, e.g. 'LA' for 'Los Angeles, CA'.",
    parameters: { 
      type: "object",
      properties: { 
        city: {
          type: "string",
          description: "The city and state, e.g. San Francisco, CA"
        } 
      }
    }
  }
};

// Example of a function with one required and one optional, enum parameter
const getWeatherAtLocation = (location, temperatureUnit = "f") => {
  switch (location) { 
    case "Atlanta, GA": 
      return temperatureUnit === "f" ? "84f" : "26c"; 
    case "Seattle, WA": 
      return temperatureUnit === "f" ? "70f" : "21c"; 
    case "Los Angeles, CA":
      return temperatureUnit === "f" ? "90f" : "28c"; 
    default: 
      return "Unknown"; 
  }
};

const getWeatherAtLocationTool = { 
  type: "function",
  function: {
    name: "getWeatherAtLocation",
    description: "Gets the current weather at a provided location.",
    parameters: { 
      type: "object",
      properties: { 
        location: {
          type: "string",
          description: "The city and state, e.g. San Francisco, CA"
        },
        temperatureUnit: {
          type: "string",
          enum: ["f", "c"],
        }
      },
      required: ["location"]
    }
  }
};

Com as funções definidas em suas ferramentas apropriadas, agora é possível criar uma assistente que tenha essas ferramentas habilitadas:

  const weatherAssistant = await assistantsClient.createAssistant({
  // note: parallel function calling is only supported with newer models like gpt-4-1106-preview
  model: "gpt-4-1106-preview",
  name: "JS SDK Test Assistant - Weather",
  instructions: `You are a weather bot. Use the provided functions to help answer questions.
    Customize your responses to the user's preferences as much as possible and use friendly
    nicknames for cities whenever possible.
  `,
  tools: [getUserFavoriteCityTool, getCityNicknameTool, getWeatherAtLocationTool]
});

Se o assistente chamar ferramentas, o código de chamada precisará resolve ToolCall instâncias em instâncias correspondentesToolOutputSubmission. Para conveniência, um exemplo básico é extraído aqui:

const getResolvedToolOutput = (toolCall) => {
  const toolOutput = { toolCallId: toolCall.id };

  if (toolCall["function"]) {
    const functionCall = toolCall["function"];
    const functionName = functionCall.name;
    const functionArgs = JSON.parse(functionCall["arguments"] ?? {});

    switch (functionName) {
      case "getUserFavoriteCity":
        toolOutput.output = getFavoriteCity();
        break;
      case "getCityNickname":
        toolOutput.output = getCityNickname(functionArgs["city"]);
        break;
      case "getWeatherAtLocation":
        toolOutput.output = getWeatherAtLocation(functionArgs.location, functionArgs.temperatureUnit);
        break;
      default:
        toolOutput.output = `Unknown function: ${functionName}`;
        break;
    }
  }
  return toolOutput;
};

Para lidar com a entrada do usuário como "como é o clima agora na minha cidade favorita?", a sondagem da resposta para conclusão deve ser complementada por um RunStatus marcar para RequiresAction ou, neste caso, a presença da RequiredAction propriedade em execução. Em seguida, a coleção de ToolOutputSubmissions deve ser enviada para a execução por meio do SubmitRunToolOutputs método para que a execução possa continuar:

const question = "What's the weather like right now in my favorite city?";
let runResponse = await assistantsClient.createThreadAndRun({ 
  assistantId: weatherAssistant.id, 
  thread: { messages: [{ role: "user", content: question }] },
  tools: [getUserFavoriteCityTool, getCityNicknameTool, getWeatherAtLocationTool]
});

do {
  await new Promise((resolve) => setTimeout(resolve, 500));
  runResponse = await assistantsClient.getRun(runResponse.threadId, runResponse.id);
  
  if (runResponse.status === "requires_action" && runResponse.requiredAction.type === "submit_tool_outputs") {
    const toolOutputs = [];

    for (const toolCall of runResponse.requiredAction.submitToolOutputs.toolCalls) {
      toolOutputs.push(getResolvedToolOutput(toolCall));
    }
    runResponse = await assistantsClient.submitToolOutputsToRun(runResponse.threadId, runResponse.id, toolOutputs);
  }
} while (runResponse.status === "queued" || runResponse.status === "in_progress")

Observe que, ao usar modelos com suporte, o assistente pode solicitar que várias funções sejam chamadas em paralelo. Modelos mais antigos só podem chamar uma função por vez.

Depois que todas as chamadas de função necessárias forem resolvidas, a execução continuará normalmente e as mensagens concluídas no thread conterão a saída do modelo complementada pelas saídas da ferramenta de função fornecida.

Solução de problemas

Registro em log

A habilitação do log pode ajudar a descobrir informações úteis sobre falhas. Para ver um log de solicitações e respostas HTTP, defina a variável de ambiente AZURE_LOG_LEVEL como info. Como alternativa, o log pode ser habilitado no runtime chamando setLogLevel em @azure/logger:

const { setLogLevel } = require("@azure/logger");

setLogLevel("info");

Para obter instruções mais detalhadas sobre como habilitar logs, veja os documentos do pacote @azure/logger.