@jsm-mit/sultana-agent-tools-package 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +106 -8
  2. package/dist/confirmations.d.ts +61 -0
  3. package/dist/confirmations.d.ts.map +1 -0
  4. package/dist/confirmations.js +100 -0
  5. package/dist/create-salon-tools.d.ts +13 -2
  6. package/dist/create-salon-tools.d.ts.map +1 -1
  7. package/dist/create-salon-tools.js +10 -4
  8. package/dist/customer/create-customer-tools.d.ts +38 -0
  9. package/dist/customer/create-customer-tools.d.ts.map +1 -0
  10. package/dist/customer/create-customer-tools.js +47 -0
  11. package/dist/customer/customer-core-port.d.ts +94 -0
  12. package/dist/customer/customer-core-port.d.ts.map +1 -0
  13. package/dist/customer/customer-core-port.js +1 -0
  14. package/dist/customer/ic-customer-core-port.d.ts +45 -0
  15. package/dist/customer/ic-customer-core-port.d.ts.map +1 -0
  16. package/dist/customer/ic-customer-core-port.js +132 -0
  17. package/dist/customer/persona.d.ts +6 -0
  18. package/dist/customer/persona.d.ts.map +1 -0
  19. package/dist/customer/persona.js +27 -0
  20. package/dist/customer/time.d.ts +27 -0
  21. package/dist/customer/time.d.ts.map +1 -0
  22. package/dist/customer/time.js +113 -0
  23. package/dist/customer/tools.d.ts +17 -0
  24. package/dist/customer/tools.d.ts.map +1 -0
  25. package/dist/customer/tools.js +415 -0
  26. package/dist/index.d.ts +10 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +5 -0
  29. package/dist/persona.d.ts +4 -1
  30. package/dist/persona.d.ts.map +1 -1
  31. package/dist/persona.js +18 -7
  32. package/dist/tools/promos.d.ts +2 -1
  33. package/dist/tools/promos.d.ts.map +1 -1
  34. package/dist/tools/promos.js +79 -39
  35. package/dist/tools/schedule.d.ts +2 -1
  36. package/dist/tools/schedule.d.ts.map +1 -1
  37. package/dist/tools/schedule.js +51 -31
  38. package/dist/tools/services.d.ts +4 -2
  39. package/dist/tools/services.d.ts.map +1 -1
  40. package/dist/tools/services.js +75 -30
  41. package/dist/tools/write-gate.d.ts +29 -0
  42. package/dist/tools/write-gate.d.ts.map +1 -0
  43. package/dist/tools/write-gate.js +37 -0
  44. package/package.json +4 -3
