@guava-ai/guava-sdk 0.24.0 → 0.25.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 (53) hide show
  1. package/README.md +13 -3
  2. package/bin/example-runner.ts +138 -11
  3. package/dist/bin/example-runner.d.ts +23 -6
  4. package/dist/bin/example-runner.js +112 -9
  5. package/dist/bin/example-runner.js.map +1 -1
  6. package/dist/examples/{example.test.js → agent-testing.test.js} +2 -2
  7. package/dist/examples/agent-testing.test.js.map +1 -0
  8. package/dist/examples/multiple-agents.d.ts +1 -0
  9. package/dist/examples/multiple-agents.js +52 -0
  10. package/dist/examples/multiple-agents.js.map +1 -0
  11. package/dist/examples/scheduling-outbound.js +3 -2
  12. package/dist/examples/scheduling-outbound.js.map +1 -1
  13. package/dist/src/agent.d.ts +13 -5
  14. package/dist/src/agent.js +9 -14
  15. package/dist/src/agent.js.map +1 -1
  16. package/dist/src/call.d.ts +3 -0
  17. package/dist/src/call.js +63 -23
  18. package/dist/src/call.js.map +1 -1
  19. package/dist/src/client.d.ts +3 -0
  20. package/dist/src/client.js +6 -0
  21. package/dist/src/client.js.map +1 -1
  22. package/dist/src/example-utils.d.ts +2 -0
  23. package/dist/src/example-utils.js +115 -0
  24. package/dist/src/example-utils.js.map +1 -0
  25. package/dist/src/helpers/llm.d.ts +10 -0
  26. package/dist/src/helpers/llm.js +54 -27
  27. package/dist/src/helpers/llm.js.map +1 -1
  28. package/dist/src/index.d.ts +3 -1
  29. package/dist/src/index.js +4 -1
  30. package/dist/src/index.js.map +1 -1
  31. package/dist/src/runner.d.ts +10 -0
  32. package/dist/src/runner.js +101 -0
  33. package/dist/src/runner.js.map +1 -0
  34. package/dist/src/testing/session.d.ts +7 -0
  35. package/dist/src/testing/session.js +3 -22
  36. package/dist/src/testing/session.js.map +1 -1
  37. package/dist/src/version.d.ts +1 -1
  38. package/dist/src/version.js +1 -1
  39. package/examples/{example.test.ts → agent-testing.test.ts} +1 -1
  40. package/examples/multiple-agents.ts +18 -0
  41. package/examples/scheduling-outbound.ts +3 -2
  42. package/package.json +5 -1
  43. package/src/agent.ts +13 -21
  44. package/src/call.ts +78 -23
  45. package/src/client.ts +7 -0
  46. package/src/example-utils.ts +87 -0
  47. package/src/helpers/llm.ts +23 -31
  48. package/src/index.ts +3 -1
  49. package/src/runner.ts +39 -0
  50. package/src/testing/session.ts +2 -22
  51. package/src/version.ts +1 -1
  52. package/dist/examples/example.test.js.map +0 -1
  53. /package/dist/examples/{example.test.d.ts → agent-testing.test.d.ts} +0 -0
package/src/call.ts CHANGED
@@ -33,6 +33,29 @@ export type ReachPersonOutcome = {
33
33
  nextActionPreview?: string;
34
34
  };
35
35
 
36
+ export const DEFAULT_REACH_PERSON_OUTCOMES: ReachPersonOutcome[] = [
37
+ { key: "available", description: "The intended contact is confirmed on the line." },
38
+ {
39
+ key: "unavailable",
40
+ description:
41
+ "The contact could not be reached. A third party, gatekeeper, or IVR was unable to transfer the call to the contact.",
42
+ },
43
+ { key: "voicemail", description: "An answering machine or voicemail system was reached." },
44
+ { key: "wrong_number", description: "The number does not reach the intended contact." },
45
+ {
46
+ key: "do_not_contact",
47
+ description: "The person on the line has indicated this number should not be called.",
48
+ },
49
+ ];
50
+
51
+ function voicemailHangupInstruction(): string {
52
+ return "DO NOT leave a message. REMAIN SILENT AND HANG UP WITHOUT RESPONDING.";
53
+ }
54
+
55
+ function voicemailMessageInstruction(message: string): string {
56
+ return `Say this message VERBATIM: "${message}" Then hang up.`;
57
+ }
58
+
36
59
  @telemetryClient.trackClass()
