Skip to content

Commit

Permalink
Add CultureInfo to SKContext (#1519)
Browse files Browse the repository at this point in the history
### Motivation and Context

Enable callers to configure culture information that flows into native
functions and that controls how implicit parsing / formatting is
performed.

#1226
#1374

### Description

The culture defaults to CurrentCulture but can be explicitly set to
change it. Native functions can declare a CultureInfo or IFormatProvider
argument, and the culture from the context will implicitly flow as the
value of that argument, such that functions can then naturally use it
for culture-related customization. The culture from the context is also
used for all implicit parsing / formatting operations performed as part
of function invocation.

### Contribution Checklist
<!-- Before submitting this PR, please make sure: -->
- [x] The code builds clean without any errors or warnings
- [x] The PR follows SK Contribution Guidelines
(https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
- [x] The code follows the .NET coding conventions
(https://learn.microsoft.com/dotnet/csharp/fundamentals/coding-style/coding-conventions)
verified with `dotnet format`
- [x] All unit tests pass, and I have added new tests where possible
- [ ] I didn't break anyone 😄

Co-authored-by: Shawn Callegari <36091529+shawncal@users.noreply.github.com>
  • Loading branch information
stephentoub and shawncal committed Jun 20, 2023
1 parent 33858a4 commit 4817187
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Threading;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
Expand All @@ -17,6 +18,11 @@ namespace Microsoft.SemanticKernel.Orchestration;
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public sealed class SKContext
{
/// <summary>
/// The culture currently associated with this context.
/// </summary>
private CultureInfo _culture;

/// <summary>
/// Print the processed input, aka the current data after any processing occurred.
/// </summary>
Expand Down Expand Up @@ -54,6 +60,15 @@ public sealed class SKContext
/// </summary>
public CancellationToken CancellationToken { get; }

/// <summary>
/// The culture currently associated with this context.
/// </summary>
public CultureInfo Culture
{
get => this._culture;
set => this._culture = value ?? CultureInfo.CurrentCulture;
}

/// <summary>
/// Shortcut into user data, access variables by name
/// </summary>
Expand Down Expand Up @@ -138,6 +153,7 @@ public ISKFunction Func(string skillName, string functionName)
this.Skills = skills ?? NullReadOnlySkillCollection.Instance;
this.Log = logger ?? NullLogger.Instance;
this.CancellationToken = cancellationToken;
this._culture = CultureInfo.CurrentCulture;
}

/// <summary>
Expand Down Expand Up @@ -180,9 +196,10 @@ public SKContext Clone()
logger: this.Log,
cancellationToken: this.CancellationToken)
{
Culture = this.Culture,
ErrorOccurred = this.ErrorOccurred,
LastErrorDescription = this.LastErrorDescription,
LastException = this.LastException
LastException = this.LastException,
};
}

Expand All @@ -209,6 +226,8 @@ private string DebuggerDisplay
display += $", Memory = {memory.GetType().Name}";
}

display += $", Culture = {this.Culture.EnglishName}";

return display;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public void DaysAgo()
double interval = 2;
DateTime expected = DateTime.Now.AddDays(-interval);
var skill = new TimeSkill();
string result = skill.DaysAgo(interval);
string result = skill.DaysAgo(interval, CultureInfo.CurrentCulture);
DateTime returned = DateTime.Parse(result, CultureInfo.CurrentCulture);
Assert.Equal(expected.Day, returned.Day);
Assert.Equal(expected.Month, returned.Month);
Expand All @@ -48,7 +48,7 @@ public void Day()
{
string expected = DateTime.Now.ToString("dd", CultureInfo.CurrentCulture);
var skill = new TimeSkill();
string result = skill.Day();
string result = skill.Day(CultureInfo.CurrentCulture);
Assert.Equal(expected, result);
Assert.True(int.TryParse(result, out _));
}
Expand Down Expand Up @@ -76,7 +76,7 @@ public void LastMatchingDay(DayOfWeek dayName)
Assert.True(found);

var skill = new TimeSkill();
string result = skill.DateMatchingLastDayName(dayName);
string result = skill.DateMatchingLastDayName(dayName, CultureInfo.CurrentCulture);
DateTime returned = DateTime.Parse(result, CultureInfo.CurrentCulture);
Assert.Equal(date.Day, returned.Day);
Assert.Equal(date.Month, returned.Month);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -852,6 +852,37 @@ static async Task AssertResult(Delegate d, SKContext context, string expected)
await AssertResult((Uri input) => new Uri(input, "kernel"), context, "http://example.com/kernel");
}