@@ -0,0 +1,132 @@
1
+ import { Principal } from "@icp-sdk/core/principal";
2
+ import { AppointmentsActor, AvailabilityActor, filterServiceTypes, PromoActor, SalonServicesActor, SalonsActor, searchAsapAsyncUnsafe, searchAtMomentsAsyncUnsafe, SearchActor, ServiceTypesActor, } from "@jsm-mit/sultana-core-motoko-package";
3
+ /**
4
+ * The customer port over the published canister wrapper. Conversions live here and nowhere
5
+ * else: the canister speaks `bigint`, `Principal` and nanoseconds, the tools speak numbers,
6
+ * strings and milliseconds.
7
+ *
8
+ * The public reads go out ANONYMOUSLY even when an identity is present: they answer the same
9
+ * either way, and a signed query would only spend the customer's delegation on nothing.
10
+ */
11
+ export class IcCustomerCorePort {
12
+ location;
13
+ language;
14
+ search;
15
+ salons;
16
+ services;
17
+ serviceTypes;
18
+ promo;
19
+ availability;
20
+ appointments;
21
+ constructor(options) {
22
+ this.location = options.location ?? null;
23
+ this.language = options.language;
24
+ this.search = new SearchActor(options.canisterId);
25
+ this.salons = new SalonsActor(options.canisterId);
26
+ this.services = new SalonServicesActor(options.canisterId);
27
+ this.serviceTypes = new ServiceTypesActor(options.canisterId);
28
+ this.promo = new PromoActor(options.canisterId);
29
+ this.availability = new AvailabilityActor(options.canisterId);
30
+ this.appointments = new AppointmentsActor(options.canisterId, options.identity);
31
+ }
32
+ async findServiceTypes(query) {
33
+ const catalog = await this.serviceTypes.getServiceTypesCatalogAsyncUnsafe(this.language);
34
+ return filterServiceTypes(catalog, query).map((entry) => ({
35
+ id: entry.id,
36
+ label: entry.text,
37
+ category: entry.category,
38
+ }));
39
+ }
40
+ async searchAsap(serviceTypeId, radiusKm, now) {
41
+ const state = await searchAsapAsyncUnsafe(this.search, {
42
+ location: this.requireLocation(),
43
+ serviceTypeId,
44
+ radiusKm,
45
+ now,
46
+ });
47
+ return { phase: state.phase, salons: state.results.map(toSalonSlots), failedProbes: state.failedProbes };
48
+ }
49
+ async searchAtMoments(serviceTypeId, radiusKm, moments) {
50
+ const state = await searchAtMomentsAsyncUnsafe(this.search, {
51
+ location: this.requireLocation(),
52
+ serviceTypeId,
53
+ radiusKm,
54
+ moments,
55
+ });
56
+ return { salons: state.results.map(toSalonSlots), failedProbes: state.failedProbes };
57
+ }
58
+ async getSalons(salonIds) {
59
+ if (salonIds.length === 0)
60
+ return [];
61
+ // The canister answers for at most 100 ids per call.
62
+ const salons = await this.salons.getSalonsPublicInfoAsyncUnsafe(salonIds.slice(0, 100));
63
+ return salons.map((salon) => ({ id: salon.id, name: salon.name, address: salon.address }));
64
+ }
65
+ async listSalonServices(salonId) {
66
+ const services = await this.services.getSalonServicesPublicAsyncUnsafe(salonId);
67
+ return services.map(toService);
68
+ }
69
+ async listSalonPromos(salonId) {
70
+ const promos = await this.promo.getSalonPromosAsyncUnsafe(salonId);
71
+ return promos
72
+ .filter((promo) => promo.active)
73
+ .map((promo) => ({
74
+ name: promo.name,
75
+ textLines: [...promo.textLines],
76
+ discountCode: promo.cta.discountCode[0],
77
+ targetServiceId: "SalonService" in promo.target ? promo.target.SalonService : undefined,
78
+ }));
79
+ }
80
+ async freeWindows(salonId, workerId, day) {
81
+ // `day` stays a "YYYY-MM-DD" string: the wrapper reads a date's UTC parts, so a Date of
82
+ // local midnight would land a day early. The offset is the one of that day's noon, so a
83
+ // day on the far side of a clock change is read with its own offset.
84
+ const [year, month, date] = day.split("-").map(Number);
85
+ const offsetSlots = BigInt(-new Date(year, month - 1, date, 12).getTimezoneOffset() / 5);
86
+ return this.availability.getEffectiveMaskForSalonAsyncUnsafe(day, salonId, Principal.fromText(workerId), offsetSlots);
87
+ }
88
+ async book(input) {
89
+ return this.appointments.bookAppointmentAsyncUnsafe(input.notes, input.salonId, input.serviceId, input.startAt, input.workerId, input.discountCode);
90
+ }
91
+ async listMyAppointments() {
92
+ const appointments = await this.appointments.getMyAppointmentsAsyncUnsafe();
93
+ return appointments.map((appointment) => ({
94
+ id: appointment.id,
95
+ salonId: appointment.salonId,
96
+ serviceId: appointment.salonServiceId,
97
+ startAt: Number(appointment.startTimeUTC / 1000000n),
98
+ durationMinutes: Number(appointment.durationMinutes),
99
+ pricePln: Number(appointment.priceSnapshot),
100
+ status: Object.keys(appointment.status)[0],
101
+ discountCode: appointment.discountCode[0],
102
+ }));
103
+ }
104
+ async cancel(appointmentId) {
105
+ await this.appointments.cancelAppointmentAsyncUnsafe(appointmentId);
106
+ }
107
+ requireLocation() {
108
+ if (!this.location)
109
+ throw new Error("IcCustomerCorePort: no location — the tools check it before they search");
110
+ return this.location;
111
+ }
112
+ }
113
+ function toService(service) {
114
+ return {
115
+ id: service.id,
116
+ salonId: service.salonId,
117
+ name: service.name,
118
+ pricePln: Number(service.price),
119
+ durationMinutes: Number(service.duration),
120
+ serviceTypeIds: [...service.serviceTypeIds],
121
+ workerIds: service.workerIds.map((worker) => worker.toText()),
122
+ };
123
+ }
124
+ function toSalonSlots(found) {
125
+ return {
126
+ salonId: found.salonId,
127
+ distanceKm: found.distanceKm,
128
+ services: found.services.map(toService),
129
+ availableAt: [...found.availableAt],
130
+ soonestAt: found.soonest ? found.soonest.at : null,
131
+ };
132
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The operating rules of the customer tools, as a system prompt — next to the tools for the same
3
+ * reason as the salon persona: they describe the tool contract. A host appends who the agent is.
4
+ */
5
+ export declare const CUSTOMER_AGENT_PERSONA_PL = "Jeste\u015B asystentem klienta w aplikacji Sultana: pomagasz znale\u017A\u0107 salon i wolny termin,\nrezerwujesz i odwo\u0142ujesz wizyty w imieniu klienta. Rozmawiasz po polsku, kr\u00F3tko i konkretnie.\n\nZasady pracy:\n- Szukasz zawsze wok\u00F3\u0142 pozycji klienta, kt\u00F3r\u0105 poda\u0142a aplikacja. Nie pytaj o adres.\n- Typ us\u0142ugi ustal przez find_service_type (np. \u201Estrzy\u017Cenie m\u0119skie\u201D). Nigdy nie wymy\u015Blaj id.\n Gdy pasuje kilka typ\u00F3w, dopytaj klienta, o kt\u00F3ry chodzi.\n- Terminy znajd\u017A przez search_slots: bez daty = jak najszybciej, z date i time = okolice tej godziny.\n Dzisiejsz\u0105 dat\u0119 we\u017A z get_current_date \u2014 \u201Ejutro o 10\u201D przelicz na RRRR-MM-DD i GG:MM.\n- Poka\u017C klientowi najwy\u017Cej kilka propozycji: salon, us\u0142uga, cena, odleg\u0142o\u015B\u0107, godzina. Nie pokazuj id.\n- Inne godziny w wybranym salonie sprawd\u017A przez get_free_times.\n- Rezerwacja i odwo\u0142anie to dwa kroki w dw\u00F3ch r\u00F3\u017Cnych wiadomo\u015Bciach klienta:\n 1. Wywo\u0142aj book_appointment albo cancel_appointment z confirmed=false. Poka\u017C klientowi zdanie\n z pola summary, zapytaj o zgod\u0119 i na tym zako\u0144cz odpowied\u017A.\n 2. Dopiero gdy w swojej NAST\u0118PNEJ wiadomo\u015Bci wyra\u017Anie si\u0119 zgodzi, wywo\u0142aj to samo narz\u0119dzie\n z confirmed=true, z confirmationId z tego podgl\u0105du i z tymi samymi argumentami (pole echo).\n Nigdy nie potwierdzaj w tej samej odpowiedzi, w kt\u00F3rej pokaza\u0142e\u015B podgl\u0105d.\n- Kod rabatowy podawaj tylko wtedy, gdy klient go wpisa\u0142 albo wybra\u0142 promocj\u0119 z get_salon.\n- Je\u015Bli masz tylko narz\u0119dzia do szukania (bez book_appointment), nie mo\u017Cesz rezerwowa\u0107 \u2014\n powiedz, \u017Ce klient musi si\u0119 zalogowa\u0107 ponownie w aplikacji i otworzy\u0107 czat jeszcze raz.\n- Je\u015Bli narz\u0119dzie zwr\u00F3ci status \"error\", przeczytaj summary i powiedz klientowi, co posz\u0142o nie tak.\n Nie powtarzaj tego samego wywo\u0142ania w k\u00F3\u0142ko: nieudana pr\u00F3ba rezerwacji zu\u017Cywa dzienny limit klienta.\n- Nie obiecuj rezerwacji, kt\u00F3rej nie wykona\u0142e\u015B narz\u0119dziem.";
6
+ //# sourceMappingURL=persona.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persona.d.ts","sourceRoot":"","sources":["../../src/customer/persona.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,yBAAyB,isEAsBqB,CAAC"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The operating rules of the customer tools, as a system prompt — next to the tools for the same
3
+ * reason as the salon persona: they describe the tool contract. A host appends who the agent is.
4
+ */
5
+ export const CUSTOMER_AGENT_PERSONA_PL = `Jesteś asystentem klienta w aplikacji Sultana: pomagasz znaleźć salon i wolny termin,
6
+ rezerwujesz i odwołujesz wizyty w imieniu klienta. Rozmawiasz po polsku, krótko i konkretnie.
7
+
8
+ Zasady pracy:
9
+ - Szukasz zawsze wokół pozycji klienta, którą podała aplikacja. Nie pytaj o adres.
10
+ - Typ usługi ustal przez find_service_type (np. „strzyżenie męskie”). Nigdy nie wymyślaj id.
11
+ Gdy pasuje kilka typów, dopytaj klienta, o który chodzi.
12
+ - Terminy znajdź przez search_slots: bez daty = jak najszybciej, z date i time = okolice tej godziny.
13
+ Dzisiejszą datę weź z get_current_date — „jutro o 10” przelicz na RRRR-MM-DD i GG:MM.
14
+ - Pokaż klientowi najwyżej kilka propozycji: salon, usługa, cena, odległość, godzina. Nie pokazuj id.
15
+ - Inne godziny w wybranym salonie sprawdź przez get_free_times.
16
+ - Rezerwacja i odwołanie to dwa kroki w dwóch różnych wiadomościach klienta:
17
+ 1. Wywołaj book_appointment albo cancel_appointment z confirmed=false. Pokaż klientowi zdanie
18
+ z pola summary, zapytaj o zgodę i na tym zakończ odpowiedź.
19
+ 2. Dopiero gdy w swojej NASTĘPNEJ wiadomości wyraźnie się zgodzi, wywołaj to samo narzędzie
20
+ z confirmed=true, z confirmationId z tego podglądu i z tymi samymi argumentami (pole echo).
21
+ Nigdy nie potwierdzaj w tej samej odpowiedzi, w której pokazałeś podgląd.
22
+ - Kod rabatowy podawaj tylko wtedy, gdy klient go wpisał albo wybrał promocję z get_salon.
23
+ - Jeśli masz tylko narzędzia do szukania (bez book_appointment), nie możesz rezerwować —
24
+ powiedz, że klient musi się zalogować ponownie w aplikacji i otworzyć czat jeszcze raz.
25
+ - Jeśli narzędzie zwróci status "error", przeczytaj summary i powiedz klientowi, co poszło nie tak.
26
+ Nie powtarzaj tego samego wywołania w kółko: nieudana próba rezerwacji zużywa dzienny limit klienta.
27
+ - Nie obiecuj rezerwacji, której nie wykonałeś narzędziem.`;
@@ -0,0 +1,27 @@
1
+ import type { TimeRange } from "../salon-core-port.js";
2
+ /** The local day of a moment, `"YYYY-MM-DD"`. */
3
+ export declare function localDayOf(at: number | Date): string;
4
+ /** The local time of a moment, `"HH:mm"`. */
5
+ export declare function localTimeOf(at: number | Date): string;
6
+ /** `"YYYY-MM-DD HH:mm"` — what a model reads and sends back as `date` + `time`. */
7
+ export declare function localStampOf(at: number | Date): string;
8
+ export declare function isDay(text: string): boolean;
9
+ /** Minutes from midnight of `"HH:mm"` (`"9:30"` too), or `null` when it is no time of day. */
10
+ export declare function minutesOf(time: string): number | null;
11
+ export declare function timeOfMinutes(minutes: number): string;
12
+ /** The moment of a local day at minutes from its midnight. `day` must pass `isDay`. */
13
+ export declare function momentOf(day: string, minutes: number): number;
14
+ export declare function onSlotGrid(minutes: number): boolean;
15
+ /** Start minutes, on the 5-minute grid, where a service of this duration fits in one window. */
16
+ export declare function freeStartMinutes(windows: TimeRange[], durationMinutes: number): number[];
17
+ /** Consecutive grid minutes folded into ranges — dozens of start times are noise to a model. */
18
+ export declare function foldStartRanges(starts: number[]): {
19
+ from: string;
20
+ to: string;
21
+ }[];
22
+ /**
23
+ * Why this process must not book for salons in `expectedTimeZone`, or `null` when it may: the
24
+ * process clock has to run in that zone, because the canister wrapper takes every offset from it.
25
+ */
26
+ export declare function hostTimeZoneProblem(expectedTimeZone: string, at?: Date): string | null;
27
+ //# sourceMappingURL=time.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"time.d.ts","sourceRoot":"","sources":["../../src/customer/time.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAevD,iDAAiD;AACjD,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAGpD;AAED,6CAA6C;AAC7C,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAGrD;AAED,mFAAmF;AACnF,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAEtD;AAED,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAO3C;AAED,8FAA8F;AAC9F,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQrD;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,uFAAuF;AACvF,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAG7D;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED,gGAAgG;AAChG,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,SAAS,EAAE,EAAE,eAAe,EAAE,MAAM,GAAG,MAAM,EAAE,CAcxF;AAED,gGAAgG;AAChG,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,EAAE,CAUhF;AAqBD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,gBAAgB,EAAE,MAAM,EAAE,EAAE,GAAE,IAAiB,GAAG,MAAM,GAAG,IAAI,CAalG"}
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Local-time arithmetic of the customer tools. "Local" is the zone of the HOST PROCESS: the
3
+ * canister wrapper derives every `timeOffset` from the process zone too, so the two cannot
4
+ * disagree — but the process zone must then be the salons' zone. `hostTimeZoneProblem` is the
5
+ * guard: a host in UTC would book every visit two hours off, and refusing is better than that.
6
+ */
7
+ const SLOT_MINUTES = 5;
8
+ const DAY_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
9
+ const TIME_PATTERN = /^(\d{1,2}):(\d{2})$/;
10
+ const pad = (value) => String(value).padStart(2, "0");
11
+ /** The local day of a moment, `"YYYY-MM-DD"`. */
12
+ export function localDayOf(at) {
13
+ const date = new Date(at);
14
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
15
+ }
16
+ /** The local time of a moment, `"HH:mm"`. */
17
+ export function localTimeOf(at) {
18
+ const date = new Date(at);
19
+ return `${pad(date.getHours())}:${pad(date.getMinutes())}`;
20
+ }
21
+ /** `"YYYY-MM-DD HH:mm"` — what a model reads and sends back as `date` + `time`. */
22
+ export function localStampOf(at) {
23
+ return `${localDayOf(at)} ${localTimeOf(at)}`;
24
+ }
25
+ export function isDay(text) {
26
+ const match = DAY_PATTERN.exec(text);
27
+ if (!match)
28
+ return false;
29
+ const [year, month, day] = [Number(match[1]), Number(match[2]), Number(match[3])];
30
+ const date = new Date(year, month - 1, day);
31
+ return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day;
32
+ }
33
+ /** Minutes from midnight of `"HH:mm"` (`"9:30"` too), or `null` when it is no time of day. */
34
+ export function minutesOf(time) {
35
+ const match = TIME_PATTERN.exec(time.trim());
36
+ if (!match)
37
+ return null;
38
+ const [hours, minutes] = [Number(match[1]), Number(match[2])];
39
+ if (hours > 23 || minutes > 59)
40
+ return null;
41
+ return hours * 60 + minutes;
42
+ }
43
+ export function timeOfMinutes(minutes) {
44
+ return `${pad(Math.floor(minutes / 60))}:${pad(minutes % 60)}`;
45
+ }
46
+ /** The moment of a local day at minutes from its midnight. `day` must pass `isDay`. */
47
+ export function momentOf(day, minutes) {
48
+ const [year, month, date] = day.split("-").map(Number);
49
+ return new Date(year, month - 1, date, Math.floor(minutes / 60), minutes % 60, 0, 0).getTime();
50
+ }
51
+ export function onSlotGrid(minutes) {
52
+ return minutes % SLOT_MINUTES === 0;
53
+ }
54
+ /** Start minutes, on the 5-minute grid, where a service of this duration fits in one window. */
55
+ export function freeStartMinutes(windows, durationMinutes) {
56
+ const starts = new Set();
57
+ for (const window of windows) {
58
+ const from = minutesOf(window.startTime);
59
+ // The wrapper writes the end of the day as "24:00" or "00:00".
60
+ const to = window.endTime === "24:00" || window.endTime === "00:00" ? 24 * 60 : minutesOf(window.endTime);
61
+ if (from === null || to === null)
62
+ continue;
63
+ const first = Math.ceil(from / SLOT_MINUTES) * SLOT_MINUTES;
64
+ for (let start = first; start + durationMinutes <= to; start += SLOT_MINUTES)
65
+ starts.add(start);
66
+ }
67
+ return [...starts].sort((a, b) => a - b);
68
+ }
69
+ /** Consecutive grid minutes folded into ranges — dozens of start times are noise to a model. */
70
+ export function foldStartRanges(starts) {
71
+ const ranges = [];
72
+ for (const start of starts) {
73
+ const last = ranges[ranges.length - 1];
74
+ if (last && start === last.to + SLOT_MINUTES)
75
+ last.to = start;
76
+ else
77
+ ranges.push({ from: start, to: start });
78
+ }
79
+ return ranges.map((range) => ({ from: timeOfMinutes(range.from), to: timeOfMinutes(range.to) }));
80
+ }
81
+ /** Minutes east of UTC of a named zone at a moment. */
82
+ function zoneOffsetMinutes(timeZone, at) {
83
+ const parts = new Intl.DateTimeFormat("en-US", {
84
+ timeZone,
85
+ hourCycle: "h23",
86
+ year: "numeric",
87
+ month: "numeric",
88
+ day: "numeric",
89
+ hour: "numeric",
90
+ minute: "numeric",
91
+ second: "numeric",
92
+ }).formatToParts(at);
93
+ const read = (type) => Number(parts.find((part) => part.type === type)?.value);
94
+ const asUtc = Date.UTC(read("year"), read("month") - 1, read("day"), read("hour"), read("minute"), read("second"));
95
+ return Math.round((asUtc - at.getTime()) / 60_000);
96
+ }
97
+ /**
98
+ * Why this process must not book for salons in `expectedTimeZone`, or `null` when it may: the
99
+ * process clock has to run in that zone, because the canister wrapper takes every offset from it.
100
+ */
101
+ export function hostTimeZoneProblem(expectedTimeZone, at = new Date()) {
102
+ let expected;
103
+ try {
104
+ expected = zoneOffsetMinutes(expectedTimeZone, at);
105
+ }
106
+ catch {
107
+ return `nieznana strefa czasowa „${expectedTimeZone}”`;
108
+ }
109
+ const actual = -at.getTimezoneOffset();
110
+ if (actual === expected)
111
+ return null;
112
+ return `proces działa w innej strefie czasowej niż salony (${expectedTimeZone}): różnica ${Math.abs(expected - actual)} min — ustaw TZ=${expectedTimeZone}`;
113
+ }
@@ -0,0 +1,17 @@
1
+ import type { TurnConfirmations } from "../confirmations.js";
2
+ import { type AgentTool } from "../types.js";
3
+ import type { CustomerCorePort } from "./customer-core-port.js";
4
+ export interface CustomerToolsContext {
5
+ port: CustomerCorePort;
6
+ /** The clock — a test sets it. */
7
+ now: () => Date;
8
+ /** The salons' time zone the host process must run in; `undefined` skips the guard. */
9
+ timeZone?: string;
10
+ }
11
+ export declare function searchSlotsTool(context: CustomerToolsContext): AgentTool;
12
+ export declare function getSalonTool(context: CustomerToolsContext): AgentTool;
13
+ export declare function getFreeTimesTool(context: CustomerToolsContext): AgentTool;
14
+ export declare function bookAppointmentTool(context: CustomerToolsContext, confirmations: TurnConfirmations | undefined): AgentTool;
15
+ export declare function listMyAppointmentsTool(context: CustomerToolsContext): AgentTool;
16
+ export declare function cancelAppointmentTool(context: CustomerToolsContext, confirmations: TurnConfirmations | undefined): AgentTool;
17
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/customer/tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAI7D,OAAO,EAAW,KAAK,SAAS,EAAmB,MAAM,aAAa,CAAC;AACvE,OAAO,KAAK,EAAmB,gBAAgB,EAAkD,MAAM,yBAAyB,CAAC;AAejI,MAAM,WAAW,oBAAoB;IACjC,IAAI,EAAE,gBAAgB,CAAC;IACvB,kCAAkC;IAClC,GAAG,EAAE,MAAM,IAAI,CAAC;IAChB,uFAAuF;IACvF,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAwCD,wBAAgB,eAAe,CAAC,OAAO,EAAE,oBAAoB,GAAG,SAAS,CAgFxE;AAUD,wBAAgB,YAAY,CAAC,OAAO,EAAE,oBAAoB,GAAG,SAAS,CAkCrE;AAkCD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,oBAAoB,GAAG,SAAS,CA2CzE;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,iBAAiB,GAAG,SAAS,GAAG,SAAS,CA6E1H;AA6CD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,oBAAoB,GAAG,SAAS,CAkC/E;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,iBAAiB,GAAG,SAAS,GAAG,SAAS,CA4C5H"}