37
60
  export class Call {
38
61
  private _callId: string;
@@ -242,19 +265,38 @@ export class Call {
242
265
 
243
266
  async reachPerson(
244
267
  contactFullName: string,
245
- options: { outcomes?: ReachPersonOutcome[]; greeting?: string } = {},
268
+ options: {
269
+ outcomes?: ReachPersonOutcome[];
270
+ greeting?: string;
271
+ voicemailMessage?: string;
272
+ voicemailHangup?: boolean;
273
+ } = {},
246
274
  ) {
247
- const outcomes = options.outcomes ?? [
248
- { key: "available", description: "The contact is available to speak." },
249
- {
250
- key: "unavailable",
251
- description:
252
- "The contact is not available to speak. This includes reaching a wrong number.",
253
- },
254
- ];
275
+ if ((await this.getVariable("_voicemail_handler")) === "set_voicemail_action") {
276
+ throw new Error(
277
+ "Cannot call reachPerson() after setVoicemailAction(). " +
278
+ "Use the voicemailMessage or voicemailHangup parameters on reachPerson() instead.",
279
+ );
280
+ }
281
+ await this.setVariable("_voicemail_handler", "reach_person");
282
+
283
+ if (options.voicemailMessage && options.voicemailHangup) {
284
+ throw new Error("Cannot specify both 'voicemailMessage' and 'voicemailHangup'.");
285
+ }
286
+
287
+ const outcomes = options.outcomes ?? DEFAULT_REACH_PERSON_OUTCOMES;
288
+
289
+ let voicemailRule: string;
290
+ if (options.voicemailHangup) {
291
+ voicemailRule = voicemailHangupInstruction();
292
+ } else if (options.voicemailMessage) {
293
+ voicemailRule = voicemailMessageInstruction(options.voicemailMessage);
294
+ } else {
295
+ voicemailRule = "Leave an appropriate voicemail message.";
296
+ }
255
297
 
256
298
  const availabilityDescription =
257
- `The availability of ${contactFullName}` +
299
+ `The availability of ${contactFullName}.` +
258
300
  (outcomes.some((o) => o.description)
259
301
  ? "\nDetailed descriptions of each choice:\n" +
260
302
  outcomes
@@ -266,7 +308,7 @@ export class Call {
266
308
  const checklist: (FieldItem | SayItem | string)[] = [
267
309
  options.greeting !== undefined
268
310
  ? Say(options.greeting)
269
- : `Greet the person who answered the phone. Notify them who you are calling on behalf of and the purpose of the call. Ask to speak with ${contactFullName}`,
311
+ : `Greet the person, IVR, or system who answered the phone. Notify them who you are calling on behalf of and the purpose of the call. Ask to speak with ${contactFullName}. Do not greet if you detect an answering machine or voicemail system.`,
270
312
  {
271
313
  item_type: "field",
272
314
  key: "contact_availability",
@@ -281,38 +323,51 @@ export class Call {
281
323
  .map((o) => `- ${o.key} → ${o.nextActionPreview}`);
282
324
  if (nextActionLines.length > 0) {
283
325
  checklist.push(
284
- "If a next action is defined below for the value of `contact_availability`, briefly ask the contact to wait just a second while you perform it.\n" +
326
+ "If a next action is defined below for the recorded value of `contact_availability`, briefly let the contact know while you perform it.\n" +
285
327
  nextActionLines.join("\n"),
286
328
  );
287
329
  }
288
330
 
289
331
  const objective = `\
290
332
  OBJECTIVE:
291
- Your goal is to reach ${contactFullName} and determine their availability to proceed with this call.
333
+ Your goal is to reach ${contactFullName} and confirm they are on the line.
292
334
 
293
335
  RULES:
294
- 1. If the initial respondent is NOT ${contactFullName}:
295
- - Politely ask to speak with ${contactFullName}
296
- - Wait to be transferred or for ${contactFullName} to come to the phone
297
- 2. Once you have ${contactFullName} on the line:
336
+ 1. If someone other than ${contactFullName} answers - including a person or IVR:
337
+ - Politely ask to speak with ${contactFullName}, or navigate menus and prompts to reach them.
338
+ - Wait to be transferred or for ${contactFullName} to come to the phone.
339
+ - If ${contactFullName} cannot be reached, record \`contact_availability\` appropriately.
340
+ 2. Once ${contactFullName} is confirmed on the line:
298
341
  - Briefly restate who you are and the purpose of your call
299
- - Determine and record their current availability status
300
- 3. DO NOT hang up the call under any circumstances, unless it's a wrong number.
342
+ - Record their availability as available, or equivalent, in \`contact_availability\`.
343
+ 3. If it is clearly a wrong number or you have been asked not to call, politely end the call and hang up.
344
+ 4. If you reach an answering machine or voicemail: ${voicemailRule}
345
+ `;
301
346
 
347
+ const completionCriteria = `\
302
348
  TASK COMPLETION REQUIREMENTS:
303
- - The availability of ${contactFullName} must be recorded in \`contact_availability\`.`;
349
+ - The availability of ${contactFullName} must be recorded in \`contact_availability\`.
350
+ `;
304
351
 
305
- await this.setTask({ taskId: "reach_person", objective, checklist });
352
+ await this.setTask({ taskId: "reach_person", objective, checklist, completionCriteria });
306
353
  }
307
354
 
308
355
  async setVoicemailAction(action: { hangup: true } | { message: string }) {
356
+ if ((await this.getVariable("_voicemail_handler")) === "reach_person") {
357
+ throw new Error(
358
+ "Cannot call setVoicemailAction() after reachPerson(). " +
359
+ "Use the voicemailMessage or voicemailHangup parameters on reachPerson() instead.",
360
+ );
361
+ }
362
+ await this.setVariable("_voicemail_handler", "set_voicemail_action");
363
+
309
364
  if ("hangup" in action) {
310
365
  await this.sendInstruction(
311
- "If you encounter an answering machine, DO NOT leave a message. REMAIN SILENT AND HANG UP WITHOUT RESPONDING. You should only do this when it's clear you are unable to reach the person.",
366
+ `If you encounter an answering machine, ${voicemailHangupInstruction()} You should only do this when it's clear you are unable to reach the person.`,
312
367
  );
313
368
  } else {
314
369
  await this.sendInstruction(
315
- `If you encounter an answering machine, say this message VERBATIM: ${action.message}. You should only leave this message if it's clear you are unable to reach the person.`,
370
+ `If you encounter an answering machine, ${voicemailMessageInstruction(action.message)} You should only do this when it's clear you are unable to reach the person.`,
316
371
  );
317
372
  }
318
373
  }
package/src/client.ts CHANGED
@@ -172,6 +172,13 @@ export class Client {
172
172
  return body.webrtc_code;
173
173
  }
174
174
 
175
+ async listNumbers(): Promise<{ phoneNumber: string }[]> {
176
+ const url = new URL("v1/phone-numbers", this.getHttpBase());
177
+ const response = await fetchOrThrow(url, { headers: await this.headers() });
178
+ const items = (await response.json()) as { phone_number: string }[];
179
+ return items.map((item) => ({ phoneNumber: item.phone_number }));
180
+ }
181
+
175
182
  /**
176
183
  * Sends an SMS message from one of your Guava numbers.
177
184
  * @param fromNumber - One of your Guava numbers (E.164). Must have SMS configured.
@@ -0,0 +1,87 @@
1
+ import * as process from "node:process";
2
+ import { Client } from "./client.ts";
3
+
4
+ export async function getAgentNumber(): Promise<string> {
5
+ if (process.env.GUAVA_AGENT_NUMBER) return process.env.GUAVA_AGENT_NUMBER;
6
+
7
+ const client = new Client();
8
+ const numbers = await client.listNumbers();
9
+
10
+ if (numbers.length === 0) {
11
+ console.error("No agent phone numbers found. Please purchase a number first.");
12
+ process.exit(1);
13
+ }
14
+ if (numbers.length === 1) return numbers[0].phoneNumber;
15
+ if (process.stdin.isTTY && process.stdout.isTTY) {
16
+ return terminalPicker(
17
+ numbers.map((n) => n.phoneNumber),
18
+ "Multiple agent phone numbers found. Select one",
19
+ );
20
+ }
21
+ console.error(
22
+ `Multiple agent phone numbers found: ${numbers.map((n) => n.phoneNumber).join(", ")}. Please set GUAVA_AGENT_NUMBER.`,
23
+ );
24
+ process.exit(1);
25
+ }
26
+
27
+ export async function terminalPicker(
28
+ options: string[],
29
+ prompt = "Select an option",
30
+ ): Promise<string> {
31
+ if (!options.length) throw new Error("options must be non-empty");
32
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
33
+ throw new Error("terminalPicker requires a TTY stdin and stdout");
34
+
35
+ return new Promise((resolve, reject) => {
36
+ let selected = 0;
37
+ let renderedLines = 0;
38
+
39
+ function render() {
40
+ const lines = [`${prompt}:`];
41
+ for (let i = 0; i < options.length; i++) {
42
+ lines.push(`${i === selected ? "❯" : " "} ${options[i]}`);
43
+ }
44
+ if (renderedLines > 0) process.stdout.write(`\x1b[${renderedLines}F`);
45
+ for (const line of lines) {
46
+ process.stdout.write("\r\x1b[2K");
47
+ process.stdout.write(line);
48
+ process.stdout.write("\n");
49
+ }
50
+ for (let i = 0; i < renderedLines - lines.length; i++) {
51
+ process.stdout.write("\r\x1b[2K\n");
52
+ }
53
+ renderedLines = lines.length;
54
+ }
55
+
56
+ function cleanup() {
57
+ process.stdin.removeListener("data", onData);
58
+ process.stdin.setRawMode(false);
59
+ process.stdin.pause();
60
+ process.stdout.write("\x1b[?25h");
61
+ }
62
+
63
+ function onData(data: Buffer) {
64
+ const ch = data.toString();
65
+ if (ch === "\r" || ch === "\n") {
66
+ cleanup();
67
+ process.stdout.write(`\r\x1b[2KSelected: ${options[selected]}\n`);
68
+ resolve(options[selected]);
69
+ } else if (ch === "\x03") {
70
+ cleanup();
71
+ reject(new Error("Interrupted"));
72
+ } else if (ch === "\x1b[A") {
73
+ selected = (selected - 1 + options.length) % options.length;
74
+ render();
75
+ } else if (ch === "\x1b[B") {
76
+ selected = (selected + 1) % options.length;
77
+ render();
78
+ }
79
+ }
80
+
81
+ process.stdin.setRawMode(true);
82
+ process.stdin.resume();
83
+ process.stdout.write("\x1b[?25l");
84
+ render();
85
+ process.stdin.on("data", onData);
86
+ });
87
+ }
@@ -1,3 +1,4 @@
1
+ import * as z from "zod";
1
2
  import type { SuggestedAction } from "../agent.ts";
2
3
  import { Client } from "../client.ts";
3
4
  import { telemetryClient } from "../telemetry.ts";
@@ -81,15 +82,10 @@ export class DocumentQA {
81
82
  }
82
83
  }
83
84
 
84
- const _filterSchema = {
85
- type: "object",
86
- properties: {
87
- matching_appointments: { type: "array", items: { type: "string" } },
88
- other_appointments: { type: "array", items: { type: "string" } },
89
- },
90
- required: ["matching_appointments", "other_appointments"],
91
- additionalProperties: false,
92
- };
85
+ export const _filterSchema = z.object({
86
+ matching_appointments: z.array(z.string()),
87
+ other_appointments: z.array(z.string()),
88
+ });
93
89
 
94
90
  @telemetryClient.trackClass()
95
91
  export class DatetimeFilter {
@@ -122,11 +118,8 @@ ${this._slotsStr}
122
118
 
123
119
  Return at most ${maxResults} items per list.`;
124
120
 
125
- const text = await _generate(this._client, prompt, _filterSchema);
126
- const result = JSON.parse(text) as {
127
- matching_appointments: string[];
128
- other_appointments: string[];
129
- };
121
+ const text = await _generate(this._client, prompt, z.toJSONSchema(_filterSchema));
122
+ const result = _filterSchema.parse(JSON.parse(text));
130
123
  return [
131
124
  result.matching_appointments.slice(0, maxResults),
132
125
  result.other_appointments.slice(0, maxResults),
@@ -134,30 +127,29 @@ Return at most ${maxResults} items per list.`;
134
127
  }
135
128
  }
136
129
 
130
+ export function _makeIntentSchema(choiceList: [string, ...string[]]) {
131
+ return z
132
+ .object({
133
+ possible_matches: z
134
+ .array(z.enum(choiceList))
135
+ .describe(
136
+ "Choices that could match the caller's intent, ordered by likelihood. Include all plausible matches.",
137
+ ),
138
+ })
139
+ .meta({ title: "ChoiceModel" });
140
+ }
141
+
137
142
  @telemetryClient.trackClass()
138
143
  export class IntentRecognizer {
139
144
  private readonly _client: Client;
140
145
  private readonly _intentChoices: string[] | Record<string, string>;
141
- private readonly _schema: object;
146
+ private readonly _schema: ReturnType<typeof _makeIntentSchema>;
142
147
 
143
148
  constructor(intentChoices: string[] | Record<string, string>) {
144
149
  this._client = new Client();
145
150
  this._intentChoices = intentChoices;
146
151
  const choiceList = Array.isArray(intentChoices) ? intentChoices : Object.keys(intentChoices);
147
- this._schema = {
148
- type: "object",
149
- title: "ChoiceModel",
150
- properties: {
151
- possible_matches: {
152
- type: "array",
153
- items: { type: "string", enum: choiceList },
154
- description:
155
- "Choices that could match the caller's intent, ordered by likelihood. Include all plausible matches.",
156
- },
157
- },
158
- required: ["possible_matches"],
159
- additionalProperties: false,
160
- };
152
+ this._schema = _makeIntentSchema(choiceList as [string, ...string[]]);
161
153
  }
162
154
 
163
155
  async classify(intent: string): Promise<SuggestedAction[] | null> {
@@ -183,8 +175,8 @@ Rules:
183
175
  prompt += `\n\nDetailed descriptions of each choice:\n ${descriptionString}`;
184
176
  }
185
177
 
186
- const text = await _generate(this._client, prompt, this._schema);
187
- const keys = (JSON.parse(text) as { possible_matches: string[] }).possible_matches;
178
+ const text = await _generate(this._client, prompt, z.toJSONSchema(this._schema));
179
+ const { possible_matches: keys } = this._schema.parse(JSON.parse(text));
188
180
 
189
181
  if (!keys.length) return null;
190
182
 
package/src/index.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  export { Client, type InboundConnection } from "./client.ts";
2
+ export { Runner } from "./runner.ts";
2
3
  export type { SmsMessage } from "./sms.ts";
3
4
  export { CallController, type TaskObjective } from "./call-controller.ts";
4
5
  export { Say, Field } from "./action-item.ts";
5
6
  export { Logger, getConsoleLogger, getDefaultLogger } from "./logging.ts";
6
7
  export { Agent, CallInfo } from "./agent.ts";
7
- export { Call } from "./call.ts";
8
+ export { Call, DEFAULT_REACH_PERSON_OUTCOMES } from "./call.ts";
9
+ export type { ReachPersonOutcome } from "./call.ts";
8
10
  export type { BotSessionEnded, TerminationReason, DTMFPressedEvent, DTMFDigit } from "./events.ts";
9
11
  export { TestSession } from "./testing/session.ts";
10
12
  export { MockCall } from "./testing/mocks.ts";
package/src/runner.ts ADDED
@@ -0,0 +1,39 @@
1
+ import type { Agent } from "./agent.ts";
2
+ import { MultiHealthContext, getHealthServer } from "./health.ts";
3
+
4
+ export class Runner {
5
+ private _healthCtx = new MultiHealthContext();
6
+ private _tasks: Array<() => Promise<void>> = [];
7
+
8
+ listenPhone(agent: Agent, agentNumber: string): this {
9
+ const ctx = this._healthCtx.createCtx();
10
+ this._tasks.push(() => agent._listenInbound(ctx, { agent_number: agentNumber }));
11
+ return this;
12
+ }
13
+
14
+ listenWebrtc(agent: Agent, webrtcCode?: string): this {
15
+ const ctx = this._healthCtx.createCtx();
16
+ this._tasks.push(async () => {
17
+ const code = webrtcCode ?? (await agent._client.createWebrtcAgent(3600));
18
+ return agent._listenInbound(ctx, { webrtc_code: code });
19
+ });
20
+ return this;
21
+ }
22
+
23
+ listenSip(agent: Agent, sipCode: string): this {
24
+ const ctx = this._healthCtx.createCtx();
25
+ this._tasks.push(() => agent._listenInbound(ctx, { sip_code: sipCode }));
26
+ return this;
27
+ }
28
+
29
+ attachCampaign(agent: Agent, campaignCode: string): this {
30
+ const ctx = this._healthCtx.createCtx();
31
+ this._tasks.push(() => agent._serveCampaign(ctx, campaignCode));
32
+ return this;
33
+ }
34
+
35
+ async run(): Promise<void> {
36
+ await using _server = await getHealthServer(this._healthCtx);
37
+ await Promise.all(this._tasks.map((task) => task()));
38
+ }
39
+ }
@@ -7,7 +7,7 @@ import { _generate } from "../helpers/llm.ts";
7
7
 
8
8
  type SessionEvent = InjectASR | BotTTS;
9
9
 
10
- const _evalResponseSchema = z.object({
10
+ export const _evalResponseSchema = z.object({
11
11
  results: z.array(
12
12
  z.object({
13
13
  passed: z.boolean(),
@@ -166,28 +166,8 @@ ${transcript || "(empty — no conversation occurred)"}
166
166
  Criteria:
167
167
  ${criteriaList}`;
168
168
 
169
- const evalSchema = {
170
- type: "object",
171
- properties: {
172
- results: {
173
- type: "array",
174
- items: {
175
- type: "object",
176
- properties: {
177
- passed: { type: "boolean" },
178
- reasoning: { type: "string" },
179
- },
180
- required: ["passed"],
181
- additionalProperties: false,
182
- },
183
- },
184
- },
185
- required: ["results"],
186
- additionalProperties: false,
187
- };
188
-
189
169
  const evalResponse = _evalResponseSchema.parse(
190
- JSON.parse(await _generate(this._client, prompt, evalSchema)),
170
+ JSON.parse(await _generate(this._client, prompt, z.toJSONSchema(_evalResponseSchema))),
191
171
  );
192
172
 
193
173
  if (evalResponse.results.length !== allCriteria.length) {
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const SDK_VERSION = "0.24.0";
1
+ export const SDK_VERSION = "0.25.0";
@@ -1 +0,0 @@
1
- {"version":3,"file":"example.test.js","sourceRoot":"","sources":["../../examples/example.test.ts"],"names":[],"mappings":";AAAA;;;GAGG;;AAGH,mDAA+C;AAC/C,sEAA+D;AAE/D,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,IAAI,CAAC,kDAAkD,EAAE,KAAK,IAAI,EAAE;QAClE,MAAM,UAAU,GAAG,MAAM,iBAAK,CAAC,QAAQ,CAAC,eAAe,CAAC,IAAI,oBAAQ,EAAE,EAAE,qBAAqB,CAAC,CAAC;QAC/F,MAAM,CAAC,UAAU,CAAC,CAAC,cAAc,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,8BAA8B,EAAE,KAAK,IAAI,EAAE;QAC9C,MAAM,OAAO,GAAG,MAAM,iBAAK,CAAC,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;YACjD,iFAAiF;YACjF,MAAM,OAAO,CAAC,WAAW,EAAE,CAAC;YAC5B,OAAO,CAAC,GAAG,CAAC,yCAAyC,CAAC,CAAC;YAEvD,6CAA6C;YAC7C,MAAM,OAAO,CAAC,UAAU,EAAE,CAAC;QAC7B,CAAC,CAAC,CAAC;QAEH,mEAAmE;QAEnE,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QACnD,MAAM,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,gCAAgC,EAAE,KAAK,IAAI,EAAE;QAChD,mFAAmF;QACnF,MAAM,OAAO,GAAG,MAAM,iBAAK,CAAC,YAAY,CAAC,6CAA6C,CAAC,CAAC;QAExF,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QACnD,MAAM,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,yCAAyC,EAAE,KAAK,IAAI,EAAE;QACzD,4EAA4E;QAC5E,MAAM,OAAO,GAAG,iBAAK,CAAC,KAAK,EAAE,CAAC;QAE9B,OAAO,CAAC,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,IAAU,EAAE,EAAE;YAC7C,IAAI,CAAC,MAAM,CACT,8GAA8G,CAC/G,CAAC;QACJ,CAAC,CAAC,CAAC;QAEH,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,YAAY,CAAC,6CAA6C,CAAC,CAAC;QAE1F,8FAA8F;QAC9F,MAAM,OAAO,CAAC,QAAQ,CAAC;YACrB,YAAY,EAAE,CAAC,sEAAsE,CAAC;YACtF,YAAY,EAAE,CAAC,2DAA2D,CAAC;SAC5E,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}