[Fact]
public async Task ItUsesContextCultureForParsingFormatting()
{
// Arrange
var context = this.MockContext("");
ISKFunction func = SKFunction.FromNativeFunction((double input) => input * 2, functionName: "Test");

// Act/Assert

context.Culture = new CultureInfo("fr-FR");
context.Variables.Update("12,34"); // tries first to parse with the specified culture
context = await func.InvokeAsync(context);
Assert.Equal("24,68", context.Variables.Input);

context.Culture = new CultureInfo("fr-FR");
context.Variables.Update("12.34"); // falls back to invariant culture
context = await func.InvokeAsync(context);
Assert.Equal("24,68", context.Variables.Input);

context.Culture = new CultureInfo("en-US");
context.Variables.Update("12.34"); // works with current culture
context = await func.InvokeAsync(context);
Assert.Equal("24.68", context.Variables.Input);

context.Culture = new CultureInfo("en-US");
context.Variables.Update("12,34"); // not parsable with current or invariant culture
context = await func.InvokeAsync(context);
Assert.True(context.ErrorOccurred);
Assert.IsType<ArgumentOutOfRangeException>(context.LastException);
}

[Fact]
public async Task ItThrowsWhenItFailsToConvertAnArgument()
{
Expand Down
6 changes: 4 additions & 2 deletions dotnet/src/SemanticKernel/CoreSkills/TextSkill.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,10 @@ public sealed class TextSkill
/// {{text.uppercase $input}} => "HELLO WORLD"
/// </example>
/// <param name="input"> The string to convert. </param>
/// <param name="cultureInfo"> An object that supplies culture-specific casing rules. </param>
/// <returns> The converted string. </returns>
[SKFunction, Description("Convert a string to uppercase.")]
public string Uppercase(string input) => input.ToUpper(CultureInfo.CurrentCulture);
public string Uppercase(string input, CultureInfo? cultureInfo = null) => input.ToUpper(cultureInfo);

/// <summary>
/// Convert a string to lowercase.
Expand All @@ -80,9 +81,10 @@ public sealed class TextSkill
/// {{text.lowercase $input}} => "hello world"
/// </example>
/// <param name="input"> The string to convert. </param>
/// <param name="cultureInfo"> An object that supplies culture-specific casing rules. </param>
/// <returns> The converted string. </returns>
[SKFunction, Description("Convert a string to lowercase.")]
public string Lowercase(string input) => input.ToLower(CultureInfo.CurrentCulture);
public string Lowercase(string input, CultureInfo? cultureInfo = null) => input.ToLower(cultureInfo);

/// <summary>
/// Get the length of a string. Returns 0 if null or empty
Expand Down
71 changes: 37 additions & 34 deletions dotnet/src/SemanticKernel/CoreSkills/TimeSkill.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

using System;
using System.ComponentModel;
using System.Globalization;
using Microsoft.SemanticKernel.SkillDefinition;

namespace Microsoft.SemanticKernel.CoreSkills;
Expand Down Expand Up @@ -49,9 +48,9 @@ public sealed class TimeSkill
/// </example>
/// <returns> The current date </returns>
[SKFunction, Description("Get the current date")]
public string Date() =>
public string Date(IFormatProvider? formatProvider = null) =>
// Example: Sunday, 12 January, 2025
DateTimeOffset.Now.ToString("D", CultureInfo.CurrentCulture);
DateTimeOffset.Now.ToString("D", formatProvider);

/// <summary>
/// Get the current date
Expand All @@ -61,7 +60,9 @@ public sealed class TimeSkill
/// </example>
/// <returns> The current date </returns>
[SKFunction, Description("Get the current date")]
public string Today() => this.Date();
public string Today(IFormatProvider? formatProvider = null) =>
// Example: Sunday, 12 January, 2025
this.Date(formatProvider);

/// <summary>
/// Get the current date and time in the local time zone"
Expand All @@ -71,9 +72,9 @@ public sealed class TimeSkill
/// </example>
/// <returns> The current date and time in the local time zone </returns>
[SKFunction, Description("Get the current date and time in the local time zone")]
public string Now() =>
public string Now(IFormatProvider? formatProvider = null) =>
// Sunday, January 12, 2025 9:15 PM
DateTimeOffset.Now.ToString("f", CultureInfo.CurrentCulture);
DateTimeOffset.Now.ToString("f", formatProvider);

/// <summary>
/// Get the current UTC date and time
Expand All @@ -83,9 +84,9 @@ public sealed class TimeSkill
/// </example>
/// <returns> The current UTC date and time </returns>
[SKFunction, Description("Get the current UTC date and time")]
public string UtcNow() =>
public string UtcNow(IFormatProvider? formatProvider = null) =>
// Sunday, January 13, 2025 5:15 AM
DateTimeOffset.UtcNow.ToString("f", CultureInfo.CurrentCulture);
DateTimeOffset.UtcNow.ToString("f", formatProvider);

/// <summary>
/// Get the current time
Expand All @@ -95,9 +96,9 @@ public sealed class TimeSkill
/// </example>
/// <returns> The current time </returns>
[SKFunction, Description("Get the current time")]
public string Time() =>
public string Time(IFormatProvider? formatProvider = null) =>
// Example: 09:15:07 PM
DateTimeOffset.Now.ToString("hh:mm:ss tt", CultureInfo.CurrentCulture);
DateTimeOffset.Now.ToString("hh:mm:ss tt", formatProvider);

/// <summary>
/// Get the current year
Expand All @@ -107,9 +108,9 @@ public sealed class TimeSkill
/// </example>
/// <returns> The current year </returns>
[SKFunction, Description("Get the current year")]
public string Year() =>
public string Year(IFormatProvider? formatProvider = null) =>
// Example: 2025
DateTimeOffset.Now.ToString("yyyy", CultureInfo.CurrentCulture);
DateTimeOffset.Now.ToString("yyyy", formatProvider);

/// <summary>
/// Get the current month name
Expand All @@ -119,9 +120,9 @@ public sealed class TimeSkill
/// </example>
/// <returns> The current month name </returns>
[SKFunction, Description("Get the current month name")]
public string Month() =>
public string Month(IFormatProvider? formatProvider = null) =>
// Example: January
DateTimeOffset.Now.ToString("MMMM", CultureInfo.CurrentCulture);
DateTimeOffset.Now.ToString("MMMM", formatProvider);

/// <summary>
/// Get the current month number
Expand All @@ -131,9 +132,9 @@ public sealed class TimeSkill
/// </example>
/// <returns> The current month number </returns>
[SKFunction, Description("Get the current month number")]
public string MonthNumber() =>
public string MonthNumber(IFormatProvider? formatProvider = null) =>
// Example: 01
DateTimeOffset.Now.ToString("MM", CultureInfo.CurrentCulture);
DateTimeOffset.Now.ToString("MM", formatProvider);

/// <summary>
/// Get the current day of the month
Expand All @@ -143,9 +144,9 @@ public sealed class TimeSkill
/// </example>
/// <returns> The current day of the month </returns>
[SKFunction, Description("Get the current day of the month")]
public string Day() =>
public string Day(IFormatProvider? formatProvider = null) =>
// Example: 12
DateTimeOffset.Now.ToString("dd", CultureInfo.CurrentCulture);
DateTimeOffset.Now.ToString("dd", formatProvider);

/// <summary>
/// Get the date a provided number of days in the past
Expand All @@ -157,8 +158,8 @@ public sealed class TimeSkill
/// <returns> The date the provided number of days before today </returns>
[SKFunction]
[Description("Get the date offset by a provided number of days from today")]
public string DaysAgo([Description("The number of days to offset from today"), SKName("input")] double daysOffset) =>
DateTimeOffset.Now.AddDays(-daysOffset).ToString("D", CultureInfo.CurrentCulture);
public string DaysAgo([Description("The number of days to offset from today"), SKName("input")] double daysOffset, IFormatProvider? formatProvider = null) =>
DateTimeOffset.Now.AddDays(-daysOffset).ToString("D", formatProvider);

/// <summary>
/// Get the current day of the week
Expand All @@ -168,9 +169,9 @@ public sealed class TimeSkill
/// </example>
/// <returns> The current day of the week </returns>
[SKFunction, Description("Get the current day of the week")]
public string DayOfWeek() =>
public string DayOfWeek(IFormatProvider? formatProvider = null) =>
// Example: Sunday
DateTimeOffset.Now.ToString("dddd", CultureInfo.CurrentCulture);
DateTimeOffset.Now.ToString("dddd", formatProvider);

/// <summary>
/// Get the current clock hour
Expand All @@ -180,9 +181,9 @@ public sealed class TimeSkill
/// </example>
/// <returns> The current clock hour </returns>
[SKFunction, Description("Get the current clock hour")]
public string Hour() =>
public string Hour(IFormatProvider? formatProvider = null) =>
// Example: 9 PM
DateTimeOffset.Now.ToString("h tt", CultureInfo.CurrentCulture);
DateTimeOffset.Now.ToString("h tt", formatProvider);

/// <summary>
/// Get the current clock 24-hour number
Expand All @@ -192,9 +193,9 @@ public sealed class TimeSkill
/// </example>
/// <returns> The current clock 24-hour number </returns>
[SKFunction, Description("Get the current clock 24-hour number")]
public string HourNumber() =>
public string HourNumber(IFormatProvider? formatProvider = null) =>
// Example: 21
DateTimeOffset.Now.ToString("HH", CultureInfo.CurrentCulture);
DateTimeOffset.Now.ToString("HH", formatProvider);

/// <summary>
/// Get the date of the previous day matching the supplied day name
Expand All @@ -206,7 +207,9 @@ public sealed class TimeSkill
/// <exception cref="ArgumentOutOfRangeException">dayName is not a recognized name of a day of the week</exception>
[SKFunction]
[Description("Get the date of the last day matching the supplied week day name in English. Example: Che giorno era 'Martedi' scorso -> dateMatchingLastDayName 'Tuesday' => Tuesday, 16 May, 2023")]
public string DateMatchingLastDayName([Description("The day name to match"), SKName("input")] DayOfWeek dayName)
public string DateMatchingLastDayName(
[Description("The day name to match"), SKName("input")] DayOfWeek dayName,
IFormatProvider? formatProvider = null)
{
DateTimeOffset dateTime = DateTimeOffset.Now;

Expand All @@ -220,7 +223,7 @@ public string DateMatchingLastDayName([Description("The day name to match"), SKN
}
}

return dateTime.ToString("D", CultureInfo.CurrentCulture);
return dateTime.ToString("D", formatProvider);
}

/// <summary>
Expand All @@ -231,9 +234,9 @@ public string DateMatchingLastDayName([Description("The day name to match"), SKN
/// </example>
/// <returns> The minutes on the current hour </returns>
[SKFunction, Description("Get the minutes on the current hour")]
public string Minute() =>
public string Minute(IFormatProvider? formatProvider = null) =>
// Example: 15
DateTimeOffset.Now.ToString("mm", CultureInfo.CurrentCulture);
DateTimeOffset.Now.ToString("mm", formatProvider);

/// <summary>
/// Get the seconds on the current minute
Expand All @@ -243,9 +246,9 @@ public string DateMatchingLastDayName([Description("The day name to match"), SKN
/// </example>
/// <returns> The seconds on the current minute </returns>
[SKFunction, Description("Get the seconds on the current minute")]
public string Second() =>
public string Second(IFormatProvider? formatProvider = null) =>
// Example: 07
DateTimeOffset.Now.ToString("ss", CultureInfo.CurrentCulture);
DateTimeOffset.Now.ToString("ss", formatProvider);

/// <summary>
/// Get the local time zone offset from UTC
Expand All @@ -255,9 +258,9 @@ public string DateMatchingLastDayName([Description("The day name to match"), SKN
/// </example>
/// <returns> The local time zone offset from UTC </returns>
[SKFunction, Description("Get the local time zone offset from UTC")]
public string TimeZoneOffset() =>
public string TimeZoneOffset(IFormatProvider? formatProvider = null) =>
// Example: -08:00
DateTimeOffset.Now.ToString("%K", CultureInfo.CurrentCulture);
DateTimeOffset.Now.ToString("%K", formatProvider);

/// <summary>
/// Get the local time zone name
Expand Down
Loading

0 comments on commit 4817187

Please sign in to comment.