Skip to content

Commit

Permalink
Show how to render prompts
Browse files Browse the repository at this point in the history
  • Loading branch information
dluc committed Apr 28, 2023
1 parent 1dcb706 commit b0b4f00
Show file tree
Hide file tree
Showing 14 changed files with 237 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,11 @@ public sealed class AzureChatCompletion : AzureOpenAIClientBase, IChatCompletion
/// <inheritdoc/>
public Task<string> GenerateMessageAsync(
ChatHistory chat,
ChatRequestSettings requestSettings,
ChatRequestSettings? requestSettings = null,
CancellationToken cancellationToken = default)
{
if (requestSettings == null) { requestSettings = new ChatRequestSettings(); }

return this.InternalGenerateChatMessageAsync(chat, requestSettings, cancellationToken);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ public sealed class OpenAIChatCompletion : OpenAIClientBase, IChatCompletion, IT
/// <inheritdoc/>
public Task<string> GenerateMessageAsync(
ChatHistory chat,
ChatRequestSettings requestSettings,
ChatRequestSettings? requestSettings = null,
CancellationToken cancellationToken = default)
{
if (requestSettings == null) { requestSettings = new ChatRequestSettings(); }

return this.InternalGenerateChatMessageAsync(chat, requestSettings, cancellationToken);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public interface IChatCompletion
/// <returns>Generated chat message in string format</returns>
public Task<string> GenerateMessageAsync(
ChatHistory chat,
ChatRequestSettings requestSettings,
ChatRequestSettings? requestSettings = null,
CancellationToken cancellationToken = default);

/// <summary>
Expand Down
16 changes: 8 additions & 8 deletions samples/dotnet/kernel-syntax-examples/Example17_ChatGPT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,25 @@ public static async Task RunAsync()
kernel.Config.AddOpenAIChatCompletionService("chat", "gpt-3.5-turbo", Env.Var("OPENAI_API_KEY"));

IChatCompletion chatGPT = kernel.GetService<IChatCompletion>();
var chat = (OpenAIChatHistory)chatGPT.CreateNewChat("You are a librarian, expert about books");
var chatHistory = (OpenAIChatHistory)chatGPT.CreateNewChat("You are a librarian, expert about books");

// First user message
chat.AddUserMessage("Hi, I'm looking for book suggestions");
chatHistory.AddUserMessage("Hi, I'm looking for book suggestions");

// First bot message
string reply = await chatGPT.GenerateMessageAsync(chat, new ChatRequestSettings());
chat.AddAssistantMessage(reply);
string reply = await chatGPT.GenerateMessageAsync(chatHistory);
chatHistory.AddAssistantMessage(reply);

// Second user message
chat.AddUserMessage("I love history and philosophy, I'd like to learn something new about Greece, any suggestion?");
chatHistory.AddUserMessage("I love history and philosophy, I'd like to learn something new about Greece, any suggestion?");

// Second bot message
reply = await chatGPT.GenerateMessageAsync(chat, new ChatRequestSettings());
chat.AddAssistantMessage(reply);
reply = await chatGPT.GenerateMessageAsync(chatHistory);
chatHistory.AddAssistantMessage(reply);

Console.WriteLine("Chat content:");
Console.WriteLine("------------------------");
foreach (var message in chat.Messages)
foreach (var message in chatHistory.Messages)
{
Console.WriteLine($"{message.AuthorRole}: {message.Content}");
Console.WriteLine("------------------------");
Expand Down
14 changes: 7 additions & 7 deletions samples/dotnet/kernel-syntax-examples/Example18_DallE.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,28 +45,28 @@ public static async Task RunAsync()
kernel.Config.AddOpenAIChatCompletionService("chat", "gpt-3.5-turbo", Env.Var("OPENAI_API_KEY"));

IChatCompletion chatGPT = kernel.GetService<IChatCompletion>();
var chat = (OpenAIChatHistory)chatGPT.CreateNewChat(
var chatHistory = (OpenAIChatHistory)chatGPT.CreateNewChat(
"You're chatting with a user. Instead of replying directly to the user" +
" provide the description of an image that expresses what you want to say." +
" The user won't see your message, they will see only the image. The system " +
" generates an image using your description, so it's important you describe the image with details.");

var msg = "Hi, I'm from Tokyo, where are you from?";
chat.AddUserMessage(msg);
chatHistory.AddUserMessage(msg);
Console.WriteLine("User: " + msg);

string reply = await chatGPT.GenerateMessageAsync(chat, new ChatRequestSettings());
chat.AddAssistantMessage(reply);
string reply = await chatGPT.GenerateMessageAsync(chatHistory);
chatHistory.AddAssistantMessage(reply);
image = await dallE.GenerateImageAsync(reply, 256, 256);
Console.WriteLine("Bot: " + image);
Console.WriteLine("Img description: " + reply);

msg = "Oh, wow. Not sure where that is, could you provide more details?";
chat.AddUserMessage(msg);
chatHistory.AddUserMessage(msg);
Console.WriteLine("User: " + msg);

reply = await chatGPT.GenerateMessageAsync(chat, new ChatRequestSettings());
chat.AddAssistantMessage(reply);
reply = await chatGPT.GenerateMessageAsync(chatHistory, new ChatRequestSettings());
chatHistory.AddAssistantMessage(reply);
image = await dallE.GenerateImageAsync(reply, 256, 256);
Console.WriteLine("Bot: " + image);
Console.WriteLine("Img description: " + reply);
Expand Down
6 changes: 3 additions & 3 deletions samples/dotnet/kernel-syntax-examples/Example25_AADAuth.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,13 @@ public static async Task RunAsync()
new DefaultAzureCredential(authOptions));

IChatCompletion chatGPT = kernel.GetService<IChatCompletion>();
var chat = (OpenAIChatHistory)chatGPT.CreateNewChat();
var chatHistory = (OpenAIChatHistory)chatGPT.CreateNewChat();

// User message
chat.AddUserMessage("Tell me a joke about hourglasses");
chatHistory.AddUserMessage("Tell me a joke about hourglasses");

// Bot reply
string reply = await chatGPT.GenerateMessageAsync(chat, new ChatRequestSettings());
string reply = await chatGPT.GenerateMessageAsync(chatHistory);
Console.WriteLine(reply);

/* Output: Why did the hourglass go to the doctor? Because it was feeling a little run down! */
Expand Down
139 changes: 139 additions & 0 deletions samples/dotnet/kernel-syntax-examples/Example28_ChatWithPrompts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Globalization;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.AI.ChatCompletion;
using Microsoft.SemanticKernel.CoreSkills;
using Microsoft.SemanticKernel.TemplateEngine;
using RepoUtils;
using Resources;

/**
* Scenario:
* - the user is reading a wikipedia page, they select a piece of text and they ask AI to extract some information.
* - the app explicitly uses the Chat model to get a result from gpt-3.5-turbo.
*
* The following example shows how to:
*
* - Use the prompt template engine to render prompts, without executing them.
* This can be used to leverage the template engine (which executes functions internally)
* to generate prompts and use them programmatically, without executing them like semantic functions.
*
* - Use rendered prompts to create the context of System and User messages sent to Chat models
* like "gpt-3.5-turbo"
*
* Note: normally you would work with Semantic Functions to automatically send a prompt to a model
* and get a response. In this case we use the Chat model, sending a chat history object, which
* includes some instructions, some context (the text selected), and the user query.
*
* We use the prompt template engine to craft the strings with all of this information.
*
* Out of scope and not in the example: if needed, one could go further and use a semantic
* function (with extra cost) asking AI to generate the text to send to the Chat model.
*
* TLDR: how to render a prompt:
*
* var kernel = new KernelBuilder().WithLogger(ConsoleLogger.Log).Build();
* ... import skills and functions ...
* var context = kernel.CreateNewContext();
* ... set variables ...
*
* var promptRenderer = new PromptTemplateEngine();
* string renderedPrompt = await promptRenderer.RenderAsync("...prompt template...", context);
*/

// ReSharper disable CommentTypo
// ReSharper disable once InconsistentNaming
public static class Example28_ChatWithPrompts
{
public static async Task RunAsync()
{
Console.WriteLine("======== Chat with prompts ========");

/* Load 3 files:
* - 28-system-prompt.txt: the system prompt, used to initialize the chat session.
* - 28-user-context.txt: the user context, e.g. a piece of a document the user selected and is asking to process.
* - 28-user-prompt.txt: the user prompt, just for demo purpose showing that one can leverage the same approach also to augment user messages.
*/

var systemPromptTemplate = EmbeddedResource.Read("28-system-prompt.txt");
var selectedText = EmbeddedResource.Read("28-user-context.txt");
var userPromptTemplate = EmbeddedResource.Read("28-user-prompt.txt");

// Usual kernel initialization, with GPT 3.5 Turbo
IKernel kernel = new KernelBuilder().WithLogger(ConsoleLogger.Log).Build();
kernel.Config.AddOpenAIChatCompletionService("chat", "gpt-3.5-turbo", Env.Var("OPENAI_API_KEY"));

// As an example, we import the time skill, which is used in system prompt to read the current date.
// We could also use a variable, this is just to show that the prompt can invoke functions.
kernel.ImportSkill(new TimeSkill(), "time");

// We need a kernel context to store some information to pass to the prompts and the list
// of available skills needed to render prompt templates.
var context = kernel.CreateNewContext();

// Put the selected document into the variable used by the system prompt (see 28-system-prompt.txt).
context["selectedText"] = selectedText;

// Demo another variable, e.g. when the chat started, used by the system prompt (see 28-system-prompt.txt).
context["startTime"] = DateTimeOffset.Now.ToString("hh:mm:ss tt zz", CultureInfo.CurrentCulture);

// This is the user message, store it in the variable used by 28-user-prompt.txt
context["userMessage"] = "extract locations as a bullet point list";

// Instantiate the prompt renderer, which we will use to turn prompt templates
// into strings, that we will store into a Chat history object, which is then sent
// to the Chat Model.
var promptRenderer = new PromptTemplateEngine();

// Render the system prompt. This string is used to configure the chat.
// This contains the context, ie a piece of a wikipedia page selected by the user.
string systemMessage = await promptRenderer.RenderAsync(systemPromptTemplate, context);
Console.WriteLine($"------------------------------------\n{systemMessage}");

// Render the user prompt. This string is the query sent by the user
// This contains the user request, ie "extract locations as a bullet point list"
string userMessage = await promptRenderer.RenderAsync(userPromptTemplate, context);
Console.WriteLine($"------------------------------------\n{userMessage}");

// Client used to request answers to gpt-3.5-turbo
var chatGPT = kernel.GetService<IChatCompletion>();

// The full chat history. Depending on your scenario, you can pass the full chat if useful,
// or create a new one every time, assuming that the "system message" contains all the
// information needed.
var chatHistory = chatGPT.CreateNewChat(systemMessage);

// Add the user query to the chat history
chatHistory.AddMessage(ChatHistory.AuthorRoles.User, userMessage);

// Finally, get the response from AI
string answer = await chatGPT.GenerateMessageAsync(chatHistory);
Console.WriteLine($"------------------------------------\n{answer}");

/*
Output:
------------------------------------
You are an AI assistant that helps people find information.
The chat started at: 09:52:12 PM -07
The current time is: Thursday, April 27, 2023 9:52 PM
Text selected:
The central Sahara is hyperarid, with sparse vegetation. The northern and southern reaches of the desert, along with the highlands, have areas of sparse grassland and desert shrub, with trees and taller shrubs in wadis, where moisture collects. In the central, hyperarid region, there are many subdivisions of the great desert: Tanezrouft, the Ténéré, the Libyan Desert, the Eastern Desert, the Nubian Desert and others. These extremely arid areas often receive no rain for years.
------------------------------------
Thursday, April 27, 2023 2:34 PM: extract locations as a bullet point list
------------------------------------
Sure, here are the locations mentioned in the text:
- Tanezrouft
- Ténéré
- Libyan Desert
- Eastern Desert
- Nubian Desert
*/
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,9 @@
<ProjectReference Include="..\..\..\dotnet\src\Skills\Skills.Web\Skills.Web.csproj" />
<ProjectReference Include="..\..\..\dotnet\src\SemanticKernel\SemanticKernel.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\28-user-prompt.txt" />
<EmbeddedResource Include="Resources\28-system-prompt.txt" />
<EmbeddedResource Include="Resources\28-user-context.txt" />
</ItemGroup>
</Project>
3 changes: 3 additions & 0 deletions samples/dotnet/kernel-syntax-examples/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,8 @@ public static async Task Main()

await Example27_ActionPlanner.RunAsync();
Console.WriteLine("== DONE ==");

await Example28_ChatWithPrompts.RunAsync();
Console.WriteLine("== DONE ==");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) Microsoft. All rights reserved.

using System;

namespace RepoUtils;

public class ConfigurationException : Exception
{
public ConfigurationException()
{
}

public ConfigurationException(string message) : base(message)
{
}

public ConfigurationException(string message, Exception innerException) : base(message, innerException)
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
You are an AI assistant that helps people find information.
The chat started at: {{ $startTime }}
The current time is: {{ time.now }}
Text selected:
{{ $selectedText }}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The central Sahara is hyperarid, with sparse vegetation. The northern and southern reaches of the desert, along with the highlands, have areas of sparse grassland and desert shrub, with trees and taller shrubs in wadis, where moisture collects. In the central, hyperarid region, there are many subdivisions of the great desert: Tanezrouft, the Ténéré, the Libyan Desert, the Eastern Desert, the Nubian Desert and others. These extremely arid areas often receive no rain for years.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{ time.now }}: {{ $userMessage }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) Microsoft. All rights reserved.

using System.IO;
using System.Reflection;
using RepoUtils;

namespace Resources;

/// <summary>
/// Resource helper to load resources embedded in the assembly. By default we embed only
/// text files, so the helper is limited to returning text.
///
/// You can find information about embedded resources here:
/// * https://learn.microsoft.com/dotnet/core/extensions/create-resource-files
/// * https://learn.microsoft.com/dotnet/api/system.reflection.assembly.getmanifestresourcestream?view=net-7.0
///
/// To know which resources are embedded, check the csproj file.
/// </summary>
internal static class EmbeddedResource
{
private static readonly string? s_namespace = typeof(EmbeddedResource).Namespace;

internal static string Read(string fileName)
{
// Get the current assembly. Note: this class is in the same assembly where the embedded resources are stored.
Assembly? assembly = typeof(EmbeddedResource).GetTypeInfo().Assembly;
if (assembly == null) { throw new ConfigurationException($"[{s_namespace}] {fileName} assembly not found"); }

// Resources are mapped like types, using the namespace and appending "." (dot) and the file name
var resourceName = $"{s_namespace}." + fileName;
using Stream? resource = assembly.GetManifestResourceStream(resourceName);
if (resource == null) { throw new ConfigurationException($"{resourceName} resource not found"); }

// Return the resource content, in text format.
using var reader = new StreamReader(resource);
return reader.ReadToEnd();
}
}

0 comments on commit b0b4f00

Please sign in to comment.