Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.Net: Support for primitives and complex types by Kernel prompt template #4013

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public static KernelFunction GetFunction(this IReadOnlyKernelPluginCollection pl

if (!TryGetFunction(plugins, pluginName, functionName, out KernelFunction? function))
{
throw new KeyNotFoundException("The plugin collection does not contain a plugin and/or function with the specified names.");
throw new KeyNotFoundException($"The plugin collection does not contain a plugin and/or function with the specified names. Plugin name - '{pluginName}', function name - '{functionName}'.");
}

return function;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.ComponentModel;
using System.Globalization;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -45,9 +47,6 @@ public KernelPromptTemplate(PromptTemplateConfig promptConfig, ILoggerFactory? l
/// <inheritdoc/>
public Task<string> RenderAsync(Kernel kernel, KernelArguments? arguments = null, CancellationToken cancellationToken = default)
{
// Make sure all arguments are of string type. This is temporary check until non-string arguments are supported.
AssertArgumentOfStringType(arguments);

return this.RenderAsync(this._blocks.Value, kernel, arguments, cancellationToken);
}

Expand All @@ -64,7 +63,7 @@ public Task<string> RenderAsync(Kernel kernel, KernelArguments? arguments = null
/// <param name="templateText">Prompt template (see skprompt.txt files)</param>
/// <param name="validate">Whether to validate the blocks syntax, or just return the blocks found, which could contain invalid code</param>
/// <returns>A list of all the blocks, ie the template tokenized in text, variables and function calls</returns>
internal List<Block> ExtractBlocks(string? templateText, bool validate = true)
private List<Block> ExtractBlocks(string? templateText, bool validate = true)
{
this._logger.LogTrace("Extracting blocks from template: {0}", templateText);
var blocks = this._tokenizer.Tokenize(templateText);
Expand All @@ -91,7 +90,7 @@ internal List<Block> ExtractBlocks(string? templateText, bool validate = true)
/// <param name="arguments">The arguments.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>The prompt template ready to be used for an AI request.</returns>
internal async Task<string> RenderAsync(List<Block> blocks, Kernel kernel, KernelArguments? arguments, CancellationToken cancellationToken = default)
private async Task<string> RenderAsync(List<Block> blocks, Kernel kernel, KernelArguments? arguments, CancellationToken cancellationToken = default)
{
this._logger.LogTrace("Rendering list of {0} blocks", blocks.Count);

Expand All @@ -101,11 +100,11 @@ internal async Task<string> RenderAsync(List<Block> blocks, Kernel kernel, Kerne
switch (block)
{
case ITextRendering staticBlock:
result.Append(staticBlock.Render(arguments));
result.Append(ConvertToString(staticBlock.Render(arguments), kernel.Culture));
break;

case ICodeRendering dynamicBlock:
result.Append(await dynamicBlock.RenderCodeAsync(kernel, arguments, cancellationToken).ConfigureAwait(false));
result.Append(ConvertToString(await dynamicBlock.RenderCodeAsync(kernel, arguments, cancellationToken).ConfigureAwait(false), kernel.Culture));
break;

default:
Expand All @@ -123,42 +122,42 @@ internal async Task<string> RenderAsync(List<Block> blocks, Kernel kernel, Kerne
return resultString;
}

/// <summary>
/// Given a list of blocks, render the Variable Blocks, replacing placeholders with the actual value in memory.
/// </summary>
/// <param name="blocks">List of blocks, typically all the blocks found in a template.</param>
/// <param name="arguments">Arguments to use for rendering.</param>
/// <returns>An updated list of blocks where Variable Blocks have rendered to Text Blocks.</returns>
internal IList<Block> RenderVariables(IList<Block> blocks, KernelArguments? arguments)
private static string? ConvertToString(object? value, CultureInfo culture)
{
this._logger.LogTrace("Rendering variables");
return blocks.Select(block => block.Type != BlockTypes.Variable
? block
: new TextBlock(((ITextRendering)block).Render(arguments), this._loggerFactory)).ToList();
if (value == null) { return null; }

var sourceType = value.GetType();

var converterFunction = GetTypeConverterDelegate(sourceType);

return converterFunction == null
? value.ToString()
: converterFunction(value, culture);
}

/// <summary>
/// Validates that all the KernelArguments are of string type.
/// </summary>
/// <param name="arguments">The collection of KernelArguments to validate.</param>
/// <exception cref="KernelException">Thrown when an argument is not of type string.</exception>
private static void AssertArgumentOfStringType(KernelArguments? arguments)
{
if (arguments == null)
private static Func<object?, CultureInfo, string?>? GetTypeConverterDelegate(Type sourceType) =>
SergeyMenshykh marked this conversation as resolved.
Show resolved Hide resolved
s_converters.GetOrAdd(sourceType, static sourceType =>
{
return;
}
// Strings just render as themselves.
if (sourceType == typeof(string))
{
return (input, cultureInfo) => (string)input!;
}

foreach (var argument in arguments)
{
if (argument.Value is string)
// Look up and use a type converter.
if (TypeConverterFactory.GetTypeConverter(sourceType) is TypeConverter converter && converter.CanConvertTo(typeof(string)))
{
continue;
return (input, cultureInfo) =>
{
return converter.ConvertToString(context: null, cultureInfo, input);
};
}

throw new KernelException($"Non-string kernel prompt template arguments are not supported in Release Candidate 1. This feature will be available soon, but for now, please ensure that all arguments are strings. Argument '{argument.Key}' is of type '{argument.Value?.GetType()}'.");
}
}
return null;
});

/// <summary>Converter functions for converting types to strings.</summary>
private static readonly ConcurrentDictionary<Type, Func<object?, CultureInfo, string?>?> s_converters = new();

#endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public override bool IsValid(out string errorMsg)
}

/// <inheritdoc/>
public ValueTask<string> RenderCodeAsync(Kernel kernel, KernelArguments? arguments = null, CancellationToken cancellationToken = default)
public ValueTask<object?> RenderCodeAsync(Kernel kernel, KernelArguments? arguments = null, CancellationToken cancellationToken = default)
{
if (!this._validated && !this.IsValid(out var error))
{
Expand All @@ -81,7 +81,7 @@ public ValueTask<string> RenderCodeAsync(Kernel kernel, KernelArguments? argumen

return this._tokens[0].Type switch
{
BlockTypes.Value or BlockTypes.Variable => new ValueTask<string>(((ITextRendering)this._tokens[0]).Render(arguments)),
BlockTypes.Value or BlockTypes.Variable => new ValueTask<object?>(((ITextRendering)this._tokens[0]).Render(arguments)),
BlockTypes.FunctionId => this.RenderFunctionCallAsync((FunctionIdBlock)this._tokens[0], kernel, arguments),
_ => throw new KernelException($"Unexpected first token type: {this._tokens[0].Type:G}"),
};
Expand All @@ -92,7 +92,7 @@ public ValueTask<string> RenderCodeAsync(Kernel kernel, KernelArguments? argumen
private bool _validated;
private readonly List<Block> _tokens;

private async ValueTask<string> RenderFunctionCallAsync(FunctionIdBlock fBlock, Kernel kernel, KernelArguments? arguments)
private async ValueTask<object?> RenderFunctionCallAsync(FunctionIdBlock fBlock, Kernel kernel, KernelArguments? arguments)
{
// If the code syntax is {{functionName $varName}} use $varName instead of $input
// If the code syntax is {{functionName 'value'}} use "value" instead of $input
Expand All @@ -105,7 +105,7 @@ private async ValueTask<string> RenderFunctionCallAsync(FunctionIdBlock fBlock,
{
var result = await kernel.InvokeAsync(fBlock.PluginName, fBlock.FunctionName, arguments).ConfigureAwait(false);

return result.ToString();
return result.Value;
}
catch (Exception ex)
{
Expand Down Expand Up @@ -162,7 +162,7 @@ private KernelArguments EnrichFunctionArguments(KernelArguments arguments)
var namedArgsStartIndex = 1;
if (firstArg.Type is not BlockTypes.NamedArg)
{
string input = ((ITextRendering)this._tokens[1]).Render(arguments);
object? input = ((ITextRendering)this._tokens[1]).Render(arguments);
// Keep previous trust information when updating the input
arguments[KernelArguments.InputParameterName] = input;
namedArgsStartIndex++;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public override bool IsValid(out string errorMsg)
}

/// <inheritdoc/>
public string Render(KernelArguments? arguments)
public object? Render(KernelArguments? arguments)
{
return this.Content;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ internal interface ICodeRendering
/// <param name="arguments">The arguments</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>Rendered content</returns>
public ValueTask<string> RenderCodeAsync(Kernel kernel, KernelArguments? arguments = null, CancellationToken cancellationToken = default);
public ValueTask<object?> RenderCodeAsync(Kernel kernel, KernelArguments? arguments = null, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ internal interface ITextRendering
/// </summary>
/// <param name="arguments">Optional arguments the block rendering</param>
/// <returns>Rendered content</returns>
public string Render(KernelArguments? arguments);
public object? Render(KernelArguments? arguments);
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public NamedArgBlock(string? text, ILoggerFactory? logger = null)
/// </summary>
/// <param name="arguments">Arguments to use for rendering the named argument value when the value is a <see cref="VarBlock"/>.</param>
/// <returns></returns>
internal string GetValue(KernelArguments? arguments)
internal object? GetValue(KernelArguments? arguments)
{
var valueIsValidValBlock = this._valBlock != null && this._valBlock.IsValid(out var errorMessage);
if (valueIsValidValBlock)
Expand All @@ -80,7 +80,7 @@ internal string GetValue(KernelArguments? arguments)
}

/// <inheritdoc/>
public string Render(KernelArguments? arguments)
public object? Render(KernelArguments? arguments)
{
return this.Content;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public override bool IsValid(out string errorMsg)
}

/// <inheritdoc/>
public string Render(KernelArguments? arguments)
public object? Render(KernelArguments? arguments)
{
return this.Content;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public override bool IsValid(out string errorMsg)
#pragma warning restore CA2254

/// <inheritdoc/>
public string Render(KernelArguments? arguments)
public object? Render(KernelArguments? arguments)
{
return this._value;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public override bool IsValid(out string errorMsg)
#pragma warning restore CA2254

/// <inheritdoc/>
public string Render(KernelArguments? arguments)
public object? Render(KernelArguments? arguments)
{
if (arguments == null) { return string.Empty; }

Expand All @@ -75,7 +75,7 @@ public string Render(KernelArguments? arguments)

if (arguments.TryGetValue(this.Name, out object? value))
{
return (string?)value ?? string.Empty;
return value ?? string.Empty;
}

this.Logger.LogWarning("Variable `{0}{1}` not found", Symbols.VarPrefix, this.Name);
Expand Down
Loading
Loading