DotPrompt es una biblioteca simple que le permite crear mensajes utilizando una sintaxis basada en configuración, sin necesidad de incrustarlos en su aplicación. Admite plantillas para mensajes a través del lenguaje de plantillas Fluid, lo que le permite reutilizar el mismo mensaje y pasar diferentes valores en tiempo de ejecución.
Un archivo de aviso es simplemente cualquier archivo que termine con la extensión .prompt
. El archivo real en sí es un archivo de configuración YAML y la extensión permite a la biblioteca identificar rápidamente el archivo para el propósito previsto.
Existe un problema conocido con los archivos .prompt
que provocan un comportamiento inusual en herramientas como Rider e IntelliJ. Puede solucionar este problema desactivando el complemento Terminal o utilizando un editor diferente para modificar los archivos.
El contenido de un archivo de aviso contiene algunas propiedades de identificación de nivel superior, seguidas de información de configuración y finalmente los avisos.
Un archivo de solicitud completo se vería así.
nombre: Modelo de ejemplo: gpt-4oconfig: formato de salida: texto temperatura: 0,9 tokens máximos: 500 entrada:parámetros: tema: estilo de cadena?: cadena predeterminada: tema: mensajes de redes sociales: sistema: | Usted es un asistente de investigación útil que brindará respuestas descriptivas para un tema determinado y cómo afecta a la sociedad usuario: | Explica el impacto de {{ topic }} en cómo nos relacionamos con la tecnología como sociedad {% if style -%} ¿Puedes responder con el estilo de {{ style }} {% endif -%}fewShots: - usuario: ¿Qué es la respuesta de Bluetooth? Bluetooth es un estándar de tecnología inalámbrica de corto alcance que se utiliza para intercambiar datos entre dispositivos fijos y móviles en distancias cortas y construir redes de área personal. - usuario: ¿En qué se diferencia el aprendizaje automático de la programación tradicional? Respuesta: El aprendizaje automático permite que los algoritmos aprendan de los datos y mejoren con el tiempo sin estar programados explícitamente. - usuario: ¿Puede darnos un ejemplo de IA en la vida cotidiana? Respuesta: La IA se utiliza en asistentes virtuales como Siri y Alexa, que comprenden y responden a comandos de voz.
El name
es opcional en la configuración; si no se proporciona, el nombre se toma del nombre del archivo menos la extensión. Entonces, un archivo llamado gen-lookup-code.prompt
obtendría el nombre gen-lookup-code
. Esto no influye en la generación de los mensajes en sí (aunque las actualizaciones futuras podrían hacerlo), pero le permite identificar el origen del mensaje al iniciar sesión y seleccionar el mensaje desde el administrador de mensajes.
Si utiliza esta propiedad, cuando se carga el archivo, el nombre se convierte a minúsculas y los espacios se reemplazan con guiones. Entonces, el nombre de My cool Prompt
se convertiría en my-cool-prompt
. Esto se hace para garantizar que se pueda acceder fácilmente al nombre desde el código.
Este es otro elemento opcional en la configuración, pero proporciona información al usuario del archivo de solicitud sobre qué modelo (o implementación para Azure Open AI) debe usar. Como esto puede ser nulo si no se especifica, el consumidor debe asegurarse de verificarlo antes de usarlo. Por ejemplo:
var modelo = PromptFile.Model ?? "mi-predeterminado";
Sin embargo, el uso de esta opción permite al ingeniero ser muy explícito sobre qué modelo pretende utilizar para proporcionar los mejores resultados.
La sección config
tiene algunos elementos de nivel superior que se proporcionan para que el cliente los use en sus llamadas de LLM para configurar opciones en cada llamada. La propiedad outputFormat
toma un valor de text
o json
dependiendo de cómo se pretende que el LLM responda a la solicitud. Si se especifica json
, algunos LLM requieren que el sistema o el usuario indiquen que el resultado esperado también es JSON. Si la biblioteca no detecta el término JSON
en el mensaje, agregará una pequeña declaración al mensaje del sistema solicitando que la respuesta esté en formato JSON.
La sección input
contiene detalles sobre los parámetros que se proporcionan a las indicaciones. Estos no son obligatorios y puede crear mensajes a los que no se les pasa ningún valor. Pero si es así, esto es lo que necesita.
Debajo de input
se encuentra la sección parameters
que contiene una lista de pares clave-valor donde la clave es el nombre del parámetro y el valor es su tipo. Si añade al nombre del parámetro un signo de interrogación (por ejemplo, ¿ style?
), se considera un parámetro opcional y no generará errores si no le proporciona un valor.
Los tipos soportados son:
Tipo de parámetro | Tipo de red | Equivalente a C# |
---|---|---|
cadena | Sistema.Cadena | cadena |
booleano | Sistema.Booleano | booleano |
fecha y hora | Sistema.DateTimeOffset | Sistema.DateTimeOffset |
número | Sistema.Byte Sistema.SByte Sistema.UInt16 Sistema.Int16 Sistema.UInt32 Sistema.Int32 Sistema.UInt64 Sistema.Int64 Sistema.Único Sistema.Doble Sistema.Decimal | byte sbyte corto corto uint entero ulong largo flotar doble decimal |
objeto | Sistema.Objeto | objeto |
Los primeros 4 se utilizan según lo previsto. Los objetos que se pasan al indicador tendrán su método ToString
llamado para ser utilizado en el indicador.
El tipo datetime
se puede mostrar con su representación ToString
predeterminada o puede usar los filtros de Fluid para especificar su formato, cambiar la zona horaria y más.
Si proporciona un valor para un parámetro que no se ajusta al tipo especificado, se generará un error.
También en input
está la sección default
. Esta sección le permite especificar valores predeterminados para cualquiera de los parámetros. Entonces, si el parámetro no se proporciona en su aplicación, se utilizará el valor predeterminado.
La sección prompts
contiene las plantillas para el sistema y los mensajes del usuario. Si bien el mensaje del usuario es obligatorio, no es necesario especificar un mensaje del sistema.
Tanto las indicaciones system
como las user
son valores de cadena y se pueden definir de cualquier forma que admita YAML. El ejemplo anterior utiliza una cadena multilínea donde se conservan los retornos de carro.
YAML tiene un gran soporte para valores de cadenas multilínea a través de Block Scalars. Con estos admite cadenas tanto literales como plegadas . Con cadenas literales, los caracteres de nueva línea en la cadena de entrada se mantienen y la cadena permanece exactamente como está escrita. Al plegar, los caracteres de nueva línea se contraen y se reemplazan por un carácter de espacio, lo que le permite escribir cadenas muy largas en varias líneas. Al usar plegado, si usa dos caracteres de nueva línea, se agrega una nueva línea a la cadena.
# Ejemplo plegado: > Los barcos colgados en el cielo de la misma manera que los ladrillos no# Produce:# Los barcos colgados en el cielo de la misma manera que los ladrillos no
# Ejemplo literal: | Los barcos colgaban en el cielo de la misma manera que los ladrillos no# Produce:# Los barcos colgaban# en el cielo de la misma manera que los ladrillos no
La sintaxis de las indicaciones utiliza el lenguaje de plantillas Fluid, que a su vez se basa en Liquid creado por Shopify. Este lenguaje de plantillas nos permite definir mensajes de usuario que pueden cambiar según los valores que se pasan al analizador de plantillas.
En el ejemplo anterior, puede ver {{ topic }}
que es un marcador de posición para el valor que se pasa y que se sustituirá directamente en la plantilla. También está la sección {% if style -%} ... {% endif -%}
que le dice al analizador que solo incluya esta sección si el parámetro style
tiene un valor. El -%}
al final del marcador contiene el símbolo de guión que le indica al analizador que debe contraer las líneas en blanco.
Hay un excelente tutorial sobre cómo escribir plantillas con Fluid disponible en línea.
Cuando genera el mensaje, no reemplaza la plantilla, solo le brinda el resultado generado. Esto significa que puede generar el mensaje tantas veces como desee con diferentes valores de entrada.
fewShots
es una sección que permite al escritor de indicaciones proporcionar técnicas de indicaciones de pocas tomas a la solución. Al crear un mensaje, debe incluirlos, junto con el mensaje del sistema y luego el mensaje del usuario; esto proporciona ejemplos de cómo el LLM debe responder al mensaje del usuario. Si está utilizando OpenAI o Azure OpenAI, puede utilizar los métodos de extensión (ver más adelante) que crearán todos los mensajes por usted.
Se puede acceder directamente a los archivos de aviso. Si sólo tiene un par de archivos o desea probarlos rápidamente, esta es una forma bastante sencilla de hacerlo.
usando DotPrompt;var PromptFile = PromptFile.FromFile("ruta/a/prompt-file.prompt");var systemPrompt = PromptFile.GetSystemPrompt(null);var userPrompt = PromptFile.GetUserPrompt(nuevo diccionario<cadena, objeto>{{ " tema", "bluetooth" },{ "estilo", "vendedor de autos usados" }});
Si el archivo de solicitud contuviera el ejemplo anterior, produciría lo siguiente.
System Prompt:
You are a helpful research assistant who will provide descriptive responses for a given topic and how it impacts society
User Prompt:
Explain the impact of bluetooth on how we engage with technology as a society
Can you answer in the style of a used car salesman
Esto podría resultar en una respuesta del LLM similar a esta (lo siento)
Damas y caballeros, reúnanse y permítanme contarles sobre el milagro de la tecnología moderna que ha revolucionado la forma en que nos conectamos con nuestros dispositivos. ¡Estoy hablando de Bluetooth! Bluetooth es el héroe anónimo, el ingrediente secreto que ha hecho que nuestras vidas sean más cómodas, más conectadas y, definitivamente, más tecnológicas. Imagínese esto: comunicación fluida y sin cables entre sus dispositivos favoritos. No más cables enredados, no más desorden. ¡Es como tener un pase VIP para la primera fila del futuro!
...
El administrador de mensajes es el método preferido para manejar sus archivos de mensajes. Le permite cargarlos desde una ubicación, acceder luego por nombre y luego usarlos en su aplicación.
El valor predeterminado para el administrador de mensajes es acceder a los archivos en la carpeta de prompts
locales, aunque puede especificar una ruta diferente si lo desea.
// Cargar desde la ubicación predeterminada del directorio `prompts`var PromptManager = new PromptManager();var PromptFile = PromptManager.GetPromptFile("example");// Usar una carpeta diferentevar PromptManager = new PromptManager("another-location"); var PromptFile = PromptManager.GetPromptFile("example");// Listar todos los mensajes cargadosvar PromptNames = PromptManager.ListPromptFileNames();
El administrador de mensajes implementa una interfaz IPromptManager
, por lo que si desea usar esto a través de un contenedor DI o patrón IoC, puede proporcionar fácilmente una versión simulada para realizar pruebas.
El administrador de mensajes también puede tomar una instancia IPromptStore
que le permite crear un almacén personalizado que puede no estar basado en archivos (consulte Creación de un almacén de mensajes personalizado). Esto también permite proporcionar una interfaz simulada para que pueda escribir pruebas unitarias que no dependan del mecanismo de almacenamiento.
Usar el administrador de mensajes para leer un mensaje y luego usarlo en una llamada a un punto final de Azure OpenAI.
NB Este ejemplo supone que hay un directorio prompts
con el archivo de mensajes disponible.
usando System.ClientModel;usando Azure.AI.OpenAI;usando DotPrompt;var openAiClient = new(new Uri("https://endpoint"), new ApiKeyCredential("abc123"));var PromptManager = new PromptManager();var PromptFile = PromptManager.GetPromptFile("example");// Los métodos de solicitud del sistema y de solicitud del usuario toman diccionarios que contienen los valores necesarios para la // plantilla. Si no se necesita ninguno, simplemente puede pasar null.var systemPrompt = PromptFile.GetSystemPrompt(null);var userPrompt = PromptFile.GetUserPrompt(new Dictionary<string, object>{{ "topic", "bluetooth" },{ "style" , "vendedor de autos usados" }});var cliente = openAiClient.GetChatClient(promptFile.Model ?? "default-model");var finalización = await client.CompleteChatAsync([nuevo SystemChatMessage(systemPrompt),nuevo UserChatMessage(userPrompt)],new ChatCompletionOptions(ResponseFormat = PromptFile.OutputFormat == OutputFormat.Json? ChatResponseFormat.JsonObject: ChatResponseFormat.Text,Temperature = PromptFile.Config.Temperature,MaxTokens = PromptFile.Config.MaxTokens));
O bien, utilizando los métodos de extensión proporcionados por OpenAI.
usando System.ClientModel;usando Azure.AI.OpenAI;usando DotPrompt;usando DotPrompt.Extensions.OpenAi;var openAiClient = new(new Uri("https://endpoint"), new ApiKeyCredential("abc123"));var PromptManager = nuevo PromptManager();var PromptFile = PromptManager.GetPromptFile("ejemplo");var PromptValues = nuevo Diccionario<cadena, objeto>{{ "tema", "bluetooth" },{ "estilo", "vendedor de autos usados" }};var client = openAiClient.GetChatClient(promptFile.Model ?? "default-model");var finalización = esperar client.CompleteChatAsync(promptFile.ToOpenAiChatMessages(promptValues),promptFile.ToOpenAiChatCompletionOptions());var respuesta = finalización.Value;Console.WriteLine(respuesta.Contenido[0].Texto);
Y ahora, si necesitamos modificar nuestro mensaje, simplemente podemos cambiar el archivo del mensaje y dejar nuestro código en paz (suponiendo que los parámetros no cambien).
Lo anterior muestra cómo puede usar DotPrompt para leer archivos de mensajes desde el disco. Pero, ¿qué sucede si se encuentra en una situación en la que desea recibir indicaciones en un lugar más central, como un servicio de almacenamiento en la nube o una base de datos? Bueno, el administrador de mensajes puede tomar una instancia IPromptStore
como argumento. En todos los ejemplos anteriores se utiliza FilePromptStore
, que está incluido, pero también puedes crear el tuyo propio. Sólo necesita implementar la interfaz y listo.
Para darle un ejemplo, aquí hay una implementación simple que utiliza un almacén de tablas de almacenamiento de Azure para contener los detalles del mensaje.
/// <summary>/// Implementación de IPromptStore para Azure Storage Tables/// </summary>clase pública AzureTablePromptStore: IPromptStore{/// <summary>/// Carga las indicaciones del almacén de tablas/// < /summary>public IEnumerable<PromptFile> Load(){var tableClient = GetTableClient();var PromptEntities = tableClient.Query<PromptEntity>(e => e.PartitionKey == "DotPromptTest");var PromptFiles = PromptEntities.Select(pe => pe.ToPromptFile()).ToList();return PromptFiles;}/// <summary >/// Obtiene un cliente de tabla/// </summary>TableClient estático privado GetTableClient(){// Reemplace los elementos de configuración aquí con su valor o cambie a usar // cliente var de autenticación basado en Intra = new TableServiceClient(new Uri($"https://{Configuration.StorageAccountName}.table.core.windows.net/"),new TableSharedKeyCredential(Configuration.StorageAccountName, Configuration .StorageAccountKey));var tableClient = client.GetTableClient("prompts");tableClient.CreateIfNotExists();return tableClient;}}/// <summary>/// Representa un registro mantenido en la tabla de almacenamiento/// </summary>clase pública PromptEntity: ITableEntity{/// <summary>/// Obtiene y establece la clave de partición para el registro/// </summary>cadena pública PartitionKey { get; colocar; } = string.Empty;/// <summary>/// Obtiene, establece la clave de fila para el registro/// </summary>public string RowKey { get; colocar; } = string.Empty;/// <summary>/// Obtiene, establece la marca de tiempo de la entrada/// </summary>public DateTimeOffset? Marca de tiempo {obtener; colocar; }/// <summary>/// Obtiene, establece el valor de ETag de los registros/// </summary>public ETag ETag { get; colocar; }/// <summary>/// Obtiene, establece el modelo a utilizar/// </summary>¿cadena pública? Modelo {obtener; colocar; }/// <summary>/// Obtiene, establece el formato de salida/// </summary>cadena pública OutputFormat { get; colocar; } = string.Empty;/// <summary>/// Obtiene, establece el número máximo de tokens/// </summary>public int MaxTokens { get; colocar; }/// <summary>/// Obtiene, establece la información del parámetro que se mantiene como un valor de cadena JSON/// </summary>cadena pública Parámetros { get; colocar; } = string.Empty;/// <summary>/// Obtiene, establece los valores predeterminados que se mantienen como un valor de cadena JSON/// </summary>public string Default { get; colocar; } = string.Empty;/// <summary>/// Obtiene, establece la plantilla de aviso del sistema/// </summary>public string SystemPrompt { get; colocar; } = string.Empty;/// <summary>/// Obtiene, establece la plantilla de solicitud de usuario/// </summary>public string UserPrompt { get; colocar; } = string.Empty;/// <summary>/// Devuelve el registro de entidad de solicitud en una instancia <see cref="PromptFile"/>/// </summary>/// <returns></returns>public PromptFile ToPromptFile(){var parámetros = new Dictionary<string, string>();var defaults = new Dictionary<string, object>();// Si hay valores de parámetros, conviértalos en un diccionario. (!string.IsNullOrEmpty(Parameters)){var entidadParameters = (JsonObject)JsonNode.Parse(Parameters)!;foreach (var (prop, propType) en entidadParameters){parameters.Add(prop, propType?.AsValue().ToString () ?? string.Empty);}}// Si hay valores predeterminados, conviértalos en un diccionario. (!string.IsNullOrEmpty(Default)){var entidadDefaults = (JsonObject)JsonNode.Parse(Default)!;foreach (var (prop, defaultValue) en entidadDefaults){defaults.Add(prop, defaultValue?.AsValue().GetValue <object>() ?? string.Empty);}}// Generar el nuevo archivo de solicitudvar PromptFile = nuevo PromptFile{Nombre = RowKey,Model = Modelo,Config = nuevo PromptConfig{OutputFormat = Enum.Parse<OutputFormat>(OutputFormat, true),MaxTokens = MaxTokens,Input = nuevo InputSchema{Parameters = parámetros,Default = defaults}},Preguntas = nuevos mensajes{Sistema = SystemPrompt,Usuario = UserPrompt}};return PromptFile;}}
Y luego para usar esto haríamos lo siguiente
var PromptManager = new PromptManager(new AzureTablePromptStore());var PromptFile = PromptManager.GetPromptFile("ejemplo");
Todavía hay margen de trabajo por hacer aquí y algunos de los elementos que estamos analizando incluyen
Opciones de configuración adicionales
Técnicas de indicaciones adicionales
Abierto a comentarios. ¿Hay algo que te gustaría ver? Háganos saber