@recombine-ai/platform 0.2.6-test.1 → 0.2.6-test.10

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.
@@ -0,0 +1,392 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PostgresAnalyticsQueryService = exports.AnalyticsQueryError = void 0;
4
+ exports.createPostgresAnalyticsQueryServices = createPostgresAnalyticsQueryServices;
5
+ const pg_1 = require("pg");
6
+ class AnalyticsQueryError extends Error {
7
+ name = 'AnalyticsQueryError';
8
+ }
9
+ exports.AnalyticsQueryError = AnalyticsQueryError;
10
+ class PostgresAnalyticsQueryService {
11
+ database;
12
+ constructor(database) {
13
+ this.database = database;
14
+ }
15
+ async getConversation(conversationId) {
16
+ const result = await this.database.query(`SELECT c.conversation_id, c.agent_id, c.channel, c.region, c.started_at,
17
+ c.ended_at, c.analytics,
18
+ p.phone_number, p.recording_url, p.duration_seconds
19
+ FROM conversations c
20
+ LEFT JOIN phone_calls p ON p.conversation_id = c.conversation_id
21
+ WHERE c.conversation_id = $1`, [conversationId]);
22
+ const row = result.rows[0];
23
+ return row?.agent_id ? conversationRowToJson(row) : undefined;
24
+ }
25
+ async getConversations(query) {
26
+ const parsedLimit = Number.parseInt(optionalString(query.limit, 'limit') ?? '10', 10);
27
+ const requestedLimit = Number.isFinite(parsedLimit)
28
+ ? Math.min(100, Math.max(1, parsedLimit))
29
+ : 10;
30
+ const cursor = parseConversationCursor(optionalString(query.lastConversationId, 'lastConversationId'));
31
+ const { bind, conditions, values } = buildConversationConditions(query);
32
+ if (cursor) {
33
+ conditions.push(`(c.started_at, c.conversation_id) < (${bind(cursor.startedAt)}, ${bind(cursor.conversationId)})`);
34
+ }
35
+ const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
36
+ const limitParameter = bind(requestedLimit + 1);
37
+ const result = await this.database.query(`SELECT c.conversation_id, c.agent_id, c.channel, c.region, c.started_at,
38
+ c.ended_at, c.analytics,
39
+ p.phone_number, p.recording_url, p.duration_seconds
40
+ FROM conversations c
41
+ LEFT JOIN phone_calls p ON p.conversation_id = c.conversation_id
42
+ ${where}
43
+ ORDER BY c.started_at DESC, c.conversation_id DESC
44
+ LIMIT ${limitParameter}`, values);
45
+ const hasNextPage = result.rows.length > requestedLimit;
46
+ const rows = result.rows.slice(0, requestedLimit);
47
+ return {
48
+ limit: requestedLimit,
49
+ items: rows.map(conversationRowToJson),
50
+ lastConversationId: hasNextPage && rows.length > 0 ? makeConversationCursor(rows.at(-1)) : undefined,
51
+ };
52
+ }
53
+ async getAnalyticsFields(query) {
54
+ const agentId = requiredString(query.agentId, 'agentId');
55
+ const values = [agentId];
56
+ const where = "WHERE jsonb_typeof(entry.value) IN ('boolean', 'number', 'string')" +
57
+ ' AND c.agent_id = $1';
58
+ const result = await this.database.query(`WITH value_counts AS (
59
+ SELECT entry.key,
60
+ jsonb_typeof(entry.value) AS type,
61
+ entry.value AS value,
62
+ COUNT(*)::bigint AS occurrence_count
63
+ FROM conversations c
64
+ CROSS JOIN LATERAL jsonb_each(c.analytics) AS entry(key, value)
65
+ ${where}
66
+ GROUP BY entry.key, type, entry.value
67
+ ),
68
+ ranked AS (
69
+ SELECT key,
70
+ type,
71
+ value,
72
+ occurrence_count,
73
+ COUNT(*) OVER (PARTITION BY key, type)::integer AS variant_count,
74
+ ROW_NUMBER() OVER (
75
+ PARTITION BY key, type
76
+ ORDER BY occurrence_count DESC, value::text ASC
77
+ ) AS rank
78
+ FROM value_counts
79
+ )
80
+ SELECT key, type, value, occurrence_count, variant_count
81
+ FROM ranked
82
+ WHERE rank <= 50
83
+ ORDER BY key ASC, type ASC, rank ASC`, values);
84
+ return analyticsFieldRowsToJson(result.rows);
85
+ }
86
+ async getTimeSeries(query) {
87
+ const metric = parseMetric(requiredString(query.metric, 'metric'));
88
+ const granularity = (optionalString(query.granularity, 'granularity') ??
89
+ 'day');
90
+ if (!['hour', 'day', 'week', 'month'].includes(granularity)) {
91
+ throw new AnalyticsQueryError('granularity must be hour, day, week, or month');
92
+ }
93
+ const { bind, conditions, from, to, values } = buildConversationConditions(query);
94
+ if (!from || !to) {
95
+ throw new AnalyticsQueryError('from and to are required for a time series');
96
+ }
97
+ const bucketExpression = `date_trunc(${bind(granularity)}::text, c.started_at, 'UTC')`;
98
+ let valueExpression;
99
+ let numeratorExpression = 'NULL::bigint';
100
+ if (metric.operation === 'count') {
101
+ valueExpression = 'COUNT(*)::double precision';
102
+ }
103
+ else if (metric.operation === 'rate') {
104
+ const predicate = makeAnalyticsPredicate(metric.filter, bind);
105
+ numeratorExpression = `COUNT(*) FILTER (WHERE ${predicate})::bigint`;
106
+ valueExpression =
107
+ `COUNT(*) FILTER (WHERE ${predicate})::double precision / ` + 'NULLIF(COUNT(*), 0)';
108
+ }
109
+ else {
110
+ const key = bind(metric.key);
111
+ const numericValue = `CASE WHEN jsonb_typeof(c.analytics -> (${key}::text)) = 'number' ` +
112
+ `THEN (c.analytics ->> (${key}::text))::double precision END`;
113
+ valueExpression =
114
+ metric.operation === 'average' ? `AVG(${numericValue})` : `SUM(${numericValue})`;
115
+ }
116
+ const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
117
+ const result = await this.database.query(`SELECT ${bucketExpression} AS bucket_start,
118
+ ${valueExpression} AS metric_value,
119
+ ${numeratorExpression} AS numerator,
120
+ COUNT(*)::bigint AS denominator
121
+ FROM conversations c
122
+ LEFT JOIN phone_calls p ON p.conversation_id = c.conversation_id
123
+ ${where}
124
+ GROUP BY bucket_start
125
+ ORDER BY bucket_start ASC`, values);
126
+ return {
127
+ metric,
128
+ granularity,
129
+ from: from.toISOString(),
130
+ to: to.toISOString(),
131
+ items: result.rows.map((row) => ({
132
+ startAt: new Date(row.bucket_start).toISOString(),
133
+ value: row.metric_value === null ? null : Number(row.metric_value),
134
+ ...(metric.operation === 'rate'
135
+ ? {
136
+ numerator: Number(row.numerator),
137
+ denominator: Number(row.denominator),
138
+ }
139
+ : {}),
140
+ })),
141
+ };
142
+ }
143
+ }
144
+ exports.PostgresAnalyticsQueryService = PostgresAnalyticsQueryService;
145
+ function createPostgresAnalyticsQueryServices(config) {
146
+ const pool = new pg_1.Pool(config);
147
+ return {
148
+ queries: new PostgresAnalyticsQueryService(pool),
149
+ close: () => pool.end(),
150
+ };
151
+ }
152
+ function optionalString(value, name) {
153
+ if (value === undefined)
154
+ return undefined;
155
+ if (typeof value !== 'string') {
156
+ throw new AnalyticsQueryError(`${name} must be a string`);
157
+ }
158
+ return value;
159
+ }
160
+ function requiredString(value, name) {
161
+ const result = optionalString(value, name);
162
+ if (!result) {
163
+ throw new AnalyticsQueryError(`${name} is required`);
164
+ }
165
+ return result;
166
+ }
167
+ function parseDateFilter(value, name) {
168
+ const stringValue = optionalString(value, name);
169
+ if (stringValue === undefined)
170
+ return undefined;
171
+ const date = /^\d+$/.test(stringValue) ? new Date(Number(stringValue)) : new Date(stringValue);
172
+ if (Number.isNaN(date.getTime())) {
173
+ throw new AnalyticsQueryError(`${name} must be a valid date`);
174
+ }
175
+ return date;
176
+ }
177
+ function parseDurationFilter(value, name) {
178
+ const stringValue = optionalString(value, name);
179
+ if (stringValue === undefined)
180
+ return undefined;
181
+ const duration = Number(stringValue);
182
+ if (!Number.isFinite(duration) || duration < 0) {
183
+ throw new AnalyticsQueryError(`${name} must be a non-negative number`);
184
+ }
185
+ return duration;
186
+ }
187
+ function parseAnalyticsFilters(value) {
188
+ const stringValue = optionalString(value, 'filters');
189
+ if (!stringValue)
190
+ return [];
191
+ let filters;
192
+ try {
193
+ filters = JSON.parse(stringValue);
194
+ }
195
+ catch {
196
+ throw new AnalyticsQueryError('filters must be valid JSON');
197
+ }
198
+ if (!Array.isArray(filters) || filters.length > 20) {
199
+ throw new AnalyticsQueryError('filters must be an array with at most 20 entries');
200
+ }
201
+ return filters.map(validateAnalyticsFilter);
202
+ }
203
+ function validateAnalyticsFilter(value) {
204
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
205
+ throw new AnalyticsQueryError('Each filter must be an object');
206
+ }
207
+ const filter = value;
208
+ if (typeof filter.key !== 'string' || !/^[A-Za-z0-9_.:-]{1,128}$/.test(filter.key)) {
209
+ throw new AnalyticsQueryError('Filter key is invalid');
210
+ }
211
+ if (!['boolean', 'number', 'string'].includes(String(filter.type))) {
212
+ throw new AnalyticsQueryError(`Filter '${filter.key}' has an invalid type`);
213
+ }
214
+ const type = filter.type;
215
+ const allowedOperators = type === 'number'
216
+ ? ['eq', 'in', 'gt', 'gte', 'lt', 'lte', 'exists']
217
+ : ['eq', 'in', 'exists'];
218
+ if (!allowedOperators.includes(String(filter.operator))) {
219
+ throw new AnalyticsQueryError(`Filter '${filter.key}' has an invalid operator`);
220
+ }
221
+ const operator = filter.operator;
222
+ if (operator === 'in') {
223
+ if (!Array.isArray(filter.values) ||
224
+ filter.values.length === 0 ||
225
+ filter.values.length > 50 ||
226
+ filter.values.some((item) => typeof item !== type || (type === 'number' && !Number.isFinite(item)))) {
227
+ throw new AnalyticsQueryError(`Filter '${filter.key}' has invalid values`);
228
+ }
229
+ return { key: filter.key, type, operator, values: filter.values };
230
+ }
231
+ if (operator !== 'exists' && typeof filter.value !== type) {
232
+ throw new AnalyticsQueryError(`Filter '${filter.key}' has an invalid value`);
233
+ }
234
+ if (type === 'number' && !Number.isFinite(filter.value)) {
235
+ throw new AnalyticsQueryError(`Filter '${filter.key}' has an invalid value`);
236
+ }
237
+ return {
238
+ key: filter.key,
239
+ type,
240
+ operator,
241
+ ...(operator === 'exists' ? {} : { value: filter.value }),
242
+ };
243
+ }
244
+ function parseMetric(value) {
245
+ let metric;
246
+ try {
247
+ metric = JSON.parse(value);
248
+ }
249
+ catch {
250
+ throw new AnalyticsQueryError('metric must be valid JSON');
251
+ }
252
+ if (!metric || typeof metric !== 'object' || Array.isArray(metric)) {
253
+ throw new AnalyticsQueryError('metric must be an object');
254
+ }
255
+ const record = metric;
256
+ if (!['count', 'rate', 'average', 'sum'].includes(String(record.operation))) {
257
+ throw new AnalyticsQueryError('metric operation is invalid');
258
+ }
259
+ if (record.operation === 'rate') {
260
+ return { operation: 'rate', filter: validateAnalyticsFilter(record.filter) };
261
+ }
262
+ if (record.operation === 'average' || record.operation === 'sum') {
263
+ if (typeof record.key !== 'string' || !/^[A-Za-z0-9_.:-]{1,128}$/.test(record.key)) {
264
+ throw new AnalyticsQueryError('Metric key is invalid');
265
+ }
266
+ return { operation: record.operation, key: record.key };
267
+ }
268
+ return { operation: 'count' };
269
+ }
270
+ function parseConversationCursor(value) {
271
+ if (!value)
272
+ return undefined;
273
+ try {
274
+ const cursor = JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
275
+ const startedAt = new Date(String(cursor.startedAt));
276
+ if (Number.isNaN(startedAt.getTime()) ||
277
+ typeof cursor.conversationId !== 'string' ||
278
+ cursor.conversationId === '') {
279
+ throw new Error('invalid cursor');
280
+ }
281
+ return { startedAt, conversationId: cursor.conversationId };
282
+ }
283
+ catch {
284
+ throw new AnalyticsQueryError('lastConversationId is invalid');
285
+ }
286
+ }
287
+ function makeConversationCursor(row) {
288
+ return Buffer.from(JSON.stringify({
289
+ startedAt: new Date(row.started_at).toISOString(),
290
+ conversationId: row.conversation_id,
291
+ })).toString('base64url');
292
+ }
293
+ function conversationRowToJson(row) {
294
+ return {
295
+ conversationId: row.conversation_id,
296
+ medium: row.channel,
297
+ agentId: row.agent_id,
298
+ region: row.region || undefined,
299
+ phoneNumber: row.phone_number || undefined,
300
+ duration: row.duration_seconds ?? undefined,
301
+ audioUrl: row.recording_url || undefined,
302
+ createdAt: new Date(row.started_at).getTime(),
303
+ endedAt: row.ended_at ? new Date(row.ended_at).getTime() : undefined,
304
+ analytics: analyticsScalars(row.analytics),
305
+ };
306
+ }
307
+ function analyticsScalars(values) {
308
+ return Object.fromEntries(Object.entries(values ?? {}).filter((entry) => {
309
+ const value = entry[1];
310
+ return (typeof value === 'boolean' ||
311
+ typeof value === 'string' ||
312
+ (typeof value === 'number' && Number.isFinite(value)));
313
+ }));
314
+ }
315
+ function analyticsFieldRowsToJson(rows) {
316
+ const fields = new Map();
317
+ for (const row of rows) {
318
+ const id = `${row.key}\u0000${row.type}`;
319
+ let field = fields.get(id);
320
+ if (!field) {
321
+ field = {
322
+ key: row.key,
323
+ type: row.type,
324
+ variantCount: Number(row.variant_count),
325
+ values: [],
326
+ };
327
+ fields.set(id, field);
328
+ }
329
+ if (field.variantCount <= 50) {
330
+ field.values.push({ value: row.value, count: Number(row.occurrence_count) });
331
+ }
332
+ }
333
+ return { items: [...fields.values()] };
334
+ }
335
+ function buildConversationConditions(query) {
336
+ const values = [];
337
+ const bind = (value) => {
338
+ values.push(value);
339
+ return `$${values.length}`;
340
+ };
341
+ const conditions = [];
342
+ const from = parseDateFilter(query.from, 'from');
343
+ const to = parseDateFilter(query.to, 'to');
344
+ if (from && to && from >= to) {
345
+ throw new AnalyticsQueryError('from must be earlier than to');
346
+ }
347
+ const agentId = requiredString(query.agentId, 'agentId');
348
+ const region = optionalString(query.region, 'region');
349
+ const channel = optionalString(query.channel, 'channel');
350
+ const phoneNumber = optionalString(query.phoneNumber, 'phoneNumber');
351
+ conditions.push(`c.agent_id = ${bind(agentId)}`);
352
+ if (region)
353
+ conditions.push(`c.region = ${bind(region)}`);
354
+ if (channel)
355
+ conditions.push(`c.channel = ${bind(channel)}`);
356
+ if (phoneNumber)
357
+ conditions.push(`p.phone_number = ${bind(phoneNumber)}`);
358
+ if (from)
359
+ conditions.push(`c.started_at >= ${bind(from)}`);
360
+ if (to)
361
+ conditions.push(`c.started_at < ${bind(to)}`);
362
+ const minDuration = parseDurationFilter(query.minDurationSeconds, 'minDurationSeconds');
363
+ const maxDuration = parseDurationFilter(query.maxDurationSeconds, 'maxDurationSeconds');
364
+ if (minDuration !== undefined)
365
+ conditions.push(`p.duration_seconds >= ${bind(minDuration)}`);
366
+ if (maxDuration !== undefined)
367
+ conditions.push(`p.duration_seconds <= ${bind(maxDuration)}`);
368
+ if (minDuration !== undefined && maxDuration !== undefined && minDuration > maxDuration) {
369
+ throw new AnalyticsQueryError('minDurationSeconds must not exceed maxDurationSeconds');
370
+ }
371
+ for (const filter of parseAnalyticsFilters(query.filters)) {
372
+ conditions.push(makeAnalyticsPredicate(filter, bind));
373
+ }
374
+ return { bind, conditions, from, to, values };
375
+ }
376
+ function makeAnalyticsPredicate(filter, bind) {
377
+ const key = bind(filter.key);
378
+ if (filter.operator === 'exists') {
379
+ return `c.analytics ? (${key}::text)`;
380
+ }
381
+ if (filter.operator === 'eq') {
382
+ return `c.analytics @> jsonb_build_object(${key}::text, ${bind(JSON.stringify(filter.value))}::jsonb)`;
383
+ }
384
+ if (filter.operator === 'in') {
385
+ const predicates = filter.values.map((value) => `c.analytics @> jsonb_build_object(${key}::text, ${bind(JSON.stringify(value))}::jsonb)`);
386
+ return `(${predicates.join(' OR ')})`;
387
+ }
388
+ const operator = { gt: '>', gte: '>=', lt: '<', lte: '<=' }[filter.operator];
389
+ return (`(CASE WHEN jsonb_typeof(c.analytics -> (${key}::text)) = 'number' THEN ` +
390
+ `(c.analytics ->> (${key}::text))::double precision ${operator} ${bind(filter.value)} ` +
391
+ 'ELSE false END)');
392
+ }
@@ -0,0 +1,45 @@
1
+ import { Pool, PoolConfig, QueryResult } from 'pg';
2
+ import { AnalyticsScalar, AnalyticsStore, ConversationAnalyticsSummary } from './analytics';
3
+ import { PostgresAnalyticsQueryService } from './postgres-analytics-query';
4
+ export interface ConversationRecord {
5
+ conversationId: string;
6
+ agentId: string;
7
+ channel: string;
8
+ region?: string;
9
+ startedAt: Date;
10
+ endedAt?: Date;
11
+ }
12
+ export interface PhoneCallRecord {
13
+ phoneNumber?: string;
14
+ recordingUrl?: string;
15
+ endedReason?: string;
16
+ durationSeconds?: number;
17
+ }
18
+ interface Queryable {
19
+ query<TResult extends Record<string, unknown> = Record<string, unknown>>(text: string, values?: unknown[]): Promise<QueryResult<TResult>>;
20
+ }
21
+ export declare class PostgresAnalytics implements AnalyticsStore {
22
+ private readonly database;
23
+ constructor(database: Queryable);
24
+ set(conversationId: string, agentId: string, fieldName: string, value: AnalyticsScalar): Promise<void>;
25
+ clear(conversationId: string, agentId: string, fieldName: string): Promise<void>;
26
+ get(conversationId: string, agentId: string, fieldName: string): Promise<unknown | undefined>;
27
+ getSummary(conversationId: string, agentId: string): Promise<ConversationAnalyticsSummary | undefined>;
28
+ }
29
+ export declare class PostgresConversationRepository {
30
+ private readonly pool;
31
+ constructor(pool: Pool);
32
+ saveConversation(conversation: ConversationRecord, phoneCall?: PhoneCallRecord): Promise<void>;
33
+ private upsertConversation;
34
+ private upsertPhoneCall;
35
+ }
36
+ export declare function migrateAnalyticsDatabase(pool: Pool): Promise<void>;
37
+ export interface PostgresAnalyticsServices {
38
+ analytics: PostgresAnalytics;
39
+ conversations: PostgresConversationRepository;
40
+ queries: PostgresAnalyticsQueryService;
41
+ close(): Promise<void>;
42
+ }
43
+ export declare function createPostgresAnalyticsServices(config: PoolConfig): Promise<PostgresAnalyticsServices>;
44
+ export {};
45
+ //# sourceMappingURL=postgres-analytics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres-analytics.d.ts","sourceRoot":"","sources":["../src/postgres-analytics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAc,UAAU,EAAE,WAAW,EAAE,MAAM,IAAI,CAAA;AAE9D,OAAO,EACH,eAAe,EACf,cAAc,EACd,4BAA4B,EAE/B,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,6BAA6B,EAAE,MAAM,4BAA4B,CAAA;AAI1E,MAAM,WAAW,kBAAkB;IAC/B,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,IAAI,CAAA;IACf,OAAO,CAAC,EAAE,IAAI,CAAA;CACjB;AAID,MAAM,WAAW,eAAe;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,eAAe,CAAC,EAAE,MAAM,CAAA;CAC3B;AAID,UAAU,SAAS;IACf,KAAK,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnE,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,GACnB,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAA;CACnC;AAaD,qBAAa,iBAAkB,YAAW,cAAc;IACxC,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,SAAS;IAE1C,GAAG,CACL,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,eAAe,GACvB,OAAO,CAAC,IAAI,CAAC;IAeV,KAAK,CAAC,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAchF,GAAG,CACL,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAClB,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC;IAezB,UAAU,CACZ,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,MAAM,GAChB,OAAO,CAAC,4BAA4B,GAAG,SAAS,CAAC;CAqBvD;AAgBD,qBAAa,8BAA8B;IAC3B,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,IAAI;IAEjC,gBAAgB,CAClB,YAAY,EAAE,kBAAkB,EAChC,SAAS,CAAC,EAAE,eAAe,GAC5B,OAAO,CAAC,IAAI,CAAC;YAmBF,kBAAkB;YAsClB,eAAe;CAgChC;AA2FD,wBAAsB,wBAAwB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAgDxE;AA6BD,MAAM,WAAW,yBAAyB;IACtC,SAAS,EAAE,iBAAiB,CAAA;IAC5B,aAAa,EAAE,8BAA8B,CAAA;IAC7C,OAAO,EAAE,6BAA6B,CAAA;IACtC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACzB;AAID,wBAAsB,+BAA+B,CACjD,MAAM,EAAE,UAAU,GACnB,OAAO,CAAC,yBAAyB,CAAC,CAgBpC"}