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:

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 o modelo e passá-lo para a API do modelo em uma única etapa:

ctx := context.Background()

// Default to the project in GCLOUD_PROJECT and the location "us-central1".
vertexai.Init(ctx, nil)

// The .prompt file specifies vertexai/gemini-1.5-flash, which is
// automatically defined by Init(). However, if it specified a model that
// isn't automatically loaded (such as a specific version), you would need
// to define it here:
// vertexai.DefineModel("gemini-1.0-pro-002", &ai.ModelCapabilities{
// 	Multiturn:  true,
// 	Tools:      true,
// 	SystemRole: true,
// 	Media:      false,
// })

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
}

fmt.Println(response.Text())

Ou simplesmente renderize o modelo em uma string:

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

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 com o Picoschema

Dotprompt inclui um formato de definição de esquema compacto baseado em YAML chamado Picoschema para facilitar a definição dos atributos mais importantes de um esquema para uso do 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
  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 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

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:

// 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". 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 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:

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 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ê pode criar dois arquivos:

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

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

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

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:

var myPrompt *dotprompt.Prompt
var err error
if isBetaTester(user) {
	myPrompt, err = dotprompt.OpenVariant("describe_image", "geminipro")
} 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 inspetor de trace do Genkit.