@jsm-mit/sultana-agent-tools-package 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +76 -0
  2. package/dist/booksy/fetch-page.d.ts +25 -0
  3. package/dist/booksy/fetch-page.d.ts.map +1 -0
  4. package/dist/booksy/fetch-page.js +93 -0
  5. package/dist/booksy/import-tools.d.ts +22 -0
  6. package/dist/booksy/import-tools.d.ts.map +1 -0
  7. package/dist/booksy/import-tools.js +413 -0
  8. package/dist/booksy/nuxt-payload.d.ts +15 -0
  9. package/dist/booksy/nuxt-payload.d.ts.map +1 -0
  10. package/dist/booksy/nuxt-payload.js +96 -0
  11. package/dist/booksy/parse-salon.d.ts +61 -0
  12. package/dist/booksy/parse-salon.d.ts.map +1 -0
  13. package/dist/booksy/parse-salon.js +212 -0
  14. package/dist/booksy/to-sultana.d.ts +62 -0
  15. package/dist/booksy/to-sultana.d.ts.map +1 -0
  16. package/dist/booksy/to-sultana.js +77 -0
  17. package/dist/booksy/tools.d.ts +57 -0
  18. package/dist/booksy/tools.d.ts.map +1 -0
  19. package/dist/booksy/tools.js +227 -0
  20. package/dist/customer/create-customer-tools.d.ts +4 -2
  21. package/dist/customer/create-customer-tools.d.ts.map +1 -1
  22. package/dist/customer/create-customer-tools.js +7 -3
  23. package/dist/customer/persona.d.ts +5 -3
  24. package/dist/customer/persona.d.ts.map +1 -1
  25. package/dist/customer/persona.js +21 -1
  26. package/dist/customer/texts.d.ts +79 -0
  27. package/dist/customer/texts.d.ts.map +1 -0
  28. package/dist/customer/texts.js +115 -0
  29. package/dist/customer/tools.d.ts +3 -0
  30. package/dist/customer/tools.d.ts.map +1 -1
  31. package/dist/customer/tools.js +80 -63
  32. package/dist/ic-salon-core-port.d.ts +4 -0
  33. package/dist/ic-salon-core-port.d.ts.map +1 -1
  34. package/dist/ic-salon-core-port.js +19 -1
  35. package/dist/index.d.ts +14 -2
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +8 -2
  38. package/dist/persona.d.ts +5 -0
  39. package/dist/persona.d.ts.map +1 -1
  40. package/dist/persona.js +17 -0
  41. package/package.json +2 -1
