@jsm-mit/sultana-agent-tools-package 0.5.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 CHANGED
@@ -171,6 +171,16 @@ const tools = session
171
171
  ("8.3 km" in a Polish sentence). `customerFormats(locale)` is exported for a host that writes
172
172
  such text itself. Dates and times stay `YYYY-MM-DD` / `HH:mm`: the model sends them back as
173
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).
174
184
  - **The host process must run in the salons' time zone** (`TZ=Europe/Warsaw`): the canister wrapper
175
185
  takes every offset from the process clock. While it does not, every time-dependent tool answers
176
186
  `unavailable` instead of booking hours off (`timeZone: null` skips the guard; `hostTimeZoneProblem`
@@ -18,12 +18,14 @@ export interface CreateCustomerToolsFromPortOptions {
18
18
  /**
19
19
  * The customer's language and region as a BCP 47 tag ("pl-PL", "en-GB") — what the app runs
20
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.
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.
22
24
  */
23
25
  locale?: string;
24
26
  }
25
27
  export interface CreateCustomerToolsOptions extends Omit<IcCustomerCorePortOptions, "language">, CreateCustomerToolsFromPortOptions {
26
- /** Language of service-type labels. Defaults to Polish. */
28
+ /** Language of service-type labels. Defaults to the language of `locale`, else Polish. */
27
29
  language?: string;
28
30
  }
29
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;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
+ {"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,5 +1,6 @@
1
1
  import { findServiceTypeTool } from "../tools/services.js";
2
2
  import { customerFormats } from "./format.js";
3
+ import { customerLanguageOf, customerTexts } from "./texts.js";
3
4
  import { IcCustomerCorePort } from "./ic-customer-core-port.js";
4
5
  import { bookAppointmentTool, cancelAppointmentTool, getFreeTimesTool, getSalonTool, listMyAppointmentsTool, searchSlotsTool, } from "./tools.js";
5
6
  /** The zone Sultana's salons live in. The canister keeps no zone per salon. */
@@ -11,7 +12,7 @@ export const DEFAULT_SALON_TIME_ZONE = "Europe/Warsaw";
11
12
  */
12
13
  export function createCustomerTools(options) {
13
14
  const { confirmations, now, timeZone, locale, language, ...portOptions } = options;
14
- const port = new IcCustomerCorePort({ ...portOptions, language: language ?? "pl" });
15
+ const port = new IcCustomerCorePort({ ...portOptions, language: language ?? customerLanguageOf(locale) });
15
16
  return createCustomerToolsFromPort(port, { confirmations, now, timeZone, locale });
16
17
  }
17
18
  /** The same set over any port — the seam tests use. */
@@ -30,7 +31,7 @@ export function createCustomerToolsFromPort(port, options = {}) {
30
31
  */
31
32
  export function createCustomerReadTools(options) {
32
33
  const { now, timeZone, locale, language, ...portOptions } = options;
33
- const port = new IcCustomerCorePort({ ...portOptions, language: language ?? "pl" });
34
+ const port = new IcCustomerCorePort({ ...portOptions, language: language ?? customerLanguageOf(locale) });
34
35
  return createCustomerReadToolsFromPort(port, { now, timeZone, locale });
35
36
  }
36
37
  export function createCustomerReadToolsFromPort(port, options = {}) {
@@ -42,8 +43,11 @@ function contextOf(port, options) {
42
43
  now: options.now ?? (() => new Date()),
43
44
  timeZone: options.timeZone === null ? undefined : options.timeZone ?? DEFAULT_SALON_TIME_ZONE,
44
45
  formats: customerFormats(options.locale),
46
+ texts: customerTexts(options.locale),
45
47
  };
46
48
  }
47
49
  function readTools(context) {
48
- return [findServiceTypeTool(context.port), searchSlotsTool(context), getSalonTool(context), getFreeTimesTool(context)];
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)];
49
53
  }
@@ -1,6 +1,8 @@
1
1
  /**
2
- * The operating rules of the customer tools, as a system prompt — next to the tools for the same
3
- * reason as the salon persona: they describe the tool contract. A host appends who the agent is.
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 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.";
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":"AAAA;;;GAGG;AACH,eAAO,MAAM,yBAAyB,i2EAuBqB,CAAC"}
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"}
@@ -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
- export const CUSTOMER_AGENT_PERSONA_PL = `Jesteś asystentem klienta w aplikacji Sultana: pomagasz znaleźć salon i wolny termin,
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:
@@ -26,3 +44,5 @@ Zasady pracy:
26
44
  - Jeśli narzędzie zwróci status "error", przeczytaj summary i powiedz klientowi, co poszło nie tak.
27
45
  Nie powtarzaj tego samego wywołania w kółko: nieudana próba rezerwacji zużywa dzienny limit klienta.
28
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":"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"}
@@ -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
+ }
@@ -1,6 +1,7 @@
1
1
  import type { TurnConfirmations } from "../confirmations.js";
2
2
  import { type AgentTool } from "../types.js";
3
3
  import { type CustomerFormats } from "./format.js";
4
+ import { type CustomerTexts } from "./texts.js";
4
5
  import type { CustomerCorePort } from "./customer-core-port.js";
5
6
  export interface CustomerToolsContext {
6
7
  port: CustomerCorePort;
@@ -10,6 +11,8 @@ export interface CustomerToolsContext {
10
11
  timeZone?: string;
11
12
  /** How the customer's region writes distances, prices and durations. Defaults to pl-PL. */
12
13
  formats?: CustomerFormats;
14
+ /** The sentences of the customer's language — see `texts.ts`. Defaults to Polish. */
15
+ texts?: CustomerTexts;
13
16
  }
