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`
|
- 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 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
|
- `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 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 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
|
- `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
|
- weather icon/animation parity and view support
|
||||||
- broader non-local weather query handling and short-range date coverage
|
- broader non-local weather query handling and short-range date coverage
|
||||||
- provider-backed news ingestion and filtering
|
- 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
|
- coverage matrix for personal report parity gaps and test/capture exit criteria
|
||||||
- Progress update (`2026-05-10`):
|
- Progress update (`2026-05-10`):
|
||||||
- added provider-ready news briefing lane with Nimbus-compatible `news` skill payload continuity
|
- 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
|
- 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
|
- 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
|
- 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:
|
- Source anchors:
|
||||||
- `C:\Projects\jibo\pegasus\packages\report-skill\src\subskills\weather\WeatherMimLogic.ts`
|
- `C:\Projects\jibo\pegasus\packages\report-skill\src\subskills\weather\WeatherMimLogic.ts`
|
||||||
- `C:\Projects\jibo\pegasus\packages\report-skill\resources\views\weatherHiLo.json`
|
- `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
|
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)
|
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)
|
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
|
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
|
- 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
|
- 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)
|
- [persistence-architecture.md](persistence-architecture.md)
|
||||||
- [holiday-architecture.md](holiday-architecture.md)
|
- [holiday-architecture.md](holiday-architecture.md)
|
||||||
|
- [commute-architecture.md](commute-architecture.md)
|
||||||
|
|
||||||
## First Implemented Slice In `1.0.19`
|
## 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.
|
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:
|
Reference:
|
||||||
|
|
||||||
- [personal-report-parity-plan.md](personal-report-parity-plan.md)
|
- [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:
|
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:
|
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 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 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 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
|
## 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
|
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.
|
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
|
## Recovery Strategy
|
||||||
|
|
||||||
The first supported device path is:
|
The first supported device path is:
|
||||||
|
|||||||
@@ -44,5 +44,9 @@ public interface ICloudStateStore
|
|||||||
IReadOnlyList<KeyRequestRecord> GetBinaryRequests();
|
IReadOnlyList<KeyRequestRecord> GetBinaryRequests();
|
||||||
IReadOnlyList<HolidayRecord> GetHolidays(string? loopId = null);
|
IReadOnlyList<HolidayRecord> GetHolidays(string? loopId = null);
|
||||||
HolidayRecord UpsertHoliday(HolidayRecord holiday);
|
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);
|
void UpdateRobot(DeviceRegistration registration);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,4 +12,7 @@ public sealed record CommuteReportSnapshot(
|
|||||||
string Summary,
|
string Summary,
|
||||||
int DurationMinutes,
|
int DurationMinutes,
|
||||||
string? Mode = null,
|
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> CalendarNothingReplies { get; init; } = [];
|
||||||
public IReadOnlyList<string> CalendarServiceDownReplies { get; init; } = [];
|
public IReadOnlyList<string> CalendarServiceDownReplies { get; init; } = [];
|
||||||
public IReadOnlyList<string> CalendarOutroReplies { 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> 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> CommuteServiceDownReplies { get; init; } = [];
|
||||||
public IReadOnlyList<string> NewsIntroReplies { get; init; } = [];
|
public IReadOnlyList<string> NewsIntroReplies { get; init; } = [];
|
||||||
public IReadOnlyList<string> NewsCategoryIntroReplies { 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)
|
private ProtocolDispatchResult HandlePerson(string operation, ProtocolEnvelope envelope)
|
||||||
{
|
{
|
||||||
if (!operation.Equals("ListHolidays", StringComparison.OrdinalIgnoreCase))
|
|
||||||
return ProtocolDispatchResult.Ok(Array.Empty<object>());
|
|
||||||
|
|
||||||
var body = envelope.TryParseBody();
|
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)
|
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)
|
private static object MapMedia(MediaRecord item)
|
||||||
{
|
{
|
||||||
return new
|
return new
|
||||||
|
|||||||
@@ -1626,6 +1626,11 @@ public sealed class JiboInteractionService(
|
|||||||
"commute",
|
"commute",
|
||||||
ChooseCommuteServiceDownReply(catalog));
|
ChooseCommuteServiceDownReply(catalog));
|
||||||
|
|
||||||
|
if (snapshot.RequiresSetup)
|
||||||
|
return new JiboInteractionDecision(
|
||||||
|
"commute_setup",
|
||||||
|
ChooseCommuteAppSetupReply(catalog));
|
||||||
|
|
||||||
return new JiboInteractionDecision(
|
return new JiboInteractionDecision(
|
||||||
"commute",
|
"commute",
|
||||||
BuildCommuteSpokenReply(snapshot, catalog));
|
BuildCommuteSpokenReply(snapshot, catalog));
|
||||||
@@ -1754,47 +1759,122 @@ public sealed class JiboInteractionService(
|
|||||||
{
|
{
|
||||||
var duration = snapshot.DurationMinutes;
|
var duration = snapshot.DurationMinutes;
|
||||||
var durationText = duration <= 1 ? "1 minute" : $"{duration} minutes";
|
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 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,
|
if (minutesLeft > 0 && minutesLeft < 30)
|
||||||
"For your commute, it should take about ${skill.commute.durationMins} minutes.");
|
{
|
||||||
|
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
|
return template
|
||||||
.Replace("${skill.commute.durationMins}", durationText, StringComparison.OrdinalIgnoreCase)
|
.Replace("${skill.commute.durationMins}", durationText, StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace("${skill.commute.minsLeft}", minutesLeftText, StringComparison.OrdinalIgnoreCase)
|
||||||
.Replace("${speaker}", string.Empty, StringComparison.OrdinalIgnoreCase)
|
.Replace("${speaker}", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||||
.Replace(" ", " ", StringComparison.Ordinal)
|
.Replace(" ", " ", StringComparison.Ordinal)
|
||||||
.Trim();
|
.Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ChooseCommuteTemplate(
|
private static string? ChooseShortestTemplate(IEnumerable<string> templates)
|
||||||
IReadOnlyList<string> templates,
|
|
||||||
string mode,
|
|
||||||
string fallback)
|
|
||||||
{
|
{
|
||||||
if (templates.Count == 0) return fallback;
|
return templates
|
||||||
|
.Where(static template => !string.IsNullOrWhiteSpace(template))
|
||||||
var loweredMode = mode.Trim().ToLowerInvariant();
|
.OrderBy(static template => template.Length)
|
||||||
var filtered = templates.Where(template =>
|
.FirstOrDefault();
|
||||||
{
|
|
||||||
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(
|
||||||
|
|||||||
@@ -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.",
|
"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."
|
"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 =
|
CommuteReplies =
|
||||||
[
|
[
|
||||||
"I heard your commute request. That one is recognized, but not fully implemented yet.",
|
"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."
|
"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 =
|
NewsReplies =
|
||||||
[
|
[
|
||||||
"I heard your news request. That path is still a future cloud integration.",
|
"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))
|
if (fileName.StartsWith("CalendarOutro", StringComparison.OrdinalIgnoreCase))
|
||||||
return LegacyMimBucket.CalendarOutro;
|
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("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))
|
if (fileName.StartsWith("CommuteServiceDown", StringComparison.OrdinalIgnoreCase))
|
||||||
return LegacyMimBucket.CommuteServiceDown;
|
return LegacyMimBucket.CommuteServiceDown;
|
||||||
|
|
||||||
@@ -295,7 +334,26 @@ public static class LegacyMimCatalogImporter
|
|||||||
CalendarServiceDownReplies = Merge(baseCatalog.CalendarServiceDownReplies,
|
CalendarServiceDownReplies = Merge(baseCatalog.CalendarServiceDownReplies,
|
||||||
importedCatalog.CalendarServiceDownReplies),
|
importedCatalog.CalendarServiceDownReplies),
|
||||||
CalendarOutroReplies = Merge(baseCatalog.CalendarOutroReplies, importedCatalog.CalendarOutroReplies),
|
CalendarOutroReplies = Merge(baseCatalog.CalendarOutroReplies, importedCatalog.CalendarOutroReplies),
|
||||||
|
CommuteAppSetupReplies = Merge(baseCatalog.CommuteAppSetupReplies, importedCatalog.CommuteAppSetupReplies),
|
||||||
|
CommuteConfirmSpeakerReplies = Merge(baseCatalog.CommuteConfirmSpeakerReplies,
|
||||||
|
importedCatalog.CommuteConfirmSpeakerReplies),
|
||||||
CommuteNowReplies = Merge(baseCatalog.CommuteNowReplies, importedCatalog.CommuteNowReplies),
|
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,
|
CommuteServiceDownReplies = Merge(baseCatalog.CommuteServiceDownReplies,
|
||||||
importedCatalog.CommuteServiceDownReplies),
|
importedCatalog.CommuteServiceDownReplies),
|
||||||
NewsIntroReplies = Merge(baseCatalog.NewsIntroReplies, importedCatalog.NewsIntroReplies),
|
NewsIntroReplies = Merge(baseCatalog.NewsIntroReplies, importedCatalog.NewsIntroReplies),
|
||||||
@@ -449,6 +507,19 @@ public static class LegacyMimCatalogImporter
|
|||||||
CalendarServiceDown,
|
CalendarServiceDown,
|
||||||
CalendarOutro,
|
CalendarOutro,
|
||||||
CommuteNow,
|
CommuteNow,
|
||||||
|
CommuteMinutesLeft,
|
||||||
|
CommuteDepartTimeNormal,
|
||||||
|
CommuteDepartTimeNotNormal,
|
||||||
|
CommuteAppSetup,
|
||||||
|
CommuteConfirmSpeaker,
|
||||||
|
CommuteDriveNormal,
|
||||||
|
CommuteDriveLate,
|
||||||
|
CommuteDriveHurry,
|
||||||
|
CommuteDrivePoor,
|
||||||
|
CommuteDriveTerrible,
|
||||||
|
CommuteTransportNormal,
|
||||||
|
CommuteTransportLate,
|
||||||
|
CommuteTransportHurry,
|
||||||
CommuteServiceDown,
|
CommuteServiceDown,
|
||||||
NewsIntro,
|
NewsIntro,
|
||||||
NewsCategoryIntro,
|
NewsCategoryIntro,
|
||||||
@@ -462,7 +533,20 @@ public static class LegacyMimCatalogImporter
|
|||||||
private readonly List<string> _calendarNothingTodayReplies = [];
|
private readonly List<string> _calendarNothingTodayReplies = [];
|
||||||
private readonly List<string> _calendarOutroReplies = [];
|
private readonly List<string> _calendarOutroReplies = [];
|
||||||
private readonly List<string> _calendarServiceDownReplies = [];
|
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> _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> _commuteServiceDownReplies = [];
|
||||||
private readonly List<string> _birthdayCelebrationReplies = [];
|
private readonly List<string> _birthdayCelebrationReplies = [];
|
||||||
private readonly List<JiboConditionedReply> _emotionReplies = [];
|
private readonly List<JiboConditionedReply> _emotionReplies = [];
|
||||||
@@ -615,9 +699,48 @@ public static class LegacyMimCatalogImporter
|
|||||||
case LegacyMimBucket.CalendarOutro:
|
case LegacyMimBucket.CalendarOutro:
|
||||||
AddDistinct(_calendarOutroReplies, text);
|
AddDistinct(_calendarOutroReplies, text);
|
||||||
return;
|
return;
|
||||||
|
case LegacyMimBucket.CommuteAppSetup:
|
||||||
|
AddDistinct(_commuteAppSetupReplies, text);
|
||||||
|
return;
|
||||||
|
case LegacyMimBucket.CommuteConfirmSpeaker:
|
||||||
|
AddDistinct(_commuteConfirmSpeakerReplies, text);
|
||||||
|
return;
|
||||||
case LegacyMimBucket.CommuteNow:
|
case LegacyMimBucket.CommuteNow:
|
||||||
AddDistinct(_commuteNowReplies, text);
|
AddDistinct(_commuteNowReplies, text);
|
||||||
return;
|
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:
|
case LegacyMimBucket.CommuteServiceDown:
|
||||||
AddDistinct(_commuteServiceDownReplies, text);
|
AddDistinct(_commuteServiceDownReplies, text);
|
||||||
return;
|
return;
|
||||||
@@ -670,7 +793,20 @@ public static class LegacyMimCatalogImporter
|
|||||||
CalendarNothingReplies = [.. _calendarNothingReplies],
|
CalendarNothingReplies = [.. _calendarNothingReplies],
|
||||||
CalendarServiceDownReplies = [.. _calendarServiceDownReplies],
|
CalendarServiceDownReplies = [.. _calendarServiceDownReplies],
|
||||||
CalendarOutroReplies = [.. _calendarOutroReplies],
|
CalendarOutroReplies = [.. _calendarOutroReplies],
|
||||||
|
CommuteAppSetupReplies = [.. _commuteAppSetupReplies],
|
||||||
|
CommuteConfirmSpeakerReplies = [.. _commuteConfirmSpeakerReplies],
|
||||||
CommuteNowReplies = [.. _commuteNowReplies],
|
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],
|
CommuteServiceDownReplies = [.. _commuteServiceDownReplies],
|
||||||
NewsIntroReplies = [.. _newsIntroReplies],
|
NewsIntroReplies = [.. _newsIntroReplies],
|
||||||
NewsCategoryIntroReplies = [.. _newsCategoryIntroReplies],
|
NewsCategoryIntroReplies = [.. _newsCategoryIntroReplies],
|
||||||
|
|||||||
@@ -54,8 +54,10 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddHttpClient<INewsBriefingProvider, NewsApiBriefingProvider>();
|
services.AddHttpClient<INewsBriefingProvider, NewsApiBriefingProvider>();
|
||||||
services.AddSingleton<IHolidayCalendarProvider>(provider =>
|
services.AddSingleton<IHolidayCalendarProvider>(provider =>
|
||||||
new NagerDateHolidayCalendarProvider(provider.GetRequiredService<HolidayCalendarOptions>()));
|
new NagerDateHolidayCalendarProvider(provider.GetRequiredService<HolidayCalendarOptions>()));
|
||||||
services.AddSingleton<ICalendarReportProvider, UnavailableCalendarReportProvider>();
|
services.AddSingleton<ICalendarReportProvider>(provider =>
|
||||||
services.AddSingleton<ICommuteReportProvider, UnavailableCommuteReportProvider>();
|
new CloudStateCalendarReportProvider(provider.GetRequiredService<ICloudStateStore>()));
|
||||||
|
services.AddSingleton<ICommuteReportProvider>(provider =>
|
||||||
|
new CloudStateCommuteReportProvider(provider.GetRequiredService<ICloudStateStore>()));
|
||||||
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"]
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
|||||||
};
|
};
|
||||||
|
|
||||||
private readonly List<BackupRecord> _backups = [];
|
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, DeviceRegistration> _devices = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string, KeyRequestRecord>
|
private readonly ConcurrentDictionary<string, KeyRequestRecord>
|
||||||
@@ -161,6 +163,12 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
|||||||
_backups.Clear();
|
_backups.Clear();
|
||||||
_backups.AddRange(snapshot.Backups ?? []);
|
_backups.AddRange(snapshot.Backups ?? []);
|
||||||
|
|
||||||
|
_commuteProfiles.Clear();
|
||||||
|
_commuteProfiles.AddRange(snapshot.CommuteProfiles ?? []);
|
||||||
|
|
||||||
|
_calendarEvents.Clear();
|
||||||
|
_calendarEvents.AddRange(snapshot.CalendarEvents ?? []);
|
||||||
|
|
||||||
_loops.Clear();
|
_loops.Clear();
|
||||||
_loops.AddRange(snapshot.Loops ?? []);
|
_loops.AddRange(snapshot.Loops ?? []);
|
||||||
|
|
||||||
@@ -214,6 +222,8 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
|||||||
Updates = _updates.ToArray(),
|
Updates = _updates.ToArray(),
|
||||||
Media = _media.ToArray(),
|
Media = _media.ToArray(),
|
||||||
Backups = _backups.ToArray(),
|
Backups = _backups.ToArray(),
|
||||||
|
CommuteProfiles = _commuteProfiles.ToArray(),
|
||||||
|
CalendarEvents = _calendarEvents.ToArray(),
|
||||||
Loops = _loops.ToArray(),
|
Loops = _loops.ToArray(),
|
||||||
Holidays = _holidayOverrides.ToArray(),
|
Holidays = _holidayOverrides.ToArray(),
|
||||||
People = _people.ToArray()
|
People = _people.ToArray()
|
||||||
@@ -479,6 +489,55 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
|||||||
return backup;
|
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)
|
public bool ShouldCreateSymmetricKey(string loopId)
|
||||||
{
|
{
|
||||||
return !_symmetricKeys.ContainsKey(loopId);
|
return !_symmetricKeys.ContainsKey(loopId);
|
||||||
@@ -580,6 +639,59 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
|||||||
.ToArray();
|
.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)
|
public HolidayRecord UpsertHoliday(HolidayRecord holiday)
|
||||||
{
|
{
|
||||||
var resolvedLoopId = string.IsNullOrWhiteSpace(holiday.LoopId) ? ResolveDefaultLoopId() : holiday.LoopId.Trim();
|
var resolvedLoopId = string.IsNullOrWhiteSpace(holiday.LoopId) ? ResolveDefaultLoopId() : holiday.LoopId.Trim();
|
||||||
@@ -656,6 +768,7 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
|||||||
|
|
||||||
if (_people.Count != 0)
|
if (_people.Count != 0)
|
||||||
{
|
{
|
||||||
|
EnsureDefaultCommuteProfile();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -680,6 +793,29 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
|||||||
Alias = "Household Member",
|
Alias = "Household Member",
|
||||||
IsPrimary = false
|
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)
|
private static string Slugify(string value)
|
||||||
@@ -767,6 +903,8 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
|||||||
public UpdateManifest[]? Updates { get; init; }
|
public UpdateManifest[]? Updates { get; init; }
|
||||||
public MediaRecord[]? Media { get; init; }
|
public MediaRecord[]? Media { get; init; }
|
||||||
public BackupRecord[]? Backups { get; init; }
|
public BackupRecord[]? Backups { get; init; }
|
||||||
|
public CommuteProfileRecord[]? CommuteProfiles { get; init; }
|
||||||
|
public CalendarEventRecord[]? CalendarEvents { get; init; }
|
||||||
public LoopRecord[]? Loops { get; init; }
|
public LoopRecord[]? Loops { get; init; }
|
||||||
public HolidayRecord[]? Holidays { get; init; }
|
public HolidayRecord[]? Holidays { get; init; }
|
||||||
public PersonRecord[]? People { get; init; }
|
public PersonRecord[]? People { get; init; }
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Jibo.Cloud.Application.Abstractions;
|
using Jibo.Cloud.Application.Abstractions;
|
||||||
|
using Jibo.Cloud.Domain.Models;
|
||||||
using Jibo.Cloud.Infrastructure.Persistence;
|
using Jibo.Cloud.Infrastructure.Persistence;
|
||||||
|
|
||||||
namespace Jibo.Cloud.Tests.Infrastructure;
|
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 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,
|
var media = firstStore.CreateMedia("openjibo-default-loop", "persisted-photo", "image", "photo-ref", false,
|
||||||
new Dictionary<string, object?> { ["note"] = "roundtrip" });
|
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 sessionToken = firstStore.IssueRobotToken("robot-123");
|
||||||
var device = firstStore.GetOrCreateDevice("robot-123", "3.2.1", "4.5.6");
|
var device = firstStore.GetOrCreateDevice("robot-123", "3.2.1", "4.5.6");
|
||||||
firstStore.SavePersistedState();
|
firstStore.SavePersistedState();
|
||||||
@@ -117,6 +133,10 @@ public sealed class PersistenceStoreTests
|
|||||||
Assert.Equal(firstInfo.Revision, secondInfo.Revision);
|
Assert.Equal(firstInfo.Revision, secondInfo.Revision);
|
||||||
Assert.Contains(secondStore.ListUpdates("robot"), item => item.UpdateId == update.UpdateId);
|
Assert.Contains(secondStore.ListUpdates("robot"), item => item.UpdateId == update.UpdateId);
|
||||||
Assert.Contains(secondStore.ListMedia(), item => item.Path == media.Path);
|
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.NotNull(secondStore.FindSessionByToken(sessionToken));
|
||||||
Assert.Equal("3.2.1", secondStore.GetOrCreateDevice(device.DeviceId, null, null).FirmwareVersion);
|
Assert.Equal("3.2.1", secondStore.GetOrCreateDevice(device.DeviceId, null, null).FirmwareVersion);
|
||||||
Assert.NotEmpty(secondStore.GetPeople());
|
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]
|
[Fact]
|
||||||
public async Task MediaCreateAndGet_ReturnsCreatedItem()
|
public async Task MediaCreateAndGet_ReturnsCreatedItem()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ using System.Text;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Jibo.Cloud.Application.Abstractions;
|
using Jibo.Cloud.Application.Abstractions;
|
||||||
using Jibo.Cloud.Application.Services;
|
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.Content;
|
||||||
using Jibo.Cloud.Infrastructure.Persistence;
|
using Jibo.Cloud.Infrastructure.Persistence;
|
||||||
using Jibo.Runtime.Abstractions;
|
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)
|
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 service = CreateService(weatherReportProvider: weatherProvider, calendarReportProvider: calendarProvider);
|
||||||
|
|
||||||
var decision = await service.BuildDecisionAsync(new TurnContext
|
var decision = await service.BuildDecisionAsync(new TurnContext
|
||||||
@@ -1913,6 +1921,54 @@ public sealed class JiboInteractionServiceTests
|
|||||||
StringComparison.OrdinalIgnoreCase);
|
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]
|
[Fact]
|
||||||
public async Task BuildDecisionAsync_PersonalReport_NoMatchRetriesThenDeclines()
|
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