Cómo administrar instrucciones 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 se diseñó alrededor de la premisa de que los mensajes son código. Puedes escribir y mantener tus instrucciones en archivos con formato especial llamados archivos dotprompt, hacer un seguimiento de los cambios con el mismo sistema de control de versión que usas para tu código y, luego, implementarlos 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. A continuación, se muestra un ejemplo simple con el que puedes llamar greeting.prompt:

---
model: vertexai/gemini-1.0-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 este mensaje, instala el complemento dotprompt y, luego, importa la función prompt desde la biblioteca @genkit-ai/dotprompt:

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

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

Luego, carga el mensaje 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 plantilla de Handlebars. Puedes usar los asistentes if, unless y each para agregar partes condicionales a la instrucción o iterar a través de contenido estructurado. El formato de archivo usa el frontend YAML para proporcionar metadatos a 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 y optimizado para YAML llamado Picoschema, con el objetivo de 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

El esquema anterior es equivalente a la siguiente interfaz de TypeScript:

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

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

Los objetos definidos por Picoschema tienen todas las propiedades según sea necesario, a menos que ? indique que son opcionales, y no permiten propiedades adicionales.

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 instrucción

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

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

Salida estructurada

Puedes configurar el formato y el esquema de salida de una instrucción para convertirse en JSON:

---
model: vertexai/gemini-1.0-pro
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 un mensaje con 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());

Mensajes de varios mensajes

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

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

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

Instrucciones multimodales

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

---
model: vertexai/gemini-1.0-pro-vision
input:
  schema:
    photoUrl: string
---

Describe this image in a detailed paragraph:

{{media url=photoUrl}}

La URL puede ser URI de data: https:// o codificados en base64 para el uso de la imagen “intercalada”. En el código, sería la siguiente:

const describeImagePrompt = await prompt('describe_image');

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

console.log(result.text());

Variantes de instrucción

Debido a que los archivos de instrucciones son solo texto, puedes (y deberías) confirmarlos en tu sistema de control de versión, lo que te permite comparar cambios en el tiempo con facilidad. A menudo, las versiones ajustadas de los mensajes solo se pueden probar por completo en un entorno de producción junto con las versiones existentes. Dotprompt admite esto a través de su función de variantes.

Para crear una variante, crea un archivo [name].[variant].prompt. Por ejemplo, si usas Gemini 1.0 Pro en el mensaje, pero quieres comprobar si Gemini 1.5 Pro funcionaría mejor, deberías crear dos archivos:

  • my_prompt.prompt: Es el mensaje del “modelo de referencia”.
  • my_prompt.gemini15.prompt: Una variante llamada "gemini"

Para usar una variante de mensaje, especifica la opción variant cuando realices la carga:

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

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

const myPrompt = await prompt('my_prompt', {
  variant: isBetaTester(user) ? 'gemini15' : null,
});

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

Formas alternativas de cargar y definir instrucciones

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

  • loadPromptFile: Carga un mensaje desde un archivo en el directorio de instrucciones.
  • 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.0-pro',
    input: {
      schema: z.object({
        name: z.string(),
      }),
    },
  },
  `Hello {{name}}, how are you today?`
);