Como gerenciar comandos com o Dotprompt

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

O Dotprompt foi desenvolvido com base na premissa de que comandos são código. Você escreve e mantém seus comandos em arquivos especialmente formatados chamados arquivos dotprompt, rastreia alterações neles usando o mesmo sistema de controle de versão usado para seu código e os implanta com o código que chama seus modelos de IA generativa.

Para usar o Dotprompt, primeiro crie um diretório prompts na raiz do seu projeto e, em seguida, criar um arquivo .prompt nesse diretório. Aqui está um exemplo simples que você pode chamar greeting.prompt:

---
model: vertexai/gemini-1.5-flash
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 comando, instale o plug-in dotprompt e importe a função prompt do 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 modelagem Handlebars. Você pode usar os auxiliares if, unless e each para adicionar partes condicionais do seu comando ou iterar conteúdo estruturado. O formato do arquivo utiliza um front-end YAML para fornecer metadados para um comando in-line com o modelo.

Como definir esquemas de entrada/saída

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 o uso de LLMs. 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
  extra?: any, arbitrary extra data
  (*): string, wildcard field

O esquema acima é equivalente à seguinte interface do TypeScript:

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

}

O Picoschema oferece suporte aos tipos escalares string, integer, number, boolean e any. 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 indicadas como opcionais por ? e não permitam outras propriedades. Quando uma propriedade é marcada como opcional, ela também é anulável, proporcionando mais tolerância para que os LLMs retornem um valor nulo em vez da omissão de um campo.

Em uma definição de objeto, a chave especial (*) pode ser usada para declarar uma definição de campo "caractere curinga". Isso corresponderá a todas as propriedades adicionais não fornecidas por uma chave explícita.

O Picoschema não é compatível com muitos dos recursos do esquema JSON completo. Se você precisar de esquemas mais robustos, poderá fornecer um esquema JSON:

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

Como aproveitar esquemas reutilizáveis

Além de definir esquemas diretamente no arquivo .prompt, é possível referenciar um esquema registrado com defineSchema pelo nome. Para registrar um esquema, faça o seguinte:

import { defineSchema } from '@genkit-ai/core';
import { z } from 'zod';

const MySchema = defineSchema(
  'MySchema',
  z.object({
    field1: z.string(),
    field2: z.number(),
  })
);

No comando, insira o nome do esquema registrado:

# myPrompt.prompt
---
model: vertexai/gemini-1.5-flash
output:
  schema: MySchema
---

A biblioteca Dotprompt resolverá automaticamente o nome para o comando esquema Zod registrado. Depois, você pode usar o esquema para digitar fortemente saída de um Dotprompt:

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

const myPrompt = await prompt("myPrompt");

const result = await myPrompt.generate<typeof MySchema>({...});

// now strongly typed as MySchema
result.output();

Substituir metadados de comandos

Já os arquivos .prompt permitem incorporar metadados, como configuração de modelo, no próprio arquivo, você também pode substituir esses valores por chamada:

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

Saída estruturada

É possível definir o formato e o esquema de saída de um comando para forçar a conversão em JSON:

---
model: vertexai/gemini-1.5-flash
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 um comando com saída estruturada, use o auxiliar output() para recuperá-los e validá-los:

const createMenuPrompt = await prompt('create_menu');

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

console.log(menu.output());

A conformidade da saída é alcançada com a inserção de mais instruções prompt de comando. Por padrão, ele é anexado ao final da última mensagem gerada. ao comando. É possível reposicioná-la manualmente usando o {{section "output"}}. ajudante.

This is a prompt that manually positions output instructions.

== Output Instructions

{{section "output"}}

== Other Instructions

This will come after the output instructions.

Comandos com várias mensagens

Por padrão, o Dotprompt constrói uma única mensagem com o papel "user". Alguns comandos são mais bem expressos como uma combinação de várias mensagens, como um comando do sistema.

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

---
model: vertexai/gemini-1.5-flash
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 e histórico de várias interações

O Dotprompt oferece suporte a comandos de várias interações transmitindo a opção history para o Método generate:

const result = await multiTurnPrompt.generate({
  history: [
    { role: 'user', content: [{ text: 'Hello.' }] },
    { role: 'model', content: [{ text: 'Hi there!' }] },
  ],
});

