Administra mensajes con Dotprompt

Firebase Genkit proporciona el complemento Dotprompt y el formato de texto para ayudarte a escribir y organizar tus instrucciones de IA generativa.

Dotprompt está diseñado con la premisa de que las instrucciones son código. Escribes y mantienes tus instrucciones en archivos con formato especial llamados archivos dotprompt, rastreas los cambios con el mismo sistema de control de versión que usas para tu código y, luego, los implementas junto con el código que llama a tus modelos de IA generativa.

Para usar Dotprompt, primero crea un directorio prompts en la raíz del proyecto y, luego, crea un archivo .prompt en ese directorio. Aquí hay un ejemplo sencillo podría llamar a 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 esta instrucción, instala el complemento dotprompt:

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

Luego, carga la instrucción con Open:

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

Puedes llamar al método Generate de la instrucción para procesar la plantilla y pasarla a la API del modelo en un solo paso:

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())

O solo renderiza la plantilla en una cadena:

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

La sintaxis de Dotprompt se basa en el lenguaje de plantillas Handlebars. Puedes usar los asistentes de if, unless y each para agregar partes condicionales a tu instrucción o iterar a través del contenido estructurado. El formato de archivo usa YAML frontmatter para proporcionar metadatos para una instrucción intercalada con la plantilla.

Define esquemas de entrada y salida con Picoschema

Dotprompt incluye un formato de definición de esquema compacto basado en YAML llamado Picoschema para facilitar la definición de los atributos más importantes de un esquema para el uso de LLM. Este es un ejemplo de un esquema para un artículo:

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

El esquema anterior es equivalente al siguiente esquema de 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"]
}

Picoschema admite los tipos escalares string, integer, number, boolean y any. En el caso de los objetos, arrays y enums, se denotan con un paréntesis después del nombre del campo.

Los objetos definidos por Picoschema tienen todas las propiedades necesarias, a menos que se indique que son opcionales por ?, y no permiten propiedades adicionales. Cuando una propiedad se marca como opcional, también se hace anulable para proporcionar más legibilidad para que los LLMs devuelvan un valor nulo en lugar de omitir un campo.

En una definición de objeto, se puede usar la clave especial (*) para declarar una definición de campo “comodín”. Esto coincidirá con cualquier propiedad adicional que no proporcione una clave explícita.

Picoschema no es compatible con muchas de las capacidades del esquema JSON completo. Si necesitas esquemas más sólidos, puedes proporcionar un esquema JSON en su lugar:

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

Anula metadatos de instrucciones

Mientras que los archivos .prompt te permiten incorporar metadatos, como la configuración del modelo en el propio archivo, también puedes anular estos valores por llamada:

// 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,
)

Instrucciones de varios mensajes

De forma predeterminada, Dotprompt construye un solo mensaje con un rol "user". Algunas instrucciones se expresan mejor como una combinación de varios mensajes, como una instrucción del sistema.

El ayudante {{role}} proporciona una forma sencilla de crear instrucciones de varios mensajes:

---
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}}

Instrucciones multimodales

Para los modelos que admiten entradas multimodales, como imágenes junto con texto, puedes usar el ayudante {{media}}:

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

Describe this image in a detailed paragraph:

{{media url=photoUrl}}

La URL puede ser URI de https:// o data: codificado en base64 para el uso de imagen "intercalada". En el código, sería así:

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 instrucciones

Como los archivos de instrucciones son solo texto, puedes (y debes) confirmarlos en tu sistema de control de versiones, lo que te permite comparar fácilmente los cambios a lo largo del tiempo. A menudo, las versiones modificadas de instrucciones solo se pueden probar completamente en un entorno de producción en paralelo con versiones existentes. Dotprompt admite esto a través de la característica variantes.

Para crear una variante, crea un archivo [name].[variant].prompt. Por ejemplo, si estabas usando Gemini 1.5 Flash en tu instrucción, pero querías ver si Gemini 1.5 Pro tendría un mejor rendimiento, podrías crear dos archivos:

  • my_prompt.prompt: La instrucción de "modelo de referencia"
  • my_prompt.geminipro.prompt: Una variante llamada “geminipro”

Para usar una variante de instrucción, especifícala cuando realices la carga:

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

El cargador de mensajes intentará cargar la variante de ese nombre y recurrirá al modelo de referencia si no hay ninguno. Esto significa que puedes usar la carga condicional basada en cualquier criterio que tenga sentido para tu aplicación:

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

El nombre de la variante se incluye en los metadatos de los seguimientos de generación, por lo que puedes comparar y contrastar el rendimiento real entre las variantes en el seguimiento de Genkit con el inspector de registros.