14
17
  export declare function searchSlotsTool(context: CustomerToolsContext): AgentTool;
15
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,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"}
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"}
@@ -3,7 +3,11 @@ import { ArgumentError, readBoolean, readOptionalNumber, readOptionalString, rea
3
3
  import { writeGate } from "../tools/write-gate.js";
4
4
  import { err, ok } from "../types.js";
5
5
  import { customerFormats } from "./format.js";
6
+ import { customerTexts } from "./texts.js";
6
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
+ }
7
11
  /**
8
12
  * The tools never hand the model a bare distance, price or duration: it would copy "8.3" with
9
13
  * a dot into a Polish sentence. They hand it the text the customer's region writes.
@@ -21,24 +25,23 @@ const CONFIRMED_FIELD = {
21
25
  type: "boolean",
22
26
  description: "false = pokaż podgląd bez zapisu; true = wykonaj, dopiero po zgodzie klienta w jego następnej wiadomości.",
23
27
  };
24
- 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.";
25
28
  /** Refuses every time-dependent tool while the host clock runs in the wrong zone. */
26
29
  function zoneRefusal(context) {
27
30
  if (!context.timeZone)
28
31
  return null;
29
32
  const problem = hostTimeZoneProblem(context.timeZone, context.now());
30
- return problem ? err("unavailable", `Rezerwacje są chwilowo wyłączone: ${problem}.`) : null;
33
+ return problem ? err("unavailable", textsOf(context).bookingOff(problem)) : null;
31
34
  }
