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,
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

View File

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

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.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"]

View File

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

View File

@@ -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(