@recombine-ai/platform 0.2.6-test.3 → 0.2.6-test.5
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/build/analytics.d.ts +0 -2
- package/build/analytics.d.ts.map +1 -1
- package/build/analytics.js +16 -11
- package/build/context-cache.js +6 -2
- package/build/index.d.ts +7 -6
- package/build/index.d.ts.map +1 -1
- package/build/index.js +23 -6
- package/build/interfaces/platform.d.ts +4 -4
- package/build/interfaces/platform.d.ts.map +1 -1
- package/build/interfaces/platform.js +4 -1
- package/build/interfaces/rc-voice.js +2 -1
- package/build/postgres-analytics-query.d.ts +87 -0
- package/build/postgres-analytics-query.d.ts.map +1 -0
- package/build/postgres-analytics-query.js +388 -0
- package/build/postgres-analytics.d.ts +3 -1
- package/build/postgres-analytics.d.ts.map +1 -1
- package/build/postgres-analytics.js +26 -8
- package/build/timeouts-manager.d.ts +1 -1
- package/build/timeouts-manager.d.ts.map +1 -1
- package/build/timeouts-manager.js +5 -1
- package/package.json +3 -10
- package/build/analytics.spec.d.ts +0 -2
- package/build/analytics.spec.d.ts.map +0 -1
- package/build/analytics.spec.js +0 -57
- package/build/postgres-analytics.spec.d.ts +0 -2
- package/build/postgres-analytics.spec.d.ts.map +0 -1
- package/build/postgres-analytics.spec.js +0 -61
package/build/analytics.d.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
export type AnalyticsValueType = 'boolean' | 'number' | 'string' | 'json';
|
|
3
2
|
export type AnalyticsJsonValue = boolean | number | string | null | AnalyticsJsonValue[] | {
|
|
4
3
|
[key: string]: AnalyticsJsonValue;
|
|
5
4
|
};
|
|
6
5
|
export interface AnalyticsKey<T> {
|
|
7
6
|
readonly key: string;
|
|
8
|
-
readonly valueType: AnalyticsValueType;
|
|
9
7
|
readonly schema: z.ZodType<T>;
|
|
10
8
|
}
|
|
11
9
|
export type AnalyticsValue<TKey extends AnalyticsKey<unknown>> = TKey extends AnalyticsKey<infer TValue> ? TValue : never;
|
package/build/analytics.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAIvB,MAAM,MAAM,kBAAkB,GACxB,OAAO,GACP,MAAM,GACN,MAAM,GACN,IAAI,GACJ,kBAAkB,EAAE,GACpB;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,kBAAkB,CAAA;CAAE,CAAA;AAI3C,MAAM,WAAW,YAAY,CAAC,CAAC;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;CAChC;AAID,MAAM,MAAM,cAAc,CAAC,IAAI,SAAS,YAAY,CAAC,OAAO,CAAC,IACzD,IAAI,SAAS,YAAY,CAAC,MAAM,MAAM,CAAC,GAAG,MAAM,GAAG,KAAK,CAAA;AAI5D,MAAM,WAAW,4BAA4B;IACzC,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,IAAI,CAAA;IACf,OAAO,CAAC,EAAE,IAAI,CAAA;IACd,eAAe,CAAC,EAAE,MAAM,CAAA;CAC3B;AAID,MAAM,WAAW,qBAAqB;IAClC,GAAG,CAAC,IAAI,SAAS,YAAY,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,cAAc,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9F,KAAK,CAAC,GAAG,EAAE,YAAY,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAChD,GAAG,CAAC,IAAI,SAAS,YAAY,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,IAAI,GAAG,OAAO,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,CAAA;IAC7F,UAAU,IAAI,OAAO,CAAC,4BAA4B,GAAG,SAAS,CAAC,CAAA;CAClE;AAID,MAAM,WAAW,SAAS;IACtB,wBAAwB,CAAC,cAAc,EAAE,MAAM,GAAG,qBAAqB,CAAA;CAC1E;AAgBD,eAAO,MAAM,YAAY;iBACR,MAAM,GAAG,YAAY,CAAC,OAAO,CAAC;gBAG/B,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC;gBAG7B,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC;eAG9B,OAAO,SAAS,SAAS,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,OAChD,MAAM,UACH,OAAO,GAChB,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;SAK3B,CAAC,SAAS,kBAAkB,OAAO,MAAM,UAAU,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC;CAKzF,CAAA;AAID,wBAAgB,uBAAuB,CAAC,SAAS,GAAE,4BAA4B,EAAO,GAAG,SAAS,CAkDjG;AAID,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,kBAAkB,CA0B5F"}
|
package/build/analytics.js
CHANGED
|
@@ -1,28 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.analyticsKey = void 0;
|
|
4
|
+
exports.createInMemoryAnalytics = createInMemoryAnalytics;
|
|
5
|
+
exports.assertAnalyticsJsonValue = assertAnalyticsJsonValue;
|
|
6
|
+
const zod_1 = require("zod");
|
|
7
|
+
function defineKey(key, schema) {
|
|
3
8
|
if (!/^[A-Za-z0-9_.:-]{1,128}$/.test(key)) {
|
|
4
9
|
throw new Error('Analytics key contains unsupported characters');
|
|
5
10
|
}
|
|
6
|
-
return Object.freeze({ key,
|
|
11
|
+
return Object.freeze({ key, schema });
|
|
7
12
|
}
|
|
8
|
-
|
|
13
|
+
exports.analyticsKey = {
|
|
9
14
|
boolean(key) {
|
|
10
|
-
return defineKey(key,
|
|
15
|
+
return defineKey(key, zod_1.z.boolean());
|
|
11
16
|
},
|
|
12
17
|
number(key) {
|
|
13
|
-
return defineKey(key,
|
|
18
|
+
return defineKey(key, zod_1.z.number().finite());
|
|
14
19
|
},
|
|
15
20
|
string(key) {
|
|
16
|
-
return defineKey(key,
|
|
21
|
+
return defineKey(key, zod_1.z.string());
|
|
17
22
|
},
|
|
18
23
|
enum(key, values) {
|
|
19
|
-
return defineKey(key,
|
|
24
|
+
return defineKey(key, zod_1.z.enum(values));
|
|
20
25
|
},
|
|
21
26
|
json(key, schema) {
|
|
22
|
-
return defineKey(key,
|
|
27
|
+
return defineKey(key, schema);
|
|
23
28
|
},
|
|
24
29
|
};
|
|
25
|
-
|
|
30
|
+
function createInMemoryAnalytics(summaries = []) {
|
|
26
31
|
const conversations = new Map();
|
|
27
32
|
const summariesById = new Map(summaries.map((summary) => [summary.conversationId, structuredClone(summary)]));
|
|
28
33
|
return {
|
|
@@ -58,7 +63,7 @@ export function createInMemoryAnalytics(summaries = []) {
|
|
|
58
63
|
},
|
|
59
64
|
};
|
|
60
65
|
}
|
|
61
|
-
|
|
66
|
+
function assertAnalyticsJsonValue(value) {
|
|
62
67
|
if (value === null ||
|
|
63
68
|
typeof value === 'boolean' ||
|
|
64
69
|
typeof value === 'string' ||
|
package/build/context-cache.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createInMemoryCache = createInMemoryCache;
|
|
4
|
+
exports.createRedisCache = createRedisCache;
|
|
5
|
+
function createInMemoryCache() {
|
|
2
6
|
const cache = new Map();
|
|
3
7
|
async function getContext(callId, defaultContext) {
|
|
4
8
|
if (cache.has(callId)) {
|
|
@@ -21,7 +25,7 @@ export function createInMemoryCache() {
|
|
|
21
25
|
},
|
|
22
26
|
};
|
|
23
27
|
}
|
|
24
|
-
|
|
28
|
+
function createRedisCache(client, options = DEFAULT_REDIS_CACHE_OPTIONS) {
|
|
25
29
|
function createKey(callId) {
|
|
26
30
|
return options.keyPrefix + callId;
|
|
27
31
|
}
|
package/build/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
export * from './interfaces/platform
|
|
2
|
-
export * from './interfaces/rc-voice
|
|
3
|
-
export * from './context-cache
|
|
4
|
-
export * from './analytics
|
|
5
|
-
export * from './postgres-analytics
|
|
6
|
-
export * from './
|
|
1
|
+
export * from './interfaces/platform';
|
|
2
|
+
export * from './interfaces/rc-voice';
|
|
3
|
+
export * from './context-cache';
|
|
4
|
+
export * from './analytics';
|
|
5
|
+
export * from './postgres-analytics';
|
|
6
|
+
export * from './postgres-analytics-query';
|
|
7
|
+
export * from './timeouts-manager';
|
|
7
8
|
//# sourceMappingURL=index.d.ts.map
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAA;AACrC,cAAc,uBAAuB,CAAA;AACrC,cAAc,iBAAiB,CAAA;AAG/B,cAAc,aAAa,CAAA;AAC3B,cAAc,sBAAsB,CAAA;AACpC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,oBAAoB,CAAA"}
|
package/build/index.js
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./interfaces/platform"), exports);
|
|
18
|
+
__exportStar(require("./interfaces/rc-voice"), exports);
|
|
19
|
+
__exportStar(require("./context-cache"), exports);
|
|
20
|
+
__exportStar(require("./analytics"), exports);
|
|
21
|
+
__exportStar(require("./postgres-analytics"), exports);
|
|
22
|
+
__exportStar(require("./postgres-analytics-query"), exports);
|
|
23
|
+
__exportStar(require("./timeouts-manager"), exports);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ReadableStream } from 'node:stream/web';
|
|
2
2
|
import { AIEngine, AIStreamEngine, LlmAdapter, LlmStreamAdapter, Logger, Message, PromptFS, ResponseChunk } from '@recombine-ai/engine';
|
|
3
|
-
import { Analytics } from '../analytics
|
|
4
|
-
import { ContextCache } from '../context-cache
|
|
5
|
-
import { TimeoutManager } from '../timeouts-manager
|
|
6
|
-
import { IRcVoice } from './rc-voice
|
|
3
|
+
import { Analytics } from '../analytics';
|
|
4
|
+
import { ContextCache } from '../context-cache';
|
|
5
|
+
import { TimeoutManager } from '../timeouts-manager';
|
|
6
|
+
import { IRcVoice } from './rc-voice';
|
|
7
7
|
export interface IModels {
|
|
8
8
|
models: Record<string, LlmAdapter>;
|
|
9
9
|
streamingModels: Record<string, LlmStreamAdapter>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../../src/interfaces/platform.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,OAAO,EACH,QAAQ,EACR,cAAc,EACd,UAAU,EACV,gBAAgB,EAChB,MAAM,EACN,OAAO,EACP,QAAQ,EACR,aAAa,EAChB,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../../src/interfaces/platform.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,OAAO,EACH,QAAQ,EACR,cAAc,EACd,UAAU,EACV,gBAAgB,EAChB,MAAM,EACN,OAAO,EACP,QAAQ,EACR,aAAa,EAChB,MAAM,sBAAsB,CAAA;AAI7B,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAErC,MAAM,WAAW,OAAO;IACpB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;IAClC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAA;CACpD;AAID,MAAM,WAAW,QAAQ,CACrB,QAAQ,SAAS,EAAE,EACnB,OAAO,SAAS,OAAO,EACvB,QAAQ,GAAG,QAAQ,EACnB,eAAe,GAAG,cAAc,EAChC,aAAa,GAAG,YAAY,CAAC,QAAQ,CAAC;IAGtC,QAAQ,EAAE,cAAc,CAAC,QAAQ,CAAC,CAAA;IAElC,EAAE,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAA;IAEtB,EAAE,EAAE,QAAQ,CAAA;IAEZ,OAAO,EAAE,QAAQ,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,cAAc,EAAE,eAAe,CAAA;IAE/B,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAA;IACzB,eAAe,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAA;IAE3C,YAAY,EAAE,aAAa,CAAA;IAK3B,SAAS,EAAE,SAAS,CAAA;CACvB;AAED,MAAM,WAAW,QAAQ;IACrB,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IAEpB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACnC;AAED,MAAM,WAAW,UAAU;IACvB,WAAW,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,OAAO,CAAC;QACrC,cAAc,EAAE,MAAM,CAAA;QACtB,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAA;KACpD,CAAC,CAAA;IACF,WAAW,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9C,cAAc,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,KAAK,OAAO,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC,CAAA;IAC/F,SAAS,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5C,iBAAiB,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACzE,gBAAgB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IACrC,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAC5D;AAED,wBAAgB,WAAW,CAAC,QAAQ,SAAS,EAAE,EAAE,SAAS,EAAE,OAAO,SAAS,OAAO,EAC/E,OAAO,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,EAAE,QAAQ,EAAE,SAAS,KAAK,UAAU,cAA/D,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,YAAY,SAAS,KAAK,UAAU,CAGtF"}
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { PoolConfig, QueryResult } from 'pg';
|
|
2
|
+
import { AnalyticsJsonValue } from './analytics';
|
|
3
|
+
export type AnalyticsFilterScalar = boolean | number | string;
|
|
4
|
+
export type AnalyticsFilterValueType = 'boolean' | 'number' | 'string';
|
|
5
|
+
export type AnalyticsFilterOperator = 'eq' | 'exists' | 'gt' | 'gte' | 'in' | 'lt' | 'lte';
|
|
6
|
+
export interface AnalyticsFilter {
|
|
7
|
+
key: string;
|
|
8
|
+
type: AnalyticsFilterValueType;
|
|
9
|
+
operator: AnalyticsFilterOperator;
|
|
10
|
+
value?: AnalyticsFilterScalar;
|
|
11
|
+
values?: AnalyticsFilterScalar[];
|
|
12
|
+
}
|
|
13
|
+
export type AnalyticsMetric = {
|
|
14
|
+
operation: 'count';
|
|
15
|
+
} | {
|
|
16
|
+
operation: 'rate';
|
|
17
|
+
filter: AnalyticsFilter;
|
|
18
|
+
} | {
|
|
19
|
+
operation: 'average' | 'sum';
|
|
20
|
+
key: string;
|
|
21
|
+
};
|
|
22
|
+
export type AnalyticsTimeGranularity = 'hour' | 'day' | 'week' | 'month';
|
|
23
|
+
export type AnalyticsQueryParameters = Readonly<Record<string, unknown>>;
|
|
24
|
+
export interface TelescopeConversation {
|
|
25
|
+
conversationId: string;
|
|
26
|
+
medium: string;
|
|
27
|
+
agentId?: string;
|
|
28
|
+
region?: string;
|
|
29
|
+
phoneNumber?: string;
|
|
30
|
+
duration?: number;
|
|
31
|
+
audioUrl?: string;
|
|
32
|
+
createdAt: number;
|
|
33
|
+
endedAt?: number;
|
|
34
|
+
analytics: Record<string, AnalyticsJsonValue>;
|
|
35
|
+
}
|
|
36
|
+
export interface TelescopeConversationPage {
|
|
37
|
+
limit: number;
|
|
38
|
+
items: TelescopeConversation[];
|
|
39
|
+
lastConversationId?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface AnalyticsFieldValue {
|
|
42
|
+
value: AnalyticsFilterScalar;
|
|
43
|
+
count: number;
|
|
44
|
+
}
|
|
45
|
+
export interface AnalyticsField {
|
|
46
|
+
key: string;
|
|
47
|
+
type: AnalyticsFilterValueType;
|
|
48
|
+
variantCount: number;
|
|
49
|
+
values: AnalyticsFieldValue[];
|
|
50
|
+
}
|
|
51
|
+
export interface AnalyticsFields {
|
|
52
|
+
items: AnalyticsField[];
|
|
53
|
+
}
|
|
54
|
+
export interface AnalyticsTimeSeriesItem {
|
|
55
|
+
startAt: string;
|
|
56
|
+
value: number | null;
|
|
57
|
+
numerator?: number;
|
|
58
|
+
denominator?: number;
|
|
59
|
+
}
|
|
60
|
+
export interface AnalyticsTimeSeries {
|
|
61
|
+
metric: AnalyticsMetric;
|
|
62
|
+
granularity: AnalyticsTimeGranularity;
|
|
63
|
+
from: string;
|
|
64
|
+
to: string;
|
|
65
|
+
items: AnalyticsTimeSeriesItem[];
|
|
66
|
+
}
|
|
67
|
+
interface Queryable {
|
|
68
|
+
query<TResult extends Record<string, unknown> = Record<string, unknown>>(text: string, values?: unknown[]): Promise<QueryResult<TResult>>;
|
|
69
|
+
}
|
|
70
|
+
export declare class AnalyticsQueryError extends Error {
|
|
71
|
+
name: string;
|
|
72
|
+
}
|
|
73
|
+
export declare class PostgresAnalyticsQueryService {
|
|
74
|
+
private readonly database;
|
|
75
|
+
constructor(database: Queryable);
|
|
76
|
+
getConversation(conversationId: string): Promise<TelescopeConversation | undefined>;
|
|
77
|
+
getConversations(query: AnalyticsQueryParameters): Promise<TelescopeConversationPage>;
|
|
78
|
+
getAnalyticsFields(query: AnalyticsQueryParameters): Promise<AnalyticsFields>;
|
|
79
|
+
getTimeSeries(query: AnalyticsQueryParameters): Promise<AnalyticsTimeSeries>;
|
|
80
|
+
}
|
|
81
|
+
export interface PostgresAnalyticsQueryServices {
|
|
82
|
+
queries: PostgresAnalyticsQueryService;
|
|
83
|
+
close(): Promise<void>;
|
|
84
|
+
}
|
|
85
|
+
export declare function createPostgresAnalyticsQueryServices(config: PoolConfig): PostgresAnalyticsQueryServices;
|
|
86
|
+
export {};
|
|
87
|
+
//# sourceMappingURL=postgres-analytics-query.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-analytics-query.d.ts","sourceRoot":"","sources":["../src/postgres-analytics-query.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,388 @@
|
|
|
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.duration_seconds, c.analytics,
|
|
18
|
+
p.phone_number, p.recording_url
|
|
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 ? 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.duration_seconds, c.analytics,
|
|
39
|
+
p.phone_number, p.recording_url
|
|
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 values = [];
|
|
55
|
+
let where = "WHERE jsonb_typeof(entry.value) IN ('boolean', 'number', 'string')";
|
|
56
|
+
const agentId = optionalString(query.agentId, 'agentId');
|
|
57
|
+
if (agentId) {
|
|
58
|
+
values.push(agentId);
|
|
59
|
+
where += ` AND c.agent_id = $${values.length}`;
|
|
60
|
+
}
|
|
61
|
+
const result = await this.database.query(`WITH value_counts AS (
|
|
62
|
+
SELECT entry.key,
|
|
63
|
+
jsonb_typeof(entry.value) AS type,
|
|
64
|
+
entry.value AS value,
|
|
65
|
+
COUNT(*)::bigint AS occurrence_count
|
|
66
|
+
FROM conversations c
|
|
67
|
+
CROSS JOIN LATERAL jsonb_each(c.analytics) AS entry(key, value)
|
|
68
|
+
${where}
|
|
69
|
+
GROUP BY entry.key, type, entry.value
|
|
70
|
+
),
|
|
71
|
+
ranked AS (
|
|
72
|
+
SELECT key,
|
|
73
|
+
type,
|
|
74
|
+
value,
|
|
75
|
+
occurrence_count,
|
|
76
|
+
COUNT(*) OVER (PARTITION BY key, type)::integer AS variant_count,
|
|
77
|
+
ROW_NUMBER() OVER (
|
|
78
|
+
PARTITION BY key, type
|
|
79
|
+
ORDER BY occurrence_count DESC, value::text ASC
|
|
80
|
+
) AS rank
|
|
81
|
+
FROM value_counts
|
|
82
|
+
)
|
|
83
|
+
SELECT key, type, value, occurrence_count, variant_count
|
|
84
|
+
FROM ranked
|
|
85
|
+
WHERE rank <= 50
|
|
86
|
+
ORDER BY key ASC, type ASC, rank ASC`, values);
|
|
87
|
+
return analyticsFieldRowsToJson(result.rows);
|
|
88
|
+
}
|
|
89
|
+
async getTimeSeries(query) {
|
|
90
|
+
const metric = parseMetric(requiredString(query.metric, 'metric'));
|
|
91
|
+
const granularity = (optionalString(query.granularity, 'granularity') ??
|
|
92
|
+
'day');
|
|
93
|
+
if (!['hour', 'day', 'week', 'month'].includes(granularity)) {
|
|
94
|
+
throw new AnalyticsQueryError('granularity must be hour, day, week, or month');
|
|
95
|
+
}
|
|
96
|
+
const { bind, conditions, from, to, values } = buildConversationConditions(query);
|
|
97
|
+
if (!from || !to) {
|
|
98
|
+
throw new AnalyticsQueryError('from and to are required for a time series');
|
|
99
|
+
}
|
|
100
|
+
const bucketExpression = `date_trunc(${bind(granularity)}::text, c.started_at, 'UTC')`;
|
|
101
|
+
let valueExpression;
|
|
102
|
+
let numeratorExpression = 'NULL::bigint';
|
|
103
|
+
if (metric.operation === 'count') {
|
|
104
|
+
valueExpression = 'COUNT(*)::double precision';
|
|
105
|
+
}
|
|
106
|
+
else if (metric.operation === 'rate') {
|
|
107
|
+
const predicate = makeAnalyticsPredicate(metric.filter, bind);
|
|
108
|
+
numeratorExpression = `COUNT(*) FILTER (WHERE ${predicate})::bigint`;
|
|
109
|
+
valueExpression =
|
|
110
|
+
`COUNT(*) FILTER (WHERE ${predicate})::double precision / ` + 'NULLIF(COUNT(*), 0)';
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
const key = bind(metric.key);
|
|
114
|
+
const numericValue = `CASE WHEN jsonb_typeof(c.analytics -> (${key}::text)) = 'number' ` +
|
|
115
|
+
`THEN (c.analytics ->> (${key}::text))::double precision END`;
|
|
116
|
+
valueExpression =
|
|
117
|
+
metric.operation === 'average' ? `AVG(${numericValue})` : `SUM(${numericValue})`;
|
|
118
|
+
}
|
|
119
|
+
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
120
|
+
const result = await this.database.query(`SELECT ${bucketExpression} AS bucket_start,
|
|
121
|
+
${valueExpression} AS metric_value,
|
|
122
|
+
${numeratorExpression} AS numerator,
|
|
123
|
+
COUNT(*)::bigint AS denominator
|
|
124
|
+
FROM conversations c
|
|
125
|
+
LEFT JOIN phone_calls p ON p.conversation_id = c.conversation_id
|
|
126
|
+
${where}
|
|
127
|
+
GROUP BY bucket_start
|
|
128
|
+
ORDER BY bucket_start ASC`, values);
|
|
129
|
+
return {
|
|
130
|
+
metric,
|
|
131
|
+
granularity,
|
|
132
|
+
from: from.toISOString(),
|
|
133
|
+
to: to.toISOString(),
|
|
134
|
+
items: result.rows.map((row) => ({
|
|
135
|
+
startAt: new Date(row.bucket_start).toISOString(),
|
|
136
|
+
value: row.metric_value === null ? null : Number(row.metric_value),
|
|
137
|
+
...(metric.operation === 'rate'
|
|
138
|
+
? {
|
|
139
|
+
numerator: Number(row.numerator),
|
|
140
|
+
denominator: Number(row.denominator),
|
|
141
|
+
}
|
|
142
|
+
: {}),
|
|
143
|
+
})),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
exports.PostgresAnalyticsQueryService = PostgresAnalyticsQueryService;
|
|
148
|
+
function createPostgresAnalyticsQueryServices(config) {
|
|
149
|
+
const pool = new pg_1.Pool(config);
|
|
150
|
+
return {
|
|
151
|
+
queries: new PostgresAnalyticsQueryService(pool),
|
|
152
|
+
close: () => pool.end(),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function optionalString(value, name) {
|
|
156
|
+
if (value === undefined)
|
|
157
|
+
return undefined;
|
|
158
|
+
if (typeof value !== 'string') {
|
|
159
|
+
throw new AnalyticsQueryError(`${name} must be a string`);
|
|
160
|
+
}
|
|
161
|
+
return value;
|
|
162
|
+
}
|
|
163
|
+
function requiredString(value, name) {
|
|
164
|
+
const result = optionalString(value, name);
|
|
165
|
+
if (!result) {
|
|
166
|
+
throw new AnalyticsQueryError(`${name} is required`);
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
function parseDateFilter(value, name) {
|
|
171
|
+
const stringValue = optionalString(value, name);
|
|
172
|
+
if (stringValue === undefined)
|
|
173
|
+
return undefined;
|
|
174
|
+
const date = /^\d+$/.test(stringValue) ? new Date(Number(stringValue)) : new Date(stringValue);
|
|
175
|
+
if (Number.isNaN(date.getTime())) {
|
|
176
|
+
throw new AnalyticsQueryError(`${name} must be a valid date`);
|
|
177
|
+
}
|
|
178
|
+
return date;
|
|
179
|
+
}
|
|
180
|
+
function parseDurationFilter(value, name) {
|
|
181
|
+
const stringValue = optionalString(value, name);
|
|
182
|
+
if (stringValue === undefined)
|
|
183
|
+
return undefined;
|
|
184
|
+
const duration = Number(stringValue);
|
|
185
|
+
if (!Number.isFinite(duration) || duration < 0) {
|
|
186
|
+
throw new AnalyticsQueryError(`${name} must be a non-negative number`);
|
|
187
|
+
}
|
|
188
|
+
return duration;
|
|
189
|
+
}
|
|
190
|
+
function parseAnalyticsFilters(value) {
|
|
191
|
+
const stringValue = optionalString(value, 'filters');
|
|
192
|
+
if (!stringValue)
|
|
193
|
+
return [];
|
|
194
|
+
let filters;
|
|
195
|
+
try {
|
|
196
|
+
filters = JSON.parse(stringValue);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
throw new AnalyticsQueryError('filters must be valid JSON');
|
|
200
|
+
}
|
|
201
|
+
if (!Array.isArray(filters) || filters.length > 20) {
|
|
202
|
+
throw new AnalyticsQueryError('filters must be an array with at most 20 entries');
|
|
203
|
+
}
|
|
204
|
+
return filters.map(validateAnalyticsFilter);
|
|
205
|
+
}
|
|
206
|
+
function validateAnalyticsFilter(value) {
|
|
207
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
208
|
+
throw new AnalyticsQueryError('Each filter must be an object');
|
|
209
|
+
}
|
|
210
|
+
const filter = value;
|
|
211
|
+
if (typeof filter.key !== 'string' || !/^[A-Za-z0-9_.:-]{1,128}$/.test(filter.key)) {
|
|
212
|
+
throw new AnalyticsQueryError('Filter key is invalid');
|
|
213
|
+
}
|
|
214
|
+
if (!['boolean', 'number', 'string'].includes(String(filter.type))) {
|
|
215
|
+
throw new AnalyticsQueryError(`Filter '${filter.key}' has an invalid type`);
|
|
216
|
+
}
|
|
217
|
+
const type = filter.type;
|
|
218
|
+
const allowedOperators = type === 'number'
|
|
219
|
+
? ['eq', 'in', 'gt', 'gte', 'lt', 'lte', 'exists']
|
|
220
|
+
: ['eq', 'in', 'exists'];
|
|
221
|
+
if (!allowedOperators.includes(String(filter.operator))) {
|
|
222
|
+
throw new AnalyticsQueryError(`Filter '${filter.key}' has an invalid operator`);
|
|
223
|
+
}
|
|
224
|
+
const operator = filter.operator;
|
|
225
|
+
if (operator === 'in') {
|
|
226
|
+
if (!Array.isArray(filter.values) ||
|
|
227
|
+
filter.values.length === 0 ||
|
|
228
|
+
filter.values.length > 50 ||
|
|
229
|
+
filter.values.some((item) => typeof item !== type || (type === 'number' && !Number.isFinite(item)))) {
|
|
230
|
+
throw new AnalyticsQueryError(`Filter '${filter.key}' has invalid values`);
|
|
231
|
+
}
|
|
232
|
+
return { key: filter.key, type, operator, values: filter.values };
|
|
233
|
+
}
|
|
234
|
+
if (operator !== 'exists' && typeof filter.value !== type) {
|
|
235
|
+
throw new AnalyticsQueryError(`Filter '${filter.key}' has an invalid value`);
|
|
236
|
+
}
|
|
237
|
+
if (type === 'number' && !Number.isFinite(filter.value)) {
|
|
238
|
+
throw new AnalyticsQueryError(`Filter '${filter.key}' has an invalid value`);
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
key: filter.key,
|
|
242
|
+
type,
|
|
243
|
+
operator,
|
|
244
|
+
...(operator === 'exists' ? {} : { value: filter.value }),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function parseMetric(value) {
|
|
248
|
+
let metric;
|
|
249
|
+
try {
|
|
250
|
+
metric = JSON.parse(value);
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
throw new AnalyticsQueryError('metric must be valid JSON');
|
|
254
|
+
}
|
|
255
|
+
if (!metric || typeof metric !== 'object' || Array.isArray(metric)) {
|
|
256
|
+
throw new AnalyticsQueryError('metric must be an object');
|
|
257
|
+
}
|
|
258
|
+
const record = metric;
|
|
259
|
+
if (!['count', 'rate', 'average', 'sum'].includes(String(record.operation))) {
|
|
260
|
+
throw new AnalyticsQueryError('metric operation is invalid');
|
|
261
|
+
}
|
|
262
|
+
if (record.operation === 'rate') {
|
|
263
|
+
return { operation: 'rate', filter: validateAnalyticsFilter(record.filter) };
|
|
264
|
+
}
|
|
265
|
+
if (record.operation === 'average' || record.operation === 'sum') {
|
|
266
|
+
if (typeof record.key !== 'string' || !/^[A-Za-z0-9_.:-]{1,128}$/.test(record.key)) {
|
|
267
|
+
throw new AnalyticsQueryError('Metric key is invalid');
|
|
268
|
+
}
|
|
269
|
+
return { operation: record.operation, key: record.key };
|
|
270
|
+
}
|
|
271
|
+
return { operation: 'count' };
|
|
272
|
+
}
|
|
273
|
+
function parseConversationCursor(value) {
|
|
274
|
+
if (!value)
|
|
275
|
+
return undefined;
|
|
276
|
+
try {
|
|
277
|
+
const cursor = JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
|
|
278
|
+
const startedAt = new Date(String(cursor.startedAt));
|
|
279
|
+
if (Number.isNaN(startedAt.getTime()) ||
|
|
280
|
+
typeof cursor.conversationId !== 'string' ||
|
|
281
|
+
cursor.conversationId === '') {
|
|
282
|
+
throw new Error('invalid cursor');
|
|
283
|
+
}
|
|
284
|
+
return { startedAt, conversationId: cursor.conversationId };
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
throw new AnalyticsQueryError('lastConversationId is invalid');
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function makeConversationCursor(row) {
|
|
291
|
+
return Buffer.from(JSON.stringify({
|
|
292
|
+
startedAt: new Date(row.started_at).toISOString(),
|
|
293
|
+
conversationId: row.conversation_id,
|
|
294
|
+
})).toString('base64url');
|
|
295
|
+
}
|
|
296
|
+
function conversationRowToJson(row) {
|
|
297
|
+
return {
|
|
298
|
+
conversationId: row.conversation_id,
|
|
299
|
+
medium: row.channel,
|
|
300
|
+
agentId: row.agent_id || undefined,
|
|
301
|
+
region: row.region || undefined,
|
|
302
|
+
phoneNumber: row.phone_number || undefined,
|
|
303
|
+
duration: row.duration_seconds ?? undefined,
|
|
304
|
+
audioUrl: row.recording_url || undefined,
|
|
305
|
+
createdAt: new Date(row.started_at).getTime(),
|
|
306
|
+
endedAt: row.ended_at ? new Date(row.ended_at).getTime() : undefined,
|
|
307
|
+
analytics: row.analytics ?? {},
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function analyticsFieldRowsToJson(rows) {
|
|
311
|
+
const fields = new Map();
|
|
312
|
+
for (const row of rows) {
|
|
313
|
+
const id = `${row.key}\u0000${row.type}`;
|
|
314
|
+
let field = fields.get(id);
|
|
315
|
+
if (!field) {
|
|
316
|
+
field = {
|
|
317
|
+
key: row.key,
|
|
318
|
+
type: row.type,
|
|
319
|
+
variantCount: Number(row.variant_count),
|
|
320
|
+
values: [],
|
|
321
|
+
};
|
|
322
|
+
fields.set(id, field);
|
|
323
|
+
}
|
|
324
|
+
if (field.variantCount <= 50) {
|
|
325
|
+
field.values.push({ value: row.value, count: Number(row.occurrence_count) });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return { items: [...fields.values()] };
|
|
329
|
+
}
|
|
330
|
+
function buildConversationConditions(query) {
|
|
331
|
+
const values = [];
|
|
332
|
+
const bind = (value) => {
|
|
333
|
+
values.push(value);
|
|
334
|
+
return `$${values.length}`;
|
|
335
|
+
};
|
|
336
|
+
const conditions = [];
|
|
337
|
+
const from = parseDateFilter(query.from, 'from');
|
|
338
|
+
const to = parseDateFilter(query.to, 'to');
|
|
339
|
+
if (from && to && from >= to) {
|
|
340
|
+
throw new AnalyticsQueryError('from must be earlier than to');
|
|
341
|
+
}
|
|
342
|
+
const agentId = optionalString(query.agentId, 'agentId');
|
|
343
|
+
const region = optionalString(query.region, 'region');
|
|
344
|
+
const channel = optionalString(query.channel, 'channel');
|
|
345
|
+
const phoneNumber = optionalString(query.phoneNumber, 'phoneNumber');
|
|
346
|
+
if (agentId)
|
|
347
|
+
conditions.push(`c.agent_id = ${bind(agentId)}`);
|
|
348
|
+
if (region)
|
|
349
|
+
conditions.push(`c.region = ${bind(region)}`);
|
|
350
|
+
if (channel)
|
|
351
|
+
conditions.push(`c.channel = ${bind(channel)}`);
|
|
352
|
+
if (phoneNumber)
|
|
353
|
+
conditions.push(`p.phone_number = ${bind(phoneNumber)}`);
|
|
354
|
+
if (from)
|
|
355
|
+
conditions.push(`c.started_at >= ${bind(from)}`);
|
|
356
|
+
if (to)
|
|
357
|
+
conditions.push(`c.started_at < ${bind(to)}`);
|
|
358
|
+
const minDuration = parseDurationFilter(query.minDurationSeconds, 'minDurationSeconds');
|
|
359
|
+
const maxDuration = parseDurationFilter(query.maxDurationSeconds, 'maxDurationSeconds');
|
|
360
|
+
if (minDuration !== undefined)
|
|
361
|
+
conditions.push(`c.duration_seconds >= ${bind(minDuration)}`);
|
|
362
|
+
if (maxDuration !== undefined)
|
|
363
|
+
conditions.push(`c.duration_seconds <= ${bind(maxDuration)}`);
|
|
364
|
+
if (minDuration !== undefined && maxDuration !== undefined && minDuration > maxDuration) {
|
|
365
|
+
throw new AnalyticsQueryError('minDurationSeconds must not exceed maxDurationSeconds');
|
|
366
|
+
}
|
|
367
|
+
for (const filter of parseAnalyticsFilters(query.filters)) {
|
|
368
|
+
conditions.push(makeAnalyticsPredicate(filter, bind));
|
|
369
|
+
}
|
|
370
|
+
return { bind, conditions, from, to, values };
|
|
371
|
+
}
|
|
372
|
+
function makeAnalyticsPredicate(filter, bind) {
|
|
373
|
+
const key = bind(filter.key);
|
|
374
|
+
if (filter.operator === 'exists') {
|
|
375
|
+
return `c.analytics ? (${key}::text)`;
|
|
376
|
+
}
|
|
377
|
+
if (filter.operator === 'eq') {
|
|
378
|
+
return `c.analytics @> jsonb_build_object(${key}::text, ${bind(JSON.stringify(filter.value))}::jsonb)`;
|
|
379
|
+
}
|
|
380
|
+
if (filter.operator === 'in') {
|
|
381
|
+
const predicates = filter.values.map((value) => `c.analytics @> jsonb_build_object(${key}::text, ${bind(JSON.stringify(value))}::jsonb)`);
|
|
382
|
+
return `(${predicates.join(' OR ')})`;
|
|
383
|
+
}
|
|
384
|
+
const operator = { gt: '>', gte: '>=', lt: '<', lte: '<=' }[filter.operator];
|
|
385
|
+
return (`(CASE WHEN jsonb_typeof(c.analytics -> (${key}::text)) = 'number' THEN ` +
|
|
386
|
+
`(c.analytics ->> (${key}::text))::double precision ${operator} ${bind(filter.value)} ` +
|
|
387
|
+
'ELSE false END)');
|
|
388
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Pool, PoolConfig, QueryResult } from 'pg';
|
|
2
|
-
import { Analytics, ConversationAnalytics } from './analytics
|
|
2
|
+
import { Analytics, ConversationAnalytics } from './analytics';
|
|
3
|
+
import { PostgresAnalyticsQueryService } from './postgres-analytics-query';
|
|
3
4
|
export interface ConversationRecord {
|
|
4
5
|
conversationId: string;
|
|
5
6
|
agentId?: string;
|
|
@@ -34,6 +35,7 @@ export declare function migrateAnalyticsDatabase(pool: Pool): Promise<void>;
|
|
|
34
35
|
export interface PostgresAnalyticsServices {
|
|
35
36
|
analytics: PostgresAnalytics;
|
|
36
37
|
conversations: PostgresConversationRepository;
|
|
38
|
+
queries: PostgresAnalyticsQueryService;
|
|
37
39
|
close(): Promise<void>;
|
|
38
40
|
}
|
|
39
41
|
export declare function createPostgresAnalyticsServices(config: PoolConfig): Promise<PostgresAnalyticsServices>;
|
|
@@ -1 +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,SAAS,EAGT,qBAAqB,EAGxB,MAAM,
|
|
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,SAAS,EAGT,qBAAqB,EAGxB,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,6BAA6B,EAAE,MAAM,4BAA4B,CAAA;AAI1E,MAAM,WAAW,kBAAkB;IAC/B,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,IAAI,CAAA;IACf,OAAO,CAAC,EAAE,IAAI,CAAA;IACd,eAAe,CAAC,EAAE,MAAM,CAAA;CAC3B;AAID,MAAM,WAAW,eAAe;IAC5B,cAAc,EAAE,MAAM,CAAA;IACtB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;CACvB;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;AAgBD,qBAAa,iBAAkB,YAAW,SAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,SAAS;IAIhD,wBAAwB,CAAC,cAAc,EAAE,MAAM,GAAG,qBAAqB;CAqF1E;AAYD,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;YAoClB,eAAe;CAsBhC;AA4DD,wBAAsB,wBAAwB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CA2CxE;AAID,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"}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PostgresConversationRepository = exports.PostgresAnalytics = void 0;
|
|
4
|
+
exports.migrateAnalyticsDatabase = migrateAnalyticsDatabase;
|
|
5
|
+
exports.createPostgresAnalyticsServices = createPostgresAnalyticsServices;
|
|
6
|
+
const pg_1 = require("pg");
|
|
7
|
+
const analytics_1 = require("./analytics");
|
|
8
|
+
const postgres_analytics_query_1 = require("./postgres-analytics-query");
|
|
9
|
+
class PostgresAnalytics {
|
|
4
10
|
database;
|
|
5
11
|
constructor(database) {
|
|
6
12
|
this.database = database;
|
|
@@ -10,7 +16,7 @@ export class PostgresAnalytics {
|
|
|
10
16
|
return {
|
|
11
17
|
async set(key, value) {
|
|
12
18
|
const parsed = key.schema.parse(value);
|
|
13
|
-
assertAnalyticsJsonValue(parsed);
|
|
19
|
+
(0, analytics_1.assertAnalyticsJsonValue)(parsed);
|
|
14
20
|
const serialized = JSON.stringify(parsed);
|
|
15
21
|
if (serialized === undefined) {
|
|
16
22
|
throw new Error(`Analytics value for '${key.key}' is not valid JSON`);
|
|
@@ -65,12 +71,13 @@ export class PostgresAnalytics {
|
|
|
65
71
|
};
|
|
66
72
|
}
|
|
67
73
|
}
|
|
74
|
+
exports.PostgresAnalytics = PostgresAnalytics;
|
|
68
75
|
function assertConversationExists(rowCount, conversationId) {
|
|
69
76
|
if (rowCount !== 1) {
|
|
70
77
|
throw new Error(`Conversation '${conversationId}' does not exist`);
|
|
71
78
|
}
|
|
72
79
|
}
|
|
73
|
-
|
|
80
|
+
class PostgresConversationRepository {
|
|
74
81
|
pool;
|
|
75
82
|
constructor(pool) {
|
|
76
83
|
this.pool = pool;
|
|
@@ -138,10 +145,13 @@ export class PostgresConversationRepository {
|
|
|
138
145
|
]);
|
|
139
146
|
}
|
|
140
147
|
}
|
|
148
|
+
exports.PostgresConversationRepository = PostgresConversationRepository;
|
|
141
149
|
const migrations = [
|
|
142
150
|
{
|
|
143
151
|
version: 1,
|
|
144
152
|
sql: `
|
|
153
|
+
-- Analytics rationale: This is the channel-neutral parent row and top-level JSONB map.
|
|
154
|
+
-- Removing it leaves typed values with no lifecycle or common dimensions to attach to.
|
|
145
155
|
CREATE TABLE conversations (
|
|
146
156
|
conversation_id text PRIMARY KEY,
|
|
147
157
|
agent_id text,
|
|
@@ -159,6 +169,8 @@ const migrations = [
|
|
|
159
169
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
160
170
|
);
|
|
161
171
|
|
|
172
|
+
-- Analytics rationale: Phone-only data stays in a one-to-one child so text conversations
|
|
173
|
+
-- do not acquire call-specific columns or assumptions.
|
|
162
174
|
CREATE TABLE phone_calls (
|
|
163
175
|
conversation_id text PRIMARY KEY REFERENCES conversations(conversation_id)
|
|
164
176
|
ON DELETE CASCADE,
|
|
@@ -167,6 +179,8 @@ const migrations = [
|
|
|
167
179
|
ended_reason text
|
|
168
180
|
);
|
|
169
181
|
|
|
182
|
+
-- Analytics rationale: These indexes cover time-ordered listing and its common equality
|
|
183
|
+
-- filters; without them dashboards and Telescope lists degrade to full table scans.
|
|
170
184
|
CREATE INDEX conversations_started_at_idx
|
|
171
185
|
ON conversations (started_at DESC, conversation_id DESC);
|
|
172
186
|
CREATE INDEX conversations_agent_started_at_idx
|
|
@@ -175,6 +189,9 @@ const migrations = [
|
|
|
175
189
|
ON conversations (region, started_at DESC, conversation_id DESC);
|
|
176
190
|
CREATE INDEX conversations_channel_started_at_idx
|
|
177
191
|
ON conversations (channel, started_at DESC, conversation_id DESC);
|
|
192
|
+
|
|
193
|
+
-- Analytics rationale: GIN accelerates JSONB containment filters while the phone index
|
|
194
|
+
-- supports exact caller lookups joined through the conversation ID.
|
|
178
195
|
CREATE INDEX conversations_analytics_idx
|
|
179
196
|
ON conversations USING gin (analytics);
|
|
180
197
|
CREATE INDEX phone_calls_phone_number_idx
|
|
@@ -182,7 +199,7 @@ const migrations = [
|
|
|
182
199
|
`,
|
|
183
200
|
},
|
|
184
201
|
];
|
|
185
|
-
|
|
202
|
+
async function migrateAnalyticsDatabase(pool) {
|
|
186
203
|
const client = await pool.connect();
|
|
187
204
|
try {
|
|
188
205
|
await client.query("SELECT pg_advisory_lock(hashtext('recombine_analytics_migrations'))");
|
|
@@ -216,8 +233,8 @@ export async function migrateAnalyticsDatabase(pool) {
|
|
|
216
233
|
client.release();
|
|
217
234
|
}
|
|
218
235
|
}
|
|
219
|
-
|
|
220
|
-
const pool = new Pool(config);
|
|
236
|
+
async function createPostgresAnalyticsServices(config) {
|
|
237
|
+
const pool = new pg_1.Pool(config);
|
|
221
238
|
try {
|
|
222
239
|
await migrateAnalyticsDatabase(pool);
|
|
223
240
|
}
|
|
@@ -228,6 +245,7 @@ export async function createPostgresAnalyticsServices(config) {
|
|
|
228
245
|
return {
|
|
229
246
|
analytics: new PostgresAnalytics(pool),
|
|
230
247
|
conversations: new PostgresConversationRepository(pool),
|
|
248
|
+
queries: new postgres_analytics_query_1.PostgresAnalyticsQueryService(pool),
|
|
231
249
|
close: () => pool.end(),
|
|
232
250
|
};
|
|
233
251
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timeouts-manager.d.ts","sourceRoot":"","sources":["../src/timeouts-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAE7C,OAAO,EAAE,YAAY,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"timeouts-manager.d.ts","sourceRoot":"","sources":["../src/timeouts-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAE7C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAK9C,MAAM,MAAM,aAAa,GAAG;IACxB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACnC,CAAA;AAKD,KAAK,kBAAkB,GAAG;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAC,OAAO,CAAA;CACxB,CAAA;AAED,qBAAa,cAAc;IAOnB,QAAQ,CAAC,aAAa,EAAE,YAAY,CAAC,aAAa,CAAC;IACnD,QAAQ,CAAC,MAAM,EAAE,MAAM;IAEvB,OAAO,CAAC,SAAS;IANrB,QAAQ,CAAC,mBAAmB,kDAAwD;gBAGvE,aAAa,EAAE,YAAY,CAAC,aAAa,CAAC,EAC1C,MAAM,EAAE,MAAM,EAEf,SAAS,UAAQ;IAQ7B,gBAAgB,CAAC,MAAM,EAAE,MAAM;IAYzB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAWlE,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM;IA4B7D,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM;IAmEvF,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAYxD"}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TimeoutManager = void 0;
|
|
4
|
+
class TimeoutManager {
|
|
2
5
|
timeoutsCache;
|
|
3
6
|
logger;
|
|
4
7
|
shallSkip;
|
|
@@ -97,3 +100,4 @@ export class TimeoutManager {
|
|
|
97
100
|
this.logger.debug(`TimeoutManager: cleared all timeouts`, { callId });
|
|
98
101
|
}
|
|
99
102
|
}
|
|
103
|
+
exports.TimeoutManager = TimeoutManager;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@recombine-ai/platform",
|
|
3
|
-
"version": "0.2.6-test.
|
|
3
|
+
"version": "0.2.6-test.5",
|
|
4
4
|
"description": "Recombine AI: voice agent platform",
|
|
5
5
|
"license": "EULA",
|
|
6
6
|
"author": "recombine.ai",
|
|
@@ -12,23 +12,16 @@
|
|
|
12
12
|
"files": [
|
|
13
13
|
"build"
|
|
14
14
|
],
|
|
15
|
-
"
|
|
15
|
+
"main": "./build/index.js",
|
|
16
16
|
"types": "./build/index.d.ts",
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@recombine-ai/engine": "1.0.0",
|
|
19
|
-
"@recombine-ai/telescope": "0.
|
|
19
|
+
"@recombine-ai/telescope": "^0.3.0",
|
|
20
20
|
"@types/pg": "8.15.5",
|
|
21
21
|
"pg": "8.16.3",
|
|
22
22
|
"redis": "5.11.0",
|
|
23
23
|
"zod": "^4.4.3"
|
|
24
24
|
},
|
|
25
|
-
"exports": {
|
|
26
|
-
".": {
|
|
27
|
-
"types": "./build/index.d.ts",
|
|
28
|
-
"import": "./build/index.js",
|
|
29
|
-
"default": "./build/index.js"
|
|
30
|
-
}
|
|
31
|
-
},
|
|
32
25
|
"publishConfig": {
|
|
33
26
|
"registry": "https://registry.npmjs.org"
|
|
34
27
|
},
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"analytics.spec.d.ts","sourceRoot":"","sources":["../src/analytics.spec.ts"],"names":[],"mappings":""}
|
package/build/analytics.spec.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { analyticsKey, createInMemoryAnalytics } from './analytics.js';
|
|
4
|
-
describe('Analytics', () => {
|
|
5
|
-
it('rejects unsupported key characters', () => {
|
|
6
|
-
expect(() => analyticsKey.string('not allowed')).toThrow('Analytics key contains unsupported characters');
|
|
7
|
-
});
|
|
8
|
-
it('keeps key and value types paired', async () => {
|
|
9
|
-
const analytics = createInMemoryAnalytics();
|
|
10
|
-
const escalated = analyticsKey.boolean('common.escalated');
|
|
11
|
-
const conversation = analytics.getConversationAnalytics('conversation-1');
|
|
12
|
-
await conversation.set(escalated, true);
|
|
13
|
-
const value = await conversation.get(escalated);
|
|
14
|
-
expectTypeOf(value).toEqualTypeOf();
|
|
15
|
-
expect(value).toBe(true);
|
|
16
|
-
});
|
|
17
|
-
it('clears a value idempotently', async () => {
|
|
18
|
-
const analytics = createInMemoryAnalytics();
|
|
19
|
-
const score = analyticsKey.number('project.score');
|
|
20
|
-
const conversation = analytics.getConversationAnalytics('conversation-1');
|
|
21
|
-
await conversation.set(score, 3);
|
|
22
|
-
await conversation.clear(score);
|
|
23
|
-
await conversation.clear(score);
|
|
24
|
-
await expect(conversation.get(score)).resolves.toBeUndefined();
|
|
25
|
-
});
|
|
26
|
-
it('validates JSON values at runtime', async () => {
|
|
27
|
-
const analytics = createInMemoryAnalytics();
|
|
28
|
-
const result = analyticsKey.json('project.result', z.object({ reason: z.string(), accepted: z.boolean() }));
|
|
29
|
-
const conversation = analytics.getConversationAnalytics('conversation-1');
|
|
30
|
-
await expect(conversation.set(result, {
|
|
31
|
-
reason: 'done',
|
|
32
|
-
accepted: true,
|
|
33
|
-
})).resolves.toBeUndefined();
|
|
34
|
-
await expect(conversation.set(result, {
|
|
35
|
-
reason: 'done',
|
|
36
|
-
accepted: 'yes',
|
|
37
|
-
})).rejects.toThrow();
|
|
38
|
-
});
|
|
39
|
-
it('distinguishes JSON null from a missing key', async () => {
|
|
40
|
-
const analytics = createInMemoryAnalytics();
|
|
41
|
-
const result = analyticsKey.json('project.result', z.null());
|
|
42
|
-
await analytics.getConversationAnalytics('conversation-1').set(result, null);
|
|
43
|
-
await expect(analytics.getConversationAnalytics('conversation-1').get(result)).resolves.toBeNull();
|
|
44
|
-
await expect(analytics.getConversationAnalytics('conversation-2').get(result)).resolves.toBeUndefined();
|
|
45
|
-
});
|
|
46
|
-
it('returns common conversation data from getSummary', async () => {
|
|
47
|
-
const summary = {
|
|
48
|
-
conversationId: 'conversation-1',
|
|
49
|
-
agentId: 'support',
|
|
50
|
-
channel: 'phone',
|
|
51
|
-
startedAt: new Date('2026-08-22T10:00:00Z'),
|
|
52
|
-
durationSeconds: 60,
|
|
53
|
-
};
|
|
54
|
-
const analytics = createInMemoryAnalytics([summary]);
|
|
55
|
-
await expect(analytics.getConversationAnalytics('conversation-1').getSummary()).resolves.toEqual(summary);
|
|
56
|
-
});
|
|
57
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"postgres-analytics.spec.d.ts","sourceRoot":"","sources":["../src/postgres-analytics.spec.ts"],"names":[],"mappings":""}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { analyticsKey } from './analytics.js';
|
|
4
|
-
import { PostgresAnalytics } from './postgres-analytics.js';
|
|
5
|
-
function result(rows = [], rowCount = rows.length) {
|
|
6
|
-
return { rows, rowCount };
|
|
7
|
-
}
|
|
8
|
-
describe('PostgresAnalytics', () => {
|
|
9
|
-
it('stores a typed value under one literal JSON key', async () => {
|
|
10
|
-
const query = vi.fn().mockResolvedValue(result([], 1));
|
|
11
|
-
const analytics = new PostgresAnalytics({ query });
|
|
12
|
-
await analytics
|
|
13
|
-
.getConversationAnalytics('conversation-1')
|
|
14
|
-
.set(analyticsKey.boolean('common.escalated'), true);
|
|
15
|
-
expect(query).toHaveBeenCalledWith(expect.stringContaining('jsonb_set'), [
|
|
16
|
-
'conversation-1',
|
|
17
|
-
'common.escalated',
|
|
18
|
-
'true',
|
|
19
|
-
]);
|
|
20
|
-
});
|
|
21
|
-
it('validates values before writing', async () => {
|
|
22
|
-
const query = vi.fn();
|
|
23
|
-
const analytics = new PostgresAnalytics({ query });
|
|
24
|
-
await expect(analytics
|
|
25
|
-
.getConversationAnalytics('conversation-1')
|
|
26
|
-
.set(analyticsKey.number('score'), 'wrong')).rejects.toThrow();
|
|
27
|
-
expect(query).not.toHaveBeenCalled();
|
|
28
|
-
});
|
|
29
|
-
it('reads JSON null as a present value', async () => {
|
|
30
|
-
const query = vi.fn().mockResolvedValue(result([{ present: true, value: null }]));
|
|
31
|
-
const analytics = new PostgresAnalytics({ query });
|
|
32
|
-
await expect(analytics
|
|
33
|
-
.getConversationAnalytics('conversation-1')
|
|
34
|
-
.get(analyticsKey.json('result', z.null()))).resolves.toBeNull();
|
|
35
|
-
});
|
|
36
|
-
it('returns a common conversation summary', async () => {
|
|
37
|
-
const startedAt = new Date('2026-08-01T10:00:00Z');
|
|
38
|
-
const endedAt = new Date('2026-08-01T10:02:00Z');
|
|
39
|
-
const query = vi.fn().mockResolvedValue(result([
|
|
40
|
-
{
|
|
41
|
-
conversation_id: 'conversation-1',
|
|
42
|
-
agent_id: 'support-agent',
|
|
43
|
-
channel: 'phone',
|
|
44
|
-
region: 'GB',
|
|
45
|
-
started_at: startedAt,
|
|
46
|
-
ended_at: endedAt,
|
|
47
|
-
duration_seconds: 120,
|
|
48
|
-
},
|
|
49
|
-
]));
|
|
50
|
-
const analytics = new PostgresAnalytics({ query });
|
|
51
|
-
await expect(analytics.getConversationAnalytics('conversation-1').getSummary()).resolves.toEqual({
|
|
52
|
-
conversationId: 'conversation-1',
|
|
53
|
-
agentId: 'support-agent',
|
|
54
|
-
channel: 'phone',
|
|
55
|
-
region: 'GB',
|
|
56
|
-
startedAt,
|
|
57
|
-
endedAt,
|
|
58
|
-
durationSeconds: 120,
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
});
|