Add commute report support to interaction service
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
using Jibo.Runtime.Abstractions;
|
||||
|
||||
namespace Jibo.Cloud.Application.Abstractions;
|
||||
|
||||
public interface ICommuteReportProvider
|
||||
{
|
||||
Task<CommuteReportSnapshot?> GetReportAsync(TurnContext turn, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed record CommuteReportSnapshot(
|
||||
string LocationName,
|
||||
string Summary,
|
||||
int DurationMinutes,
|
||||
string? Mode = null,
|
||||
bool EventIsEarly = false);
|
||||
@@ -11,6 +11,7 @@ public sealed class JiboInteractionService(
|
||||
IJiboRandomizer randomizer,
|
||||
IPersonalMemoryStore personalMemoryStore,
|
||||
IWeatherReportProvider? weatherReportProvider = null,
|
||||
ICommuteReportProvider? commuteReportProvider = null,
|
||||
INewsBriefingProvider? newsBriefingProvider = null)
|
||||
{
|
||||
private const string GreetingRouteMetadataKey = "greetingsRoute";
|
||||
@@ -482,6 +483,7 @@ public sealed class JiboInteractionService(
|
||||
randomizer,
|
||||
personalMemoryStore,
|
||||
BuildWeatherReportDecisionAsync,
|
||||
BuildCommuteReportDecisionAsync,
|
||||
turnContext => ResolveTenantScope(turnContext),
|
||||
cancellationToken);
|
||||
if (personalReportDecision is not null) return personalReportDecision;
|
||||
@@ -1372,6 +1374,37 @@ public sealed class JiboInteractionService(
|
||||
weatherPayload);
|
||||
}
|
||||
|
||||
private async Task<JiboInteractionDecision> BuildCommuteReportDecisionAsync(
|
||||
TurnContext turn,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var catalog = await contentCache.GetCatalogAsync(cancellationToken);
|
||||
|
||||
if (commuteReportProvider is null)
|
||||
return new JiboInteractionDecision(
|
||||
"commute",
|
||||
ChooseCommuteServiceDownReply(catalog));
|
||||
|
||||
CommuteReportSnapshot? snapshot;
|
||||
try
|
||||
{
|
||||
snapshot = await commuteReportProvider.GetReportAsync(turn, cancellationToken);
|
||||
}
|
||||
catch (Exception) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
snapshot = null;
|
||||
}
|
||||
|
||||
if (snapshot is null)
|
||||
return new JiboInteractionDecision(
|
||||
"commute",
|
||||
ChooseCommuteServiceDownReply(catalog));
|
||||
|
||||
return new JiboInteractionDecision(
|
||||
"commute",
|
||||
BuildCommuteSpokenReply(snapshot, catalog));
|
||||
}
|
||||
|
||||
private static string BuildWeatherSpokenReply(
|
||||
WeatherReportSnapshot snapshot,
|
||||
WeatherDateEntity weatherDate,
|
||||
@@ -1458,6 +1491,55 @@ public sealed class JiboInteractionService(
|
||||
$"{currentIntro} In {location}, it's {summary} and {snapshot.Temperature} degrees {unit}. {currentHighLow}";
|
||||
}
|
||||
|
||||
private static string BuildCommuteSpokenReply(
|
||||
CommuteReportSnapshot snapshot,
|
||||
JiboExperienceCatalog catalog)
|
||||
{
|
||||
var duration = snapshot.DurationMinutes;
|
||||
var durationText = duration <= 1 ? "1 minute" : $"{duration} minutes";
|
||||
var mode = string.IsNullOrWhiteSpace(snapshot.Mode) ? "driving" : snapshot.Mode.Trim();
|
||||
|
||||
var template = ChooseCommuteTemplate(catalog.CommuteNowReplies, mode,
|
||||
"For your commute, it should take about ${skill.commute.durationMins} minutes.");
|
||||
|
||||
return template
|
||||
.Replace("${skill.commute.durationMins}", durationText, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("${speaker}", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace(" ", " ", StringComparison.Ordinal)
|
||||
.Trim();
|
||||
}
|
||||
|
||||
private static string ChooseCommuteTemplate(
|
||||
IReadOnlyList<string> templates,
|
||||
string mode,
|
||||
string fallback)
|
||||
{
|
||||
if (templates.Count == 0) return fallback;
|
||||
|
||||
var loweredMode = mode.Trim().ToLowerInvariant();
|
||||
var filtered = templates.Where(template =>
|
||||
{
|
||||
var lowered = template.ToLowerInvariant();
|
||||
return loweredMode switch
|
||||
{
|
||||
"walking" => lowered.Contains("walk", StringComparison.OrdinalIgnoreCase),
|
||||
"transit" => lowered.Contains("public transportation", StringComparison.OrdinalIgnoreCase) ||
|
||||
lowered.Contains("transit", StringComparison.OrdinalIgnoreCase) ||
|
||||
lowered.Contains("transportation", StringComparison.OrdinalIgnoreCase),
|
||||
"bicycling" => lowered.Contains("bike", StringComparison.OrdinalIgnoreCase) ||
|
||||
lowered.Contains("ride", StringComparison.OrdinalIgnoreCase),
|
||||
_ => lowered.Contains("drive", StringComparison.OrdinalIgnoreCase) ||
|
||||
lowered.Contains("commute", StringComparison.OrdinalIgnoreCase)
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
var selected = filtered.Count > 0
|
||||
? filtered.OrderBy(static template => template.Length).First()
|
||||
: templates.OrderBy(static template => template.Length).FirstOrDefault();
|
||||
|
||||
return string.IsNullOrWhiteSpace(selected) ? fallback : selected!;
|
||||
}
|
||||
|
||||
private static string BuildWeeklyForecastSpokenReply(
|
||||
IReadOnlyList<WeatherForecastCardSegment> segments,
|
||||
string? locationName,
|
||||
@@ -1808,6 +1890,14 @@ public sealed class JiboInteractionService(
|
||||
return template.Trim();
|
||||
}
|
||||
|
||||
private string ChooseCommuteServiceDownReply(JiboExperienceCatalog catalog)
|
||||
{
|
||||
var template = ChooseWeatherTemplate(
|
||||
catalog.CommuteServiceDownReplies,
|
||||
"Sorry, commute information isn't available right now.");
|
||||
return template.Trim();
|
||||
}
|
||||
|
||||
private static string EscapeForEsml(string value)
|
||||
{
|
||||
return value
|
||||
|
||||
@@ -70,6 +70,7 @@ internal static class PersonalReportOrchestrator
|
||||
IJiboRandomizer randomizer,
|
||||
IPersonalMemoryStore personalMemoryStore,
|
||||
Func<TurnContext, string, CancellationToken, Task<JiboInteractionDecision>> buildWeatherDecisionAsync,
|
||||
Func<TurnContext, CancellationToken, Task<JiboInteractionDecision>> buildCommuteDecisionAsync,
|
||||
Func<TurnContext, PersonalMemoryTenantScope> tenantScopeResolver,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -191,6 +192,7 @@ internal static class PersonalReportOrchestrator
|
||||
toggles,
|
||||
currentName,
|
||||
buildWeatherDecisionAsync,
|
||||
buildCommuteDecisionAsync,
|
||||
cancellationToken);
|
||||
|
||||
if (IsNegativeReply(loweredTranscript))
|
||||
@@ -235,6 +237,7 @@ internal static class PersonalReportOrchestrator
|
||||
toggles,
|
||||
parsedName,
|
||||
buildWeatherDecisionAsync,
|
||||
buildCommuteDecisionAsync,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
@@ -250,6 +253,7 @@ internal static class PersonalReportOrchestrator
|
||||
PersonalReportServiceToggles toggles,
|
||||
string userName,
|
||||
Func<TurnContext, string, CancellationToken, Task<JiboInteractionDecision>> buildWeatherDecisionAsync,
|
||||
Func<TurnContext, CancellationToken, Task<JiboInteractionDecision>> buildCommuteDecisionAsync,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var reportSections = new List<string>
|
||||
@@ -293,13 +297,7 @@ internal static class PersonalReportOrchestrator
|
||||
}
|
||||
|
||||
if (toggles.CommuteEnabled)
|
||||
reportSections.Add(
|
||||
RenderReportSkillTemplate(
|
||||
ChooseReportSkillTemplate(
|
||||
catalog.CommuteServiceDownReplies,
|
||||
catalog.CommuteNowReplies,
|
||||
"Sorry, commute information isn't available right now."),
|
||||
userName));
|
||||
reportSections.Add((await buildCommuteDecisionAsync(turn, cancellationToken)).ReplyText);
|
||||
|
||||
if (toggles.NewsEnabled)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
using Jibo.Cloud.Application.Abstractions;
|
||||
using Jibo.Runtime.Abstractions;
|
||||
|
||||
namespace Jibo.Cloud.Infrastructure.Commute;
|
||||
|
||||
public sealed class UnavailableCommuteReportProvider : ICommuteReportProvider
|
||||
{
|
||||
public Task<CommuteReportSnapshot?> GetReportAsync(
|
||||
TurnContext turn,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult<CommuteReportSnapshot?>(null);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Jibo.Cloud.Application.Abstractions;
|
||||
using Jibo.Cloud.Application.Services;
|
||||
using Jibo.Cloud.Infrastructure.Audio;
|
||||
using Jibo.Cloud.Infrastructure.Commute;
|
||||
using Jibo.Cloud.Infrastructure.Content;
|
||||
using Jibo.Cloud.Infrastructure.Media;
|
||||
using Jibo.Cloud.Infrastructure.News;
|
||||
@@ -45,6 +46,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddSingleton(newsApiOptions);
|
||||
services.AddHttpClient<IWeatherReportProvider, OpenWeatherReportProvider>();
|
||||
services.AddHttpClient<INewsBriefingProvider, NewsApiBriefingProvider>();
|
||||
services.AddSingleton<ICommuteReportProvider, UnavailableCommuteReportProvider>();
|
||||
var statePersistencePath = configuration?["OpenJibo:State:PersistencePath"]
|
||||
?? Path.Combine(AppContext.BaseDirectory, "App_Data", "cloud-state.json");
|
||||
var personalMemoryPersistencePath = configuration?["OpenJibo:PersonalMemory:PersistencePath"]
|
||||
|
||||
@@ -3974,6 +3974,7 @@ public sealed class JiboInteractionServiceTests
|
||||
private static JiboInteractionService CreateService(
|
||||
IPersonalMemoryStore? personalMemoryStore = null,
|
||||
IWeatherReportProvider? weatherReportProvider = null,
|
||||
ICommuteReportProvider? commuteReportProvider = null,
|
||||
INewsBriefingProvider? newsBriefingProvider = null,
|
||||
IJiboExperienceContentRepository? contentRepository = null,
|
||||
IJiboRandomizer? randomizer = null)
|
||||
@@ -3983,6 +3984,7 @@ public sealed class JiboInteractionServiceTests
|
||||
randomizer ?? new FirstItemRandomizer(),
|
||||
personalMemoryStore ?? new InMemoryPersonalMemoryStore(),
|
||||
weatherReportProvider,
|
||||
commuteReportProvider,
|
||||
newsBriefingProvider);
|
||||
}
|
||||
|
||||
@@ -4076,4 +4078,16 @@ public sealed class JiboInteractionServiceTests
|
||||
return Task.FromResult(catalog);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CapturingCommuteReportProvider : ICommuteReportProvider
|
||||
{
|
||||
public CommuteReportSnapshot? Snapshot { get; init; }
|
||||
|
||||
public Task<CommuteReportSnapshot?> GetReportAsync(
|
||||
TurnContext turn,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(Snapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ public sealed class JiboWebSocketServiceTests
|
||||
var contentRepository = new InMemoryJiboExperienceContentRepository();
|
||||
var contentCache = new JiboExperienceContentCache(contentRepository);
|
||||
var conversationBroker = new DemoConversationBroker(new JiboInteractionService(contentCache,
|
||||
new LastItemRandomizer(), new InMemoryPersonalMemoryStore()));
|
||||
new LastItemRandomizer(), new InMemoryPersonalMemoryStore(), null, null, null));
|
||||
var sttSelector = new DefaultSttStrategySelector(
|
||||
[
|
||||
new SyntheticBufferedAudioSttStrategy()
|
||||
@@ -4732,6 +4732,7 @@ public sealed class JiboWebSocketServiceTests
|
||||
customStore,
|
||||
new StubWeatherReportProvider(
|
||||
new WeatherReportSnapshot("Lone Jack, US", "overcast clouds", 79, 82, 78, "clouds", false)),
|
||||
null,
|
||||
new StubNewsBriefingProvider(
|
||||
new NewsBriefingSnapshot(
|
||||
[
|
||||
@@ -5122,6 +5123,7 @@ public sealed class JiboWebSocketServiceTests
|
||||
private static JiboWebSocketService CreateService(
|
||||
InMemoryCloudStateStore stateStore,
|
||||
IWeatherReportProvider? weatherReportProvider = null,
|
||||
ICommuteReportProvider? commuteReportProvider = null,
|
||||
INewsBriefingProvider? newsBriefingProvider = null)
|
||||
{
|
||||
var contentRepository = new InMemoryJiboExperienceContentRepository();
|
||||
@@ -5131,6 +5133,7 @@ public sealed class JiboWebSocketServiceTests
|
||||
new DefaultJiboRandomizer(),
|
||||
new InMemoryPersonalMemoryStore(),
|
||||
weatherReportProvider,
|
||||
commuteReportProvider,
|
||||
newsBriefingProvider);
|
||||
var conversationBroker = new DemoConversationBroker(interactionService);
|
||||
var sttSelector = new DefaultSttStrategySelector(
|
||||
|
||||
Reference in New Issue
Block a user