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 desenvolvido com base na premissa de que comandos são código. Você escreve e armazenar seus comandos em arquivos com formatação especial, chamados arquivos dotprompt, rastrear usando o mesmo sistema de controle de versões e os implanta com o código que chama a IA generativa de modelos de machine learning.

Para usar o Dotprompt, primeiro crie um diretório prompts na raiz do seu projeto e 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-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 comando:

Go

Instale o plug-in dotprompt:

go get github.com/firebase/genkit/go/plugins/dotprompt

Em seguida, carregue o comando usando Open:

import "github.com/firebase/genkit/go/plugins/dotprompt"
dotprompt.SetDirectory("prompts")
prompt, err := dotprompt.Open("greeting")

É possível chamar o método Generate do comando para renderizar e transmitir o modelo. para a API do modelo em uma única etapa:

ctx := context.Background()

// The .prompt file specifies vertexai/gemini-1.5-pro, so make sure it's set
// up.
// Default to the project in GCLOUD_PROJECT and the location "us-central1".
vertexai.Init(ctx, nil)
vertexai.DefineModel("gemini-1.5-pro", nil)

type GreetingPromptInput struct {
  Location string `json:"location"`
  Style    string `json:"style"`
  Name     string `json:"name"`
}
response, err := prompt.Generate(
  ctx,
  &dotprompt.PromptRequest{
      Variables: GreetingPromptInput{
          Location: "the beach",
          Style:    "a fancy pirate",
          Name:     "Ed",
      },
  },
  nil,
)
if err != nil {
  return err
}

if responseText, err := response.Text(); err == nil {
  fmt.Println(responseText)
}

Ou simplesmente renderize o modelo em uma string:

Go

renderedPrompt, err := prompt.RenderText(map[string]any{
  "location": "a restaurant",
  "style":    "a pirate",
})

A sintaxe do Dotprompt é baseada nos Handlebars. linguagem de modelo. Você pode usar os auxiliares if, unless e each para adicionar partes condicionais do seu comando ou itere conteúdo estruturado. O formato de arquivo utiliza um front-end YAML para fornecer metadados para um prompt in-line 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 baseado em 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 ao seguinte esquema JSON:

{
  "properties": {
    "metadata": {
      "properties": {
        "updatedAt": {
          "type": "string",
          "description": "ISO timestamp of last update"
        },
        "approvedBy": {
          "type": "integer",
          "description": "id of approver"
        }
      },
      "type": "object"
    },
    "title": {
      "type": "string"
    },
    "subtitle": {
      "type": "string"
    },
    "draft": {
      "type": "boolean",
      "description": "true when in draft state"
    },
    "date": {
      "type": "string",
      "description": "the date of publication e.g. '2024-04-09'"
    },
    "tags": {
      "items": {
        "type": "string"
      },
      "type": "array",
      "description": "relevant tags for article"
    },
    "authors": {
      "items": {
        "properties": {
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string"
          }
        },
        "type": "object",
        "required": ["name"]
      },
      "type": "array"
    }
  },
  "type": "object",
  "required": ["title", "date", "tags", "authors"]
}

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 permitem 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 de a omissão de um campo.

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

O Picoschema não é compatível com muitos dos recursos do esquema JSON completo. Se você exigir esquemas mais robustos, é possível fornecer um Esquema JSON em vez disso:

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

Substituir metadados de comandos

Já os arquivos .prompt permitem incorporar metadados, como a configuração do modelo, o próprio arquivo, também é possível substituir esses valores a cada chamada:

Go

// Make sure you set up the model you're using.
vertexai.DefineModel("gemini-1.5-flash", nil)

response, err := prompt.Generate(
  context.Background(),
  &dotprompt.PromptRequest{
      Variables: GreetingPromptInput{
          Location: "the beach",
          Style:    "a fancy pirate",
          Name:     "Ed",
      },
      Model: "vertexai/gemini-1.5-flash",
      Config: &ai.GenerationCommonConfig{
          Temperature: 1.0,
      },
  },
  nil,
)

Comandos com várias mensagens

Por padrão, o Dotprompt constrói uma única mensagem com o papel "user". Algumas comandos são mais bem expressos 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 e texto, é possível Use 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 um URI data: codificado em base64 ou https:// para "inline". imagem uso. No código, seria:

Go

dotprompt.SetDirectory("prompts")
describeImagePrompt, err := dotprompt.Open("describe_image")
if err != nil {
  return err
}

imageBytes, err := os.ReadFile("img.jpg")
if err != nil {
  return err
}
encodedImage := base64.StdEncoding.EncodeToString(imageBytes)
dataURI := "data:image/jpeg;base64," + encodedImage

type DescribeImagePromptInput struct {
  PhotoUrl string `json:"photo_url"`
}
response, err := describeImagePrompt.Generate(
  context.Background(),
  &dotprompt.PromptRequest{Variables: DescribeImagePromptInput{
      PhotoUrl: dataURI,
  }},
  nil,
)

Variantes de comando

Como os arquivos de comando são apenas textos, você pode (e deve) enviá-los ao seu 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 seu ambiente de produção lado a lado com as versões atuais. O Dotprompt oferece suporte isso 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.0 Pro no comando, mas quer saber se o Gemini 1.5 Pro teria um desempenho melhor, você pode criar dois arquivos:

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

Para usar uma variante de comando, especifique-a ao carregar:

Go

describeImagePrompt, err := dotprompt.OpenVariant("describe_image", "gemini15")

O carregador de comandos tenta carregar a variante desse nome e retorna à linha de base se não existir nenhuma. Isso significa que é possível usar o carregamento condicional com base em qualquer critério que faça sentido para seu aplicativo:

Go

var myPrompt *dotprompt.Prompt
var err error
if isBetaTester(user) {
  myPrompt, err = dotprompt.OpenVariant("describe_image", "gemini15")
} else {
  myPrompt, err = dotprompt.Open("describe_image")
}

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 rastro do Genkit. inspetor.