@@ -0,0 +1,227 @@
1
+ import { ArgumentError, readOptionalString, readOptionalStringArray, readString } from "../tools/args.js";
2
+ import { err, ok } from "../types.js";
3
+ import { BooksyReadError, fetchBooksyPage, fetchPhoto, normalizeBooksyUrl } from "./fetch-page.js";
4
+ import { parseBooksySalon } from "./parse-salon.js";
5
+ import { toSultanaDraft } from "./to-sultana.js";
6
+ export const URL_FIELD = {
7
+ type: "string",
8
+ description: "Adres profilu salonu na Booksy, np. https://booksy.com/pl-pl/12345_nazwa-salonu_fryzjer_3_warszawa.",
9
+ };
10
+ const PHOTO_KINDS = ["logo", "cover", "salon", "inspiration", "service", "staff"];
11
+ const DEFAULT_PHOTO_KINDS = ["logo", "cover", "salon"];
12
+ // A busy profile has about a hundred photos — a service portfolio alone runs to 70 or more.
13
+ const MAX_PHOTOS_PER_CALL = 150;
14
+ /**
15
+ * Read-only tools over a PUBLIC Booksy profile: they fetch one page and hand back its content in
16
+ * the shape Sultana's write tools take. Nothing here writes to a salon, so nothing here asks for
17
+ * a confirmation. Reviews and customer names are never read.
18
+ */
19
+ export function createBooksyReaderTools(options = {}) {
20
+ const reader = options.reader ?? new BooksyProfileReader(options.fetchFn);
21
+ const tools = [readSalonTool(reader), readServicesTool(reader)];
22
+ if (options.photoSink)
23
+ tools.push(downloadPhotosTool(reader, options.photoSink, options.fetchFn));
24
+ return tools;
25
+ }
26
+ /** Reads and recasts a whole profile — for a host or a script that wants the draft without a model. */
27
+ export async function readBooksySalonDraft(url, fetchFn) {
28
+ return new BooksyProfileReader(fetchFn).read(url);
29
+ }
30
+ /** Long enough for a preview, the owner's "tak" and the import to read one fetch; short enough
31
+ * that an owner who has just edited her Booksy profile is not shown yesterday's. */
32
+ const PROFILE_TTL_MS = 10 * 60_000;
33
+ /** A reader may live as long as its host — it must not grow with every salon ever named. */
34
+ const MAX_REMEMBERED_PROFILES = 50;
35
+ /**
36
+ * Every Booksy tool reads the same page — fetch it once. Profiles are public, so one reader can
37
+ * serve a whole host; a profile is remembered for ten minutes, the last fifty of them.
38
+ */
39
+ export class BooksyProfileReader {
40
+ fetchFn;
41
+ now;
42
+ drafts = new Map();
43
+ constructor(fetchFn, now = Date.now) {
44
+ this.fetchFn = fetchFn;
45
+ this.now = now;
46
+ }
47
+ read(rawUrl) {
48
+ const url = normalizeBooksyUrl(rawUrl);
49
+ const remembered = this.drafts.get(url);
50
+ if (remembered && this.now() - remembered.readAt <= PROFILE_TTL_MS)
51
+ return remembered.draft;
52
+ const draft = fetchBooksyPage(url, this.fetchFn).then((html) => toSultanaDraft(parseBooksySalon(html), url));
53
+ const entry = { readAt: this.now(), draft };
54
+ // A failed read must not be remembered — the next call tries again.
55
+ draft.catch(() => {
56
+ if (this.drafts.get(url) === entry)
57
+ this.drafts.delete(url);
58
+ });
59
+ // Re-inserting moves the profile to the end, so the first key is always the oldest.
60
+ this.drafts.delete(url);
61
+ this.drafts.set(url, entry);
62
+ while (this.drafts.size > MAX_REMEMBERED_PROFILES)
63
+ this.drafts.delete(this.drafts.keys().next().value);
64
+ return draft;
65
+ }
66
+ }
67
+ function readSalonTool(reader) {
68
+ return {
69
+ name: "read_booksy_salon",
70
+ progress: "Czytam profil salonu na Booksy…",
71
+ description: "Czyta publiczny profil salonu na Booksy i zwraca: nazwę, opis, adres, współrzędne, godziny otwarcia (dzień 0 = poniedziałek), zespół oraz liczbę usług i zdjęć. Niczego nie zapisuje. Usługi zwraca read_booksy_services.",
72
+ parameters: { type: "object", properties: { url: URL_FIELD }, required: ["url"], additionalProperties: false },
73
+ execute: (args) => guarded("Nie udało się odczytać profilu Booksy.", async () => {
74
+ const draft = await reader.read(readString(args, "url"));
75
+ const categories = new Set(draft.services.map((service) => service.booksyCategory));
76
+ return ok(`Salon „${draft.salon.name}” (${draft.salon.city || "miasto nieznane"}): ${draft.services.length} usług w ${categories.size} kategoriach, zespół ${draft.workers.length} os., ${draft.photos.length} zdjęć.${degradedSuffix(draft)}`, {
77
+ source: draft.source,
78
+ salon: draft.salon,
79
+ weeklyHours: draft.weeklyHours,
80
+ workers: draft.workers.map((worker) => worker.name),
81
+ serviceCount: draft.services.length,
82
+ serviceCategories: [...categories],
83
+ photoCounts: countPhotos(draft.photos),
84
+ });
85
+ }),
86
+ };
87
+ }
88
+ function readServicesTool(reader) {
89
+ return {
90
+ name: "read_booksy_services",
91
+ progress: "Czytam usługi salonu na Booksy…",
92
+ description: "Czyta usługi z publicznego profilu Booksy w postaci gotowej dla add_service: name, pricePln (pełne złote), durationMinutes (wielokrotność 5). Każdy wariant usługi Booksy to osobna usługa. serviceTypeIds są puste — dobierz je przez find_service_type, podpowiedzią jest booksyCategory. Pole notes opisuje, co zmieniono albo czego brakuje — powiedz o tym właścicielce. Niczego nie zapisuje.",
93
+ parameters: {
94
+ type: "object",
95
+ properties: {
96
+ url: URL_FIELD,
97
+ category: { type: "string", description: "Opcjonalnie: tylko usługi z tej kategorii Booksy (fragment nazwy)." },
98
+ },
99
+ required: ["url"],
100
+ additionalProperties: false,
101
+ },
102
+ execute: (args) => guarded("Nie udało się odczytać usług z Booksy.", async () => {
103
+ const draft = await reader.read(readString(args, "url"));
104
+ const category = readOptionalString(args, "category")?.toLowerCase();
105
+ const services = category
106
+ ? draft.services.filter((service) => service.booksyCategory.toLowerCase().includes(category))
107
+ : draft.services;
108
+ if (services.length === 0) {
109
+ return ok(category ? `Brak usług w kategorii „${category}”.` : "Profil Booksy nie ma żadnej usługi.", []);
110
+ }
111
+ const withNotes = services.filter((service) => service.notes.length > 0).length;
112
+ const notesPart = withNotes > 0 ? ` ${withNotes} z nich ma uwagi (pole notes).` : "";
113
+ return ok(`Usługi z Booksy: ${services.length}.${notesPart}${degradedSuffix(draft)}`, services);
114
+ }),
115
+ };
116
+ }
117
+ function downloadPhotosTool(reader, sink, fetchFn) {
118
+ return {
119
+ name: "download_booksy_photos",
120
+ progress: "Pobieram zdjęcia z Booksy…",
121
+ description: `Pobiera zdjęcia z publicznego profilu Booksy i odkłada je po stronie Sultana. Rodzaje: ${PHOTO_KINDS.join(", ")} („service” to portfolio usług — zdjęcia prac przypisane do usługi, „inspiration” to galeria prac). Domyślnie: ${DEFAULT_PHOTO_KINDS.join(", ")}. Najwyżej ${MAX_PHOTOS_PER_CALL} zdjęć na wywołanie. Zwraca listę zapisanych plików. Niczego nie zapisuje w salonie.`,
122
+ parameters: {
123
+ type: "object",
124
+ properties: {
125
+ url: URL_FIELD,
126
+ kinds: { type: "array", items: { type: "string", enum: PHOTO_KINDS }, description: "Które rodzaje zdjęć pobrać." },
127
+ },
128
+ required: ["url"],
129
+ additionalProperties: false,
130
+ },
131
+ execute: (args) => guarded("Nie udało się pobrać zdjęć z Booksy.", async () => {
132
+ const draft = await reader.read(readString(args, "url"));
133
+ const kinds = readKinds(args);
134
+ const wanted = draft.photos.filter((photo) => kinds.includes(photo.kind));
135
+ if (wanted.length === 0)
136
+ return ok("Profil Booksy nie ma zdjęć tych rodzajów.", []);
137
+ const saved = [];
138
+ const failed = [];
139
+ const counters = new Map();
140
+ for (const photo of wanted.slice(0, MAX_PHOTOS_PER_CALL)) {
141
+ const index = (counters.get(photo.kind) ?? 0) + 1;
142
+ counters.set(photo.kind, index);
143
+ // One broken photo must not cost the owner the rest of the gallery.
144
+ try {
145
+ const file = await fetchPhoto(photo.url, fetchFn);
146
+ const location = await sink.save({
147
+ salonKey: draft.source.booksyId ?? "unknown",
148
+ kind: photo.kind,
149
+ fileName: `${photo.kind}-${String(index).padStart(2, "0")}${captionSlug(photo)}.${extensionOf(file.contentType)}`,
150
+ contentType: file.contentType,
151
+ bytes: file.bytes,
152
+ sourceUrl: photo.url,
153
+ caption: photo.caption,
154
+ });
155
+ saved.push({ kind: photo.kind, location, caption: photo.caption });
156
+ }
157
+ catch {
158
+ failed.push(photo.url);
159
+ }
160
+ }
161
+ const skipped = wanted.length - Math.min(wanted.length, MAX_PHOTOS_PER_CALL);
162
+ const parts = [`Pobrano ${saved.length} z ${wanted.length} zdjęć.`];
163
+ if (failed.length > 0)
164
+ parts.push(`Nie udało się pobrać: ${failed.length}.`);
165
+ if (skipped > 0)
166
+ parts.push(`Pominięto ${skipped} ponad limit ${MAX_PHOTOS_PER_CALL}.`);
167
+ if (saved.length === 0)
168
+ return err("unavailable", parts.join(" "));
169
+ return ok(parts.join(" "), { saved, failed });
170
+ }),
171
+ };
172
+ }
173
+ function readKinds(args) {
174
+ const raw = readOptionalStringArray(args, "kinds");
175
+ if (!raw || raw.length === 0)
176
+ return DEFAULT_PHOTO_KINDS;
177
+ const unknown = raw.filter((kind) => !PHOTO_KINDS.includes(kind));
178
+ if (unknown.length > 0)
179
+ throw new ArgumentError(`Nieznany rodzaj zdjęć: ${unknown.join(", ")}. Dozwolone: ${PHOTO_KINDS.join(", ")}.`);
180
+ return raw;
181
+ }
182
+ function countPhotos(photos) {
183
+ const counts = {};
184
+ for (const photo of photos)
185
+ counts[photo.kind] = (counts[photo.kind] ?? 0) + 1;
186
+ return counts;
187
+ }
188
+ export function degradedSuffix(draft) {
189
+ return draft.source.degraded
190
+ ? " UWAGA: odczyt zastępczy — bez czasów trwania, kategorii i galerii; Booksy mogło zmienić stronę."
191
+ : "";
192
+ }
193
+ /** "-makijaz-okolicznosciowy" for a service or staff photo; empty for the rest. */
194
+ function captionSlug(photo) {
195
+ if ((photo.kind !== "service" && photo.kind !== "staff") || !photo.caption)
196
+ return "";
197
+ const slug = photo.caption
198
+ .toLowerCase()
199
+ .replace(/ł/g, "l")
200
+ .normalize("NFD")
201
+ .replace(/[\u0300-\u036f]/g, "")
202
+ .replace(/[^a-z0-9]+/g, "-")
203
+ .replace(/^-+|-+$/g, "")
204
+ .slice(0, 40);
205
+ return slug === "" ? "" : `-${slug}`;
206
+ }
207
+ function extensionOf(contentType) {
208
+ const subtype = contentType.split(";")[0].trim().split("/")[1] ?? "";
209
+ if (subtype === "jpeg" || subtype === "jpg")
210
+ return "jpeg";
211
+ if (subtype === "png" || subtype === "webp" || subtype === "gif")
212
+ return subtype;
213
+ return "bin";
214
+ }
215
+ /** `execute` never throws: every failure becomes a result the agent can read. */
216
+ export async function guarded(fallbackSummary, run) {
217
+ try {
218
+ return await run();
219
+ }
220
+ catch (error) {
221
+ if (error instanceof ArgumentError)
222
+ return err("invalid_arguments", error.message);
223
+ if (error instanceof BooksyReadError)
224
+ return err(error.code, error.summary);
225
+ return err("unavailable", fallbackSummary);
226
+ }
227
+ }
@@ -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"}