Como gerenciar comandos com o Dotprompt

O Firebase Genkit fornece o plug-in Dotprompt e o formato de texto para ajudar você a escrever e organizar seus comandos de IA generativa.

O Dotprompt foi projetado com a premissa de que solicitações são código. Você escreve e mantém os prompts em arquivos especialmente formatados, chamados arquivos pontoprompt, rastreia as alterações neles usando o mesmo sistema de controle de versões usado no código e as implanta com o código que chama os modelos de IA generativa.

Para usar o Dotprompt, primeiro crie um diretório prompts na raiz do projeto e depois crie um arquivo .prompt nesse diretório. Confira um exemplo simples que pode ser chamado de greeting.prompt:

---
model: vertexai/gemini-1.0-pro
config:
  temperature: 0.9
input:
  schema:
    location: string
    style?: string
    name?: string
  default:
    location: a restaurant
---

You are the world's most welcoming AI assistant and are currently working at {{location}}.

Greet a guest{{#if name}} named {{name}}{{/if}}{{#if style}} in the style of {{style}}{{/if}}.

Para usar esse prompt, instale o plug-in dotprompt e importe a função prompt da biblioteca @genkit-ai/dotprompt:

import { dotprompt, prompt } from '@genkit-ai/dotprompt';

configureGenkit({ plugins: [dotprompt()] });

Em seguida, carregue o comando usando prompt('file_name'):

const greetingPrompt = await prompt('greeting');

const result = await greetingPrompt.generate({
  input: {
    location: 'the beach',
    style: 'a fancy pirate',
  },
});

console.log(result.text());

A sintaxe do Dotprompt é baseada na linguagem de modelo do Handlebars. Você pode usar os auxiliares if, unless e each para adicionar partes condicionais ao comando ou iterar o conteúdo estruturado. O formato de arquivo usa um front-end YAML para fornecer metadados a um comando inline com o modelo.

Como definir esquemas de entrada/saída com o Picoschema

O Dotprompt inclui um formato de definição de esquema compacto e otimizado para YAML, chamado Picoschema, para facilitar a definição dos atributos mais importantes de um esquema para uso de LLM. Veja um exemplo de esquema para um artigo:

schema:
  title: string # string, number, and boolean types are defined like this
  subtitle?: string # optional fields are marked with a `?`
  draft?: boolean, true when in draft state
  status?(enum, approval status): [PENDING, APPROVED]
  date: string, the date of publication e.g. '2024-04-09' # descriptions follow a comma
  tags(array, relevant tags for article): string # arrays are denoted via parentheses
  authors(array):
    name: string
    email?: string
  metadata?(object): # objects are also denoted via parentheses
    updatedAt?: string, ISO timestamp of last update
    approvedBy?: integer, id of approver

O esquema acima é equivalente à seguinte interface do TypeScript:

interface Article {
  title: string;
  subtitle?: string;
  /** true when in draft state */
  draft?: boolean;
  /** approval status */
  status?: 'PENDING' | 'APPROVED';
  /** the date of publication e.g. '2024-04-09' */
  date: string;
  /** relevant tags for article */
  tags: string[];
  authors: {
    name: string;
    email?: string;
  }[];
  metadata?: {
    /** ISO timestamp of last update */
    updatedAt?: string;
    /** id of approver */
    approvedBy?: number;
  };
}

O Picoschema é compatível com os tipos escalares string, integer, number e boolean. Para objetos, matrizes e tipos enumerados, eles são indicados por um parêntese após o nome do campo.

Os objetos definidos pelo Picoschema têm todas as propriedades conforme necessário, a menos que sejam indicados como opcionais por ?, e não permitem outras propriedades.

O Picoschema não é compatível com muitas das capacidades do esquema JSON completo. Se você precisar de esquemas mais robustos, forneça um esquema JSON:

output:
  schema:
    type: object
    properties:
      field1:
        type: number
        minimum: 20

Como substituir metadados de comandos

Embora os arquivos .prompt permitam que você incorpore metadados, como configuração do modelo, no próprio arquivo, também é possível modificar esses valores por chamada:

const result = await greetingPrompt.generate({
  model: 'google-genai/gemini-pro',
  config: {
    temperature: 1.0,
  },
  input: {
    location: 'the beach',
    style: 'a fancy pirate',
  },
});

Saída estruturada

Você pode definir o formato e o esquema de saída de um comando para converter em JSON:

---
model: vertexai/gemini-1.0-pro
input:
  schema:
    theme: string
output:
  format: json
  schema:
    name: string
    price: integer
    ingredients(array): string
---

Generate a menu item that could be found at a {{theme}} themed restaurant.

Ao gerar uma solicitação com saída estruturada, use o auxiliar output() para extraí-la e validá-la:

const createMenuPrompt = await prompt('create_menu');

const menu = await createMenuPrompt.generate({
  input: {
    theme: 'banana',
  },
});

console.log(menu.output());

Comandos com várias mensagens

Por padrão, o Dotprompt constrói uma única mensagem com um papel "user". Algumas solicitações são melhor expressas como uma combinação de várias mensagens, como uma solicitação do sistema.

O auxiliar {{role}} oferece uma maneira simples de criar comandos de várias mensagens:

---
model: vertexai/gemini-1.0-pro
input:
  schema:
    userQuestion: string
---

{{role "system"}}
You are a helpful AI assistant that really loves to talk about food. Try to work
food items into all of your conversations.
{{role "user"}}
{{userQuestion}}

Comandos multimodais

Para modelos com suporte a entrada multimodal, como imagens com texto, é possível usar o auxiliar {{media}}:

---
model: vertexai/gemini-1.0-pro-vision
input:
  schema:
    photoUrl: string
---

Describe this image in a detailed paragraph:

{{media url=photoUrl}}

O URL pode ser https:// ou URIs data: codificados em base64 para uso de imagem in-line. No código, isso seria:

const describeImagePrompt = await prompt('describe_image');

const result = await describeImagePrompt.generate({
  input: {
    photoUrl: 'https://example.com/image.png',
  },
});

console.log(result.text());

Variantes de comando

Como os arquivos de comandos são apenas texto, você pode (e precisa) fazer commit deles no seu sistema de controle de versões, permitindo comparar mudanças ao longo do tempo com facilidade. Muitas vezes, as versões ajustadas dos comandos só podem ser totalmente testadas em um ambiente de produção lado a lado com as versões atuais. O Dotprompt oferece suporte para isso por meio do recurso de variants.

Para criar uma variante, crie um arquivo [name].[variant].prompt. Por exemplo, se você estivesse usando o Gemini 1.0 Pro no comando, mas quisesse saber se o Gemini 1.5 Pro funcionaria melhor, poderia criar dois arquivos:

  • my_prompt.prompt: o comando de "valor de referência"
  • my_prompt.gemini15.prompt: uma variante chamada "gêmeos"

Para usar uma variante de comando, especifique a opção variant ao carregar:

const myPrompt = await prompt('my_prompt', { variant: 'gemini15' });

O carregador de comandos vai tentar carregar a variante desse nome e, se não houver, recorrerá ao valor de referência. Isso significa que é possível usar o carregamento condicional com base em qualquer critério que faça sentido para o aplicativo:

const myPrompt = await prompt('my_prompt', {
  variant: isBetaTester(user) ? 'gemini15' : null,
});

O nome da variante é incluído nos metadados dos traces de geração para que você possa comparar e contrastar o desempenho real entre as variantes no inspetor de rastreamento do Genkit.

Formas alternativas de carregar e definir comandos

O Dotprompt é otimizado para organização no diretório de prompt. No entanto, existem algumas outras maneiras de carregar e definir comandos:

  • loadPromptFile: carrega um prompt de um arquivo no diretório de prompts.
  • loadPromptUrl: carrega uma solicitação de um URL.
  • defineDotprompt: define uma solicitação no código.

Por exemplo:

import {
  loadPromptFile,
  loadPromptUrl,
  defineDotprompt,
} from '@genkit-ai/dotprompt';
import path from 'path';
import { z } from 'zod';

// Load a prompt from a file
const myPrompt = await loadPromptFile(
  path.resolve(__dirname, './path/to/my_prompt.prompt')
);

// Load a prompt from a URL
const myPrompt = await loadPromptUrl('https://example.com/my_prompt.prompt');

// Define a prompt in code
const myPrompt = defineDotprompt(
  {
    model: 'vertexai/gemini-1.0-pro',
    input: {
      schema: z.object({
        name: z.string(),
      }),
    },
  },
  `Hello {{name}}, how are you today?`
);