Document commute provider seam for personal report
This commit is contained in:
24
OpenJibo/docs/calendar-architecture.md
Normal file
24
OpenJibo/docs/calendar-architecture.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Calendar Architecture
|
||||
|
||||
Pegasus treated calendar as a loop-scoped report surface, with report output fed by the
|
||||
household context instead of an isolated generic calendar service.
|
||||
|
||||
In OpenJibo, the current calendar path follows the same broad shape:
|
||||
|
||||
- calendar report output is loop-scoped
|
||||
- the report provider can read persisted loop calendar events
|
||||
- birthday and other personal dates already live in the loop-scoped holiday list
|
||||
- the personal report merges the report provider output into the spoken flow
|
||||
|
||||
Current behavior:
|
||||
|
||||
- if loop calendar events exist, the provider surfaces the next matching items
|
||||
- if no loop calendar events exist, the provider falls back to the merged holiday list
|
||||
- birthdays and custom holiday entries can therefore appear in the calendar section
|
||||
- the personal report still degrades safely when no calendar data is available
|
||||
|
||||
Notes:
|
||||
|
||||
- the current provider is intentionally lightweight and in-process
|
||||
- this gives us a swappable seam for later Azure-backed calendar sync
|
||||
- commute remains a separate report gap for the next pass
|
||||
72
OpenJibo/docs/commute-architecture.md
Normal file
72
OpenJibo/docs/commute-architecture.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Commute Architecture
|
||||
|
||||
## Purpose
|
||||
|
||||
Commute is part of personal report parity and household-aware personality.
|
||||
|
||||
The original Jibo report-skill had a commute section that could speak about getting to work, leaving soon, or being too early or too late. In OpenJibo, that behavior now starts with a loop-scoped commute profile so we can stay faithful to stock behavior first and add richer routing later.
|
||||
|
||||
## Current Shape
|
||||
|
||||
The cloud now models commute as persisted loop data instead of a hardcoded reply.
|
||||
|
||||
The current pieces are:
|
||||
|
||||
- `CommuteProfileRecord`
|
||||
- `ICommuteReportProvider`
|
||||
- `CloudStateCommuteReportProvider`
|
||||
- `Person/ListCommute`
|
||||
- `Person/UpsertCommute`
|
||||
|
||||
## Data Model
|
||||
|
||||
The commute profile is stored per loop and can optionally be tied to a person.
|
||||
|
||||
Typical fields include:
|
||||
|
||||
- `LoopId`
|
||||
- `MemberId`
|
||||
- `Mode`
|
||||
- `WorkHour`
|
||||
- `WorkMinute`
|
||||
- `OriginName`
|
||||
- `DestinationName`
|
||||
- `TypicalDurationMinutes`
|
||||
- `IsEnabled`
|
||||
- `IsComplete`
|
||||
|
||||
The provider uses the loop-scoped profile to decide whether commute is ready, missing setup, or ready to render a spoken answer.
|
||||
|
||||
## Runtime Behavior
|
||||
|
||||
At runtime, the commute provider:
|
||||
|
||||
- reads the current loop from the request context
|
||||
- loads the loop commute profile from cloud state
|
||||
- uses the profile plus current time to compute minutes until work
|
||||
- merges in same-day calendar pressure when a calendar event exists before the commute window
|
||||
- returns a safe setup response when the commute profile is missing or incomplete
|
||||
|
||||
## Personal Report Integration
|
||||
|
||||
Personal report uses the commute provider as a section in the broader household report.
|
||||
|
||||
That means the report can now speak in the familiar Jibo shape:
|
||||
|
||||
- weather
|
||||
- calendar
|
||||
- commute
|
||||
- news
|
||||
|
||||
## Next Gaps
|
||||
|
||||
The current commute provider is intentionally conservative.
|
||||
|
||||
Next steps can include:
|
||||
|
||||
- a richer travel-time source
|
||||
- map or transit integration
|
||||
- better depart-time commentary
|
||||
- preference-based commute suppression or reminders
|
||||
|
||||
For now the goal is to keep the interface stable and the behavior stock-like.
|
||||
@@ -44,6 +44,7 @@ Current release theme:
|
||||
- radio, ESML apostrophe cleanup, and first news are implemented in source/tests; radio and basic news are live-proven as of `jibo test 23`
|
||||
- `jibo test 22` validated radio, exposed backup/load interference, exposed a shared yes/no no-input gap, exposed repeated create keeper prompts after photo handoff, and showed local whisper `ffmpeg` failures on unusable buffered audio
|
||||
- `jibo test 23` validated basic news, proved one alarm set/fire path at `7:43 AM`, exposed comma-separated/short alarm follow-up parsing risk, showed stock alarm replacement yes/no rules that needed cloud handling, and showed photo gallery still failing when `shared/yes_no` ASR came back empty
|
||||
- personal report parity now has loop-scoped calendar and commute provider seams that merge persisted loop events, birthday/holiday dates, and commute profiles; the remaining report gap is richer travel-time data, not missing structure
|
||||
- `jibo test 24` showed alarm replacement yes/no working, but exposed empty `clock/alarm_set_value` and `gallery/gallery_preview` turns falling into generic `I heard you` fallback speech; it also showed `CLIENT_NLU cancel` inside `clock/alarm_set_value` re-asking for an alarm value instead of closing the prompt
|
||||
- `jibo test 25` proved a broader regression path but exposed repeated backup-in-progress/update-menu blockage, timer/alarm stale state and delete/menu disagreement, gallery `shared/yes_no` hangs under `@be/gallery`, punctuated `Never mind.` falling through to chat, volume homophone parsing (`Set Volume 2-6.`), and settings volume-control cleanup falling into `I heard you`
|
||||
- `jibo test 26` live-proved punctuated stop, volume homophone parsing, gallery launch/yes/create/save, and good morning; it still exposed robot-local backup warnings, long blue-ring buffering without a fresh `LISTEN`, alarm replacement drifting into the value/manual screen, and alarm delete phrases/mishears falling to chat
|
||||
@@ -731,7 +732,7 @@ Current release theme:
|
||||
- weather icon/animation parity and view support
|
||||
- broader non-local weather query handling and short-range date coverage
|
||||
- provider-backed news ingestion and filtering
|
||||
- commute provider path and settings schema
|
||||
- commute provider path, settings schema, and loop-scoped commute profile storage
|
||||
- coverage matrix for personal report parity gaps and test/capture exit criteria
|
||||
- Progress update (`2026-05-10`):
|
||||
- added provider-ready news briefing lane with Nimbus-compatible `news` skill payload continuity
|
||||
@@ -739,6 +740,7 @@ Current release theme:
|
||||
- fallback synthetic news behavior remains active when no provider key is configured
|
||||
- added TTL caching for weather/news provider calls to reduce repeated external requests
|
||||
- vendored Pegasus `report-skill` templates for weather and personal-report phrasing so the next pass can focus on renderer coverage for calendar, commute, and news templates instead of rediscovering source text
|
||||
- commute now has a loop-scoped provider seam plus persisted commute profiles, so the next pass can focus on richer travel-time data instead of basic storage shape
|
||||
- Source anchors:
|
||||
- `C:\Projects\jibo\pegasus\packages\report-skill\src\subskills\weather\WeatherMimLogic.ts`
|
||||
- `C:\Projects\jibo\pegasus\packages\report-skill\resources\views\weatherHiLo.json`
|
||||
@@ -927,7 +929,7 @@ For `1.0.19`:
|
||||
4. Weather report-skill launch compatibility - implemented
|
||||
5. Dialog parsing expansion and ambiguity guardrails - in progress (`2026-05-09` third guardrail slice implemented; Pegasus affinity phrase families + continuation guardrails expanded)
|
||||
6. Presence-aware greetings and identity-triggered proactivity - implemented (trigger path, identity-aware reactive/proactive replies, cooldown metadata wiring, focused websocket coverage)
|
||||
7. Personal report parity track (weather visuals, live news path, commute path, calendar parity matrix) - in progress (`2026-05-10` first live-news provider slice implemented)
|
||||
7. Personal report parity track (weather visuals, live news path, commute path, calendar parity matrix) - in progress (`2026-05-10` first live-news provider slice implemented; commute now has a loop-scoped provider seam)
|
||||
8. Holidays and seasonal personality behavior built on the new memory/proactivity foundation
|
||||
- system holidays should come from an up-to-date provider and merge with loop-scoped custom holiday records
|
||||
- allow disabled holiday records to suppress reminders for people who do not celebrate a holiday
|
||||
|
||||
@@ -120,6 +120,7 @@ Reference design:
|
||||
|
||||
- [persistence-architecture.md](persistence-architecture.md)
|
||||
- [holiday-architecture.md](holiday-architecture.md)
|
||||
- [commute-architecture.md](commute-architecture.md)
|
||||
|
||||
## First Implemented Slice In `1.0.19`
|
||||
|
||||
@@ -291,6 +292,10 @@ This confirms the pizza-fact offer state now keeps the yes/no branch open throug
|
||||
|
||||
Personal report parity planning is now captured with Pegasus source anchors for weather visuals/animations, live news, commute, and calendar gap coverage.
|
||||
|
||||
Calendar is now backed by a loop-scoped provider seam that can merge persisted loop events with birthday and holiday dates, keeping the report aligned with household context.
|
||||
|
||||
Commute now uses a loop-scoped commute profile and provider seam so the report can speak in the legacy commute shape without inventing a separate hosted travel service yet.
|
||||
|
||||
Reference:
|
||||
|
||||
- [personal-report-parity-plan.md](personal-report-parity-plan.md)
|
||||
@@ -335,7 +340,7 @@ Third completed guardrail slice under this queue:
|
||||
|
||||
Next queued implementation track after parser guardrails:
|
||||
|
||||
- personal report parity slices (weather visual parity, live news path, commute/calendar gap closure)
|
||||
- personal report parity slices (weather visual parity, live news path, commute/calendar refinement)
|
||||
|
||||
First completed slice in this personal-report parity track:
|
||||
|
||||
@@ -344,6 +349,7 @@ First completed slice in this personal-report parity track:
|
||||
- added memory/transcript category hinting for provider requests (`sports`, `technology`, `business`, etc.)
|
||||
- added provider-side request caching for both news and weather to reduce integration churn and repeated lookups
|
||||
- added focused interaction + websocket tests for provider-backed news speech output and request-hint plumbing
|
||||
- added loop-scoped calendar and commute provider seams so personal report can use persisted household context instead of static placeholders
|
||||
|
||||
## Next Slices
|
||||
|
||||
|
||||
@@ -103,6 +103,33 @@ The default country code is `US`, but you can override it with:
|
||||
If you later add custom holiday authoring, disabled records can be used to suppress a holiday for a
|
||||
loop without removing the underlying system holiday source.
|
||||
|
||||
## Calendar Wiring
|
||||
|
||||
Calendar report output is now driven by a loop-scoped in-process provider.
|
||||
|
||||
The provider currently:
|
||||
|
||||
- reads persisted loop calendar events
|
||||
- folds in birthday and holiday dates that already live in the loop-scoped holiday list
|
||||
- returns a safe empty calendar view when nothing is scheduled
|
||||
|
||||
This keeps the personal report moving toward Pegasus-style household-aware output without forcing a
|
||||
full external calendar integration yet.
|
||||
|
||||
## Commute Wiring
|
||||
|
||||
Commute report output is now driven by a loop-scoped commute profile plus a provider seam.
|
||||
|
||||
The provider currently:
|
||||
|
||||
- reads persisted loop commute profiles
|
||||
- returns a setup view when commute is missing or incomplete
|
||||
- computes commute timing from the loop profile and the current clock
|
||||
- keeps the personal report flow aligned with the stock `Commute_*` shape
|
||||
|
||||
The provider is intentionally conservative for now. It preserves the old report shape and gives us
|
||||
room to add a richer travel-time source later without changing the behavior layer again.
|
||||
|
||||
## Recovery Strategy
|
||||
|
||||
The first supported device path is:
|
||||
|
||||
@@ -44,5 +44,9 @@ public interface ICloudStateStore
|
||||
IReadOnlyList<KeyRequestRecord> GetBinaryRequests();
|
||||
IReadOnlyList<HolidayRecord> GetHolidays(string? loopId = null);
|
||||
HolidayRecord UpsertHoliday(HolidayRecord holiday);
|
||||
IReadOnlyList<CommuteProfileRecord> GetCommuteProfiles(string? loopId = null);
|
||||
CommuteProfileRecord UpsertCommuteProfile(CommuteProfileRecord commuteProfile);
|
||||
IReadOnlyList<CalendarEventRecord> GetCalendarEvents(string? loopId = null);
|
||||
CalendarEventRecord UpsertCalendarEvent(CalendarEventRecord calendarEvent);
|
||||
void UpdateRobot(DeviceRegistration registration);
|
||||
}
|
||||
|
||||
@@ -12,4 +12,7 @@ public sealed record CommuteReportSnapshot(
|
||||
string Summary,
|
||||
int DurationMinutes,
|
||||
string? Mode = null,
|
||||
bool EventIsEarly = false);
|
||||
bool EventIsEarly = false,
|
||||
int MinutesUntilWork = 0,
|
||||
int ExtraMinutes = 0,
|
||||
bool RequiresSetup = false);
|
||||
|
||||
@@ -44,7 +44,20 @@ public sealed class JiboConditionedReply
|
||||
public IReadOnlyList<string> CalendarNothingReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CalendarServiceDownReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CalendarOutroReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CommuteAppSetupReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CommuteConfirmSpeakerReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CommuteNowReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CommuteMinutesLeftReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CommuteDepartTimeNormalReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CommuteDepartTimeNotNormalReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CommuteDriveNormalReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CommuteDriveLateReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CommuteDriveHurryReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CommuteDrivePoorReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CommuteDriveTerribleReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CommuteTransportNormalReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CommuteTransportLateReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CommuteTransportHurryReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CommuteServiceDownReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> NewsIntroReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> NewsCategoryIntroReplies { get; init; } = [];
|
||||
|
||||
@@ -355,12 +355,47 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore, IMedia
|
||||
|
||||
private ProtocolDispatchResult HandlePerson(string operation, ProtocolEnvelope envelope)
|
||||
{
|
||||
if (!operation.Equals("ListHolidays", StringComparison.OrdinalIgnoreCase))
|
||||
return ProtocolDispatchResult.Ok(Array.Empty<object>());
|
||||
|
||||
var body = envelope.TryParseBody();
|
||||
var loopId = ReadString(body, "loopId");
|
||||
return ProtocolDispatchResult.Ok(stateStore.GetHolidays(loopId).Select(MapHoliday));
|
||||
|
||||
if (operation.Equals("ListHolidays", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var loopId = ReadString(body, "loopId");
|
||||
return ProtocolDispatchResult.Ok(stateStore.GetHolidays(loopId).Select(MapHoliday));
|
||||
}
|
||||
|
||||
if (operation.Equals("ListCommute", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var loopId = ReadString(body, "loopId");
|
||||
return ProtocolDispatchResult.Ok(stateStore.GetCommuteProfiles(loopId).Select(MapCommute));
|
||||
}
|
||||
|
||||
if (operation.Equals("UpsertCommute", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var hasIsEnabled = body is { } enabledBody && enabledBody.TryGetProperty("isEnabled", out _);
|
||||
var hasIsComplete = body is { } completeBody && completeBody.TryGetProperty("isComplete", out _);
|
||||
var workHour = ReadLong(body, "workHour");
|
||||
var workMinute = ReadLong(body, "workMinute");
|
||||
var typicalDurationMinutes = ReadLong(body, "typicalDurationMinutes");
|
||||
var commute = new CommuteProfileRecord
|
||||
{
|
||||
Id = ReadString(body, "id") ?? string.Empty,
|
||||
LoopId = ReadString(body, "loopId") ?? string.Empty,
|
||||
MemberId = ReadString(body, "memberId"),
|
||||
IsEnabled = hasIsEnabled ? ReadBool(body, "isEnabled") : true,
|
||||
IsComplete = hasIsComplete ? ReadBool(body, "isComplete") : true,
|
||||
Mode = ReadString(body, "mode") ?? "driving",
|
||||
WorkHour = workHour is > 0 and < 24 ? (int)workHour.Value : 8,
|
||||
WorkMinute = workMinute is >= 0 and < 60 ? (int)workMinute.Value : 30,
|
||||
OriginName = ReadString(body, "originName"),
|
||||
DestinationName = ReadString(body, "destinationName"),
|
||||
TypicalDurationMinutes = typicalDurationMinutes is > 0
|
||||
? (int)typicalDurationMinutes.Value
|
||||
: 25
|
||||
};
|
||||
return ProtocolDispatchResult.Ok(MapCommute(stateStore.UpsertCommuteProfile(commute)));
|
||||
}
|
||||
|
||||
return ProtocolDispatchResult.Ok(Array.Empty<object>());
|
||||
}
|
||||
|
||||
private ProtocolDispatchResult HandleBackup(string operation, ProtocolEnvelope envelope)
|
||||
@@ -572,6 +607,26 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore, IMedia
|
||||
};
|
||||
}
|
||||
|
||||
private static object MapCommute(CommuteProfileRecord commute)
|
||||
{
|
||||
return new
|
||||
{
|
||||
id = commute.Id,
|
||||
loopId = commute.LoopId,
|
||||
memberId = commute.MemberId,
|
||||
isEnabled = commute.IsEnabled,
|
||||
isComplete = commute.IsComplete,
|
||||
mode = commute.Mode,
|
||||
workHour = commute.WorkHour,
|
||||
workMinute = commute.WorkMinute,
|
||||
originName = commute.OriginName,
|
||||
destinationName = commute.DestinationName,
|
||||
typicalDurationMinutes = commute.TypicalDurationMinutes,
|
||||
created = commute.Created,
|
||||
updated = commute.Updated
|
||||
};
|
||||
}
|
||||
|
||||
private static object MapMedia(MediaRecord item)
|
||||
{
|
||||
return new
|
||||
|
||||
@@ -1626,6 +1626,11 @@ public sealed class JiboInteractionService(
|
||||
"commute",
|
||||
ChooseCommuteServiceDownReply(catalog));
|
||||
|
||||
if (snapshot.RequiresSetup)
|
||||
return new JiboInteractionDecision(
|
||||
"commute_setup",
|
||||
ChooseCommuteAppSetupReply(catalog));
|
||||
|
||||
return new JiboInteractionDecision(
|
||||
"commute",
|
||||
BuildCommuteSpokenReply(snapshot, catalog));
|
||||
@@ -1754,47 +1759,122 @@ public sealed class JiboInteractionService(
|
||||
{
|
||||
var duration = snapshot.DurationMinutes;
|
||||
var durationText = duration <= 1 ? "1 minute" : $"{duration} minutes";
|
||||
var minutesLeft = snapshot.MinutesUntilWork;
|
||||
var minutesLeftText = minutesLeft <= 1 ? "1 minute" : $"{Math.Abs(minutesLeft)} minutes";
|
||||
var mode = string.IsNullOrWhiteSpace(snapshot.Mode) ? "driving" : snapshot.Mode.Trim();
|
||||
var template = ChooseCommuteTemplate(snapshot, catalog, mode);
|
||||
var reply = RenderCommuteTemplate(template, durationText, minutesLeftText);
|
||||
|
||||
var template = ChooseCommuteTemplate(catalog.CommuteNowReplies, mode,
|
||||
"For your commute, it should take about ${skill.commute.durationMins} minutes.");
|
||||
if (minutesLeft > 0 && minutesLeft < 30)
|
||||
{
|
||||
var minutesTemplate = ChooseShortestTemplate(catalog.CommuteMinutesLeftReplies)
|
||||
?? "That's in about ${skill.commute.minsLeft} minutes.";
|
||||
reply = $"{reply} {RenderCommuteTemplate(minutesTemplate, durationText, minutesLeftText)}";
|
||||
}
|
||||
|
||||
if (minutesLeft > 0 && minutesLeft < 120)
|
||||
{
|
||||
var departTemplate = ChooseCommuteDepartTimeTemplate(snapshot, catalog, mode);
|
||||
if (!string.IsNullOrWhiteSpace(departTemplate))
|
||||
reply = $"{reply} {RenderCommuteTemplate(departTemplate, durationText, minutesLeftText)}";
|
||||
}
|
||||
|
||||
return reply.Replace(" ", " ", StringComparison.Ordinal).Trim();
|
||||
}
|
||||
|
||||
private string ChooseCommuteAppSetupReply(JiboExperienceCatalog catalog)
|
||||
{
|
||||
return SelectLegacyReply(
|
||||
catalog.CommuteAppSetupReplies,
|
||||
[
|
||||
"I need your commute settings before I can give you a commute report."
|
||||
]);
|
||||
}
|
||||
|
||||
private static string ChooseCommuteTemplate(
|
||||
CommuteReportSnapshot snapshot,
|
||||
JiboExperienceCatalog catalog,
|
||||
string mode)
|
||||
{
|
||||
var minutesUntilWork = snapshot.MinutesUntilWork;
|
||||
var extraMinutes = Math.Max(0, snapshot.ExtraMinutes);
|
||||
var isLate = minutesUntilWork <= 0;
|
||||
var isHurry = minutesUntilWork > 0 && minutesUntilWork <= 10;
|
||||
var isNormal = !isLate && !isHurry;
|
||||
var isFarAway = minutesUntilWork > 120 || minutesUntilWork < -30;
|
||||
var hasTrafficSeverity = minutesUntilWork > 0;
|
||||
var isTerrible = hasTrafficSeverity && extraMinutes >= 15;
|
||||
var isPoor = hasTrafficSeverity && extraMinutes >= 5;
|
||||
|
||||
var loweredMode = mode.Trim().ToLowerInvariant();
|
||||
IReadOnlyList<string> candidates = loweredMode switch
|
||||
{
|
||||
"walking" when isHurry => catalog.CommuteTransportHurryReplies,
|
||||
"walking" when isLate => catalog.CommuteTransportLateReplies,
|
||||
"walking" => catalog.CommuteTransportNormalReplies,
|
||||
"transit" when isHurry => catalog.CommuteTransportHurryReplies,
|
||||
"transit" when isLate => catalog.CommuteTransportLateReplies,
|
||||
"transit" => catalog.CommuteTransportNormalReplies,
|
||||
"bicycling" when isHurry => catalog.CommuteDriveHurryReplies,
|
||||
"bicycling" when isLate => catalog.CommuteDriveLateReplies,
|
||||
"bicycling" => catalog.CommuteDriveNormalReplies,
|
||||
_ when isFarAway => catalog.CommuteNowReplies,
|
||||
_ when isTerrible => catalog.CommuteDriveTerribleReplies,
|
||||
_ when isPoor => catalog.CommuteDrivePoorReplies,
|
||||
_ when isHurry => catalog.CommuteDriveHurryReplies,
|
||||
_ when isLate => catalog.CommuteDriveLateReplies,
|
||||
_ when isNormal => catalog.CommuteDriveNormalReplies,
|
||||
_ => catalog.CommuteNowReplies
|
||||
};
|
||||
|
||||
if (candidates.Count == 0)
|
||||
return "For your commute, it should take about ${skill.commute.durationMins} minutes.";
|
||||
|
||||
var selected = ChooseShortestTemplate(candidates);
|
||||
return string.IsNullOrWhiteSpace(selected)
|
||||
? "For your commute, it should take about ${skill.commute.durationMins} minutes."
|
||||
: selected!;
|
||||
}
|
||||
|
||||
private static string ChooseCommuteDepartTimeTemplate(
|
||||
CommuteReportSnapshot snapshot,
|
||||
JiboExperienceCatalog catalog,
|
||||
string mode)
|
||||
{
|
||||
var loweredMode = mode.Trim().ToLowerInvariant();
|
||||
var templates = snapshot.MinutesUntilWork <= 0
|
||||
? catalog.CommuteDepartTimeNotNormalReplies
|
||||
: catalog.CommuteDepartTimeNormalReplies;
|
||||
|
||||
if (templates.Count == 0) return string.Empty;
|
||||
|
||||
var selected = ChooseShortestTemplate(templates);
|
||||
if (!string.IsNullOrWhiteSpace(selected)) return selected!;
|
||||
|
||||
return loweredMode switch
|
||||
{
|
||||
"walking" => "If you leave at the usual time, that should work out fine.",
|
||||
"transit" => "If you leave at the usual time, that should work out fine.",
|
||||
_ => "If you leave at the usual time, that should work out fine."
|
||||
};
|
||||
}
|
||||
|
||||
private static string RenderCommuteTemplate(string template, string durationText, string minutesLeftText)
|
||||
{
|
||||
return template
|
||||
.Replace("${skill.commute.durationMins}", durationText, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("${skill.commute.minsLeft}", minutesLeftText, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("${speaker}", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace(" ", " ", StringComparison.Ordinal)
|
||||
.Trim();
|
||||
}
|
||||
|
||||
private static string ChooseCommuteTemplate(
|
||||
IReadOnlyList<string> templates,
|
||||
string mode,
|
||||
string fallback)
|
||||
private static string? ChooseShortestTemplate(IEnumerable<string> templates)
|
||||
{
|
||||
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!;
|
||||
return templates
|
||||
.Where(static template => !string.IsNullOrWhiteSpace(template))
|
||||
.OrderBy(static template => template.Length)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private static string BuildWeeklyForecastSpokenReply(
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace Jibo.Cloud.Domain.Models;
|
||||
|
||||
public sealed class CalendarEventRecord
|
||||
{
|
||||
public string Id { get; init; } = $"calendar-{Guid.NewGuid():N}";
|
||||
public string LoopId { get; init; } = "openjibo-default-loop";
|
||||
public string Summary { get; init; } = "Calendar event";
|
||||
public string? TimeLabel { get; init; }
|
||||
public DateOnly Date { get; init; } = DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
public DateOnly? EndDate { get; init; }
|
||||
public bool IsAllDay { get; init; }
|
||||
public bool IsEnabled { get; init; } = true;
|
||||
public string Source { get; init; } = "manual";
|
||||
public string? MemberId { get; init; }
|
||||
public DateTimeOffset Created { get; init; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace Jibo.Cloud.Domain.Models;
|
||||
|
||||
public sealed class CommuteProfileRecord
|
||||
{
|
||||
public string Id { get; init; } = $"commute-{Guid.NewGuid():N}";
|
||||
public string LoopId { get; init; } = "openjibo-default-loop";
|
||||
public string? MemberId { get; init; }
|
||||
public bool IsEnabled { get; init; } = true;
|
||||
public bool IsComplete { get; init; } = true;
|
||||
public string Mode { get; init; } = "driving";
|
||||
public int WorkHour { get; init; } = 8;
|
||||
public int WorkMinute { get; init; } = 30;
|
||||
public string? OriginName { get; init; } = "home";
|
||||
public string? DestinationName { get; init; } = "work";
|
||||
public int TypicalDurationMinutes { get; init; } = 25;
|
||||
public DateTimeOffset Created { get; init; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset Updated { get; init; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using Jibo.Cloud.Application.Abstractions;
|
||||
using Jibo.Cloud.Domain.Models;
|
||||
using Jibo.Cloud.Infrastructure.Persistence;
|
||||
using Jibo.Runtime.Abstractions;
|
||||
|
||||
namespace Jibo.Cloud.Infrastructure.Calendar;
|
||||
|
||||
public sealed class CloudStateCalendarReportProvider(ICloudStateStore cloudStateStore) : ICalendarReportProvider
|
||||
{
|
||||
public Task<CalendarReportSnapshot?> GetReportAsync(
|
||||
TurnContext turn,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var loopId = ResolveLoopId(turn);
|
||||
var today = DateOnly.FromDateTime(DateTimeOffset.UtcNow.Date);
|
||||
var tomorrow = today.AddDays(1);
|
||||
|
||||
var calendarEvents = cloudStateStore.GetCalendarEvents(loopId)
|
||||
.Where(static calendarEvent => calendarEvent.IsEnabled)
|
||||
.Where(calendarEvent => calendarEvent.Date != default)
|
||||
.ToArray();
|
||||
|
||||
var holidays = cloudStateStore.GetHolidays(loopId)
|
||||
.Where(static holiday => holiday.IsEnabled)
|
||||
.Where(holiday => holiday.Date != default)
|
||||
.ToArray();
|
||||
|
||||
var todaySummaries = new List<string>();
|
||||
var todayTimes = new List<string>();
|
||||
var tomorrowSummaries = new List<string>();
|
||||
|
||||
foreach (var entry in calendarEvents
|
||||
.Select(calendarEvent => (
|
||||
Summary: calendarEvent.Summary,
|
||||
TimeLabel: calendarEvent.TimeLabel ?? "all day",
|
||||
Date: calendarEvent.Date))
|
||||
.Concat(ToCalendarEntries(holidays)))
|
||||
{
|
||||
if (entry.Date == today)
|
||||
{
|
||||
todaySummaries.Add(entry.Summary);
|
||||
todayTimes.Add(entry.TimeLabel);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.Date == tomorrow)
|
||||
tomorrowSummaries.Add(entry.Summary);
|
||||
}
|
||||
|
||||
return Task.FromResult<CalendarReportSnapshot?>(
|
||||
new CalendarReportSnapshot(todaySummaries, todayTimes, tomorrowSummaries));
|
||||
}
|
||||
|
||||
private static string ResolveLoopId(TurnContext turn)
|
||||
{
|
||||
if (turn.Attributes.TryGetValue("loopId", out var loopValue) &&
|
||||
loopValue is not null &&
|
||||
!string.IsNullOrWhiteSpace(loopValue.ToString()))
|
||||
return loopValue.ToString()!.Trim();
|
||||
|
||||
return "openjibo-default-loop";
|
||||
}
|
||||
|
||||
private static IEnumerable<(string Summary, string TimeLabel, DateOnly Date)> ToCalendarEntries(
|
||||
IEnumerable<HolidayRecord> holidays)
|
||||
{
|
||||
foreach (var holiday in holidays)
|
||||
{
|
||||
yield return (
|
||||
holiday.Name,
|
||||
"all day",
|
||||
holiday.Date);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Jibo.Cloud.Application.Abstractions;
|
||||
using Jibo.Cloud.Domain.Models;
|
||||
using Jibo.Runtime.Abstractions;
|
||||
|
||||
namespace Jibo.Cloud.Infrastructure.Commute;
|
||||
|
||||
public sealed class CloudStateCommuteReportProvider(ICloudStateStore cloudStateStore) : ICommuteReportProvider
|
||||
{
|
||||
private static readonly Regex TimeLabelRegex = new(
|
||||
@"(?<hour>\d{1,2})(?::(?<minute>\d{2}))?\s*(?<period>a\.?m\.?|p\.?m\.?)",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
public Task<CommuteReportSnapshot?> GetReportAsync(
|
||||
TurnContext turn,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var loopId = ResolveLoopId(turn);
|
||||
var memberId = ResolveMemberId(turn);
|
||||
var commuteProfiles = cloudStateStore.GetCommuteProfiles(loopId);
|
||||
var commute = !string.IsNullOrWhiteSpace(memberId)
|
||||
? commuteProfiles.FirstOrDefault(profile =>
|
||||
profile.IsEnabled &&
|
||||
!string.IsNullOrWhiteSpace(profile.MemberId) &&
|
||||
string.Equals(profile.MemberId, memberId, StringComparison.OrdinalIgnoreCase))
|
||||
: null;
|
||||
|
||||
commute ??= commuteProfiles.FirstOrDefault(profile => profile.IsEnabled);
|
||||
|
||||
if (commute is null || !commute.IsComplete)
|
||||
{
|
||||
return Task.FromResult<CommuteReportSnapshot?>(
|
||||
new CommuteReportSnapshot(string.Empty, string.Empty, 0, RequiresSetup: true));
|
||||
}
|
||||
|
||||
var now = DateTimeOffset.Now;
|
||||
var workTarget = ResolveWorkTarget(now, commute);
|
||||
var earlyTarget = ResolveEarlyCalendarTarget(loopId, now, workTarget);
|
||||
var arrivalTarget = earlyTarget ?? workTarget;
|
||||
var minutesUntilWork = (int)Math.Round((arrivalTarget - now).TotalMinutes);
|
||||
var durationMinutes = commute.TypicalDurationMinutes > 0 ? commute.TypicalDurationMinutes : 25;
|
||||
var extraMinutes = Math.Max(0, durationMinutes - Math.Max(0, minutesUntilWork));
|
||||
|
||||
var summary = commute.Mode.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"walking" => "your walk to work",
|
||||
"transit" => "your trip to work by public transportation",
|
||||
"bicycling" => "your bike ride to work",
|
||||
_ => "your drive to work"
|
||||
};
|
||||
|
||||
return Task.FromResult<CommuteReportSnapshot?>(
|
||||
new CommuteReportSnapshot(
|
||||
string.IsNullOrWhiteSpace(commute.DestinationName) ? "work" : commute.DestinationName.Trim(),
|
||||
summary,
|
||||
durationMinutes,
|
||||
commute.Mode,
|
||||
earlyTarget is not null,
|
||||
minutesUntilWork,
|
||||
extraMinutes));
|
||||
}
|
||||
|
||||
private static DateTimeOffset ResolveWorkTarget(DateTimeOffset now, CommuteProfileRecord commute)
|
||||
{
|
||||
var localDate = now.Date;
|
||||
var workTime = new DateTimeOffset(
|
||||
localDate.Year,
|
||||
localDate.Month,
|
||||
localDate.Day,
|
||||
Math.Clamp(commute.WorkHour, 0, 23),
|
||||
Math.Clamp(commute.WorkMinute, 0, 59),
|
||||
0,
|
||||
now.Offset);
|
||||
|
||||
return workTime;
|
||||
}
|
||||
|
||||
private DateTimeOffset? ResolveEarlyCalendarTarget(
|
||||
string loopId,
|
||||
DateTimeOffset now,
|
||||
DateTimeOffset workTarget)
|
||||
{
|
||||
var today = DateOnly.FromDateTime(now.DateTime);
|
||||
DateTimeOffset? earliest = null;
|
||||
|
||||
foreach (var calendarEvent in cloudStateStore.GetCalendarEvents(loopId)
|
||||
.Where(static calendarEvent => calendarEvent.IsEnabled)
|
||||
.Where(calendarEvent => calendarEvent.Date == today))
|
||||
{
|
||||
if (!TryParseTimeLabel(calendarEvent.TimeLabel, now, out var eventTime)) continue;
|
||||
if (eventTime >= workTarget) continue;
|
||||
if (earliest is null || eventTime < earliest)
|
||||
earliest = eventTime;
|
||||
}
|
||||
|
||||
return earliest;
|
||||
}
|
||||
|
||||
private static bool TryParseTimeLabel(string? timeLabel, DateTimeOffset now, out DateTimeOffset parsed)
|
||||
{
|
||||
parsed = default;
|
||||
if (string.IsNullOrWhiteSpace(timeLabel)) return false;
|
||||
|
||||
var match = TimeLabelRegex.Match(timeLabel);
|
||||
if (!match.Success) return false;
|
||||
|
||||
if (!int.TryParse(match.Groups["hour"].Value, out var hour)) return false;
|
||||
var minute = match.Groups["minute"].Success && int.TryParse(match.Groups["minute"].Value, out var parsedMinute)
|
||||
? parsedMinute
|
||||
: 0;
|
||||
var period = match.Groups["period"].Value.ToLowerInvariant();
|
||||
|
||||
hour %= 12;
|
||||
if (period.StartsWith("p", StringComparison.Ordinal) && hour < 12) hour += 12;
|
||||
if (period.StartsWith("a", StringComparison.Ordinal) && hour == 12) hour = 0;
|
||||
|
||||
parsed = new DateTimeOffset(
|
||||
now.Year,
|
||||
now.Month,
|
||||
now.Day,
|
||||
hour,
|
||||
minute,
|
||||
0,
|
||||
now.Offset);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string? ResolveLoopId(TurnContext turn)
|
||||
{
|
||||
if (turn.Attributes.TryGetValue("loopId", out var loopValue) &&
|
||||
loopValue is not null &&
|
||||
!string.IsNullOrWhiteSpace(loopValue.ToString()))
|
||||
return loopValue.ToString()!.Trim();
|
||||
|
||||
return "openjibo-default-loop";
|
||||
}
|
||||
|
||||
private static string? ResolveMemberId(TurnContext turn)
|
||||
{
|
||||
if (turn.Attributes.TryGetValue("personId", out var personValue) &&
|
||||
personValue is not null &&
|
||||
!string.IsNullOrWhiteSpace(personValue.ToString()))
|
||||
return personValue.ToString()!.Trim();
|
||||
|
||||
if (turn.Attributes.TryGetValue("speakerId", out var speakerValue) &&
|
||||
speakerValue is not null &&
|
||||
!string.IsNullOrWhiteSpace(speakerValue.ToString()))
|
||||
return speakerValue.ToString()!.Trim();
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -180,11 +180,77 @@ public sealed class InMemoryJiboExperienceContentRepository : IJiboExperienceCon
|
||||
"I heard your calendar request. The cloud knows the phrase, but the real calendar integration is still ahead.",
|
||||
"Calendar is recognized. We still need to connect the actual service path."
|
||||
],
|
||||
CommuteAppSetupReplies =
|
||||
[
|
||||
"I need your commute settings before I can give you a commute report."
|
||||
],
|
||||
CommuteConfirmSpeakerReplies =
|
||||
[
|
||||
"Let me make sure I have the right speaker for your commute."
|
||||
],
|
||||
CommuteReplies =
|
||||
[
|
||||
"I heard your commute request. That one is recognized, but not fully implemented yet.",
|
||||
"Commute is on the discovery list now. The real travel answer still needs a provider."
|
||||
],
|
||||
CommuteNowReplies =
|
||||
[
|
||||
"For your commute, it should take about {duration}.",
|
||||
"If you head out now, it should take about {duration}."
|
||||
],
|
||||
CommuteMinutesLeftReplies =
|
||||
[
|
||||
"That's in about {minutes} minutes.",
|
||||
"That's about {minutes} minutes from now."
|
||||
],
|
||||
CommuteDepartTimeNormalReplies =
|
||||
[
|
||||
"If you leave at the usual time, that should work out fine."
|
||||
],
|
||||
CommuteDepartTimeNotNormalReplies =
|
||||
[
|
||||
"Your leave-time looks a little off today."
|
||||
],
|
||||
CommuteDriveNormalReplies =
|
||||
[
|
||||
"Traffic looks about normal today.",
|
||||
"Your drive today looks pretty normal."
|
||||
],
|
||||
CommuteDriveLateReplies =
|
||||
[
|
||||
"Looking at traffic, if you left now, it'd be a little late for work.",
|
||||
"For your drive, you look a little late today."
|
||||
],
|
||||
CommuteDriveHurryReplies =
|
||||
[
|
||||
"You should've left a few minutes ago!",
|
||||
"You'd better get moving."
|
||||
],
|
||||
CommuteDrivePoorReplies =
|
||||
[
|
||||
"Traffic looks a little rough today.",
|
||||
"Your drive looks pretty slow right now."
|
||||
],
|
||||
CommuteDriveTerribleReplies =
|
||||
[
|
||||
"Traffic looks terrible today.",
|
||||
"Your drive is going to be rough."
|
||||
],
|
||||
CommuteTransportNormalReplies =
|
||||
[
|
||||
"Your public transportation commute looks pretty normal.",
|
||||
"Transit looks about normal today."
|
||||
],
|
||||
CommuteTransportLateReplies =
|
||||
[
|
||||
"Your transit commute looks like it may be a little late today.",
|
||||
"You might be late if you leave now and take transit."
|
||||
],
|
||||
CommuteTransportHurryReplies =
|
||||
[
|
||||
"You should've left a few minutes ago if you want transit to work.",
|
||||
"You're running a little late for transit."
|
||||
],
|
||||
NewsReplies =
|
||||
[
|
||||
"I heard your news request. That path is still a future cloud integration.",
|
||||
|
||||
@@ -170,8 +170,47 @@ public static class LegacyMimCatalogImporter
|
||||
if (fileName.StartsWith("CalendarOutro", StringComparison.OrdinalIgnoreCase))
|
||||
return LegacyMimBucket.CalendarOutro;
|
||||
|
||||
if (fileName.StartsWith("CommuteAppSetup", StringComparison.OrdinalIgnoreCase))
|
||||
return LegacyMimBucket.CommuteAppSetup;
|
||||
|
||||
if (fileName.StartsWith("CommuteConfirmSpeaker", StringComparison.OrdinalIgnoreCase))
|
||||
return LegacyMimBucket.CommuteConfirmSpeaker;
|
||||
|
||||
if (fileName.StartsWith("CommuteNow", StringComparison.OrdinalIgnoreCase)) return LegacyMimBucket.CommuteNow;
|
||||
|
||||
if (fileName.StartsWith("CommuteMinutesLeft", StringComparison.OrdinalIgnoreCase))
|
||||
return LegacyMimBucket.CommuteMinutesLeft;
|
||||
|
||||
if (fileName.StartsWith("CommuteDepartTimeNormal", StringComparison.OrdinalIgnoreCase))
|
||||
return LegacyMimBucket.CommuteDepartTimeNormal;
|
||||
|
||||
if (fileName.StartsWith("CommuteDepartTimeNotNormal", StringComparison.OrdinalIgnoreCase))
|
||||
return LegacyMimBucket.CommuteDepartTimeNotNormal;
|
||||
|
||||
if (fileName.StartsWith("CommuteDriveNormal", StringComparison.OrdinalIgnoreCase))
|
||||
return LegacyMimBucket.CommuteDriveNormal;
|
||||
|
||||
if (fileName.StartsWith("CommuteDriveLate", StringComparison.OrdinalIgnoreCase))
|
||||
return LegacyMimBucket.CommuteDriveLate;
|
||||
|
||||
if (fileName.StartsWith("CommuteDriveHurry", StringComparison.OrdinalIgnoreCase))
|
||||
return LegacyMimBucket.CommuteDriveHurry;
|
||||
|
||||
if (fileName.StartsWith("CommuteDrivePoor", StringComparison.OrdinalIgnoreCase))
|
||||
return LegacyMimBucket.CommuteDrivePoor;
|
||||
|
||||
if (fileName.StartsWith("CommuteDriveTerrible", StringComparison.OrdinalIgnoreCase))
|
||||
return LegacyMimBucket.CommuteDriveTerrible;
|
||||
|
||||
if (fileName.StartsWith("CommuteTransportNormal", StringComparison.OrdinalIgnoreCase))
|
||||
return LegacyMimBucket.CommuteTransportNormal;
|
||||
|
||||
if (fileName.StartsWith("CommuteTransportLate", StringComparison.OrdinalIgnoreCase))
|
||||
return LegacyMimBucket.CommuteTransportLate;
|
||||
|
||||
if (fileName.StartsWith("CommuteTransportHurry", StringComparison.OrdinalIgnoreCase))
|
||||
return LegacyMimBucket.CommuteTransportHurry;
|
||||
|
||||
if (fileName.StartsWith("CommuteServiceDown", StringComparison.OrdinalIgnoreCase))
|
||||
return LegacyMimBucket.CommuteServiceDown;
|
||||
|
||||
@@ -295,7 +334,26 @@ public static class LegacyMimCatalogImporter
|
||||
CalendarServiceDownReplies = Merge(baseCatalog.CalendarServiceDownReplies,
|
||||
importedCatalog.CalendarServiceDownReplies),
|
||||
CalendarOutroReplies = Merge(baseCatalog.CalendarOutroReplies, importedCatalog.CalendarOutroReplies),
|
||||
CommuteAppSetupReplies = Merge(baseCatalog.CommuteAppSetupReplies, importedCatalog.CommuteAppSetupReplies),
|
||||
CommuteConfirmSpeakerReplies = Merge(baseCatalog.CommuteConfirmSpeakerReplies,
|
||||
importedCatalog.CommuteConfirmSpeakerReplies),
|
||||
CommuteNowReplies = Merge(baseCatalog.CommuteNowReplies, importedCatalog.CommuteNowReplies),
|
||||
CommuteMinutesLeftReplies = Merge(baseCatalog.CommuteMinutesLeftReplies, importedCatalog.CommuteMinutesLeftReplies),
|
||||
CommuteDepartTimeNormalReplies = Merge(baseCatalog.CommuteDepartTimeNormalReplies,
|
||||
importedCatalog.CommuteDepartTimeNormalReplies),
|
||||
CommuteDepartTimeNotNormalReplies = Merge(baseCatalog.CommuteDepartTimeNotNormalReplies,
|
||||
importedCatalog.CommuteDepartTimeNotNormalReplies),
|
||||
CommuteDriveNormalReplies = Merge(baseCatalog.CommuteDriveNormalReplies, importedCatalog.CommuteDriveNormalReplies),
|
||||
CommuteDriveLateReplies = Merge(baseCatalog.CommuteDriveLateReplies, importedCatalog.CommuteDriveLateReplies),
|
||||
CommuteDriveHurryReplies = Merge(baseCatalog.CommuteDriveHurryReplies, importedCatalog.CommuteDriveHurryReplies),
|
||||
CommuteDrivePoorReplies = Merge(baseCatalog.CommuteDrivePoorReplies, importedCatalog.CommuteDrivePoorReplies),
|
||||
CommuteDriveTerribleReplies = Merge(baseCatalog.CommuteDriveTerribleReplies, importedCatalog.CommuteDriveTerribleReplies),
|
||||
CommuteTransportNormalReplies = Merge(baseCatalog.CommuteTransportNormalReplies,
|
||||
importedCatalog.CommuteTransportNormalReplies),
|
||||
CommuteTransportLateReplies = Merge(baseCatalog.CommuteTransportLateReplies,
|
||||
importedCatalog.CommuteTransportLateReplies),
|
||||
CommuteTransportHurryReplies = Merge(baseCatalog.CommuteTransportHurryReplies,
|
||||
importedCatalog.CommuteTransportHurryReplies),
|
||||
CommuteServiceDownReplies = Merge(baseCatalog.CommuteServiceDownReplies,
|
||||
importedCatalog.CommuteServiceDownReplies),
|
||||
NewsIntroReplies = Merge(baseCatalog.NewsIntroReplies, importedCatalog.NewsIntroReplies),
|
||||
@@ -449,6 +507,19 @@ public static class LegacyMimCatalogImporter
|
||||
CalendarServiceDown,
|
||||
CalendarOutro,
|
||||
CommuteNow,
|
||||
CommuteMinutesLeft,
|
||||
CommuteDepartTimeNormal,
|
||||
CommuteDepartTimeNotNormal,
|
||||
CommuteAppSetup,
|
||||
CommuteConfirmSpeaker,
|
||||
CommuteDriveNormal,
|
||||
CommuteDriveLate,
|
||||
CommuteDriveHurry,
|
||||
CommuteDrivePoor,
|
||||
CommuteDriveTerrible,
|
||||
CommuteTransportNormal,
|
||||
CommuteTransportLate,
|
||||
CommuteTransportHurry,
|
||||
CommuteServiceDown,
|
||||
NewsIntro,
|
||||
NewsCategoryIntro,
|
||||
@@ -462,7 +533,20 @@ public static class LegacyMimCatalogImporter
|
||||
private readonly List<string> _calendarNothingTodayReplies = [];
|
||||
private readonly List<string> _calendarOutroReplies = [];
|
||||
private readonly List<string> _calendarServiceDownReplies = [];
|
||||
private readonly List<string> _commuteAppSetupReplies = [];
|
||||
private readonly List<string> _commuteConfirmSpeakerReplies = [];
|
||||
private readonly List<string> _commuteDepartTimeNormalReplies = [];
|
||||
private readonly List<string> _commuteDepartTimeNotNormalReplies = [];
|
||||
private readonly List<string> _commuteNowReplies = [];
|
||||
private readonly List<string> _commuteMinutesLeftReplies = [];
|
||||
private readonly List<string> _commuteDriveNormalReplies = [];
|
||||
private readonly List<string> _commuteDriveLateReplies = [];
|
||||
private readonly List<string> _commuteDriveHurryReplies = [];
|
||||
private readonly List<string> _commuteDrivePoorReplies = [];
|
||||
private readonly List<string> _commuteDriveTerribleReplies = [];
|
||||
private readonly List<string> _commuteTransportNormalReplies = [];
|
||||
private readonly List<string> _commuteTransportLateReplies = [];
|
||||
private readonly List<string> _commuteTransportHurryReplies = [];
|
||||
private readonly List<string> _commuteServiceDownReplies = [];
|
||||
private readonly List<string> _birthdayCelebrationReplies = [];
|
||||
private readonly List<JiboConditionedReply> _emotionReplies = [];
|
||||
@@ -615,9 +699,48 @@ public static class LegacyMimCatalogImporter
|
||||
case LegacyMimBucket.CalendarOutro:
|
||||
AddDistinct(_calendarOutroReplies, text);
|
||||
return;
|
||||
case LegacyMimBucket.CommuteAppSetup:
|
||||
AddDistinct(_commuteAppSetupReplies, text);
|
||||
return;
|
||||
case LegacyMimBucket.CommuteConfirmSpeaker:
|
||||
AddDistinct(_commuteConfirmSpeakerReplies, text);
|
||||
return;
|
||||
case LegacyMimBucket.CommuteNow:
|
||||
AddDistinct(_commuteNowReplies, text);
|
||||
return;
|
||||
case LegacyMimBucket.CommuteMinutesLeft:
|
||||
AddDistinct(_commuteMinutesLeftReplies, text);
|
||||
return;
|
||||
case LegacyMimBucket.CommuteDepartTimeNormal:
|
||||
AddDistinct(_commuteDepartTimeNormalReplies, text);
|
||||
return;
|
||||
case LegacyMimBucket.CommuteDepartTimeNotNormal:
|
||||
AddDistinct(_commuteDepartTimeNotNormalReplies, text);
|
||||
return;
|
||||
case LegacyMimBucket.CommuteDriveNormal:
|
||||
AddDistinct(_commuteDriveNormalReplies, text);
|
||||
return;
|
||||
case LegacyMimBucket.CommuteDriveLate:
|
||||
AddDistinct(_commuteDriveLateReplies, text);
|
||||
return;
|
||||
case LegacyMimBucket.CommuteDriveHurry:
|
||||
AddDistinct(_commuteDriveHurryReplies, text);
|
||||
return;
|
||||
case LegacyMimBucket.CommuteDrivePoor:
|
||||
AddDistinct(_commuteDrivePoorReplies, text);
|
||||
return;
|
||||
case LegacyMimBucket.CommuteDriveTerrible:
|
||||
AddDistinct(_commuteDriveTerribleReplies, text);
|
||||
return;
|
||||
case LegacyMimBucket.CommuteTransportNormal:
|
||||
AddDistinct(_commuteTransportNormalReplies, text);
|
||||
return;
|
||||
case LegacyMimBucket.CommuteTransportLate:
|
||||
AddDistinct(_commuteTransportLateReplies, text);
|
||||
return;
|
||||
case LegacyMimBucket.CommuteTransportHurry:
|
||||
AddDistinct(_commuteTransportHurryReplies, text);
|
||||
return;
|
||||
case LegacyMimBucket.CommuteServiceDown:
|
||||
AddDistinct(_commuteServiceDownReplies, text);
|
||||
return;
|
||||
@@ -670,7 +793,20 @@ public static class LegacyMimCatalogImporter
|
||||
CalendarNothingReplies = [.. _calendarNothingReplies],
|
||||
CalendarServiceDownReplies = [.. _calendarServiceDownReplies],
|
||||
CalendarOutroReplies = [.. _calendarOutroReplies],
|
||||
CommuteAppSetupReplies = [.. _commuteAppSetupReplies],
|
||||
CommuteConfirmSpeakerReplies = [.. _commuteConfirmSpeakerReplies],
|
||||
CommuteNowReplies = [.. _commuteNowReplies],
|
||||
CommuteMinutesLeftReplies = [.. _commuteMinutesLeftReplies],
|
||||
CommuteDepartTimeNormalReplies = [.. _commuteDepartTimeNormalReplies],
|
||||
CommuteDepartTimeNotNormalReplies = [.. _commuteDepartTimeNotNormalReplies],
|
||||
CommuteDriveNormalReplies = [.. _commuteDriveNormalReplies],
|
||||
CommuteDriveLateReplies = [.. _commuteDriveLateReplies],
|
||||
CommuteDriveHurryReplies = [.. _commuteDriveHurryReplies],
|
||||
CommuteDrivePoorReplies = [.. _commuteDrivePoorReplies],
|
||||
CommuteDriveTerribleReplies = [.. _commuteDriveTerribleReplies],
|
||||
CommuteTransportNormalReplies = [.. _commuteTransportNormalReplies],
|
||||
CommuteTransportLateReplies = [.. _commuteTransportLateReplies],
|
||||
CommuteTransportHurryReplies = [.. _commuteTransportHurryReplies],
|
||||
CommuteServiceDownReplies = [.. _commuteServiceDownReplies],
|
||||
NewsIntroReplies = [.. _newsIntroReplies],
|
||||
NewsCategoryIntroReplies = [.. _newsCategoryIntroReplies],
|
||||
|
||||
@@ -54,8 +54,10 @@ public static class ServiceCollectionExtensions
|
||||
services.AddHttpClient<INewsBriefingProvider, NewsApiBriefingProvider>();
|
||||
services.AddSingleton<IHolidayCalendarProvider>(provider =>
|
||||
new NagerDateHolidayCalendarProvider(provider.GetRequiredService<HolidayCalendarOptions>()));
|
||||
services.AddSingleton<ICalendarReportProvider, UnavailableCalendarReportProvider>();
|
||||
services.AddSingleton<ICommuteReportProvider, UnavailableCommuteReportProvider>();
|
||||
services.AddSingleton<ICalendarReportProvider>(provider =>
|
||||
new CloudStateCalendarReportProvider(provider.GetRequiredService<ICloudStateStore>()));
|
||||
services.AddSingleton<ICommuteReportProvider>(provider =>
|
||||
new CloudStateCommuteReportProvider(provider.GetRequiredService<ICloudStateStore>()));
|
||||
var statePersistencePath = configuration?["OpenJibo:State:PersistencePath"]
|
||||
?? Path.Combine(AppContext.BaseDirectory, "App_Data", "cloud-state.json");
|
||||
var personalMemoryPersistencePath = configuration?["OpenJibo:PersonalMemory:PersistencePath"]
|
||||
|
||||
@@ -18,6 +18,8 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
||||
};
|
||||
|
||||
private readonly List<BackupRecord> _backups = [];
|
||||
private readonly List<CommuteProfileRecord> _commuteProfiles = [];
|
||||
private readonly List<CalendarEventRecord> _calendarEvents = [];
|
||||
private readonly ConcurrentDictionary<string, DeviceRegistration> _devices = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private readonly ConcurrentDictionary<string, KeyRequestRecord>
|
||||
@@ -161,6 +163,12 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
||||
_backups.Clear();
|
||||
_backups.AddRange(snapshot.Backups ?? []);
|
||||
|
||||
_commuteProfiles.Clear();
|
||||
_commuteProfiles.AddRange(snapshot.CommuteProfiles ?? []);
|
||||
|
||||
_calendarEvents.Clear();
|
||||
_calendarEvents.AddRange(snapshot.CalendarEvents ?? []);
|
||||
|
||||
_loops.Clear();
|
||||
_loops.AddRange(snapshot.Loops ?? []);
|
||||
|
||||
@@ -214,6 +222,8 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
||||
Updates = _updates.ToArray(),
|
||||
Media = _media.ToArray(),
|
||||
Backups = _backups.ToArray(),
|
||||
CommuteProfiles = _commuteProfiles.ToArray(),
|
||||
CalendarEvents = _calendarEvents.ToArray(),
|
||||
Loops = _loops.ToArray(),
|
||||
Holidays = _holidayOverrides.ToArray(),
|
||||
People = _people.ToArray()
|
||||
@@ -479,6 +489,55 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
||||
return backup;
|
||||
}
|
||||
|
||||
public IReadOnlyList<CalendarEventRecord> GetCalendarEvents(string? loopId = null)
|
||||
{
|
||||
var resolvedLoopId = string.IsNullOrWhiteSpace(loopId) ? ResolveDefaultLoopId() : loopId.Trim();
|
||||
return _calendarEvents
|
||||
.Where(calendarEvent => calendarEvent.IsEnabled)
|
||||
.Where(calendarEvent =>
|
||||
string.Equals(calendarEvent.LoopId, resolvedLoopId, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderBy(calendarEvent => calendarEvent.Date)
|
||||
.ThenBy(calendarEvent => calendarEvent.TimeLabel ?? string.Empty, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(calendarEvent => calendarEvent.Summary, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public CalendarEventRecord UpsertCalendarEvent(CalendarEventRecord calendarEvent)
|
||||
{
|
||||
var resolvedLoopId = string.IsNullOrWhiteSpace(calendarEvent.LoopId)
|
||||
? ResolveDefaultLoopId()
|
||||
: calendarEvent.LoopId.Trim();
|
||||
var normalizedId = string.IsNullOrWhiteSpace(calendarEvent.Id)
|
||||
? $"calendar-{resolvedLoopId}-{Slugify(calendarEvent.Summary)}"
|
||||
: calendarEvent.Id.Trim();
|
||||
|
||||
var resolvedCalendarEvent = new CalendarEventRecord
|
||||
{
|
||||
Id = normalizedId,
|
||||
LoopId = resolvedLoopId,
|
||||
Summary = string.IsNullOrWhiteSpace(calendarEvent.Summary) ? "Calendar event" : calendarEvent.Summary.Trim(),
|
||||
TimeLabel = string.IsNullOrWhiteSpace(calendarEvent.TimeLabel) ? null : calendarEvent.TimeLabel.Trim(),
|
||||
Date = calendarEvent.Date,
|
||||
EndDate = calendarEvent.EndDate,
|
||||
IsAllDay = calendarEvent.IsAllDay,
|
||||
IsEnabled = calendarEvent.IsEnabled,
|
||||
Source = string.IsNullOrWhiteSpace(calendarEvent.Source) ? "manual" : calendarEvent.Source.Trim(),
|
||||
MemberId = calendarEvent.MemberId,
|
||||
Created = calendarEvent.Created
|
||||
};
|
||||
|
||||
var existingIndex = _calendarEvents.FindIndex(existing =>
|
||||
string.Equals(existing.Id, normalizedId, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (existingIndex >= 0)
|
||||
_calendarEvents[existingIndex] = resolvedCalendarEvent;
|
||||
else
|
||||
_calendarEvents.Add(resolvedCalendarEvent);
|
||||
|
||||
TouchState();
|
||||
return resolvedCalendarEvent;
|
||||
}
|
||||
|
||||
public bool ShouldCreateSymmetricKey(string loopId)
|
||||
{
|
||||
return !_symmetricKeys.ContainsKey(loopId);
|
||||
@@ -580,6 +639,59 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public IReadOnlyList<CommuteProfileRecord> GetCommuteProfiles(string? loopId = null)
|
||||
{
|
||||
var resolvedLoopId = string.IsNullOrWhiteSpace(loopId) ? ResolveDefaultLoopId() : loopId.Trim();
|
||||
return _commuteProfiles
|
||||
.Where(commute => commute.IsEnabled)
|
||||
.Where(commute => string.Equals(commute.LoopId, resolvedLoopId, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderByDescending(static commute => commute.IsComplete)
|
||||
.ThenByDescending(static commute => commute.Updated)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public CommuteProfileRecord UpsertCommuteProfile(CommuteProfileRecord commuteProfile)
|
||||
{
|
||||
var resolvedLoopId = string.IsNullOrWhiteSpace(commuteProfile.LoopId)
|
||||
? ResolveDefaultLoopId()
|
||||
: commuteProfile.LoopId.Trim();
|
||||
var normalizedId = string.IsNullOrWhiteSpace(commuteProfile.Id)
|
||||
? $"commute-{resolvedLoopId}"
|
||||
: commuteProfile.Id.Trim();
|
||||
|
||||
var resolvedProfile = new CommuteProfileRecord
|
||||
{
|
||||
Id = normalizedId,
|
||||
LoopId = resolvedLoopId,
|
||||
MemberId = string.IsNullOrWhiteSpace(commuteProfile.MemberId) ? null : commuteProfile.MemberId.Trim(),
|
||||
IsEnabled = commuteProfile.IsEnabled,
|
||||
IsComplete = commuteProfile.IsComplete,
|
||||
Mode = string.IsNullOrWhiteSpace(commuteProfile.Mode) ? "driving" : commuteProfile.Mode.Trim(),
|
||||
WorkHour = commuteProfile.WorkHour,
|
||||
WorkMinute = commuteProfile.WorkMinute,
|
||||
OriginName = string.IsNullOrWhiteSpace(commuteProfile.OriginName) ? null : commuteProfile.OriginName.Trim(),
|
||||
DestinationName = string.IsNullOrWhiteSpace(commuteProfile.DestinationName)
|
||||
? null
|
||||
: commuteProfile.DestinationName.Trim(),
|
||||
TypicalDurationMinutes = commuteProfile.TypicalDurationMinutes > 0
|
||||
? commuteProfile.TypicalDurationMinutes
|
||||
: 25,
|
||||
Created = commuteProfile.Created == default ? DateTimeOffset.UtcNow : commuteProfile.Created,
|
||||
Updated = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
var existingIndex = _commuteProfiles.FindIndex(existing =>
|
||||
string.Equals(existing.Id, normalizedId, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (existingIndex >= 0)
|
||||
_commuteProfiles[existingIndex] = resolvedProfile;
|
||||
else
|
||||
_commuteProfiles.Add(resolvedProfile);
|
||||
|
||||
TouchState();
|
||||
return resolvedProfile;
|
||||
}
|
||||
|
||||
public HolidayRecord UpsertHoliday(HolidayRecord holiday)
|
||||
{
|
||||
var resolvedLoopId = string.IsNullOrWhiteSpace(holiday.LoopId) ? ResolveDefaultLoopId() : holiday.LoopId.Trim();
|
||||
@@ -656,6 +768,7 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
||||
|
||||
if (_people.Count != 0)
|
||||
{
|
||||
EnsureDefaultCommuteProfile();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -680,6 +793,29 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
||||
Alias = "Household Member",
|
||||
IsPrimary = false
|
||||
});
|
||||
|
||||
EnsureDefaultCommuteProfile();
|
||||
}
|
||||
|
||||
private void EnsureDefaultCommuteProfile()
|
||||
{
|
||||
if (_commuteProfiles.Any(commute =>
|
||||
string.Equals(commute.LoopId, ResolveDefaultLoopId(), StringComparison.OrdinalIgnoreCase)))
|
||||
return;
|
||||
|
||||
_commuteProfiles.Add(new CommuteProfileRecord
|
||||
{
|
||||
Id = $"commute-{ResolveDefaultLoopId()}",
|
||||
LoopId = ResolveDefaultLoopId(),
|
||||
IsEnabled = true,
|
||||
IsComplete = true,
|
||||
Mode = "driving",
|
||||
WorkHour = 8,
|
||||
WorkMinute = 30,
|
||||
OriginName = "home",
|
||||
DestinationName = "work",
|
||||
TypicalDurationMinutes = 25
|
||||
});
|
||||
}
|
||||
|
||||
private static string Slugify(string value)
|
||||
@@ -767,6 +903,8 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
||||
public UpdateManifest[]? Updates { get; init; }
|
||||
public MediaRecord[]? Media { get; init; }
|
||||
public BackupRecord[]? Backups { get; init; }
|
||||
public CommuteProfileRecord[]? CommuteProfiles { get; init; }
|
||||
public CalendarEventRecord[]? CalendarEvents { get; init; }
|
||||
public LoopRecord[]? Loops { get; init; }
|
||||
public HolidayRecord[]? Holidays { get; init; }
|
||||
public PersonRecord[]? People { get; init; }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Jibo.Cloud.Application.Abstractions;
|
||||
using Jibo.Cloud.Domain.Models;
|
||||
using Jibo.Cloud.Infrastructure.Persistence;
|
||||
|
||||
namespace Jibo.Cloud.Tests.Infrastructure;
|
||||
@@ -103,6 +104,21 @@ public sealed class PersistenceStoreTests
|
||||
var update = firstStore.CreateUpdate("1.0.0", "1.0.1", "Bug fix", null, 42, "robot", null, null);
|
||||
var media = firstStore.CreateMedia("openjibo-default-loop", "persisted-photo", "image", "photo-ref", false,
|
||||
new Dictionary<string, object?> { ["note"] = "roundtrip" });
|
||||
var commute = firstStore.UpsertCommuteProfile(new CommuteProfileRecord
|
||||
{
|
||||
LoopId = "openjibo-default-loop",
|
||||
Mode = "driving",
|
||||
WorkHour = 8,
|
||||
WorkMinute = 30,
|
||||
TypicalDurationMinutes = 25
|
||||
});
|
||||
var calendarEvent = firstStore.UpsertCalendarEvent(new CalendarEventRecord
|
||||
{
|
||||
LoopId = "openjibo-default-loop",
|
||||
Summary = "Report review",
|
||||
TimeLabel = "at 6:00 p.m.",
|
||||
Date = DateOnly.FromDateTime(DateTime.UtcNow)
|
||||
});
|
||||
var sessionToken = firstStore.IssueRobotToken("robot-123");
|
||||
var device = firstStore.GetOrCreateDevice("robot-123", "3.2.1", "4.5.6");
|
||||
firstStore.SavePersistedState();
|
||||
@@ -117,6 +133,10 @@ public sealed class PersistenceStoreTests
|
||||
Assert.Equal(firstInfo.Revision, secondInfo.Revision);
|
||||
Assert.Contains(secondStore.ListUpdates("robot"), item => item.UpdateId == update.UpdateId);
|
||||
Assert.Contains(secondStore.ListMedia(), item => item.Path == media.Path);
|
||||
Assert.Contains(secondStore.GetCommuteProfiles("openjibo-default-loop"),
|
||||
item => item.Id == commute.Id && item.Mode == commute.Mode);
|
||||
Assert.Contains(secondStore.GetCalendarEvents("openjibo-default-loop"),
|
||||
item => item.Id == calendarEvent.Id && item.Summary == calendarEvent.Summary);
|
||||
Assert.NotNull(secondStore.FindSessionByToken(sessionToken));
|
||||
Assert.Equal("3.2.1", secondStore.GetOrCreateDevice(device.DeviceId, null, null).FirmwareVersion);
|
||||
Assert.NotEmpty(secondStore.GetPeople());
|
||||
|
||||
@@ -167,6 +167,43 @@ public sealed class JiboCloudProtocolServiceTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PersonUpsertCommute_ThenListCommute_ReturnsPersistedLoopProfile()
|
||||
{
|
||||
var service = new JiboCloudProtocolService(new InMemoryCloudStateStore());
|
||||
|
||||
var upsert = await service.DispatchAsync(new ProtocolEnvelope
|
||||
{
|
||||
HostName = "api.jibo.com",
|
||||
Method = "POST",
|
||||
ServicePrefix = "Person_20160715",
|
||||
Operation = "UpsertCommute",
|
||||
BodyText =
|
||||
"""{"loopId":"loop-123","mode":"walking","workHour":8,"workMinute":15,"typicalDurationMinutes":22}"""
|
||||
});
|
||||
|
||||
using var upsertPayload = JsonDocument.Parse(upsert.BodyText);
|
||||
Assert.Equal(200, upsert.StatusCode);
|
||||
Assert.Equal("loop-123", upsertPayload.RootElement.GetProperty("loopId").GetString());
|
||||
Assert.Equal("walking", upsertPayload.RootElement.GetProperty("mode").GetString());
|
||||
|
||||
var listed = await service.DispatchAsync(new ProtocolEnvelope
|
||||
{
|
||||
HostName = "api.jibo.com",
|
||||
Method = "POST",
|
||||
ServicePrefix = "Person_20160715",
|
||||
Operation = "ListCommute",
|
||||
BodyText = """{"loopId":"loop-123"}"""
|
||||
});
|
||||
|
||||
using var listedPayload = JsonDocument.Parse(listed.BodyText);
|
||||
Assert.Equal(200, listed.StatusCode);
|
||||
Assert.Contains(listedPayload.RootElement.EnumerateArray(),
|
||||
item => item.GetProperty("loopId").GetString() == "loop-123" &&
|
||||
item.GetProperty("mode").GetString() == "walking" &&
|
||||
item.GetProperty("workHour").GetInt32() == 8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MediaCreateAndGet_ReturnsCreatedItem()
|
||||
{
|
||||
|
||||
@@ -2,6 +2,9 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using Jibo.Cloud.Application.Abstractions;
|
||||
using Jibo.Cloud.Application.Services;
|
||||
using Jibo.Cloud.Domain.Models;
|
||||
using Jibo.Cloud.Infrastructure.Calendar;
|
||||
using Jibo.Cloud.Infrastructure.Commute;
|
||||
using Jibo.Cloud.Infrastructure.Content;
|
||||
using Jibo.Cloud.Infrastructure.Persistence;
|
||||
using Jibo.Runtime.Abstractions;
|
||||
@@ -1891,10 +1894,15 @@ public sealed class JiboInteractionServiceTests
|
||||
{
|
||||
Snapshot = new WeatherReportSnapshot("Boston, U.S.", "light rain", 61, 65, 54, "rain", false)
|
||||
};
|
||||
var calendarProvider = new CapturingCalendarReportProvider
|
||||
var cloudStateStore = new InMemoryCloudStateStore();
|
||||
cloudStateStore.UpsertCalendarEvent(new CalendarEventRecord
|
||||
{
|
||||
Snapshot = new CalendarReportSnapshot(["get personal report from jibo"], ["at 6:00 p.m."], [])
|
||||
};
|
||||
LoopId = "openjibo-default-loop",
|
||||
Summary = "get personal report from jibo",
|
||||
TimeLabel = "at 6:00 p.m.",
|
||||
Date = DateOnly.FromDateTime(DateTime.UtcNow)
|
||||
});
|
||||
var calendarProvider = new CloudStateCalendarReportProvider(cloudStateStore);
|
||||
var service = CreateService(weatherReportProvider: weatherProvider, calendarReportProvider: calendarProvider);
|
||||
|
||||
var decision = await service.BuildDecisionAsync(new TurnContext
|
||||
@@ -1913,6 +1921,54 @@ public sealed class JiboInteractionServiceTests
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildDecisionAsync_PersonalReport_UsesCommuteProviderAndNormalTraffic()
|
||||
{
|
||||
var weatherProvider = new CapturingWeatherReportProvider
|
||||
{
|
||||
Snapshot = new WeatherReportSnapshot("Boston, U.S.", "light rain", 61, 65, 54, "rain", false)
|
||||
};
|
||||
var calendarStore = new InMemoryCloudStateStore();
|
||||
calendarStore.UpsertCalendarEvent(new CalendarEventRecord
|
||||
{
|
||||
LoopId = "openjibo-default-loop",
|
||||
Summary = "get personal report from jibo",
|
||||
TimeLabel = "at 6:00 p.m.",
|
||||
Date = DateOnly.FromDateTime(DateTime.UtcNow)
|
||||
});
|
||||
var calendarProvider = new CloudStateCalendarReportProvider(calendarStore);
|
||||
var cloudStateStore = new InMemoryCloudStateStore();
|
||||
var commuteProvider = new CloudStateCommuteReportProvider(cloudStateStore);
|
||||
var commuteTime = DateTimeOffset.Now.AddMinutes(45);
|
||||
cloudStateStore.UpsertCommuteProfile(new CommuteProfileRecord
|
||||
{
|
||||
LoopId = "openjibo-default-loop",
|
||||
Mode = "driving",
|
||||
WorkHour = commuteTime.Hour,
|
||||
WorkMinute = commuteTime.Minute,
|
||||
TypicalDurationMinutes = 25
|
||||
});
|
||||
|
||||
var service = CreateService(
|
||||
weatherReportProvider: weatherProvider,
|
||||
calendarReportProvider: calendarProvider,
|
||||
commuteReportProvider: commuteProvider);
|
||||
|
||||
var decision = await service.BuildDecisionAsync(new TurnContext
|
||||
{
|
||||
RawTranscript = "yes",
|
||||
NormalizedTranscript = "yes",
|
||||
Attributes = new Dictionary<string, object?>
|
||||
{
|
||||
[PersonalReportStateKey] = "awaiting_identity_confirmation",
|
||||
[PersonalReportUserNameKey] = "alex"
|
||||
}
|
||||
});
|
||||
|
||||
Assert.Equal("personal_report_delivered", decision.IntentName);
|
||||
Assert.Contains("commute", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildDecisionAsync_PersonalReport_NoMatchRetriesThenDeclines()
|
||||
{
|
||||
@@ -4192,15 +4248,4 @@ public sealed class JiboInteractionServiceTests
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CapturingCalendarReportProvider : ICalendarReportProvider
|
||||
{
|
||||
public CalendarReportSnapshot? Snapshot { get; init; }
|
||||
|
||||
public Task<CalendarReportSnapshot?> GetReportAsync(
|
||||
TurnContext turn,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(Snapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user