32
- function readDay(args, key) {
35
+ function readDay(texts, args, key) {
33
36
  const day = readString(args, key);
34
37
  if (!isDay(day))
35
- throw new ArgumentError(`Pole "${key}" musi być datą w formacie RRRR-MM-DD.`);
38
+ throw new ArgumentError(texts.badDay(key));
36
39
  return day;
37
40
  }
38
- function readTimeMinutes(args, key) {
41
+ function readTimeMinutes(texts, args, key) {
39
42
  const minutes = minutesOf(readString(args, key));
40
43
  if (minutes === null)
41
- throw new ArgumentError(`Pole "${key}" musi być godziną w formacie GG:MM.`);
44
+ throw new ArgumentError(texts.badTime(key));
42
45
  return minutes;
43
46
  }
44
47
  function serviceLine(formats) {
@@ -46,9 +49,10 @@ function serviceLine(formats) {
46
49
  }
47
50
  export function searchSlotsTool(context) {
48
51
  const { port } = context;
52
+ const texts = textsOf(context);
49
53
  return {
50
54
  name: "search_slots",
51
- progress: "Szukam wolnych terminów w okolicy…",
55
+ progress: texts.progress.searchSlots,
52
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.",
53
57
  parameters: {
54
58
  type: "object",
@@ -67,7 +71,7 @@ export function searchSlotsTool(context) {
67
71
  if (refusal)
68
72
  return refusal;
69
73
  if (!port.location)
70
- return err("invalid_arguments", NO_LOCATION);
74
+ return err("invalid_arguments", texts.noLocation);
71
75
  const formats = formatsOf(context);
72
76
  const serviceTypeId = readString(args, "serviceTypeId");
73
77
  const radiusKm = Math.min(Math.max(readOptionalNumber(args, "radiusKm") ?? DEFAULT_RADIUS_KM, 1), MAX_RADIUS_KM);
@@ -75,47 +79,46 @@ export function searchSlotsTool(context) {
75
79
  const timeText = readOptionalString(args, "time");
76
80
  const now = context.now();
77
81
  if ((dayText === undefined) !== (timeText === undefined)) {
78
- throw new ArgumentError("Podaj date i time razem albo pomiń oba (jak najszybciej).");
82
+ throw new ArgumentError(texts.dateAndTimeTogether);
79
83
  }
80
84
  let found;
81
85
  let lead;
82
86
  if (dayText === undefined) {
83
87
  found = await port.searchAsap(serviceTypeId, radiusKm, now);
84
- lead = found.phase === "extended" ? "W najbliższej godzinie nic wolnego, na które klient zdąży. Najbliższe terminy:" : "Najbliższe wolne terminy:";
88
+ lead = found.phase === "extended" ? texts.leadExtended : texts.leadSoon;
85
89
  }
86
90
  else {
87
- const day = readDay(args, "date");
88
- const at = readTimeMinutes(args, "time");
91
+ const day = readDay(texts, args, "date");
92
+ const at = readTimeMinutes(texts, args, "time");
89
93
  const moments = AROUND_TIME_MINUTES.map((shift) => momentOf(day, at) + shift * 60_000)
90
94
  // Only moments of the asked day's grid that are still ahead.
91
95
  .filter((moment) => moment > now.getTime() && onSlotGrid(new Date(moment).getMinutes()));
92
96
  if (moments.length === 0)
93
- return err("invalid_arguments", "Ten termin już minął. Zapytaj klienta o późniejszy.");
97
+ return err("invalid_arguments", texts.momentPassed);
94
98
  found = await port.searchAtMoments(serviceTypeId, radiusKm, moments);
95
- lead = `Wolne terminy w okolicach ${day} ${timeOfMinutes(at)}:`;
99
+ lead = texts.leadAround(day, timeOfMinutes(at));
96
100
  }
97
101
  if (found.salons.length === 0) {
98
102
  if (found.failedProbes > 0)
99
- return err("unavailable", "Nie udało się sprawdzić terminów — kanister nie odpowiadał. Spróbuj ponownie za chwilę.");
100
- return ok(`Nic wolnego w promieniu ${formats.distance(radiusKm)}. Zaproponuj inną godzinę, inny dzień albo większy promień.`, []);
103
+ return err("unavailable", texts.probesFailed);
104
+ return ok(texts.nothingFree(formats.distance(radiusKm)), []);
101
105
  }
102
106
  const ranked = [...found.salons].sort(bySoonestThenNearest).slice(0, MAX_SALONS_SHOWN);
103
107
  const names = new Map((await port.getSalons(ranked.map((salon) => salon.salonId))).map((salon) => [salon.id, salon]));
104
108
  const data = ranked.map((salon) => ({
105
109
  salonId: salon.salonId,
106
- name: names.get(salon.salonId)?.name ?? "Salon",
110
+ name: names.get(salon.salonId)?.name ?? texts.salonFallbackName,
107
111
  address: names.get(salon.salonId)?.address ?? "",
108
112
  distance: formats.distance(salon.distanceKm),
109
113
  services: salon.services.map(serviceLine(formats)),
110
114
  freeAt: salon.availableAt.slice(0, MAX_TIMES_SHOWN).map(localStampOf),
111
115
  }));
112
- const more = found.salons.length > ranked.length ? ` Pokazuję ${ranked.length} z ${found.salons.length} salonów.` : "";
113
- 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);
114
117
  }
115
118
  catch (error) {
116
119
  if (error instanceof ArgumentError)
117
120
  return err("invalid_arguments", error.message);
118
- return toToolError(error, "Nie udało się wyszukać terminów.");
121
+ return toToolError(error, texts.searchFailed);
119
122
  }
120
123
  },
121
124
  };
@@ -128,9 +131,10 @@ function bySoonestThenNearest(a, b) {
128
131
  }
129
132
  export function getSalonTool(context) {
130
133
  const { port } = context;
134
+ const texts = textsOf(context);
131
135
  return {
132
136
  name: "get_salon",
133
- progress: "Sprawdzam salon…",
137
+ progress: texts.progress.getSalon,
134
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.",
135
139
  parameters: {
136
140
  type: "object",
@@ -143,9 +147,9 @@ export function getSalonTool(context) {
143
147
  const salonId = readString(args, "salonId");
144
148
  const [salon] = await port.getSalons([salonId]);
145
149
  if (!salon)
146
- return err("not_found", "Nie ma takiego salonu. Id weź z search_slots.");
150
+ return err("not_found", texts.noSuchSalon);
147
151
  const [services, promos] = await Promise.all([port.listSalonServices(salonId), port.listSalonPromos(salonId)]);
148
- return ok(`${salon.name}, ${salon.address}: ${services.length} usług(i), ${promos.length} promocji.`, {
152
+ return ok(texts.salonSummary(salon.name, salon.address, services.length, promos.length), {
149
153
  salonId,
150
154
  name: salon.name,
151
155
  address: salon.address,
@@ -156,19 +160,19 @@ export function getSalonTool(context) {
156
160
  catch (error) {
157
161
  if (error instanceof ArgumentError)
158
162
  return err("invalid_arguments", error.message);
159
- return toToolError(error, "Nie udało się odczytać salonu.");
163
+ return toToolError(error, texts.salonFailed);
160
164
  }
161
165
  },
162
166
  };
163
167
  }
164
168
  /** The salon's active service, or the tool error that says why not. */
165
- async function findService(port, salonId, serviceId) {
169
+ async function findService(port, texts, salonId, serviceId) {
166
170
  const services = await port.listSalonServices(salonId);
167
171
  const service = services.find((candidate) => candidate.id === serviceId);
168
172
  if (!service)
169
- return err("not_found", "Ten salon nie ma takiej aktywnej usługi. Id salonu i usługi weź z search_slots albo get_salon.");
173
+ return err("not_found", texts.noSuchService);
170
174
  if (service.workerIds.length === 0)
171
- return err("not_found", `Usługi „${service.name}” nikt teraz nie wykonuje — nie da się jej zarezerwować.`);
175
+ return err("not_found", texts.nobodyDoesService(service.name));
172
176
  return service;
173
177
  }
174
178
  /** Free start minutes of a service on a day → the first worker free then (the app's rule). */
@@ -192,9 +196,10 @@ async function freeStartsByWorker(port, service, day) {
192
196
  }
193
197
  export function getFreeTimesTool(context) {
194
198
  const { port } = context;
199
+ const texts = textsOf(context);
195
200
  return {
196
201
  name: "get_free_times",
197
- progress: "Sprawdzam wolne godziny…",
202
+ progress: texts.progress.getFreeTimes,
198
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).",
199
204
  parameters: {
200
205
  type: "object",
@@ -212,33 +217,34 @@ export function getFreeTimesTool(context) {
212
217
  if (refusal)
213
218
  return refusal;
214
219
  const salonId = readString(args, "salonId");
215
- const day = readDay(args, "date");
220
+ const day = readDay(texts, args, "date");
216
221
  const now = context.now();
217
222
  if (day < localDayOf(now))
218
- return err("invalid_arguments", "Ten dzień już minął.");
219
- 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"));
220
225
  if ("status" in service)
221
226
  return service;
222
227
  const nowMinutes = day === localDayOf(now) ? now.getHours() * 60 + now.getMinutes() : -1;
223
228
  const starts = [...(await freeStartsByWorker(port, service, day)).keys()].filter((start) => start > nowMinutes).sort((a, b) => a - b);
224
229
  if (starts.length === 0)
225
- return ok(`„${service.name}” — ${day}: brak wolnych godzin.`, []);
226
- return ok(`„${service.name}” — ${day}: wolne godziny rozpoczęcia (${formatsOf(context).duration(service.durationMinutes)}, ${formatsOf(context).price(service.pricePln)}).`, foldStartRanges(starts));
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));
227
232
  }
228
233
  catch (error) {
229
234
  if (error instanceof ArgumentError)
230
235
  return err("invalid_arguments", error.message);
231
- return toToolError(error, "Nie udało się sprawdzić wolnych godzin.");
236
+ return toToolError(error, texts.freeTimesFailed);
232
237
  }
233
238
  },
234
239
  };
235
240
  }
236
241
  export function bookAppointmentTool(context, confirmations) {
237
242
  const { port } = context;
243
+ const texts = textsOf(context);
238
244
  const gate = writeGate("book_appointment", confirmations);
239
245
  return {
240
246
  name: "book_appointment",
241
- progress: "Rezerwuję wizytę…",
247
+ progress: texts.progress.book,
242
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.",
243
249
  parameters: gate.parameters({
244
250
  type: "object",
@@ -261,12 +267,12 @@ export function bookAppointmentTool(context, confirmations) {
261
267
  return zone;
262
268
  const salonId = readString(args, "salonId");
263
269
  const serviceId = readString(args, "serviceId");
264
- const day = readDay(args, "date");
265
- const startMinutes = readTimeMinutes(args, "time");
270
+ const day = readDay(texts, args, "date");
271
+ const startMinutes = readTimeMinutes(texts, args, "time");
266
272
  const discountCode = readOptionalString(args, "discountCode");
267
273
  const notes = readOptionalString(args, "notes");
268
274
  if (!onSlotGrid(startMinutes))
269
- throw new ArgumentError("Wizyta może zacząć się tylko o pełnych 5 minutach (np. 10:00, 10:05).");
275
+ throw new ArgumentError(texts.offGrid);
270
276
  // The echo is the change in its normal form, whichever spelling the model used.
271
277
  const echo = { salonId, serviceId, date: day, time: timeOfMinutes(startMinutes) };
272
278
  if (discountCode !== undefined)
@@ -278,45 +284,51 @@ export function bookAppointmentTool(context, confirmations) {
278
284
  return refusal;
279
285
  const startAt = momentOf(day, startMinutes);
280
286
  if (startAt <= context.now().getTime())
281
- return err("invalid_arguments", "Ten termin już minął. Zapytaj klienta o późniejszy.");
282
- const service = await findService(port, salonId, serviceId);
287
+ return err("invalid_arguments", texts.momentPassed);
288
+ const service = await findService(port, texts, salonId, serviceId);
283
289
  if ("status" in service)
284
290
  return service;
285
291
  // Checked on BOTH calls: a refused booking still spends the customer's daily quota,
286
292
  // and the slot may have gone between the preview and the "yes".
287
293
  const workerId = (await freeStartsByWorker(port, service, day)).get(startMinutes);
288
294
  if (workerId === undefined) {
289
- return err("not_found", `Termin ${day} ${timeOfMinutes(startMinutes)} nie jest już wolny. Sprawdź inne godziny przez get_free_times.`);
295
+ return err("not_found", texts.slotGone(day, timeOfMinutes(startMinutes)));
290
296
  }
291
297
  const formats = formatsOf(context);
292
298
  const [salon] = await port.getSalons([salonId]);
293
- const where = salon ? `${salon.name} (${salon.address})` : "salon";
294
- const code = discountCode === undefined ? "" : `, z kodem rabatowym ${discountCode}`;
295
- const what = `„${service.name} w ${where}, ${day} o ${timeOfMinutes(startMinutes)}, ${formats.duration(service.durationMinutes)}, cena ${formats.price(service.pricePln)}${code}`;
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
+ });
296
308
  if (!readBoolean(args, "confirmed"))
297
- return gate.preview(`Zarezerwuję: ${what}.`, echo);
309
+ return gate.preview(texts.willBook(what), echo);
298
310
  const refused = gate.redeem(args, echo);
299
311
  if (refused)
300
312
  return refused;
301
313
  const appointmentId = await port.book({ salonId, serviceId, workerId, startAt, notes: notes ?? "", discountCode });
302
- return ok(`Zarezerwowano: ${what}.${await pricePaidNote(port, appointmentId, service.pricePln, formats)} Wizyta jest w zakładce „Wizyty”.`, { appointmentId });
314
+ return ok(texts.booked(what, await pricePaidNote(port, appointmentId, service.pricePln, formats, texts)), { appointmentId });
303
315
  }
304
316
  catch (error) {
305
317
  if (error instanceof ArgumentError)
306
318
  return err("invalid_arguments", error.message);
307
- return toToolError(error, "Nie udało się zarezerwować wizyty.");
319
+ return toToolError(error, texts.bookFailed);
308
320
  }
309
321
  },
310
322
  };
311
323
  }
312
324
  /** What the customer really pays, when a discount code changed it. Never fails a booking that is
313
325
  * already made — and a query right after an update may not see it yet. */
314
- async function pricePaidNote(port, appointmentId, listPricePln, formats) {
326
+ async function pricePaidNote(port, appointmentId, listPricePln, formats, texts) {
315
327
  try {
316
328
  const booked = (await port.listMyAppointments()).find((appointment) => appointment.id === appointmentId);
317
329
  if (!booked || booked.pricePln === listPricePln)
318
330
  return "";
319
- return ` Cena po rabacie: ${formats.price(booked.pricePln)}.`;
331
+ return texts.pricePaid(formats.price(booked.pricePln));
320
332
  }
321
333
  catch {
322
334
  return "";
@@ -341,16 +353,20 @@ async function upcomingAppointments(port, now) {
341
353
  }));
342
354
  return upcoming.map((appointment) => ({ ...appointment, salon: salons.get(appointment.salonId), serviceName: serviceNames.get(appointment.serviceId) }));
343
355
  }
344
- function describeAppointment(appointment) {
345
- const what = appointment.serviceName ? `„${appointment.serviceName}”` : "wizyta";
346
- const where = appointment.salon ? ` w ${appointment.salon.name} (${appointment.salon.address})` : "";
347
- return `${what}${where}, ${localDayOf(appointment.startAt)} o ${localTimeOf(appointment.startAt)}`;
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
+ });
348
363
  }
349
364
  export function listMyAppointmentsTool(context) {
350
365
  const { port } = context;
366
+ const texts = textsOf(context);
351
367
  return {
352
368
  name: "list_my_appointments",
353
- progress: "Sprawdzam Twoje wizyty…",
369
+ progress: texts.progress.listAppointments,
354
370
  description: "Zwraca nadchodzące wizyty klienta (potwierdzone, jeszcze nie odbyte): id wizyty, salon, usługę, termin i cenę.",
355
371
  parameters: { type: "object", properties: {}, additionalProperties: false },
356
372
  execute: async () => {
@@ -360,12 +376,12 @@ export function listMyAppointmentsTool(context) {
360
376
  return zone;
361
377
  const upcoming = await upcomingAppointments(port, context.now());
362
378
  if (upcoming.length === 0)
363
- return ok("Klient nie ma nadchodzących wizyt.", []);
379
+ return ok(texts.noUpcoming, []);
364
380
  const formats = formatsOf(context);
365
- return ok(`Nadchodzące wizyty: ${upcoming.length}.`, upcoming.map((appointment) => ({
381
+ return ok(texts.upcoming(upcoming.length), upcoming.map((appointment) => ({
366
382
  appointmentId: appointment.id,
367
383
  salonId: appointment.salonId,
368
- salonName: appointment.salon?.name ?? "Salon",
384
+ salonName: appointment.salon?.name ?? texts.salonFallbackName,
369
385
  address: appointment.salon?.address ?? "",
370
386
  serviceName: appointment.serviceName ?? "",
371
387
  start: localStampOf(appointment.startAt),
@@ -374,17 +390,18 @@ export function listMyAppointmentsTool(context) {
374
390
  })));
375
391
  }
376
392
  catch (error) {
377
- return toToolError(error, "Nie udało się odczytać wizyt.");
393
+ return toToolError(error, texts.listFailed);
378
394
  }
379
395
  },
380
396
  };
381
397
  }
382
398
  export function cancelAppointmentTool(context, confirmations) {
383
399
  const { port } = context;
400
+ const texts = textsOf(context);
384
401
  const gate = writeGate("cancel_appointment", confirmations);
385
402
  return {
386
403
  name: "cancel_appointment",
387
- progress: "Odwołuję wizytę…",
404
+ progress: texts.progress.cancel,
388
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.",
389
406
  parameters: gate.parameters({
390
407
  type: "object",
@@ -407,19 +424,19 @@ export function cancelAppointmentTool(context, confirmations) {
407
424
  return refusal;
408
425
  const appointment = (await upcomingAppointments(port, context.now())).find((candidate) => candidate.id === appointmentId);
409
426
  if (!appointment)
410
- return err("not_found", "Klient nie ma takiej nadchodzącej wizyty. Id weź z list_my_appointments.");
427
+ return err("not_found", texts.noSuchAppointment);
411
428
  if (!readBoolean(args, "confirmed"))
412
- return gate.preview(`Odwołam wizytę: ${describeAppointment(appointment)}.`, echo);
429
+ return gate.preview(texts.willCancel(describeAppointment(texts, appointment)), echo);
413
430
  const refused = gate.redeem(args, echo);
414
431
  if (refused)
415
432
  return refused;
416
433
  await port.cancel(appointmentId);
417
- return ok(`Odwołano wizytę: ${describeAppointment(appointment)}.`, { appointmentId });
434
+ return ok(texts.cancelled(describeAppointment(texts, appointment)), { appointmentId });
418
435
  }
419
436
  catch (error) {
420
437
  if (error instanceof ArgumentError)
421
438
  return err("invalid_arguments", error.message);
422
- return toToolError(error, "Nie udało się odwołać wizyty.");
439
+ return toToolError(error, texts.cancelFailed);
423
440
  }
424
441
  },
425
442
  };
package/dist/index.d.ts CHANGED
@@ -9,7 +9,9 @@ export { IcCustomerCorePort } from "./customer/ic-customer-core-port.js";
9
9
  export type { IcCustomerCorePortOptions } from "./customer/ic-customer-core-port.js";
10
10
  export { customerFormats, DEFAULT_CUSTOMER_LOCALE } from "./customer/format.js";
11
11
  export type { CustomerFormats } from "./customer/format.js";
12
- export { CUSTOMER_AGENT_PERSONA_PL } from "./customer/persona.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";
13
15
  export { hostTimeZoneProblem } from "./customer/time.js";
14
16
  export { IcSalonCorePort } from "./ic-salon-core-port.js";
15
17
  export type { IcSalonCorePortOptions } from "./ic-salon-core-port.js";
@@ -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,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"}
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
@@ -3,7 +3,8 @@ export { createSalonReadTools, createSalonReadToolsFromPort, createSalonTools, c
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
5
  export { customerFormats, DEFAULT_CUSTOMER_LOCALE } from "./customer/format.js";
6
- export { CUSTOMER_AGENT_PERSONA_PL } from "./customer/persona.js";
6
+ export { CUSTOMER_AGENT_PERSONA_PL, customerAgentPersona } from "./customer/persona.js";
7
+ export { customerLanguageOf, customerTexts } from "./customer/texts.js";
7
8
  export { hostTimeZoneProblem } from "./customer/time.js";
8
9
  export { IcSalonCorePort } from "./ic-salon-core-port.js";
9
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.5.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": {