Add commute report support to interaction service

This commit is contained in:
Jacob Dubin
2026-05-18 06:33:35 -05:00
parent b25793443f
commit d3f9de9503
7 changed files with 144 additions and 8 deletions

View File

@@ -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);

View File

@@ -11,6 +11,7 @@ public sealed class JiboInteractionService(
IJiboRandomizer randomizer, IJiboRandomizer randomizer,
IPersonalMemoryStore personalMemoryStore, IPersonalMemoryStore personalMemoryStore,
IWeatherReportProvider? weatherReportProvider = null, IWeatherReportProvider? weatherReportProvider = null,
ICommuteReportProvider? commuteReportProvider = null,
INewsBriefingProvider? newsBriefingProvider = null) INewsBriefingProvider? newsBriefingProvider = null)
{ {
private const string GreetingRouteMetadataKey = "greetingsRoute"; private const string GreetingRouteMetadataKey = "greetingsRoute";
@@ -482,6 +483,7 @@ public sealed class JiboInteractionService(
randomizer, randomizer,
personalMemoryStore, personalMemoryStore,
BuildWeatherReportDecisionAsync, BuildWeatherReportDecisionAsync,
BuildCommuteReportDecisionAsync,
turnContext => ResolveTenantScope(turnContext), turnContext => ResolveTenantScope(turnContext),
cancellationToken); cancellationToken);
if (personalReportDecision is not null) return personalReportDecision; if (personalReportDecision is not null) return personalReportDecision;
@@ -1372,6 +1374,37 @@ public sealed class JiboInteractionService(
weatherPayload); 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( private static string BuildWeatherSpokenReply(
WeatherReportSnapshot snapshot, WeatherReportSnapshot snapshot,
WeatherDateEntity weatherDate, WeatherDateEntity weatherDate,
@@ -1458,6 +1491,55 @@ public sealed class JiboInteractionService(
$"{currentIntro} In {location}, it's {summary} and {snapshot.Temperature} degrees {unit}. {currentHighLow}"; $"{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( private static string BuildWeeklyForecastSpokenReply(
IReadOnlyList<WeatherForecastCardSegment> segments, IReadOnlyList<WeatherForecastCardSegment> segments,
string? locationName, string? locationName,
@@ -1808,6 +1890,14 @@ public sealed class JiboInteractionService(
return template.Trim(); 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) private static string EscapeForEsml(string value)
{ {
return value return value

View File

@@ -70,6 +70,7 @@ internal static class PersonalReportOrchestrator
IJiboRandomizer randomizer, IJiboRandomizer randomizer,
IPersonalMemoryStore personalMemoryStore, IPersonalMemoryStore personalMemoryStore,
Func<TurnContext, string, CancellationToken, Task<JiboInteractionDecision>> buildWeatherDecisionAsync, Func<TurnContext, string, CancellationToken, Task<JiboInteractionDecision>> buildWeatherDecisionAsync,
Func<TurnContext, CancellationToken, Task<JiboInteractionDecision>> buildCommuteDecisionAsync,
Func<TurnContext, PersonalMemoryTenantScope> tenantScopeResolver, Func<TurnContext, PersonalMemoryTenantScope> tenantScopeResolver,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
@@ -191,6 +192,7 @@ internal static class PersonalReportOrchestrator
toggles, toggles,
currentName, currentName,
buildWeatherDecisionAsync, buildWeatherDecisionAsync,
buildCommuteDecisionAsync,
cancellationToken); cancellationToken);
if (IsNegativeReply(loweredTranscript)) if (IsNegativeReply(loweredTranscript))
@@ -235,6 +237,7 @@ internal static class PersonalReportOrchestrator
toggles, toggles,
parsedName, parsedName,
buildWeatherDecisionAsync, buildWeatherDecisionAsync,
buildCommuteDecisionAsync,
cancellationToken); cancellationToken);
} }
@@ -250,6 +253,7 @@ internal static class PersonalReportOrchestrator
PersonalReportServiceToggles toggles, PersonalReportServiceToggles toggles,
string userName, string userName,
Func<TurnContext, string, CancellationToken, Task<JiboInteractionDecision>> buildWeatherDecisionAsync, Func<TurnContext, string, CancellationToken, Task<JiboInteractionDecision>> buildWeatherDecisionAsync,
Func<TurnContext, CancellationToken, Task<JiboInteractionDecision>> buildCommuteDecisionAsync,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var reportSections = new List<string> var reportSections = new List<string>
@@ -293,13 +297,7 @@ internal static class PersonalReportOrchestrator
} }
if (toggles.CommuteEnabled) if (toggles.CommuteEnabled)
reportSections.Add( reportSections.Add((await buildCommuteDecisionAsync(turn, cancellationToken)).ReplyText);
RenderReportSkillTemplate(
ChooseReportSkillTemplate(
catalog.CommuteServiceDownReplies,
catalog.CommuteNowReplies,
"Sorry, commute information isn't available right now."),
userName));
if (toggles.NewsEnabled) if (toggles.NewsEnabled)
{ {

View File

@@ -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);
}
}

View File

