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 e importa la función prompt desde la biblioteca @genkit-ai/dotprompt:

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

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

Luego, carga la instrucción con 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());

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.

Definición de esquemas de entrada y salida

Dotprompt incluye un formato de definición de esquema compacto y optimizado para 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 a la siguiente interfaz de 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 */

}

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

Aprovecha esquemas reutilizables

Además de definir esquemas directamente en el archivo .prompt, puedes hacer referencia a un esquema registrado con defineSchema por nombre. Sigue estos pasos para registrar un esquema:

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

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

En la instrucción, puedes proporcionar el nombre del esquema registrado:

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

La biblioteca Dotprompt resolverá automáticamente el nombre del esquema de Zod registrado. Luego, puedes usar el esquema para escribir fuertemente Resultado de un mensaje de 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();

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:

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

Resultados estructurados

Puedes configurar el formato y el esquema de salida de una instrucción para convertirla en 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.

Cuando generes una instrucción con un resultado estructurado, usa el ayudante output() para recuperarlo y validarlo:

const createMenuPrompt = await prompt('create_menu');

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

console.log(menu.output());

El cumplimiento de salida se logra insertando instrucciones adicionales en el mensaje. De forma predeterminada, se agrega al final del último mensaje generado por el mensaje. Puedes cambiar su posición manualmente con el {{section "output"}}. como un asistente de chat.

This is a prompt that manually positions output instructions.

== Output Instructions

{{section "output"}}

== Other Instructions

This will come after the output instructions.

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

Historial y mensajes de varios turnos

Dotprompt admite instrucciones de varios turnos pasando la opción history al Método generate:

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

De forma predeterminada, el historial se insertará antes del mensaje final generado por el mensaje. Sin embargo, puedes posicionar el historial manualmente usando el {{history}}. ayudante:

{{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.

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í:

const describeImagePrompt = await prompt('describe_image');

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

console.log(result.text());

Parcial

Los parciales son plantillas reutilizables que se pueden incluir dentro de cualquier instrucción. Parcial puede ser especialmente útil para instrucciones relacionadas que comparten un comportamiento común.

Cuando se carga un directorio de instrucciones, cualquier archivo con el prefijo _ se considera un parciales. Por lo tanto, un archivo _personality.prompt podría contener lo siguiente:

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

Esto se puede incluir en otras instrucciones:

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

Los parciales se insertan con la sintaxis {{>NAME_OF_PARTIAL args...}}. Si la respuesta es no se proporcionan argumentos al parcial, esta se ejecuta con el mismo contexto que mensaje principal.

Los parciales aceptan los argumentos con nombre como se indica más arriba o un solo argumento posicional que representan el contexto. Esto puede ser útil para las siguientes situaciones: renderizando miembros de una lista.

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

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

Cómo definir parciales en el código

También puedes definir parciales en el código con definePartial:

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

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

Los parciales definidos por código están disponibles en todas las instrucciones.

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á un mejor rendimiento, por lo que podrías crear dos archivos:

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

Para usar una variante de instrucción, especifica la opción variant durante la carga:

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

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.

Cómo definir asistentes personalizados

Puedes definir asistentes personalizados para procesar y administrar datos dentro de una instrucción. Ayudantes se registran globalmente con defineHelper:

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

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

Una vez que se define un asistente, puedes usarlo en cualquier instrucción:

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

HELLO, {{shout name}}!!!

Para obtener más información sobre los argumentos que se pasaron a los asistentes, consulta el Documentación sobre Handlebars sobre cómo crear asistentes personalizados.

Alternativas para cargar y definir instrucciones

Dotprompt está optimizado para la organización en el directorio de instrucciones. Sin embargo, hay hay algunas otras formas de cargar y definir instrucciones:

  • loadPromptFile: Carga un mensaje desde un archivo en el directorio del mensaje.
  • loadPromptUrl: Carga un mensaje desde una URL.
  • defineDotprompt: Define un mensaje en el código.

Ejemplos:

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?`
);