Por padrão, o histórico será inserido antes da mensagem final gerada pelo ao comando. No entanto, você pode posicionar o histórico manualmente usando o {{history}} ajudante:

{{role "system"}}
This is the system prompt.
{{history}}
{{role "user"}}
This is a user message.
{{role "model"}}
This is a model message.
{{role "user"}}
This is the final user message.

Comandos multimodais

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

---
model: vertexai/gemini-1.5-flash
input:
  schema:
    photoUrl: string
---

Describe this image in a detailed paragraph:

{{media url=photoUrl}}

O URL pode ser um URI data: codificado em base64 ou https:// para uso de imagem "inline". No código, seria:

const describeImagePrompt = await prompt('describe_image');

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

console.log(result.text());

Parcial

Parcials são modelos reutilizáveis que podem ser incluídos em qualquer comando. Parcial pode ser especialmente útil para comandos relacionados que compartilham um comportamento comum.

Ao carregar um diretório de prompt, qualquer arquivo prefixado com _ é considerado um parcial. Portanto, o arquivo _personality.prompt pode conter:

You should speak like a {{#if style}}{{style}}{{else}}helpful assistant.{{/else}}.

Isso pode ser incluído em outros comandos:

---
model: vertexai/gemini-1.5-flash
input:
  schema:
    name: string
    style?: string
---

{{ role "system" }}
{{>personality style=style}}

{{ role "user" }}
Give the user a friendly greeting.

User's Name: {{name}}

As parciais são inseridas usando a sintaxe {{>NAME_OF_PARTIAL args...}}. Em caso negativo forem fornecidos à parcial, ela será executada com o mesmo contexto comando principal.

As parciais aceitam ambos os argumentos nomeados como acima ou um único argumento posicional. que representam o contexto. Isso pode ser útil para, por exemplo, renderização de membros de uma lista.

# _destination.prompt
- {{name}} ({{country}})

# chooseDestination.prompt
Help the user decide between these vacation destinations:
{{#each destinations}}
{{>destination this}}{{/each}}

Definição de parciais no código

Você também pode definir parciais no código usando definePartial:

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

definePartial(
  'personality',
  'Talk like a {{#if style}}{{style}}{{else}}helpful assistant{{/if}}.'
);

Dados parciais definidos por código estão disponíveis em todos os comandos.

Variantes de comando

Como os arquivos de comando são apenas textos, você pode (e deve) enviá-los ao seu sistema de controle de versões, o que facilita a comparação das mudanças ao longo do tempo. 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 por meio do recurso de variantes.

Para criar uma variante, crie um arquivo [name].[variant].prompt. Por exemplo, se você estava usando o Gemini 1.5 Flash no comando, mas quer saber se o Gemini 1.5 Pro teria um desempenho melhor, você poderia criar dois arquivos:

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

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

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

O nome da variante é incluído nos metadados dos traces de geração. Assim, você pode comparar e contrastar o desempenho real entre variantes no inspetor de trace do Genkit.

Definição de auxiliares personalizados

É possível definir auxiliares personalizados para processar e gerenciar dados em um prompt. Auxiliares estão registrados globalmente usando defineHelper:

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

defineHelper('shout', (text: string) => text.toUpperCase());

Depois de definido, o assistente pode ser usado em qualquer prompt:

---
model: vertexai/gemini-1.5-flash
input:
  schema:
    name: string
---

HELLO, {{shout name}}!!!

Para obter mais informações sobre os argumentos passados para os auxiliares, consulte a seção Documentação do Handlebars sobre criação assistentes personalizados.

Formas alternativas de carregar e definir comandos

O Dotprompt é otimizado para organização no diretório do prompt. No entanto, não há há outras formas de carregar e definir comandos:

  • loadPromptFile: carrega um prompt de um arquivo no diretório de comandos.
  • loadPromptUrl: carrega um prompt de um URL.
  • defineDotprompt: define um comando no código.

Exemplos:

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.5-flash',
    input: {
      schema: z.object({
        name: z.string(),
      }),
    },
  },
  `Hello {{name}}, how are you today?`
);