@@ -1,6 +1,7 @@
using Jibo.Cloud.Application.Abstractions; using Jibo.Cloud.Application.Abstractions;
using Jibo.Cloud.Application.Services; using Jibo.Cloud.Application.Services;
using Jibo.Cloud.Infrastructure.Audio; using Jibo.Cloud.Infrastructure.Audio;
using Jibo.Cloud.Infrastructure.Commute;
using Jibo.Cloud.Infrastructure.Content; using Jibo.Cloud.Infrastructure.Content;
using Jibo.Cloud.Infrastructure.Media; using Jibo.Cloud.Infrastructure.Media;
using Jibo.Cloud.Infrastructure.News; using Jibo.Cloud.Infrastructure.News;
@@ -45,6 +46,7 @@ public static class ServiceCollectionExtensions
services.AddSingleton(newsApiOptions); services.AddSingleton(newsApiOptions);
services.AddHttpClient<IWeatherReportProvider, OpenWeatherReportProvider>(); services.AddHttpClient<IWeatherReportProvider, OpenWeatherReportProvider>();
services.AddHttpClient<INewsBriefingProvider, NewsApiBriefingProvider>(); services.AddHttpClient<INewsBriefingProvider, NewsApiBriefingProvider>();
services.AddSingleton<ICommuteReportProvider, UnavailableCommuteReportProvider>();
var statePersistencePath = configuration?["OpenJibo:State:PersistencePath"] var statePersistencePath = configuration?["OpenJibo:State:PersistencePath"]
?? Path.Combine(AppContext.BaseDirectory, "App_Data", "cloud-state.json"); ?? Path.Combine(AppContext.BaseDirectory, "App_Data", "cloud-state.json");
var personalMemoryPersistencePath = configuration?["OpenJibo:PersonalMemory:PersistencePath"] var personalMemoryPersistencePath = configuration?["OpenJibo:PersonalMemory:PersistencePath"]

View File

@@ -3974,6 +3974,7 @@ public sealed class JiboInteractionServiceTests
private static JiboInteractionService CreateService( private static JiboInteractionService CreateService(
IPersonalMemoryStore? personalMemoryStore = null, IPersonalMemoryStore? personalMemoryStore = null,
IWeatherReportProvider? weatherReportProvider = null, IWeatherReportProvider? weatherReportProvider = null,
ICommuteReportProvider? commuteReportProvider = null,
INewsBriefingProvider? newsBriefingProvider = null, INewsBriefingProvider? newsBriefingProvider = null,
IJiboExperienceContentRepository? contentRepository = null, IJiboExperienceContentRepository? contentRepository = null,
IJiboRandomizer? randomizer = null) IJiboRandomizer? randomizer = null)
@@ -3983,6 +3984,7 @@ public sealed class JiboInteractionServiceTests
randomizer ?? new FirstItemRandomizer(), randomizer ?? new FirstItemRandomizer(),
personalMemoryStore ?? new InMemoryPersonalMemoryStore(), personalMemoryStore ?? new InMemoryPersonalMemoryStore(),
weatherReportProvider, weatherReportProvider,
commuteReportProvider,
newsBriefingProvider); newsBriefingProvider);
} }
@@ -4076,4 +4078,16 @@ public sealed class JiboInteractionServiceTests
return Task.FromResult(catalog); 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);
}
}
} }

View File

@@ -21,7 +21,7 @@ public sealed class JiboWebSocketServiceTests
var contentRepository = new InMemoryJiboExperienceContentRepository(); var contentRepository = new InMemoryJiboExperienceContentRepository();
var contentCache = new JiboExperienceContentCache(contentRepository); var contentCache = new JiboExperienceContentCache(contentRepository);
var conversationBroker = new DemoConversationBroker(new JiboInteractionService(contentCache, var conversationBroker = new DemoConversationBroker(new JiboInteractionService(contentCache,
new LastItemRandomizer(), new InMemoryPersonalMemoryStore())); new LastItemRandomizer(), new InMemoryPersonalMemoryStore(), null, null, null));
var sttSelector = new DefaultSttStrategySelector( var sttSelector = new DefaultSttStrategySelector(
[ [
new SyntheticBufferedAudioSttStrategy() new SyntheticBufferedAudioSttStrategy()
@@ -4732,6 +4732,7 @@ public sealed class JiboWebSocketServiceTests
customStore, customStore,
new StubWeatherReportProvider( new StubWeatherReportProvider(
new WeatherReportSnapshot("Lone Jack, US", "overcast clouds", 79, 82, 78, "clouds", false)), new WeatherReportSnapshot("Lone Jack, US", "overcast clouds", 79, 82, 78, "clouds", false)),
null,
new StubNewsBriefingProvider( new StubNewsBriefingProvider(
new NewsBriefingSnapshot( new NewsBriefingSnapshot(
[ [
@@ -5122,6 +5123,7 @@ public sealed class JiboWebSocketServiceTests
private static JiboWebSocketService CreateService( private static JiboWebSocketService CreateService(
InMemoryCloudStateStore stateStore, InMemoryCloudStateStore stateStore,
IWeatherReportProvider? weatherReportProvider = null, IWeatherReportProvider? weatherReportProvider = null,
ICommuteReportProvider? commuteReportProvider = null,
INewsBriefingProvider? newsBriefingProvider = null) INewsBriefingProvider? newsBriefingProvider = null)
{ {
var contentRepository = new InMemoryJiboExperienceContentRepository(); var contentRepository = new InMemoryJiboExperienceContentRepository();
@@ -5131,6 +5133,7 @@ public sealed class JiboWebSocketServiceTests
new DefaultJiboRandomizer(), new DefaultJiboRandomizer(),
new InMemoryPersonalMemoryStore(), new InMemoryPersonalMemoryStore(),
weatherReportProvider, weatherReportProvider,
commuteReportProvider,
newsBriefingProvider); newsBriefingProvider);
var conversationBroker = new DemoConversationBroker(interactionService); var conversationBroker = new DemoConversationBroker(interactionService);
var sttSelector = new DefaultSttStrategySelector( var sttSelector = new DefaultSttStrategySelector(