@jsm-mit/sultana-agent-tools-package 0.4.0 → 0.5.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.
- package/README.md +8 -0
- package/dist/customer/create-customer-tools.d.ts +6 -0
- package/dist/customer/create-customer-tools.d.ts.map +1 -1
- package/dist/customer/create-customer-tools.js +6 -4
- package/dist/customer/format.d.ts +19 -0
- package/dist/customer/format.d.ts.map +1 -0
- package/dist/customer/format.js +32 -0
- package/dist/customer/persona.d.ts +1 -1
- package/dist/customer/persona.d.ts.map +1 -1
- package/dist/customer/persona.js +1 -0
- package/dist/customer/tools.d.ts +3 -0
- package/dist/customer/tools.d.ts.map +1 -1
- package/dist/customer/tools.js +25 -14
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -163,6 +163,14 @@ const tools = session
|
|
|
163
163
|
AND on the confirmed call — a booking the canister refuses still spends the customer's daily quota.
|
|
164
164
|
- The public reads go out anonymously even with an identity; only booking, the visits and
|
|
165
165
|
cancelling are signed.
|
|
166
|
+
- **Numbers are written for the customer's region, by the tools, not by the prompt.** Pass
|
|
167
|
+
`locale` (a BCP 47 tag the app runs in — `"pl-PL"`, `"en-GB"`; default `pl-PL`, an unknown tag
|
|
168
|
+
falls back to it). Distances, prices and durations reach the model as ready text —
|
|
169
|
+
`distance: "8,3 km"`, `price: "60 zł"`, `duration: "45 min"` — in the data and in every
|
|
170
|
+
summary; there is no `distanceKm` / `pricePln` / `durationMinutes` for it to spell its own way
|
|
171
|
+
("8.3 km" in a Polish sentence). `customerFormats(locale)` is exported for a host that writes
|
|
172
|
+
such text itself. Dates and times stay `YYYY-MM-DD` / `HH:mm`: the model sends them back as
|
|
173
|
+
arguments.
|
|
166
174
|
- **The host process must run in the salons' time zone** (`TZ=Europe/Warsaw`): the canister wrapper
|
|
167
175
|
takes every offset from the process clock. While it does not, every time-dependent tool answers
|
|
168
176
|
`unavailable` instead of booking hours off (`timeZone: null` skips the guard; `hostTimeZoneProblem`
|
|
@@ -15,6 +15,12 @@ export interface CreateCustomerToolsFromPortOptions {
|
|
|
15
15
|
* touches a time answers with an error instead of booking hours off. `null` skips the guard.
|
|
16
16
|
*/
|
|
17
17
|
timeZone?: string | null;
|
|
18
|
+
/**
|
|
19
|
+
* The customer's language and region as a BCP 47 tag ("pl-PL", "en-GB") — what the app runs
|
|
20
|
+
* in. Distances, prices and durations reach the model already written that way ("8,3 km").
|
|
21
|
+
* Defaults to pl-PL; a tag the runtime does not know falls back to it.
|
|
22
|
+
*/
|
|
23
|
+
locale?: string;
|
|
18
24
|
}
|
|
19
25
|
export interface CreateCustomerToolsOptions extends Omit<IcCustomerCorePortOptions, "language">, CreateCustomerToolsFromPortOptions {
|
|
20
26
|
/** Language of service-type labels. Defaults to Polish. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-customer-tools.d.ts","sourceRoot":"","sources":["../../src/customer/create-customer-tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAE7D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"create-customer-tools.d.ts","sourceRoot":"","sources":["../../src/customer/create-customer-tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAE7D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,OAAO,EAAsB,KAAK,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AAWhG,+EAA+E;AAC/E,eAAO,MAAM,uBAAuB,kBAAkB,CAAC;AAEvD,MAAM,WAAW,kCAAkC;IAC/C,8EAA8E;IAC9E,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAClC,2CAA2C;IAC3C,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,0BAA2B,SAAQ,IAAI,CAAC,yBAAyB,EAAE,UAAU,CAAC,EAAE,kCAAkC;IAC/H,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,0BAA0B,GAAG,SAAS,EAAE,CAKpF;AAED,uDAAuD;AACvD,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,gBAAgB,EAAE,OAAO,GAAE,kCAAuC,GAAG,SAAS,EAAE,CASjI;AAED,MAAM,MAAM,8BAA8B,GAAG,IAAI,CAAC,0BAA0B,EAAE,UAAU,GAAG,eAAe,CAAC,CAAC;AAE5G;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,8BAA8B,GAAG,SAAS,EAAE,CAK5F;AAED,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,gBAAgB,EAAE,OAAO,GAAE,IAAI,CAAC,kCAAkC,EAAE,eAAe,CAAM,GAAG,SAAS,EAAE,CAE5J"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { findServiceTypeTool } from "../tools/services.js";
|
|
2
|
+
import { customerFormats } from "./format.js";
|
|
2
3
|
import { IcCustomerCorePort } from "./ic-customer-core-port.js";
|
|
3
4
|
import { bookAppointmentTool, cancelAppointmentTool, getFreeTimesTool, getSalonTool, listMyAppointmentsTool, searchSlotsTool, } from "./tools.js";
|
|
4
5
|
/** The zone Sultana's salons live in. The canister keeps no zone per salon. */
|
|
@@ -9,9 +10,9 @@ export const DEFAULT_SALON_TIME_ZONE = "Europe/Warsaw";
|
|
|
9
10
|
* model holding these tools can act for nobody else and search around nowhere else.
|
|
10
11
|
*/
|
|
11
12
|
export function createCustomerTools(options) {
|
|
12
|
-
const { confirmations, now, timeZone, language, ...portOptions } = options;
|
|
13
|
+
const { confirmations, now, timeZone, locale, language, ...portOptions } = options;
|
|
13
14
|
const port = new IcCustomerCorePort({ ...portOptions, language: language ?? "pl" });
|
|
14
|
-
return createCustomerToolsFromPort(port, { confirmations, now, timeZone });
|
|
15
|
+
return createCustomerToolsFromPort(port, { confirmations, now, timeZone, locale });
|
|
15
16
|
}
|
|
16
17
|
/** The same set over any port — the seam tests use. */
|
|
17
18
|
export function createCustomerToolsFromPort(port, options = {}) {
|
|
@@ -28,9 +29,9 @@ export function createCustomerToolsFromPort(port, options = {}) {
|
|
|
28
29
|
* identity — finding a service type, searching slots, a salon, its free times. Nothing books.
|
|
29
30
|
*/
|
|
30
31
|
export function createCustomerReadTools(options) {
|
|
31
|
-
const { now, timeZone, language, ...portOptions } = options;
|
|
32
|
+
const { now, timeZone, locale, language, ...portOptions } = options;
|
|
32
33
|
const port = new IcCustomerCorePort({ ...portOptions, language: language ?? "pl" });
|
|
33
|
-
return createCustomerReadToolsFromPort(port, { now, timeZone });
|
|
34
|
+
return createCustomerReadToolsFromPort(port, { now, timeZone, locale });
|
|
34
35
|
}
|
|
35
36
|
export function createCustomerReadToolsFromPort(port, options = {}) {
|
|
36
37
|
return readTools(contextOf(port, options));
|
|
@@ -40,6 +41,7 @@ function contextOf(port, options) {
|
|
|
40
41
|
port,
|
|
41
42
|
now: options.now ?? (() => new Date()),
|
|
42
43
|
timeZone: options.timeZone === null ? undefined : options.timeZone ?? DEFAULT_SALON_TIME_ZONE,
|
|
44
|
+
formats: customerFormats(options.locale),
|
|
43
45
|
};
|
|
44
46
|
}
|
|
45
47
|
function readTools(context) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** The region the tools write numbers for when the app sent none: Sultana's home market. */
|
|
2
|
+
export declare const DEFAULT_CUSTOMER_LOCALE = "pl-PL";
|
|
3
|
+
/**
|
|
4
|
+
* Numbers as the customer's region writes them — "8,3 km" and "60 zł" in pl-PL, "8.3 km" and
|
|
5
|
+
* "PLN 60" in en-GB. A model copies a number the way it got it, and a prompt rule about decimal
|
|
6
|
+
* commas is followed only most of the time; so the tools hand the model TEXT that is already
|
|
7
|
+
* right, and no bare number it could spell its own way.
|
|
8
|
+
*/
|
|
9
|
+
export interface CustomerFormats {
|
|
10
|
+
readonly locale: string;
|
|
11
|
+
/** One decimal: "8,3 km". */
|
|
12
|
+
distance(km: number): string;
|
|
13
|
+
/** Whole złoty without decimals, anything else with two: "60 zł", "59,50 zł". */
|
|
14
|
+
price(pln: number): string;
|
|
15
|
+
/** "45 min". */
|
|
16
|
+
duration(minutes: number): string;
|
|
17
|
+
}
|
|
18
|
+
export declare function customerFormats(locale?: string): CustomerFormats;
|
|
19
|
+
//# sourceMappingURL=format.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/customer/format.ts"],"names":[],"mappings":"AAAA,4FAA4F;AAC5F,eAAO,MAAM,uBAAuB,UAAU,CAAC;AAE/C;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,6BAA6B;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7B,iFAAiF;IACjF,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,gBAAgB;IAChB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;CACrC;AAkBD,wBAAgB,eAAe,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,eAAe,CAgBhE"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** The region the tools write numbers for when the app sent none: Sultana's home market. */
|
|
2
|
+
export const DEFAULT_CUSTOMER_LOCALE = "pl-PL";
|
|
3
|
+
/** A tag `Intl` does not know falls back to the default instead of failing every tool. */
|
|
4
|
+
function supported(locale) {
|
|
5
|
+
if (!locale)
|
|
6
|
+
return DEFAULT_CUSTOMER_LOCALE;
|
|
7
|
+
try {
|
|
8
|
+
return Intl.NumberFormat.supportedLocalesOf([locale])[0] ?? DEFAULT_CUSTOMER_LOCALE;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return DEFAULT_CUSTOMER_LOCALE;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Intl joins a number and its unit with a no-break space; a plain one is what a chat line and a test expect. */
|
|
15
|
+
function plainSpaces(text) {
|
|
16
|
+
return text.replace(/[\u00a0\u202f]/g, " ");
|
|
17
|
+
}
|
|
18
|
+
export function customerFormats(locale) {
|
|
19
|
+
const tag = supported(locale);
|
|
20
|
+
const distance = new Intl.NumberFormat(tag, { style: "unit", unit: "kilometer", maximumFractionDigits: 1 });
|
|
21
|
+
const wholePrice = new Intl.NumberFormat(tag, { style: "currency", currency: "PLN", maximumFractionDigits: 0 });
|
|
22
|
+
const exactPrice = new Intl.NumberFormat(tag, { style: "currency", currency: "PLN" });
|
|
23
|
+
// "min" is read the same in every language the app speaks; Intl's own short form is
|
|
24
|
+
// "mins" in English and its narrow one "45m".
|
|
25
|
+
const count = new Intl.NumberFormat(tag, { maximumFractionDigits: 0 });
|
|
26
|
+
return {
|
|
27
|
+
locale: tag,
|
|
28
|
+
distance: (km) => plainSpaces(distance.format(km)),
|
|
29
|
+
price: (pln) => plainSpaces((Number.isInteger(pln) ? wholePrice : exactPrice).format(pln)),
|
|
30
|
+
duration: (minutes) => `${plainSpaces(count.format(minutes))} min`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* The operating rules of the customer tools, as a system prompt — next to the tools for the same
|
|
3
3
|
* reason as the salon persona: they describe the tool contract. A host appends who the agent is.
|
|
4
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.";
|
|
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 Cen\u0119, odleg\u0142o\u015B\u0107 i czas trwania przepisuj dok\u0142adnie tak, jak je dosta\u0142e\u015B (np. \u201E8,3 km\u201D, \u201E60 z\u0142\u201D).\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
6
|
//# sourceMappingURL=persona.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"persona.d.ts","sourceRoot":"","sources":["../../src/customer/persona.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,yBAAyB,
|
|
1
|
+
{"version":3,"file":"persona.d.ts","sourceRoot":"","sources":["../../src/customer/persona.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,yBAAyB,i2EAuBqB,CAAC"}
|
package/dist/customer/persona.js
CHANGED
|
@@ -12,6 +12,7 @@ Zasady pracy:
|
|
|
12
12
|
- Terminy znajdź przez search_slots: bez daty = jak najszybciej, z date i time = okolice tej godziny.
|
|
13
13
|
Dzisiejszą datę weź z get_current_date — „jutro o 10” przelicz na RRRR-MM-DD i GG:MM.
|
|
14
14
|
- Pokaż klientowi najwyżej kilka propozycji: salon, usługa, cena, odległość, godzina. Nie pokazuj id.
|
|
15
|
+
Cenę, odległość i czas trwania przepisuj dokładnie tak, jak je dostałeś (np. „8,3 km”, „60 zł”).
|
|
15
16
|
- Inne godziny w wybranym salonie sprawdź przez get_free_times.
|
|
16
17
|
- Rezerwacja i odwołanie to dwa kroki w dwóch różnych wiadomościach klienta:
|
|
17
18
|
1. Wywołaj book_appointment albo cancel_appointment z confirmed=false. Pokaż klientowi zdanie
|
package/dist/customer/tools.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TurnConfirmations } from "../confirmations.js";
|
|
2
2
|
import { type AgentTool } from "../types.js";
|
|
3
|
+
import { type CustomerFormats } from "./format.js";
|
|
3
4
|
import type { CustomerCorePort } from "./customer-core-port.js";
|
|
4
5
|
export interface CustomerToolsContext {
|
|
5
6
|
port: CustomerCorePort;
|
|
@@ -7,6 +8,8 @@ export interface CustomerToolsContext {
|
|
|
7
8
|
now: () => Date;
|
|
8
9
|
/** The salons' time zone the host process must run in; `undefined` skips the guard. */
|
|
9
10
|
timeZone?: string;
|
|
11
|
+
/** How the customer's region writes distances, prices and durations. Defaults to pl-PL. */
|
|
12
|
+
formats?: CustomerFormats;
|
|
10
13
|
}
|
|
11
14
|
export declare function searchSlotsTool(context: CustomerToolsContext): AgentTool;
|
|
12
15
|
export declare function getSalonTool(context: CustomerToolsContext): AgentTool;
|
|
@@ -1 +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;
|
|
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,EAAmB,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AACpE,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;IAClB,2FAA2F;IAC3F,OAAO,CAAC,EAAE,eAAe,CAAC;CAC7B;AAgDD,wBAAgB,eAAe,CAAC,OAAO,EAAE,oBAAoB,GAAG,SAAS,CAkFxE;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,CA8E1H;AA6CD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,oBAAoB,GAAG,SAAS,CAoC/E;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,iBAAiB,GAAG,SAAS,GAAG,SAAS,CA4C5H"}
|
package/dist/customer/tools.js
CHANGED
|
@@ -2,7 +2,15 @@ import { toToolError } from "../errors.js";
|
|
|
2
2
|
import { ArgumentError, readBoolean, readOptionalNumber, readOptionalString, readString } from "../tools/args.js";
|
|
3
3
|
import { writeGate } from "../tools/write-gate.js";
|
|
4
4
|
import { err, ok } from "../types.js";
|
|
5
|
+
import { customerFormats } from "./format.js";
|
|
5
6
|
import { foldStartRanges, freeStartMinutes, hostTimeZoneProblem, isDay, localDayOf, localStampOf, localTimeOf, minutesOf, momentOf, onSlotGrid, timeOfMinutes, } from "./time.js";
|
|
7
|
+
/**
|
|
8
|
+
* The tools never hand the model a bare distance, price or duration: it would copy "8.3" with
|
|
9
|
+
* a dot into a Polish sentence. They hand it the text the customer's region writes.
|
|
10
|
+
*/
|
|
11
|
+
function formatsOf(context) {
|
|
12
|
+
return context.formats ?? customerFormats();
|
|
13
|
+
}
|
|
6
14
|
const DEFAULT_RADIUS_KM = 10;
|
|
7
15
|
const MAX_RADIUS_KM = 50;
|
|
8
16
|
const MAX_SALONS_SHOWN = 6;
|
|
@@ -33,15 +41,15 @@ function readTimeMinutes(args, key) {
|
|
|
33
41
|
throw new ArgumentError(`Pole "${key}" musi być godziną w formacie GG:MM.`);
|
|
34
42
|
return minutes;
|
|
35
43
|
}
|
|
36
|
-
function serviceLine(
|
|
37
|
-
return { serviceId: service.id, name: service.name,
|
|
44
|
+
function serviceLine(formats) {
|
|
45
|
+
return (service) => ({ serviceId: service.id, name: service.name, price: formats.price(service.pricePln), duration: formats.duration(service.durationMinutes) });
|
|
38
46
|
}
|
|
39
47
|
export function searchSlotsTool(context) {
|
|
40
48
|
const { port } = context;
|
|
41
49
|
return {
|
|
42
50
|
name: "search_slots",
|
|
43
51
|
progress: "Szukam wolnych terminów w okolicy…",
|
|
44
|
-
description: "Szuka salonów w okolicy klienta, które mają wolny termin na usługę danego typu. Bez date i time szuka „jak najszybciej” (najbliższa godzina, a gdy nic nie ma — do 24 godzin naprzód). Z date i time sprawdza okolice tej godziny (pół godziny wcześniej do godziny później). Zwraca salony z usługami (serviceId, cena, czas) i wolnymi terminami. serviceTypeId weź z find_service_type.",
|
|
52
|
+
description: "Szuka salonów w okolicy klienta, które mają wolny termin na usługę danego typu. Bez date i time szuka „jak najszybciej” (najbliższa godzina, a gdy nic nie ma — do 24 godzin naprzód). Z date i time sprawdza okolice tej godziny (pół godziny wcześniej do godziny później). Zwraca salony z usługami (serviceId, cena, czas) i wolnymi terminami. Odległość, cenę i czas trwania dostajesz jako gotowy tekst — przepisz go bez zmian. serviceTypeId weź z find_service_type.",
|
|
45
53
|
parameters: {
|
|
46
54
|
type: "object",
|
|
47
55
|
properties: {
|
|
@@ -60,6 +68,7 @@ export function searchSlotsTool(context) {
|
|
|
60
68
|
return refusal;
|
|
61
69
|
if (!port.location)
|
|
62
70
|
return err("invalid_arguments", NO_LOCATION);
|
|
71
|
+
const formats = formatsOf(context);
|
|
63
72
|
const serviceTypeId = readString(args, "serviceTypeId");
|
|
64
73
|
const radiusKm = Math.min(Math.max(readOptionalNumber(args, "radiusKm") ?? DEFAULT_RADIUS_KM, 1), MAX_RADIUS_KM);
|
|
65
74
|
const dayText = readOptionalString(args, "date");
|
|
@@ -88,7 +97,7 @@ export function searchSlotsTool(context) {
|
|
|
88
97
|
if (found.salons.length === 0) {
|
|
89
98
|
if (found.failedProbes > 0)
|
|
90
99
|
return err("unavailable", "Nie udało się sprawdzić terminów — kanister nie odpowiadał. Spróbuj ponownie za chwilę.");
|
|
91
|
-
return ok(`Nic wolnego w promieniu ${radiusKm}
|
|
100
|
+
return ok(`Nic wolnego w promieniu ${formats.distance(radiusKm)}. Zaproponuj inną godzinę, inny dzień albo większy promień.`, []);
|
|
92
101
|
}
|
|
93
102
|
const ranked = [...found.salons].sort(bySoonestThenNearest).slice(0, MAX_SALONS_SHOWN);
|
|
94
103
|
const names = new Map((await port.getSalons(ranked.map((salon) => salon.salonId))).map((salon) => [salon.id, salon]));
|
|
@@ -96,8 +105,8 @@ export function searchSlotsTool(context) {
|
|
|
96
105
|
salonId: salon.salonId,
|
|
97
106
|
name: names.get(salon.salonId)?.name ?? "Salon",
|
|
98
107
|
address: names.get(salon.salonId)?.address ?? "",
|
|
99
|
-
|
|
100
|
-
services: salon.services.map(serviceLine),
|
|
108
|
+
distance: formats.distance(salon.distanceKm),
|
|
109
|
+
services: salon.services.map(serviceLine(formats)),
|
|
101
110
|
freeAt: salon.availableAt.slice(0, MAX_TIMES_SHOWN).map(localStampOf),
|
|
102
111
|
}));
|
|
103
112
|
const more = found.salons.length > ranked.length ? ` Pokazuję ${ranked.length} z ${found.salons.length} salonów.` : "";
|
|
@@ -140,7 +149,7 @@ export function getSalonTool(context) {
|
|
|
140
149
|
salonId,
|
|
141
150
|
name: salon.name,
|
|
142
151
|
address: salon.address,
|
|
143
|
-
services: services.map(serviceLine),
|
|
152
|
+
services: services.map(serviceLine(formatsOf(context))),
|
|
144
153
|
promos,
|
|
145
154
|
});
|
|
146
155
|
}
|
|
@@ -214,7 +223,7 @@ export function getFreeTimesTool(context) {
|
|
|
214
223
|
const starts = [...(await freeStartsByWorker(port, service, day)).keys()].filter((start) => start > nowMinutes).sort((a, b) => a - b);
|
|
215
224
|
if (starts.length === 0)
|
|
216
225
|
return ok(`„${service.name}” — ${day}: brak wolnych godzin.`, []);
|
|
217
|
-
return ok(`„${service.name}” — ${day}: wolne godziny rozpoczęcia (${service.durationMinutes}
|
|
226
|
+
return ok(`„${service.name}” — ${day}: wolne godziny rozpoczęcia (${formatsOf(context).duration(service.durationMinutes)}, ${formatsOf(context).price(service.pricePln)}).`, foldStartRanges(starts));
|
|
218
227
|
}
|
|
219
228
|
catch (error) {
|
|
220
229
|
if (error instanceof ArgumentError)
|
|
@@ -279,17 +288,18 @@ export function bookAppointmentTool(context, confirmations) {
|
|
|
279
288
|
if (workerId === undefined) {
|
|
280
289
|
return err("not_found", `Termin ${day} ${timeOfMinutes(startMinutes)} nie jest już wolny. Sprawdź inne godziny przez get_free_times.`);
|
|
281
290
|
}
|
|
291
|
+
const formats = formatsOf(context);
|
|
282
292
|
const [salon] = await port.getSalons([salonId]);
|
|
283
293
|
const where = salon ? `${salon.name} (${salon.address})` : "salon";
|
|
284
294
|
const code = discountCode === undefined ? "" : `, z kodem rabatowym ${discountCode}`;
|
|
285
|
-
const what = `„${service.name}” w ${where}, ${day} o ${timeOfMinutes(startMinutes)}, ${service.durationMinutes}
|
|
295
|
+
const what = `„${service.name}” w ${where}, ${day} o ${timeOfMinutes(startMinutes)}, ${formats.duration(service.durationMinutes)}, cena ${formats.price(service.pricePln)}${code}`;
|
|
286
296
|
if (!readBoolean(args, "confirmed"))
|
|
287
297
|
return gate.preview(`Zarezerwuję: ${what}.`, echo);
|
|
288
298
|
const refused = gate.redeem(args, echo);
|
|
289
299
|
if (refused)
|
|
290
300
|
return refused;
|
|
291
301
|
const appointmentId = await port.book({ salonId, serviceId, workerId, startAt, notes: notes ?? "", discountCode });
|
|
292
|
-
return ok(`Zarezerwowano: ${what}.${await pricePaidNote(port, appointmentId, service.pricePln)} Wizyta jest w zakładce „Wizyty”.`, { appointmentId });
|
|
302
|
+
return ok(`Zarezerwowano: ${what}.${await pricePaidNote(port, appointmentId, service.pricePln, formats)} Wizyta jest w zakładce „Wizyty”.`, { appointmentId });
|
|
293
303
|
}
|
|
294
304
|
catch (error) {
|
|
295
305
|
if (error instanceof ArgumentError)
|
|
@@ -301,12 +311,12 @@ export function bookAppointmentTool(context, confirmations) {
|
|
|
301
311
|
}
|
|
302
312
|
/** What the customer really pays, when a discount code changed it. Never fails a booking that is
|
|
303
313
|
* already made — and a query right after an update may not see it yet. */
|
|
304
|
-
async function pricePaidNote(port, appointmentId, listPricePln) {
|
|
314
|
+
async function pricePaidNote(port, appointmentId, listPricePln, formats) {
|
|
305
315
|
try {
|
|
306
316
|
const booked = (await port.listMyAppointments()).find((appointment) => appointment.id === appointmentId);
|
|
307
317
|
if (!booked || booked.pricePln === listPricePln)
|
|
308
318
|
return "";
|
|
309
|
-
return ` Cena po rabacie: ${booked.pricePln}
|
|
319
|
+
return ` Cena po rabacie: ${formats.price(booked.pricePln)}.`;
|
|
310
320
|
}
|
|
311
321
|
catch {
|
|
312
322
|
return "";
|
|
@@ -351,6 +361,7 @@ export function listMyAppointmentsTool(context) {
|
|
|
351
361
|
const upcoming = await upcomingAppointments(port, context.now());
|
|
352
362
|
if (upcoming.length === 0)
|
|
353
363
|
return ok("Klient nie ma nadchodzących wizyt.", []);
|
|
364
|
+
const formats = formatsOf(context);
|
|
354
365
|
return ok(`Nadchodzące wizyty: ${upcoming.length}.`, upcoming.map((appointment) => ({
|
|
355
366
|
appointmentId: appointment.id,
|
|
356
367
|
salonId: appointment.salonId,
|
|
@@ -358,8 +369,8 @@ export function listMyAppointmentsTool(context) {
|
|
|
358
369
|
address: appointment.salon?.address ?? "",
|
|
359
370
|
serviceName: appointment.serviceName ?? "",
|
|
360
371
|
start: localStampOf(appointment.startAt),
|
|
361
|
-
|
|
362
|
-
|
|
372
|
+
duration: formats.duration(appointment.durationMinutes),
|
|
373
|
+
price: formats.price(appointment.pricePln),
|
|
363
374
|
})));
|
|
364
375
|
}
|
|
365
376
|
catch (error) {
|
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export type { CreateCustomerReadToolsOptions, CreateCustomerToolsFromPortOptions
|
|
|
7
7
|
export type { AppointmentStatusView, AppointmentView, BookingInput, Coordinates, CustomerCorePort, CustomerPromoView, CustomerServiceView, SalonSlotsView, SalonView, SlotSearchView, } from "./customer/customer-core-port.js";
|
|
8
8
|
export { IcCustomerCorePort } from "./customer/ic-customer-core-port.js";
|
|
9
9
|
export type { IcCustomerCorePortOptions } from "./customer/ic-customer-core-port.js";
|
|
10
|
+
export { customerFormats, DEFAULT_CUSTOMER_LOCALE } from "./customer/format.js";
|
|
11
|
+
export type { CustomerFormats } from "./customer/format.js";
|
|
10
12
|
export { CUSTOMER_AGENT_PERSONA_PL } from "./customer/persona.js";
|
|
11
13
|
export { hostTimeZoneProblem } from "./customer/time.js";
|
|
12
14
|
export { IcSalonCorePort } from "./ic-salon-core-port.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,2BAA2B,EAAE,MAAM,oBAAoB,CAAC;AACrF,YAAY,EACR,yBAAyB,EACzB,mBAAmB,EACnB,YAAY,EACZ,iBAAiB,GACpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACH,oBAAoB,EACpB,4BAA4B,EAC5B,gBAAgB,EAChB,wBAAwB,GAC3B,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACR,2BAA2B,EAC3B,+BAA+B,EAC/B,uBAAuB,GAC1B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACH,uBAAuB,EACvB,+BAA+B,EAC/B,mBAAmB,EACnB,2BAA2B,EAC3B,uBAAuB,GAC1B,MAAM,qCAAqC,CAAC;AAC7C,YAAY,EACR,8BAA8B,EAC9B,kCAAkC,EAClC,0BAA0B,GAC7B,MAAM,qCAAqC,CAAC;AAC7C,YAAY,EACR,qBAAqB,EACrB,eAAe,EACf,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,cAAc,EACd,SAAS,EACT,cAAc,GACjB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AACzE,YAAY,EAAE,yBAAyB,EAAE,MAAM,qCAAqC,CAAC;AACrF,OAAO,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,YAAY,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AACtE,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAC7E,YAAY,EACR,iBAAiB,EACjB,gBAAgB,EAChB,SAAS,EACT,SAAS,EACT,eAAe,EACf,WAAW,EACX,aAAa,EACb,cAAc,EACd,eAAe,EACf,WAAW,EACX,iBAAiB,EACjB,iBAAiB,EACjB,SAAS,EACT,UAAU,GACb,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,GAAG,EAAE,iBAAiB,EAAE,EAAE,EAAE,MAAM,YAAY,CAAC;AACxD,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,2BAA2B,EAAE,MAAM,oBAAoB,CAAC;AACrF,YAAY,EACR,yBAAyB,EACzB,mBAAmB,EACnB,YAAY,EACZ,iBAAiB,GACpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACH,oBAAoB,EACpB,4BAA4B,EAC5B,gBAAgB,EAChB,wBAAwB,GAC3B,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACR,2BAA2B,EAC3B,+BAA+B,EAC/B,uBAAuB,GAC1B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACH,uBAAuB,EACvB,+BAA+B,EAC/B,mBAAmB,EACnB,2BAA2B,EAC3B,uBAAuB,GAC1B,MAAM,qCAAqC,CAAC;AAC7C,YAAY,EACR,8BAA8B,EAC9B,kCAAkC,EAClC,0BAA0B,GAC7B,MAAM,qCAAqC,CAAC;AAC7C,YAAY,EACR,qBAAqB,EACrB,eAAe,EACf,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,cAAc,EACd,SAAS,EACT,cAAc,GACjB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AACzE,YAAY,EAAE,yBAAyB,EAAE,MAAM,qCAAqC,CAAC;AACrF,OAAO,EAAE,eAAe,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAChF,YAAY,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,YAAY,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AACtE,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAC7E,YAAY,EACR,iBAAiB,EACjB,gBAAgB,EAChB,SAAS,EACT,SAAS,EACT,eAAe,EACf,WAAW,EACX,aAAa,EACb,cAAc,EACd,eAAe,EACf,WAAW,EACX,iBAAiB,EACjB,iBAAiB,EACjB,SAAS,EACT,UAAU,GACb,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,GAAG,EAAE,iBAAiB,EAAE,EAAE,EAAE,MAAM,YAAY,CAAC;AACxD,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export { ConfirmationLedger, DEFAULT_CONFIRMATION_TTL_MS } from "./confirmations
|
|
|
2
2
|
export { createSalonReadTools, createSalonReadToolsFromPort, createSalonTools, createSalonToolsFromPort, } from "./create-salon-tools.js";
|
|
3
3
|
export { createCustomerReadTools, createCustomerReadToolsFromPort, createCustomerTools, createCustomerToolsFromPort, DEFAULT_SALON_TIME_ZONE, } from "./customer/create-customer-tools.js";
|
|
4
4
|
export { IcCustomerCorePort } from "./customer/ic-customer-core-port.js";
|
|
5
|
+
export { customerFormats, DEFAULT_CUSTOMER_LOCALE } from "./customer/format.js";
|
|
5
6
|
export { CUSTOMER_AGENT_PERSONA_PL } from "./customer/persona.js";
|
|
6
7
|
export { hostTimeZoneProblem } from "./customer/time.js";
|
|
7
8
|
export { IcSalonCorePort } from "./ic-salon-core-port.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsm-mit/sultana-agent-tools-package",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Agent tool layer for Sultana: framework-agnostic tools over the Sultana core canister wrapper — a salon owner's set (services, promos, schedule) and a customer's set (search, free times, booking), each with a read-only set that needs no identity.",
|
|
5
5
|
"homepage": "https://github.com/JSM-Sultana/sultana-agent-tools-package#readme",
|
|
6
6
|
"bugs": {
|