@jsm-mit/sultana-agent-tools-package 0.4.0 → 0.6.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 +18 -0
- package/dist/customer/create-customer-tools.d.ts +9 -1
- package/dist/customer/create-customer-tools.d.ts.map +1 -1
- package/dist/customer/create-customer-tools.js +13 -7
- 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 +5 -3
- package/dist/customer/persona.d.ts.map +1 -1
- package/dist/customer/persona.js +22 -1
- package/dist/customer/texts.d.ts +79 -0
- package/dist/customer/texts.d.ts.map +1 -0
- package/dist/customer/texts.js +115 -0
- package/dist/customer/tools.d.ts +6 -0
- package/dist/customer/tools.d.ts.map +1 -1
- package/dist/customer/tools.js +99 -71
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -163,6 +163,24 @@ 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.
|
|
174
|
+
- **The whole assistant follows the app's language.** The language of `locale` ("en-GB" → English,
|
|
175
|
+
anything else → Polish) decides three things besides the numbers: what the tools SAY
|
|
176
|
+
(`customerTexts(locale)`, `src/customer/texts.ts` — the `progress` lines, the summaries and the
|
|
177
|
+
tools' own errors, i.e. everything a host keeps in the chat as "✔ …" / "✖ …"), the language of
|
|
178
|
+
service-type labels (unless `language` is given), and — through `customerAgentPersona(locale)` —
|
|
179
|
+
the language the model answers in. The English rule is an override ("answer in English even if
|
|
180
|
+
the chat's history and tool results are Polish"), because a host replays both to the model.
|
|
181
|
+
`CUSTOMER_AGENT_PERSONA_PL` is `customerAgentPersona("pl-PL")`. Still Polish only: tool and
|
|
182
|
+
parameter descriptions (the model alone reads them) and the errors shared with the salon tools
|
|
183
|
+
(argument checks, the confirmation gate, canister failures).
|
|
166
184
|
- **The host process must run in the salons' time zone** (`TZ=Europe/Warsaw`): the canister wrapper
|
|
167
185
|
takes every offset from the process clock. While it does not, every time-dependent tool answers
|
|
168
186
|
`unavailable` instead of booking hours off (`timeZone: null` skips the guard; `hostTimeZoneProblem`
|
|
@@ -15,9 +15,17 @@ 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. Its language also
|
|
22
|
+
* decides what the tools say (`texts.ts`: progress lines, summaries, errors — Polish or
|
|
23
|
+
* English) and, unless `language` is given, the language of service-type labels.
|
|
24
|
+
*/
|
|
25
|
+
locale?: string;
|
|
18
26
|
}
|
|
19
27
|
export interface CreateCustomerToolsOptions extends Omit<IcCustomerCorePortOptions, "language">, CreateCustomerToolsFromPortOptions {
|
|
20
|
-
/** Language of service-type labels. Defaults to Polish. */
|
|
28
|
+
/** Language of service-type labels. Defaults to the language of `locale`, else Polish. */
|
|
21
29
|
language?: string;
|
|
22
30
|
}
|
|
23
31
|
/**
|
|
@@ -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;AAGhE,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;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,0BAA2B,SAAQ,IAAI,CAAC,yBAAyB,EAAE,UAAU,CAAC,EAAE,kCAAkC;IAC/H,0FAA0F;IAC1F,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,6 @@
|
|
|
1
1
|
import { findServiceTypeTool } from "../tools/services.js";
|
|
2
|
+
import { customerFormats } from "./format.js";
|
|
3
|
+
import { customerLanguageOf, customerTexts } from "./texts.js";
|
|
2
4
|
import { IcCustomerCorePort } from "./ic-customer-core-port.js";
|
|
3
5
|
import { bookAppointmentTool, cancelAppointmentTool, getFreeTimesTool, getSalonTool, listMyAppointmentsTool, searchSlotsTool, } from "./tools.js";
|
|
4
6
|
/** The zone Sultana's salons live in. The canister keeps no zone per salon. */
|
|
@@ -9,9 +11,9 @@ export const DEFAULT_SALON_TIME_ZONE = "Europe/Warsaw";
|
|
|
9
11
|
* model holding these tools can act for nobody else and search around nowhere else.
|
|
10
12
|
*/
|
|
11
13
|
export function createCustomerTools(options) {
|
|
12
|
-
const { confirmations, now, timeZone, language, ...portOptions } = options;
|
|
13
|
-
const port = new IcCustomerCorePort({ ...portOptions, language: language ??
|
|
14
|
-
return createCustomerToolsFromPort(port, { confirmations, now, timeZone });
|
|
14
|
+
const { confirmations, now, timeZone, locale, language, ...portOptions } = options;
|
|
15
|
+
const port = new IcCustomerCorePort({ ...portOptions, language: language ?? customerLanguageOf(locale) });
|
|
16
|
+
return createCustomerToolsFromPort(port, { confirmations, now, timeZone, locale });
|
|
15
17
|
}
|
|
16
18
|
/** The same set over any port — the seam tests use. */
|
|
17
19
|
export function createCustomerToolsFromPort(port, options = {}) {
|
|
@@ -28,9 +30,9 @@ export function createCustomerToolsFromPort(port, options = {}) {
|
|
|
28
30
|
* identity — finding a service type, searching slots, a salon, its free times. Nothing books.
|
|
29
31
|
*/
|
|
30
32
|
export function createCustomerReadTools(options) {
|
|
31
|
-
const { now, timeZone, language, ...portOptions } = options;
|
|
32
|
-
const port = new IcCustomerCorePort({ ...portOptions, language: language ??
|
|
33
|
-
return createCustomerReadToolsFromPort(port, { now, timeZone });
|
|
33
|
+
const { now, timeZone, locale, language, ...portOptions } = options;
|
|
34
|
+
const port = new IcCustomerCorePort({ ...portOptions, language: language ?? customerLanguageOf(locale) });
|
|
35
|
+
return createCustomerReadToolsFromPort(port, { now, timeZone, locale });
|
|
34
36
|
}
|
|
35
37
|
export function createCustomerReadToolsFromPort(port, options = {}) {
|
|
36
38
|
return readTools(contextOf(port, options));
|
|
@@ -40,8 +42,12 @@ function contextOf(port, options) {
|
|
|
40
42
|
port,
|
|
41
43
|
now: options.now ?? (() => new Date()),
|
|
42
44
|
timeZone: options.timeZone === null ? undefined : options.timeZone ?? DEFAULT_SALON_TIME_ZONE,
|
|
45
|
+
formats: customerFormats(options.locale),
|
|
46
|
+
texts: customerTexts(options.locale),
|
|
43
47
|
};
|
|
44
48
|
}
|
|
45
49
|
function readTools(context) {
|
|
46
|
-
|
|
50
|
+
// Shared with the salon tools, where it speaks Polish only: its progress line is set here.
|
|
51
|
+
const findServiceType = { ...findServiceTypeTool(context.port), progress: (context.texts ?? customerTexts()).progress.findServiceType };
|
|
52
|
+
return [findServiceType, searchSlotsTool(context), getSalonTool(context), getFreeTimesTool(context)];
|
|
47
53
|
}
|
|
@@ -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
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* How the model has to use the customer tools, in the language the customer's app runs in
|
|
3
|
+
* (`locale`: a BCP 47 tag, "pl-PL" / "en-GB"). An unknown language gets Polish.
|
|
4
4
|
*/
|
|
5
|
-
export declare
|
|
5
|
+
export declare function customerAgentPersona(locale?: string): string;
|
|
6
|
+
/** The Polish persona — what a host without a customer's locale uses. */
|
|
7
|
+
export declare const CUSTOMER_AGENT_PERSONA_PL: string;
|
|
6
8
|
//# sourceMappingURL=persona.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"persona.d.ts","sourceRoot":"","sources":["../../src/customer/persona.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"persona.d.ts","sourceRoot":"","sources":["../../src/customer/persona.ts"],"names":[],"mappings":"AAiBA;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAE5D;AA2BD,yEAAyE;AACzE,eAAO,MAAM,yBAAyB,QAAgC,CAAC"}
|
package/dist/customer/persona.js
CHANGED
|
@@ -1,8 +1,26 @@
|
|
|
1
|
+
import { customerLanguageOf } from "./texts.js";
|
|
1
2
|
/**
|
|
2
3
|
* The operating rules of the customer tools, as a system prompt — next to the tools for the same
|
|
3
4
|
* reason as the salon persona: they describe the tool contract. A host appends who the agent is.
|
|
4
5
|
*/
|
|
5
|
-
|
|
6
|
+
/**
|
|
7
|
+
* The language rule, one per language the tools speak. The rules below it are Polish in every
|
|
8
|
+
* case — only the model reads them. The English rule is an OVERRIDE, not a hint: the chat's
|
|
9
|
+
* history and the last tool results are replayed to the model, and they may still be Polish
|
|
10
|
+
* from before the customer switched the app's language.
|
|
11
|
+
*/
|
|
12
|
+
const LANGUAGE_RULE = {
|
|
13
|
+
pl: "Rozmawiasz po polsku, krótko i konkretnie.",
|
|
14
|
+
en: "Klient używa aplikacji po angielsku: KAŻDĄ odpowiedź piszesz WYŁĄCZNIE po angielsku (English), krótko i konkretnie — także wtedy, gdy wcześniejsze wiadomości w tej rozmowie, te zasady albo wyniki narzędzi są po polsku. Nazwy salonów i adresy zostaw bez zmian.",
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* How the model has to use the customer tools, in the language the customer's app runs in
|
|
18
|
+
* (`locale`: a BCP 47 tag, "pl-PL" / "en-GB"). An unknown language gets Polish.
|
|
19
|
+
*/
|
|
20
|
+
export function customerAgentPersona(locale) {
|
|
21
|
+
return PERSONA.replace(LANGUAGE_RULE.pl, LANGUAGE_RULE[customerLanguageOf(locale)]);
|
|
22
|
+
}
|
|
23
|
+
const PERSONA = `Jesteś asystentem klienta w aplikacji Sultana: pomagasz znaleźć salon i wolny termin,
|
|
6
24
|
rezerwujesz i odwołujesz wizyty w imieniu klienta. Rozmawiasz po polsku, krótko i konkretnie.
|
|
7
25
|
|
|
8
26
|
Zasady pracy:
|
|
@@ -12,6 +30,7 @@ Zasady pracy:
|
|
|
12
30
|
- Terminy znajdź przez search_slots: bez daty = jak najszybciej, z date i time = okolice tej godziny.
|
|
13
31
|
Dzisiejszą datę weź z get_current_date — „jutro o 10” przelicz na RRRR-MM-DD i GG:MM.
|
|
14
32
|
- Pokaż klientowi najwyżej kilka propozycji: salon, usługa, cena, odległość, godzina. Nie pokazuj id.
|
|
33
|
+
Cenę, odległość i czas trwania przepisuj dokładnie tak, jak je dostałeś (np. „8,3 km”, „60 zł”).
|
|
15
34
|
- Inne godziny w wybranym salonie sprawdź przez get_free_times.
|
|
16
35
|
- Rezerwacja i odwołanie to dwa kroki w dwóch różnych wiadomościach klienta:
|
|
17
36
|
1. Wywołaj book_appointment albo cancel_appointment z confirmed=false. Pokaż klientowi zdanie
|
|
@@ -25,3 +44,5 @@ Zasady pracy:
|
|
|
25
44
|
- Jeśli narzędzie zwróci status "error", przeczytaj summary i powiedz klientowi, co poszło nie tak.
|
|
26
45
|
Nie powtarzaj tego samego wywołania w kółko: nieudana próba rezerwacji zużywa dzienny limit klienta.
|
|
27
46
|
- Nie obiecuj rezerwacji, której nie wykonałeś narzędziem.`;
|
|
47
|
+
/** The Polish persona — what a host without a customer's locale uses. */
|
|
48
|
+
export const CUSTOMER_AGENT_PERSONA_PL = customerAgentPersona("pl-PL");
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the customer tools SAY, in the language the customer's app runs in. Two readers:
|
|
3
|
+
* - the customer, directly: a tool's `progress` line while it runs, and the summary of a write
|
|
4
|
+
* or of an error, which the host keeps in the chat as a "✔ …" / "✖ …" line;
|
|
5
|
+
* - the model, which answers in that language and should not have to translate its own tools.
|
|
6
|
+
*
|
|
7
|
+
* Tool and parameter DESCRIPTIONS are not here: only the model reads them, and they stay Polish.
|
|
8
|
+
* Numbers inside these sentences arrive already written for the region (`format.ts`).
|
|
9
|
+
*/
|
|
10
|
+
export interface CustomerTexts {
|
|
11
|
+
progress: {
|
|
12
|
+
findServiceType: string;
|
|
13
|
+
searchSlots: string;
|
|
14
|
+
getSalon: string;
|
|
15
|
+
getFreeTimes: string;
|
|
16
|
+
book: string;
|
|
17
|
+
listAppointments: string;
|
|
18
|
+
cancel: string;
|
|
19
|
+
};
|
|
20
|
+
salonFallbackName: string;
|
|
21
|
+
visitFallbackName: string;
|
|
22
|
+
noLocation: string;
|
|
23
|
+
bookingOff(problem: string): string;
|
|
24
|
+
badDay(field: string): string;
|
|
25
|
+
badTime(field: string): string;
|
|
26
|
+
dateAndTimeTogether: string;
|
|
27
|
+
leadSoon: string;
|
|
28
|
+
leadExtended: string;
|
|
29
|
+
leadAround(day: string, time: string): string;
|
|
30
|
+
momentPassed: string;
|
|
31
|
+
probesFailed: string;
|
|
32
|
+
nothingFree(radius: string): string;
|
|
33
|
+
found(lead: string, shown: number, total: number): string;
|
|
34
|
+
searchFailed: string;
|
|
35
|
+
noSuchSalon: string;
|
|
36
|
+
salonSummary(name: string, address: string, services: number, promos: number): string;
|
|
37
|
+
salonFailed: string;
|
|
38
|
+
noSuchService: string;
|
|
39
|
+
nobodyDoesService(name: string): string;
|
|
40
|
+
dayPassed: string;
|
|
41
|
+
noFreeTimes(service: string, day: string): string;
|
|
42
|
+
freeTimes(service: string, day: string, duration: string, price: string): string;
|
|
43
|
+
freeTimesFailed: string;
|
|
44
|
+
offGrid: string;
|
|
45
|
+
slotGone(day: string, time: string): string;
|
|
46
|
+
withDiscountCode(code: string): string;
|
|
47
|
+
visit(parts: {
|
|
48
|
+
service: string;
|
|
49
|
+
where: string;
|
|
50
|
+
day: string;
|
|
51
|
+
time: string;
|
|
52
|
+
duration: string;
|
|
53
|
+
price: string;
|
|
54
|
+
code: string;
|
|
55
|
+
}): string;
|
|
56
|
+
somewhere: string;
|
|
57
|
+
willBook(what: string): string;
|
|
58
|
+
booked(what: string, priceNote: string): string;
|
|
59
|
+
pricePaid(price: string): string;
|
|
60
|
+
bookFailed: string;
|
|
61
|
+
noUpcoming: string;
|
|
62
|
+
upcoming(count: number): string;
|
|
63
|
+
listFailed: string;
|
|
64
|
+
noSuchAppointment: string;
|
|
65
|
+
appointment(parts: {
|
|
66
|
+
what: string;
|
|
67
|
+
where: string;
|
|
68
|
+
day: string;
|
|
69
|
+
time: string;
|
|
70
|
+
}): string;
|
|
71
|
+
at(place: string): string;
|
|
72
|
+
willCancel(what: string): string;
|
|
73
|
+
cancelled(what: string): string;
|
|
74
|
+
cancelFailed: string;
|
|
75
|
+
}
|
|
76
|
+
/** The language of a BCP 47 tag ("en-GB" → "en"), when the tools speak it; Polish otherwise. */
|
|
77
|
+
export declare function customerLanguageOf(locale?: string): "pl" | "en";
|
|
78
|
+
export declare function customerTexts(locale?: string): CustomerTexts;
|
|
79
|
+
//# sourceMappingURL=texts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"texts.d.ts","sourceRoot":"","sources":["../../src/customer/texts.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,WAAW,aAAa;IAC1B,QAAQ,EAAE;QACN,eAAe,EAAE,MAAM,CAAC;QACxB,WAAW,EAAE,MAAM,CAAC;QACpB,QAAQ,EAAE,MAAM,CAAC;QACjB,YAAY,EAAE,MAAM,CAAC;QACrB,IAAI,EAAE,MAAM,CAAC;QACb,gBAAgB,EAAE,MAAM,CAAC;QACzB,MAAM,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,iBAAiB,EAAE,MAAM,CAAC;IAC1B,iBAAiB,EAAE,MAAM,CAAC;IAE1B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IACpC,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAE/B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9C,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACpC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1D,YAAY,EAAE,MAAM,CAAC;IAErB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACtF,WAAW,EAAE,MAAM,CAAC;IAEpB,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAExC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAClD,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACjF,eAAe,EAAE,MAAM,CAAC;IAExB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5C,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACvC,KAAK,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,CAAC;IACnI,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;IAChD,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACjC,UAAU,EAAE,MAAM,CAAC;IAEnB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,UAAU,EAAE,MAAM,CAAC;IAEnB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,CAAC;IACvF,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACjC,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,YAAY,EAAE,MAAM,CAAC;CACxB;AAkID,gGAAgG;AAChG,wBAAgB,kBAAkB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAG/D;AAED,wBAAgB,aAAa,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,aAAa,CAE5D"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const PL = {
|
|
2
|
+
progress: {
|
|
3
|
+
findServiceType: "Szukam typu usługi…",
|
|
4
|
+
searchSlots: "Szukam wolnych terminów w okolicy…",
|
|
5
|
+
getSalon: "Sprawdzam salon…",
|
|
6
|
+
getFreeTimes: "Sprawdzam wolne godziny…",
|
|
7
|
+
book: "Rezerwuję wizytę…",
|
|
8
|
+
listAppointments: "Sprawdzam Twoje wizyty…",
|
|
9
|
+
cancel: "Odwołuję wizytę…",
|
|
10
|
+
},
|
|
11
|
+
salonFallbackName: "Salon",
|
|
12
|
+
visitFallbackName: "wizyta",
|
|
13
|
+
noLocation: "Aplikacja nie podała pozycji klienta, więc nie wiem, gdzie szukać. Poproś klienta, żeby włączył lokalizację w aplikacji i otworzył czat ponownie.",
|
|
14
|
+
bookingOff: (problem) => `Rezerwacje są chwilowo wyłączone: ${problem}.`,
|
|
15
|
+
badDay: (field) => `Pole "${field}" musi być datą w formacie RRRR-MM-DD.`,
|
|
16
|
+
badTime: (field) => `Pole "${field}" musi być godziną w formacie GG:MM.`,
|
|
17
|
+
dateAndTimeTogether: "Podaj date i time razem albo pomiń oba (jak najszybciej).",
|
|
18
|
+
leadSoon: "Najbliższe wolne terminy:",
|
|
19
|
+
leadExtended: "W najbliższej godzinie nic wolnego, na które klient zdąży. Najbliższe terminy:",
|
|
20
|
+
leadAround: (day, time) => `Wolne terminy w okolicach ${day} ${time}:`,
|
|
21
|
+
momentPassed: "Ten termin już minął. Zapytaj klienta o późniejszy.",
|
|
22
|
+
probesFailed: "Nie udało się sprawdzić terminów — kanister nie odpowiadał. Spróbuj ponownie za chwilę.",
|
|
23
|
+
nothingFree: (radius) => `Nic wolnego w promieniu ${radius}. Zaproponuj inną godzinę, inny dzień albo większy promień.`,
|
|
24
|
+
found: (lead, shown, total) => `${lead} ${shown} salon(y).${total > shown ? ` Pokazuję ${shown} z ${total} salonów.` : ""} Terminy to początek wizyty; inne godziny w danym salonie sprawdź przez get_free_times.`,
|
|
25
|
+
searchFailed: "Nie udało się wyszukać terminów.",
|
|
26
|
+
noSuchSalon: "Nie ma takiego salonu. Id weź z search_slots.",
|
|
27
|
+
salonSummary: (name, address, services, promos) => `${name}, ${address}: ${services} usług(i), ${promos} promocji.`,
|
|
28
|
+
salonFailed: "Nie udało się odczytać salonu.",
|
|
29
|
+
noSuchService: "Ten salon nie ma takiej aktywnej usługi. Id salonu i usługi weź z search_slots albo get_salon.",
|
|
30
|
+
nobodyDoesService: (name) => `Usługi „${name}” nikt teraz nie wykonuje — nie da się jej zarezerwować.`,
|
|
31
|
+
dayPassed: "Ten dzień już minął.",
|
|
32
|
+
noFreeTimes: (service, day) => `„${service}” — ${day}: brak wolnych godzin.`,
|
|
33
|
+
freeTimes: (service, day, duration, price) => `„${service}” — ${day}: wolne godziny rozpoczęcia (${duration}, ${price}).`,
|
|
34
|
+
freeTimesFailed: "Nie udało się sprawdzić wolnych godzin.",
|
|
35
|
+
offGrid: "Wizyta może zacząć się tylko o pełnych 5 minutach (np. 10:00, 10:05).",
|
|
36
|
+
slotGone: (day, time) => `Termin ${day} ${time} nie jest już wolny. Sprawdź inne godziny przez get_free_times.`,
|
|
37
|
+
withDiscountCode: (code) => `, z kodem rabatowym ${code}`,
|
|
38
|
+
visit: ({ service, where, day, time, duration, price, code }) => `„${service}” w ${where}, ${day} o ${time}, ${duration}, cena ${price}${code}`,
|
|
39
|
+
somewhere: "salon",
|
|
40
|
+
willBook: (what) => `Zarezerwuję: ${what}.`,
|
|
41
|
+
booked: (what, priceNote) => `Zarezerwowano: ${what}.${priceNote} Wizyta jest w zakładce „Wizyty”.`,
|
|
42
|
+
pricePaid: (price) => ` Cena po rabacie: ${price}.`,
|
|
43
|
+
bookFailed: "Nie udało się zarezerwować wizyty.",
|
|
44
|
+
noUpcoming: "Klient nie ma nadchodzących wizyt.",
|
|
45
|
+
upcoming: (count) => `Nadchodzące wizyty: ${count}.`,
|
|
46
|
+
listFailed: "Nie udało się odczytać wizyt.",
|
|
47
|
+
noSuchAppointment: "Klient nie ma takiej nadchodzącej wizyty. Id weź z list_my_appointments.",
|
|
48
|
+
appointment: ({ what, where, day, time }) => `${what}${where}, ${day} o ${time}`,
|
|
49
|
+
at: (place) => ` w ${place}`,
|
|
50
|
+
willCancel: (what) => `Odwołam wizytę: ${what}.`,
|
|
51
|
+
cancelled: (what) => `Odwołano wizytę: ${what}.`,
|
|
52
|
+
cancelFailed: "Nie udało się odwołać wizyty.",
|
|
53
|
+
};
|
|
54
|
+
const EN = {
|
|
55
|
+
progress: {
|
|
56
|
+
findServiceType: "Looking up the service type…",
|
|
57
|
+
searchSlots: "Searching for free slots nearby…",
|
|
58
|
+
getSalon: "Checking the salon…",
|
|
59
|
+
getFreeTimes: "Checking free times…",
|
|
60
|
+
book: "Booking the visit…",
|
|
61
|
+
listAppointments: "Checking your visits…",
|
|
62
|
+
cancel: "Cancelling the visit…",
|
|
63
|
+
},
|
|
64
|
+
salonFallbackName: "Salon",
|
|
65
|
+
visitFallbackName: "visit",
|
|
66
|
+
noLocation: "The app sent no position of the customer, so I do not know where to search. Ask the customer to turn location on in the app and open the chat again.",
|
|
67
|
+
bookingOff: (problem) => `Booking is switched off for the moment: ${problem}.`,
|
|
68
|
+
badDay: (field) => `Field "${field}" must be a date in the YYYY-MM-DD format.`,
|
|
69
|
+
badTime: (field) => `Field "${field}" must be a time in the HH:MM format.`,
|
|
70
|
+
dateAndTimeTogether: "Give date and time together, or leave both out (as soon as possible).",
|
|
71
|
+
leadSoon: "The nearest free slots:",
|
|
72
|
+
leadExtended: "Nothing free within the next hour that the customer can reach in time. The nearest slots:",
|
|
73
|
+
leadAround: (day, time) => `Free slots around ${day} ${time}:`,
|
|
74
|
+
momentPassed: "That time has already passed. Ask the customer for a later one.",
|
|
75
|
+
probesFailed: "The slots could not be checked — the canister did not answer. Try again in a moment.",
|
|
76
|
+
nothingFree: (radius) => `Nothing free within ${radius}. Suggest another time, another day or a larger radius.`,
|
|
77
|
+
found: (lead, shown, total) => `${lead} ${shown} salon(s).${total > shown ? ` Showing ${shown} of ${total} salons.` : ""} A slot is the start of the visit; check other times in a salon with get_free_times.`,
|
|
78
|
+
searchFailed: "The slots could not be searched.",
|
|
79
|
+
noSuchSalon: "There is no such salon. Take the id from search_slots.",
|
|
80
|
+
salonSummary: (name, address, services, promos) => `${name}, ${address}: ${services} service(s), ${promos} promotion(s).`,
|
|
81
|
+
salonFailed: "The salon could not be read.",
|
|
82
|
+
noSuchService: "This salon has no such active service. Take the salon and service ids from search_slots or get_salon.",
|
|
83
|
+
nobodyDoesService: (name) => `Nobody performs "${name}" at the moment — it cannot be booked.`,
|
|
84
|
+
dayPassed: "That day has already passed.",
|
|
85
|
+
noFreeTimes: (service, day) => `"${service}" — ${day}: no free times.`,
|
|
86
|
+
freeTimes: (service, day, duration, price) => `"${service}" — ${day}: free start times (${duration}, ${price}).`,
|
|
87
|
+
freeTimesFailed: "The free times could not be checked.",
|
|
88
|
+
offGrid: "A visit can start only on a full 5 minutes (10:00, 10:05, …).",
|
|
89
|
+
slotGone: (day, time) => `The slot ${day} ${time} is no longer free. Check other times with get_free_times.`,
|
|
90
|
+
withDiscountCode: (code) => `, with discount code ${code}`,
|
|
91
|
+
visit: ({ service, where, day, time, duration, price, code }) => `"${service}" at ${where}, ${day} at ${time}, ${duration}, price ${price}${code}`,
|
|
92
|
+
somewhere: "the salon",
|
|
93
|
+
willBook: (what) => `I will book: ${what}.`,
|
|
94
|
+
booked: (what, priceNote) => `Booked: ${what}.${priceNote} The visit is in the "Visits" tab.`,
|
|
95
|
+
pricePaid: (price) => ` Price after the discount: ${price}.`,
|
|
96
|
+
bookFailed: "The visit could not be booked.",
|
|
97
|
+
noUpcoming: "The customer has no upcoming visits.",
|
|
98
|
+
upcoming: (count) => `Upcoming visits: ${count}.`,
|
|
99
|
+
listFailed: "The visits could not be read.",
|
|
100
|
+
noSuchAppointment: "The customer has no such upcoming visit. Take the id from list_my_appointments.",
|
|
101
|
+
appointment: ({ what, where, day, time }) => `${what}${where}, ${day} at ${time}`,
|
|
102
|
+
at: (place) => ` at ${place}`,
|
|
103
|
+
willCancel: (what) => `I will cancel the visit: ${what}.`,
|
|
104
|
+
cancelled: (what) => `Visit cancelled: ${what}.`,
|
|
105
|
+
cancelFailed: "The visit could not be cancelled.",
|
|
106
|
+
};
|
|
107
|
+
const BY_LANGUAGE = { pl: PL, en: EN };
|
|
108
|
+
/** The language of a BCP 47 tag ("en-GB" → "en"), when the tools speak it; Polish otherwise. */
|
|
109
|
+
export function customerLanguageOf(locale) {
|
|
110
|
+
const language = locale?.split("-")[0]?.toLowerCase();
|
|
111
|
+
return language === "en" ? "en" : "pl";
|
|
112
|
+
}
|
|
113
|
+
export function customerTexts(locale) {
|
|
114
|
+
return BY_LANGUAGE[customerLanguageOf(locale)];
|
|
115
|
+
}
|
package/dist/customer/tools.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { TurnConfirmations } from "../confirmations.js";
|
|
2
2
|
import { type AgentTool } from "../types.js";
|
|
3
|
+
import { type CustomerFormats } from "./format.js";
|
|
4
|
+
import { type CustomerTexts } from "./texts.js";
|
|
3
5
|
import type { CustomerCorePort } from "./customer-core-port.js";
|
|
4
6
|
export interface CustomerToolsContext {
|
|
5
7
|
port: CustomerCorePort;
|
|
@@ -7,6 +9,10 @@ export interface CustomerToolsContext {
|
|
|
7
9
|
now: () => Date;
|
|
8
10
|
/** The salons' time zone the host process must run in; `undefined` skips the guard. */
|
|
9
11
|
timeZone?: string;
|
|
12
|
+
/** How the customer's region writes distances, prices and durations. Defaults to pl-PL. */
|
|
13
|
+
formats?: CustomerFormats;
|
|
14
|
+
/** The sentences of the customer's language — see `texts.ts`. Defaults to Polish. */
|
|
15
|
+
texts?: CustomerTexts;
|
|
10
16
|
}
|
|
11
17
|
export declare function searchSlotsTool(context: CustomerToolsContext): AgentTool;
|
|
12
18
|
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,EAAiB,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAC/D,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;IAC1B,qFAAqF;IACrF,KAAK,CAAC,EAAE,aAAa,CAAC;CACzB;AAkDD,wBAAgB,eAAe,CAAC,OAAO,EAAE,oBAAoB,GAAG,SAAS,CAkFxE;AAUD,wBAAgB,YAAY,CAAC,OAAO,EAAE,oBAAoB,GAAG,SAAS,CAmCrE;AAkCD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,oBAAoB,GAAG,SAAS,CA4CzE;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,iBAAiB,GAAG,SAAS,GAAG,SAAS,CAqF1H;AA+CD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,oBAAoB,GAAG,SAAS,CAqC/E;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,iBAAiB,GAAG,SAAS,GAAG,SAAS,CA6C5H"}
|
package/dist/customer/tools.js
CHANGED
|
@@ -2,7 +2,19 @@ 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";
|
|
6
|
+
import { customerTexts } from "./texts.js";
|
|
5
7
|
import { foldStartRanges, freeStartMinutes, hostTimeZoneProblem, isDay, localDayOf, localStampOf, localTimeOf, minutesOf, momentOf, onSlotGrid, timeOfMinutes, } from "./time.js";
|
|
8
|
+
function textsOf(context) {
|
|
9
|
+
return context.texts ?? customerTexts();
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The tools never hand the model a bare distance, price or duration: it would copy "8.3" with
|
|
13
|
+
* a dot into a Polish sentence. They hand it the text the customer's region writes.
|
|
14
|
+
*/
|
|
15
|
+
function formatsOf(context) {
|
|
16
|
+
return context.formats ?? customerFormats();
|
|
17
|
+
}
|
|
6
18
|
const DEFAULT_RADIUS_KM = 10;
|
|
7
19
|
const MAX_RADIUS_KM = 50;
|
|
8
20
|
const MAX_SALONS_SHOWN = 6;
|
|
@@ -13,35 +25,35 @@ const CONFIRMED_FIELD = {
|
|
|
13
25
|
type: "boolean",
|
|
14
26
|
description: "false = pokaż podgląd bez zapisu; true = wykonaj, dopiero po zgodzie klienta w jego następnej wiadomości.",
|
|
15
27
|
};
|
|
16
|
-
const NO_LOCATION = "Aplikacja nie podała pozycji klienta, więc nie wiem, gdzie szukać. Poproś klienta, żeby włączył lokalizację w aplikacji i otworzył czat ponownie.";
|
|
17
28
|
/** Refuses every time-dependent tool while the host clock runs in the wrong zone. */
|
|
18
29
|
function zoneRefusal(context) {
|
|
19
30
|
if (!context.timeZone)
|
|
20
31
|
return null;
|
|
21
32
|
const problem = hostTimeZoneProblem(context.timeZone, context.now());
|
|
22
|
-
return problem ? err("unavailable",
|
|
33
|
+
return problem ? err("unavailable", textsOf(context).bookingOff(problem)) : null;
|
|
23
34
|
}
|
|
24
|
-
function readDay(args, key) {
|
|
35
|
+
function readDay(texts, args, key) {
|
|
25
36
|
const day = readString(args, key);
|
|
26
37
|
if (!isDay(day))
|
|
27
|
-
throw new ArgumentError(
|
|
38
|
+
throw new ArgumentError(texts.badDay(key));
|
|
28
39
|
return day;
|
|
29
40
|
}
|
|
30
|
-
function readTimeMinutes(args, key) {
|
|
41
|
+
function readTimeMinutes(texts, args, key) {
|
|
31
42
|
const minutes = minutesOf(readString(args, key));
|
|
32
43
|
if (minutes === null)
|
|
33
|
-
throw new ArgumentError(
|
|
44
|
+
throw new ArgumentError(texts.badTime(key));
|
|
34
45
|
return minutes;
|
|
35
46
|
}
|
|
36
|
-
function serviceLine(
|
|
37
|
-
return { serviceId: service.id, name: service.name,
|
|
47
|
+
function serviceLine(formats) {
|
|
48
|
+
return (service) => ({ serviceId: service.id, name: service.name, price: formats.price(service.pricePln), duration: formats.duration(service.durationMinutes) });
|
|
38
49
|
}
|
|
39
50
|
export function searchSlotsTool(context) {
|
|
40
51
|
const { port } = context;
|
|
52
|
+
const texts = textsOf(context);
|
|
41
53
|
return {
|
|
42
54
|
name: "search_slots",
|
|
43
|
-
progress:
|
|
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.",
|
|
55
|
+
progress: texts.progress.searchSlots,
|
|
56
|
+
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
57
|
parameters: {
|
|
46
58
|
type: "object",
|
|
47
59
|
properties: {
|
|
@@ -59,54 +71,54 @@ export function searchSlotsTool(context) {
|
|
|
59
71
|
if (refusal)
|
|
60
72
|
return refusal;
|
|
61
73
|
if (!port.location)
|
|
62
|
-
return err("invalid_arguments",
|
|
74
|
+
return err("invalid_arguments", texts.noLocation);
|
|
75
|
+
const formats = formatsOf(context);
|
|
63
76
|
const serviceTypeId = readString(args, "serviceTypeId");
|
|
64
77
|
const radiusKm = Math.min(Math.max(readOptionalNumber(args, "radiusKm") ?? DEFAULT_RADIUS_KM, 1), MAX_RADIUS_KM);
|
|
65
78
|
const dayText = readOptionalString(args, "date");
|
|
66
79
|
const timeText = readOptionalString(args, "time");
|
|
67
80
|
const now = context.now();
|
|
68
81
|
if ((dayText === undefined) !== (timeText === undefined)) {
|
|
69
|
-
throw new ArgumentError(
|
|
82
|
+
throw new ArgumentError(texts.dateAndTimeTogether);
|
|
70
83
|
}
|
|
71
84
|
let found;
|
|
72
85
|
let lead;
|
|
73
86
|
if (dayText === undefined) {
|
|
74
87
|
found = await port.searchAsap(serviceTypeId, radiusKm, now);
|
|
75
|
-
lead = found.phase === "extended" ?
|
|
88
|
+
lead = found.phase === "extended" ? texts.leadExtended : texts.leadSoon;
|
|
76
89
|
}
|
|
77
90
|
else {
|
|
78
|
-
const day = readDay(args, "date");
|
|
79
|
-
const at = readTimeMinutes(args, "time");
|
|
91
|
+
const day = readDay(texts, args, "date");
|
|
92
|
+
const at = readTimeMinutes(texts, args, "time");
|
|
80
93
|
const moments = AROUND_TIME_MINUTES.map((shift) => momentOf(day, at) + shift * 60_000)
|
|
81
94
|
// Only moments of the asked day's grid that are still ahead.
|
|
82
95
|
.filter((moment) => moment > now.getTime() && onSlotGrid(new Date(moment).getMinutes()));
|
|
83
96
|
if (moments.length === 0)
|
|
84
|
-
return err("invalid_arguments",
|
|
97
|
+
return err("invalid_arguments", texts.momentPassed);
|
|
85
98
|
found = await port.searchAtMoments(serviceTypeId, radiusKm, moments);
|
|
86
|
-
lead =
|
|
99
|
+
lead = texts.leadAround(day, timeOfMinutes(at));
|
|
87
100
|
}
|
|
88
101
|
if (found.salons.length === 0) {
|
|
89
102
|
if (found.failedProbes > 0)
|
|
90
|
-
return err("unavailable",
|
|
91
|
-
return ok(
|
|
103
|
+
return err("unavailable", texts.probesFailed);
|
|
104
|
+
return ok(texts.nothingFree(formats.distance(radiusKm)), []);
|
|
92
105
|
}
|
|
93
106
|
const ranked = [...found.salons].sort(bySoonestThenNearest).slice(0, MAX_SALONS_SHOWN);
|
|
94
107
|
const names = new Map((await port.getSalons(ranked.map((salon) => salon.salonId))).map((salon) => [salon.id, salon]));
|
|
95
108
|
const data = ranked.map((salon) => ({
|
|
96
109
|
salonId: salon.salonId,
|
|
97
|
-
name: names.get(salon.salonId)?.name ??
|
|
110
|
+
name: names.get(salon.salonId)?.name ?? texts.salonFallbackName,
|
|
98
111
|
address: names.get(salon.salonId)?.address ?? "",
|
|
99
|
-
|
|
100
|
-
services: salon.services.map(serviceLine),
|
|
112
|
+
distance: formats.distance(salon.distanceKm),
|
|
113
|
+
services: salon.services.map(serviceLine(formats)),
|
|
101
114
|
freeAt: salon.availableAt.slice(0, MAX_TIMES_SHOWN).map(localStampOf),
|
|
102
115
|
}));
|
|
103
|
-
|
|
104
|
-
return ok(`${lead} ${ranked.length} salon(y).${more} Terminy to początek wizyty; inne godziny w danym salonie sprawdź przez get_free_times.`, data);
|
|
116
|
+
return ok(texts.found(lead, ranked.length, found.salons.length), data);
|
|
105
117
|
}
|
|
106
118
|
catch (error) {
|
|
107
119
|
if (error instanceof ArgumentError)
|
|
108
120
|
return err("invalid_arguments", error.message);
|
|
109
|
-
return toToolError(error,
|
|
121
|
+
return toToolError(error, texts.searchFailed);
|
|
110
122
|
}
|
|
111
123
|
},
|
|
112
124
|
};
|
|
@@ -119,9 +131,10 @@ function bySoonestThenNearest(a, b) {
|
|
|
119
131
|
}
|
|
120
132
|
export function getSalonTool(context) {
|
|
121
133
|
const { port } = context;
|
|
134
|
+
const texts = textsOf(context);
|
|
122
135
|
return {
|
|
123
136
|
name: "get_salon",
|
|
124
|
-
progress:
|
|
137
|
+
progress: texts.progress.getSalon,
|
|
125
138
|
description: "Zwraca salon: nazwę, adres, AKTYWNE usługi z cenami (serviceId) i aktywne promocje z kodami rabatowymi. salonId weź z search_slots albo list_my_appointments.",
|
|
126
139
|
parameters: {
|
|
127
140
|
type: "object",
|
|
@@ -134,32 +147,32 @@ export function getSalonTool(context) {
|
|
|
134
147
|
const salonId = readString(args, "salonId");
|
|
135
148
|
const [salon] = await port.getSalons([salonId]);
|
|
136
149
|
if (!salon)
|
|
137
|
-
return err("not_found",
|
|
150
|
+
return err("not_found", texts.noSuchSalon);
|
|
138
151
|
const [services, promos] = await Promise.all([port.listSalonServices(salonId), port.listSalonPromos(salonId)]);
|
|
139
|
-
return ok(
|
|
152
|
+
return ok(texts.salonSummary(salon.name, salon.address, services.length, promos.length), {
|
|
140
153
|
salonId,
|
|
141
154
|
name: salon.name,
|
|
142
155
|
address: salon.address,
|
|
143
|
-
services: services.map(serviceLine),
|
|
156
|
+
services: services.map(serviceLine(formatsOf(context))),
|
|
144
157
|
promos,
|
|
145
158
|
});
|
|
146
159
|
}
|
|
147
160
|
catch (error) {
|
|
148
161
|
if (error instanceof ArgumentError)
|
|
149
162
|
return err("invalid_arguments", error.message);
|
|
150
|
-
return toToolError(error,
|
|
163
|
+
return toToolError(error, texts.salonFailed);
|
|
151
164
|
}
|
|
152
165
|
},
|
|
153
166
|
};
|
|
154
167
|
}
|
|
155
168
|
/** The salon's active service, or the tool error that says why not. */
|
|
156
|
-
async function findService(port, salonId, serviceId) {
|
|
169
|
+
async function findService(port, texts, salonId, serviceId) {
|
|
157
170
|
const services = await port.listSalonServices(salonId);
|
|
158
171
|
const service = services.find((candidate) => candidate.id === serviceId);
|
|
159
172
|
if (!service)
|
|
160
|
-
return err("not_found",
|
|
173
|
+
return err("not_found", texts.noSuchService);
|
|
161
174
|
if (service.workerIds.length === 0)
|
|
162
|
-
return err("not_found",
|
|
175
|
+
return err("not_found", texts.nobodyDoesService(service.name));
|
|
163
176
|
return service;
|
|
164
177
|
}
|
|
165
178
|
/** Free start minutes of a service on a day → the first worker free then (the app's rule). */
|
|
@@ -183,9 +196,10 @@ async function freeStartsByWorker(port, service, day) {
|
|
|
183
196
|
}
|
|
184
197
|
export function getFreeTimesTool(context) {
|
|
185
198
|
const { port } = context;
|
|
199
|
+
const texts = textsOf(context);
|
|
186
200
|
return {
|
|
187
201
|
name: "get_free_times",
|
|
188
|
-
progress:
|
|
202
|
+
progress: texts.progress.getFreeTimes,
|
|
189
203
|
description: "Zwraca wolne godziny rozpoczęcia jednej usługi w jednym salonie w danym dniu, jako przedziały (wizyta może zacząć się co 5 minut od from do to włącznie).",
|
|
190
204
|
parameters: {
|
|
191
205
|
type: "object",
|
|
@@ -203,33 +217,34 @@ export function getFreeTimesTool(context) {
|
|
|
203
217
|
if (refusal)
|
|
204
218
|
return refusal;
|
|
205
219
|
const salonId = readString(args, "salonId");
|
|
206
|
-
const day = readDay(args, "date");
|
|
220
|
+
const day = readDay(texts, args, "date");
|
|
207
221
|
const now = context.now();
|
|
208
222
|
if (day < localDayOf(now))
|
|
209
|
-
return err("invalid_arguments",
|
|
210
|
-
const service = await findService(port, salonId, readString(args, "serviceId"));
|
|
223
|
+
return err("invalid_arguments", texts.dayPassed);
|
|
224
|
+
const service = await findService(port, texts, salonId, readString(args, "serviceId"));
|
|
211
225
|
if ("status" in service)
|
|
212
226
|
return service;
|
|
213
227
|
const nowMinutes = day === localDayOf(now) ? now.getHours() * 60 + now.getMinutes() : -1;
|
|
214
228
|
const starts = [...(await freeStartsByWorker(port, service, day)).keys()].filter((start) => start > nowMinutes).sort((a, b) => a - b);
|
|
215
229
|
if (starts.length === 0)
|
|
216
|
-
return ok(
|
|
217
|
-
return ok(
|
|
230
|
+
return ok(texts.noFreeTimes(service.name, day), []);
|
|
231
|
+
return ok(texts.freeTimes(service.name, day, formatsOf(context).duration(service.durationMinutes), formatsOf(context).price(service.pricePln)), foldStartRanges(starts));
|
|
218
232
|
}
|
|
219
233
|
catch (error) {
|
|
220
234
|
if (error instanceof ArgumentError)
|
|
221
235
|
return err("invalid_arguments", error.message);
|
|
222
|
-
return toToolError(error,
|
|
236
|
+
return toToolError(error, texts.freeTimesFailed);
|
|
223
237
|
}
|
|
224
238
|
},
|
|
225
239
|
};
|
|
226
240
|
}
|
|
227
241
|
export function bookAppointmentTool(context, confirmations) {
|
|
228
242
|
const { port } = context;
|
|
243
|
+
const texts = textsOf(context);
|
|
229
244
|
const gate = writeGate("book_appointment", confirmations);
|
|
230
245
|
return {
|
|
231
246
|
name: "book_appointment",
|
|
232
|
-
progress:
|
|
247
|
+
progress: texts.progress.book,
|
|
233
248
|
description: "Rezerwuje wizytę w imieniu klienta. Najpierw confirmed=false: dostaniesz podsumowanie do pokazania klientowi. Rezerwacja następuje dopiero przy confirmed=true po jego zgodzie. Pracownika dobiera system.",
|
|
234
249
|
parameters: gate.parameters({
|
|
235
250
|
type: "object",
|
|
@@ -252,12 +267,12 @@ export function bookAppointmentTool(context, confirmations) {
|
|
|
252
267
|
return zone;
|
|
253
268
|
const salonId = readString(args, "salonId");
|
|
254
269
|
const serviceId = readString(args, "serviceId");
|
|
255
|
-
const day = readDay(args, "date");
|
|
256
|
-
const startMinutes = readTimeMinutes(args, "time");
|
|
270
|
+
const day = readDay(texts, args, "date");
|
|
271
|
+
const startMinutes = readTimeMinutes(texts, args, "time");
|
|
257
272
|
const discountCode = readOptionalString(args, "discountCode");
|
|
258
273
|
const notes = readOptionalString(args, "notes");
|
|
259
274
|
if (!onSlotGrid(startMinutes))
|
|
260
|
-
throw new ArgumentError(
|
|
275
|
+
throw new ArgumentError(texts.offGrid);
|
|
261
276
|
// The echo is the change in its normal form, whichever spelling the model used.
|
|
262
277
|
const echo = { salonId, serviceId, date: day, time: timeOfMinutes(startMinutes) };
|
|
263
278
|
if (discountCode !== undefined)
|
|
@@ -269,44 +284,51 @@ export function bookAppointmentTool(context, confirmations) {
|
|
|
269
284
|
return refusal;
|
|
270
285
|
const startAt = momentOf(day, startMinutes);
|
|
271
286
|
if (startAt <= context.now().getTime())
|
|
272
|
-
return err("invalid_arguments",
|
|
273
|
-
const service = await findService(port, salonId, serviceId);
|
|
287
|
+
return err("invalid_arguments", texts.momentPassed);
|
|
288
|
+
const service = await findService(port, texts, salonId, serviceId);
|
|
274
289
|
if ("status" in service)
|
|
275
290
|
return service;
|
|
276
291
|
// Checked on BOTH calls: a refused booking still spends the customer's daily quota,
|
|
277
292
|
// and the slot may have gone between the preview and the "yes".
|
|
278
293
|
const workerId = (await freeStartsByWorker(port, service, day)).get(startMinutes);
|
|
279
294
|
if (workerId === undefined) {
|
|
280
|
-
return err("not_found",
|
|
295
|
+
return err("not_found", texts.slotGone(day, timeOfMinutes(startMinutes)));
|
|
281
296
|
}
|
|
297
|
+
const formats = formatsOf(context);
|
|
282
298
|
const [salon] = await port.getSalons([salonId]);
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
299
|
+
const what = texts.visit({
|
|
300
|
+
service: service.name,
|
|
301
|
+
where: salon ? `${salon.name} (${salon.address})` : texts.somewhere,
|
|
302
|
+
day,
|
|
303
|
+
time: timeOfMinutes(startMinutes),
|
|
304
|
+
duration: formats.duration(service.durationMinutes),
|
|
305
|
+
price: formats.price(service.pricePln),
|
|
306
|
+
code: discountCode === undefined ? "" : texts.withDiscountCode(discountCode),
|
|
307
|
+
});
|
|
286
308
|
if (!readBoolean(args, "confirmed"))
|
|
287
|
-
return gate.preview(
|
|
309
|
+
return gate.preview(texts.willBook(what), echo);
|
|
288
310
|
const refused = gate.redeem(args, echo);
|
|
289
311
|
if (refused)
|
|
290
312
|
return refused;
|
|
291
313
|
const appointmentId = await port.book({ salonId, serviceId, workerId, startAt, notes: notes ?? "", discountCode });
|
|
292
|
-
return ok(
|
|
314
|
+
return ok(texts.booked(what, await pricePaidNote(port, appointmentId, service.pricePln, formats, texts)), { appointmentId });
|
|
293
315
|
}
|
|
294
316
|
catch (error) {
|
|
295
317
|
if (error instanceof ArgumentError)
|
|
296
318
|
return err("invalid_arguments", error.message);
|
|
297
|
-
return toToolError(error,
|
|
319
|
+
return toToolError(error, texts.bookFailed);
|
|
298
320
|
}
|
|
299
321
|
},
|
|
300
322
|
};
|
|
301
323
|
}
|
|
302
324
|
/** What the customer really pays, when a discount code changed it. Never fails a booking that is
|
|
303
325
|
* already made — and a query right after an update may not see it yet. */
|
|
304
|
-
async function pricePaidNote(port, appointmentId, listPricePln) {
|
|
326
|
+
async function pricePaidNote(port, appointmentId, listPricePln, formats, texts) {
|
|
305
327
|
try {
|
|
306
328
|
const booked = (await port.listMyAppointments()).find((appointment) => appointment.id === appointmentId);
|
|
307
329
|
if (!booked || booked.pricePln === listPricePln)
|
|
308
330
|
return "";
|
|
309
|
-
return
|
|
331
|
+
return texts.pricePaid(formats.price(booked.pricePln));
|
|
310
332
|
}
|
|
311
333
|
catch {
|
|
312
334
|
return "";
|
|
@@ -331,16 +353,20 @@ async function upcomingAppointments(port, now) {
|
|
|
331
353
|
}));
|
|
332
354
|
return upcoming.map((appointment) => ({ ...appointment, salon: salons.get(appointment.salonId), serviceName: serviceNames.get(appointment.serviceId) }));
|
|
333
355
|
}
|
|
334
|
-
function describeAppointment(appointment) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
356
|
+
function describeAppointment(texts, appointment) {
|
|
357
|
+
return texts.appointment({
|
|
358
|
+
what: appointment.serviceName ? `„${appointment.serviceName}”` : texts.visitFallbackName,
|
|
359
|
+
where: appointment.salon ? texts.at(`${appointment.salon.name} (${appointment.salon.address})`) : "",
|
|
360
|
+
day: localDayOf(appointment.startAt),
|
|
361
|
+
time: localTimeOf(appointment.startAt),
|
|
362
|
+
});
|
|
338
363
|
}
|
|
339
364
|
export function listMyAppointmentsTool(context) {
|
|
340
365
|
const { port } = context;
|
|
366
|
+
const texts = textsOf(context);
|
|
341
367
|
return {
|
|
342
368
|
name: "list_my_appointments",
|
|
343
|
-
progress:
|
|
369
|
+
progress: texts.progress.listAppointments,
|
|
344
370
|
description: "Zwraca nadchodzące wizyty klienta (potwierdzone, jeszcze nie odbyte): id wizyty, salon, usługę, termin i cenę.",
|
|
345
371
|
parameters: { type: "object", properties: {}, additionalProperties: false },
|
|
346
372
|
execute: async () => {
|
|
@@ -350,30 +376,32 @@ export function listMyAppointmentsTool(context) {
|
|
|
350
376
|
return zone;
|
|
351
377
|
const upcoming = await upcomingAppointments(port, context.now());
|
|
352
378
|
if (upcoming.length === 0)
|
|
353
|
-
return ok(
|
|
354
|
-
|
|
379
|
+
return ok(texts.noUpcoming, []);
|
|
380
|
+
const formats = formatsOf(context);
|
|
381
|
+
return ok(texts.upcoming(upcoming.length), upcoming.map((appointment) => ({
|
|
355
382
|
appointmentId: appointment.id,
|
|
356
383
|
salonId: appointment.salonId,
|
|
357
|
-
salonName: appointment.salon?.name ??
|
|
384
|
+
salonName: appointment.salon?.name ?? texts.salonFallbackName,
|
|
358
385
|
address: appointment.salon?.address ?? "",
|
|
359
386
|
serviceName: appointment.serviceName ?? "",
|
|
360
387
|
start: localStampOf(appointment.startAt),
|
|
361
|
-
|
|
362
|
-
|
|
388
|
+
duration: formats.duration(appointment.durationMinutes),
|
|
389
|
+
price: formats.price(appointment.pricePln),
|
|
363
390
|
})));
|
|
364
391
|
}
|
|
365
392
|
catch (error) {
|
|
366
|
-
return toToolError(error,
|
|
393
|
+
return toToolError(error, texts.listFailed);
|
|
367
394
|
}
|
|
368
395
|
},
|
|
369
396
|
};
|
|
370
397
|
}
|
|
371
398
|
export function cancelAppointmentTool(context, confirmations) {
|
|
372
399
|
const { port } = context;
|
|
400
|
+
const texts = textsOf(context);
|
|
373
401
|
const gate = writeGate("cancel_appointment", confirmations);
|
|
374
402
|
return {
|
|
375
403
|
name: "cancel_appointment",
|
|
376
|
-
progress:
|
|
404
|
+
progress: texts.progress.cancel,
|
|
377
405
|
description: "Odwołuje nadchodzącą wizytę klienta. Najpierw confirmed=false (podsumowanie dla klienta), potem confirmed=true po jego zgodzie. appointmentId weź z list_my_appointments.",
|
|
378
406
|
parameters: gate.parameters({
|
|
379
407
|
type: "object",
|
|
@@ -396,19 +424,19 @@ export function cancelAppointmentTool(context, confirmations) {
|
|
|
396
424
|
return refusal;
|
|
397
425
|
const appointment = (await upcomingAppointments(port, context.now())).find((candidate) => candidate.id === appointmentId);
|
|
398
426
|
if (!appointment)
|
|
399
|
-
return err("not_found",
|
|
427
|
+
return err("not_found", texts.noSuchAppointment);
|
|
400
428
|
if (!readBoolean(args, "confirmed"))
|
|
401
|
-
return gate.preview(
|
|
429
|
+
return gate.preview(texts.willCancel(describeAppointment(texts, appointment)), echo);
|
|
402
430
|
const refused = gate.redeem(args, echo);
|
|
403
431
|
if (refused)
|
|
404
432
|
return refused;
|
|
405
433
|
await port.cancel(appointmentId);
|
|
406
|
-
return ok(
|
|
434
|
+
return ok(texts.cancelled(describeAppointment(texts, appointment)), { appointmentId });
|
|
407
435
|
}
|
|
408
436
|
catch (error) {
|
|
409
437
|
if (error instanceof ArgumentError)
|
|
410
438
|
return err("invalid_arguments", error.message);
|
|
411
|
-
return toToolError(error,
|
|
439
|
+
return toToolError(error, texts.cancelFailed);
|
|
412
440
|
}
|
|
413
441
|
},
|
|
414
442
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,11 @@ 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 {
|
|
10
|
+
export { customerFormats, DEFAULT_CUSTOMER_LOCALE } from "./customer/format.js";
|
|
11
|
+
export type { CustomerFormats } from "./customer/format.js";
|
|
12
|
+
export { CUSTOMER_AGENT_PERSONA_PL, customerAgentPersona } from "./customer/persona.js";
|
|
13
|
+
export { customerLanguageOf, customerTexts } from "./customer/texts.js";
|
|
14
|
+
export type { CustomerTexts } from "./customer/texts.js";
|
|
11
15
|
export { hostTimeZoneProblem } from "./customer/time.js";
|
|
12
16
|
export { IcSalonCorePort } from "./ic-salon-core-port.js";
|
|
13
17
|
export type { IcSalonCorePortOptions } 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;
|
|
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,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AACxF,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACxE,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,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,7 +2,9 @@ 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 {
|
|
5
|
+
export { customerFormats, DEFAULT_CUSTOMER_LOCALE } from "./customer/format.js";
|
|
6
|
+
export { CUSTOMER_AGENT_PERSONA_PL, customerAgentPersona } from "./customer/persona.js";
|
|
7
|
+
export { customerLanguageOf, customerTexts } from "./customer/texts.js";
|
|
6
8
|
export { hostTimeZoneProblem } from "./customer/time.js";
|
|
7
9
|
export { IcSalonCorePort } from "./ic-salon-core-port.js";
|
|
8
10
|
export { SALON_AGENT_PERSONA_PL, SALON_READ_PERSONA_PL } from "./persona.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.6.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": {
|