@recombine-ai/platform 0.2.6-test.6 → 0.2.6-test.8
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 +25 -20
- package/build/analytics.d.ts.map +1 -1
- package/build/analytics.js +64 -46
- package/build/postgres-analytics-query.d.ts +3 -3
- package/build/postgres-analytics-query.d.ts.map +1 -1
- package/build/postgres-analytics-query.js +8 -12
- package/build/postgres-analytics.d.ts +7 -3
- package/build/postgres-analytics.d.ts.map +1 -1
- package/build/postgres-analytics.js +68 -61
- package/package.json +1 -1
package/build/analytics.d.ts
CHANGED
|
@@ -2,36 +2,41 @@ import { z } from 'zod';
|
|
|
2
2
|
export type AnalyticsJsonValue = boolean | number | string | null | AnalyticsJsonValue[] | {
|
|
3
3
|
[key: string]: AnalyticsJsonValue;
|
|
4
4
|
};
|
|
5
|
-
export
|
|
6
|
-
|
|
5
|
+
export type AnalyticsFieldType = 'boolean' | 'json' | 'number' | 'string';
|
|
6
|
+
export interface AnalyticsFieldDefinition<T> {
|
|
7
|
+
readonly type: AnalyticsFieldType;
|
|
7
8
|
readonly schema: z.ZodType<T>;
|
|
8
9
|
}
|
|
9
|
-
export
|
|
10
|
+
export interface AnalyticsField<T> extends AnalyticsFieldDefinition<T> {
|
|
11
|
+
readonly agentId: string;
|
|
12
|
+
readonly key: string;
|
|
13
|
+
}
|
|
14
|
+
export type AnalyticsFieldDefinitions = Readonly<Record<string, AnalyticsFieldDefinition<unknown>>>;
|
|
15
|
+
export type RegisteredAnalyticsFields<TFields extends AnalyticsFieldDefinitions> = {
|
|
16
|
+
readonly [TKey in keyof TFields]: AnalyticsField<AnalyticsValue<TFields[TKey]>>;
|
|
17
|
+
};
|
|
18
|
+
export type AnalyticsValue<TField extends AnalyticsFieldDefinition<unknown>> = TField extends AnalyticsFieldDefinition<infer TValue> ? TValue : never;
|
|
10
19
|
export interface ConversationAnalyticsSummary {
|
|
11
20
|
conversationId: string;
|
|
12
|
-
agentId
|
|
21
|
+
agentId: string;
|
|
13
22
|
channel: string;
|
|
14
|
-
region?: string;
|
|
15
23
|
startedAt: Date;
|
|
16
|
-
endedAt?: Date;
|
|
17
|
-
durationSeconds?: number;
|
|
18
|
-
}
|
|
19
|
-
export interface ConversationAnalytics {
|
|
20
|
-
set<TKey extends AnalyticsKey<unknown>>(key: TKey, value: AnalyticsValue<TKey>): Promise<void>;
|
|
21
|
-
clear(key: AnalyticsKey<unknown>): Promise<void>;
|
|
22
|
-
get<TKey extends AnalyticsKey<unknown>>(key: TKey): Promise<AnalyticsValue<TKey> | undefined>;
|
|
23
|
-
getSummary(): Promise<ConversationAnalyticsSummary | undefined>;
|
|
24
24
|
}
|
|
25
25
|
export interface Analytics {
|
|
26
|
-
|
|
26
|
+
registerAgent<TFields extends AnalyticsFieldDefinitions>(agentId: string, fields: TFields): RegisteredAnalyticsFields<TFields>;
|
|
27
|
+
set<TValue>(conversationId: string, field: AnalyticsField<TValue>, value: TValue): Promise<void>;
|
|
28
|
+
clear(conversationId: string, field: AnalyticsField<unknown>): Promise<void>;
|
|
29
|
+
get<TValue>(conversationId: string, field: AnalyticsField<TValue>): Promise<TValue | undefined>;
|
|
30
|
+
getSummary(conversationId: string): Promise<ConversationAnalyticsSummary | undefined>;
|
|
27
31
|
}
|
|
28
|
-
export declare const
|
|
29
|
-
boolean(
|
|
30
|
-
number(
|
|
31
|
-
string(
|
|
32
|
-
enum<const TValues extends readonly [string, ...string[]]>(
|
|
33
|
-
json<T extends AnalyticsJsonValue>(
|
|
32
|
+
export declare const analyticsField: {
|
|
33
|
+
boolean(): AnalyticsFieldDefinition<boolean>;
|
|
34
|
+
number(): AnalyticsFieldDefinition<number>;
|
|
35
|
+
string(): AnalyticsFieldDefinition<string>;
|
|
36
|
+
enum<const TValues extends readonly [string, ...string[]]>(values: TValues): AnalyticsFieldDefinition<TValues[number]>;
|
|
37
|
+
json<T extends AnalyticsJsonValue>(schema: z.ZodType<T>): AnalyticsFieldDefinition<T>;
|
|
34
38
|
};
|
|
39
|
+
export declare function registerAnalyticsFields<TFields extends AnalyticsFieldDefinitions>(agentId: string, fields: TFields): RegisteredAnalyticsFields<TFields>;
|
|
35
40
|
export declare function createInMemoryAnalytics(summaries?: ConversationAnalyticsSummary[]): Analytics;
|
|
36
41
|
export declare function assertAnalyticsJsonValue(value: unknown): asserts value is AnalyticsJsonValue;
|
|
37
42
|
//# sourceMappingURL=analytics.d.ts.map
|
package/build/analytics.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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;
|
|
1
|
+
{"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"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"}
|
package/build/analytics.js
CHANGED
|
@@ -1,65 +1,83 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.analyticsField = void 0;
|
|
4
|
+
exports.registerAnalyticsFields = registerAnalyticsFields;
|
|
4
5
|
exports.createInMemoryAnalytics = createInMemoryAnalytics;
|
|
5
6
|
exports.assertAnalyticsJsonValue = assertAnalyticsJsonValue;
|
|
6
7
|
const zod_1 = require("zod");
|
|
7
|
-
function
|
|
8
|
-
|
|
9
|
-
throw new Error('Analytics key contains unsupported characters');
|
|
10
|
-
}
|
|
11
|
-
return Object.freeze({ key, schema });
|
|
8
|
+
function defineField(type, schema) {
|
|
9
|
+
return Object.freeze({ type, schema });
|
|
12
10
|
}
|
|
13
|
-
exports.
|
|
14
|
-
boolean(
|
|
15
|
-
return
|
|
11
|
+
exports.analyticsField = {
|
|
12
|
+
boolean() {
|
|
13
|
+
return defineField('boolean', zod_1.z.boolean());
|
|
16
14
|
},
|
|
17
|
-
number(
|
|
18
|
-
return
|
|
15
|
+
number() {
|
|
16
|
+
return defineField('number', zod_1.z.number().finite());
|
|
19
17
|
},
|
|
20
|
-
string(
|
|
21
|
-
return
|
|
18
|
+
string() {
|
|
19
|
+
return defineField('string', zod_1.z.string());
|
|
22
20
|
},
|
|
23
|
-
enum(
|
|
24
|
-
return
|
|
21
|
+
enum(values) {
|
|
22
|
+
return defineField('string', zod_1.z.enum(values));
|
|
25
23
|
},
|
|
26
|
-
json(
|
|
27
|
-
return
|
|
24
|
+
json(schema) {
|
|
25
|
+
return defineField('json', schema);
|
|
28
26
|
},
|
|
29
27
|
};
|
|
28
|
+
function registerAnalyticsFields(agentId, fields) {
|
|
29
|
+
if (agentId.trim() === '') {
|
|
30
|
+
throw new Error('Analytics agent ID must not be empty');
|
|
31
|
+
}
|
|
32
|
+
const registered = Object.fromEntries(Object.entries(fields).map(([key, definition]) => {
|
|
33
|
+
if (!/^[A-Za-z0-9_.:-]{1,128}$/.test(key)) {
|
|
34
|
+
throw new Error('Analytics field name contains unsupported characters');
|
|
35
|
+
}
|
|
36
|
+
return [key, Object.freeze({ ...definition, agentId, key })];
|
|
37
|
+
}));
|
|
38
|
+
return Object.freeze(registered);
|
|
39
|
+
}
|
|
30
40
|
function createInMemoryAnalytics(summaries = []) {
|
|
31
41
|
const conversations = new Map();
|
|
42
|
+
const conversationAgents = new Map(summaries.map((summary) => [summary.conversationId, summary.agentId]));
|
|
32
43
|
const summariesById = new Map(summaries.map((summary) => [summary.conversationId, structuredClone(summary)]));
|
|
33
44
|
return {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
45
|
+
registerAgent: registerAnalyticsFields,
|
|
46
|
+
async set(conversationId, field, value) {
|
|
47
|
+
const existingAgentId = conversationAgents.get(conversationId);
|
|
48
|
+
if (existingAgentId !== undefined && existingAgentId !== field.agentId) {
|
|
49
|
+
throw new Error(`Conversation '${conversationId}' belongs to another agent`);
|
|
50
|
+
}
|
|
51
|
+
const parsed = field.schema.parse(value);
|
|
52
|
+
assertAnalyticsJsonValue(parsed);
|
|
53
|
+
const values = conversations.get(conversationId) ?? new Map();
|
|
54
|
+
values.set(field.key, structuredClone(parsed));
|
|
55
|
+
conversations.set(conversationId, values);
|
|
56
|
+
conversationAgents.set(conversationId, field.agentId);
|
|
57
|
+
},
|
|
58
|
+
async clear(conversationId, field) {
|
|
59
|
+
if (conversationAgents.get(conversationId) !== field.agentId) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const values = conversations.get(conversationId);
|
|
63
|
+
values?.delete(field.key);
|
|
64
|
+
if (values?.size === 0) {
|
|
65
|
+
conversations.delete(conversationId);
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
async get(conversationId, field) {
|
|
69
|
+
if (conversationAgents.get(conversationId) !== field.agentId) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
const values = conversations.get(conversationId);
|
|
73
|
+
if (!values?.has(field.key)) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
return field.schema.parse(structuredClone(values.get(field.key)));
|
|
77
|
+
},
|
|
78
|
+
async getSummary(conversationId) {
|
|
79
|
+
const summary = summariesById.get(conversationId);
|
|
80
|
+
return summary ? structuredClone(summary) : undefined;
|
|
63
81
|
},
|
|
64
82
|
};
|
|
65
83
|
}
|
|
@@ -24,7 +24,7 @@ export type AnalyticsQueryParameters = Readonly<Record<string, unknown>>;
|
|
|
24
24
|
export interface TelescopeConversation {
|
|
25
25
|
conversationId: string;
|
|
26
26
|
medium: string;
|
|
27
|
-
agentId
|
|
27
|
+
agentId: string;
|
|
28
28
|
region?: string;
|
|
29
29
|
phoneNumber?: string;
|
|
30
30
|
duration?: number;
|
|
@@ -42,14 +42,14 @@ export interface AnalyticsFieldValue {
|
|
|
42
42
|
value: AnalyticsFilterScalar;
|
|
43
43
|
count: number;
|
|
44
44
|
}
|
|
45
|
-
export interface
|
|
45
|
+
export interface AnalyticsFieldMetadata {
|
|
46
46
|
key: string;
|
|
47
47
|
type: AnalyticsFilterValueType;
|
|
48
48
|
variantCount: number;
|
|
49
49
|
values: AnalyticsFieldValue[];
|
|
50
50
|
}
|
|
51
51
|
export interface AnalyticsFields {
|
|
52
|
-
items:
|
|
52
|
+
items: AnalyticsFieldMetadata[];
|
|
53
53
|
}
|
|
54
54
|
export interface AnalyticsTimeSeriesItem {
|
|
55
55
|
startAt: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgres-analytics-query.d.ts","sourceRoot":"","sources":["../src/postgres-analytics-query.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,UAAU,EAAE,WAAW,EAAE,MAAM,IAAI,CAAA;AAElD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAIhD,MAAM,MAAM,qBAAqB,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAA;AAC7D,MAAM,MAAM,wBAAwB,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAA;AACtE,MAAM,MAAM,uBAAuB,GAAG,IAAI,GAAG,QAAQ,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,IAAI,GAAG,KAAK,CAAA;AAE1F,MAAM,WAAW,eAAe;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,wBAAwB,CAAA;IAC9B,QAAQ,EAAE,uBAAuB,CAAA;IACjC,KAAK,CAAC,EAAE,qBAAqB,CAAA;IAC7B,MAAM,CAAC,EAAE,qBAAqB,EAAE,CAAA;CACnC;AAED,MAAM,MAAM,eAAe,GACrB;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,GACtB;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,GAC9C;IAAE,SAAS,EAAE,SAAS,GAAG,KAAK,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAA;AAEnD,MAAM,MAAM,wBAAwB,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,CAAA;AACxE,MAAM,MAAM,wBAAwB,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;AAIxE,MAAM,WAAW,qBAAqB;IAClC,cAAc,EAAE,MAAM,CAAA;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,
|
|
1
|
+
{"version":3,"file":"postgres-analytics-query.d.ts","sourceRoot":"","sources":["../src/postgres-analytics-query.ts"],"names":[],"mappings":"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"}
|
|
@@ -20,7 +20,7 @@ class PostgresAnalyticsQueryService {
|
|
|
20
20
|
LEFT JOIN phone_calls p ON p.conversation_id = c.conversation_id
|
|
21
21
|
WHERE c.conversation_id = $1`, [conversationId]);
|
|
22
22
|
const row = result.rows[0];
|
|
23
|
-
return row ? conversationRowToJson(row) : undefined;
|
|
23
|
+
return row?.agent_id ? conversationRowToJson(row) : undefined;
|
|
24
24
|
}
|
|
25
25
|
async getConversations(query) {
|
|
26
26
|
const parsedLimit = Number.parseInt(optionalString(query.limit, 'limit') ?? '10', 10);
|
|
@@ -51,13 +51,10 @@ class PostgresAnalyticsQueryService {
|
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
async getAnalyticsFields(query) {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
values.push(agentId);
|
|
59
|
-
where += ` AND c.agent_id = $${values.length}`;
|
|
60
|
-
}
|
|
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';
|
|
61
58
|
const result = await this.database.query(`WITH value_counts AS (
|
|
62
59
|
SELECT entry.key,
|
|
63
60
|
jsonb_typeof(entry.value) AS type,
|
|
@@ -297,7 +294,7 @@ function conversationRowToJson(row) {
|
|
|
297
294
|
return {
|
|
298
295
|
conversationId: row.conversation_id,
|
|
299
296
|
medium: row.channel,
|
|
300
|
-
agentId: row.agent_id
|
|
297
|
+
agentId: row.agent_id,
|
|
301
298
|
region: row.region || undefined,
|
|
302
299
|
phoneNumber: row.phone_number || undefined,
|
|
303
300
|
duration: row.duration_seconds ?? undefined,
|
|
@@ -339,12 +336,11 @@ function buildConversationConditions(query) {
|
|
|
339
336
|
if (from && to && from >= to) {
|
|
340
337
|
throw new AnalyticsQueryError('from must be earlier than to');
|
|
341
338
|
}
|
|
342
|
-
const agentId =
|
|
339
|
+
const agentId = requiredString(query.agentId, 'agentId');
|
|
343
340
|
const region = optionalString(query.region, 'region');
|
|
344
341
|
const channel = optionalString(query.channel, 'channel');
|
|
345
342
|
const phoneNumber = optionalString(query.phoneNumber, 'phoneNumber');
|
|
346
|
-
|
|
347
|
-
conditions.push(`c.agent_id = ${bind(agentId)}`);
|
|
343
|
+
conditions.push(`c.agent_id = ${bind(agentId)}`);
|
|
348
344
|
if (region)
|
|
349
345
|
conditions.push(`c.region = ${bind(region)}`);
|
|
350
346
|
if (channel)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Pool, PoolConfig, QueryResult } from 'pg';
|
|
2
|
-
import { Analytics,
|
|
2
|
+
import { Analytics, AnalyticsField, AnalyticsFieldDefinitions, ConversationAnalyticsSummary, RegisteredAnalyticsFields } from './analytics';
|
|
3
3
|
import { PostgresAnalyticsQueryService } from './postgres-analytics-query';
|
|
4
4
|
export interface ConversationRecord {
|
|
5
5
|
conversationId: string;
|
|
6
|
-
agentId
|
|
6
|
+
agentId: string;
|
|
7
7
|
channel: string;
|
|
8
8
|
region?: string;
|
|
9
9
|
startedAt: Date;
|
|
@@ -21,7 +21,11 @@ interface Queryable {
|
|
|
21
21
|
export declare class PostgresAnalytics implements Analytics {
|
|
22
22
|
private readonly database;
|
|
23
23
|
constructor(database: Queryable);
|
|
24
|
-
|
|
24
|
+
registerAgent<TFields extends AnalyticsFieldDefinitions>(agentId: string, fields: TFields): RegisteredAnalyticsFields<TFields>;
|
|
25
|
+
set<TValue>(conversationId: string, field: AnalyticsField<TValue>, value: TValue): Promise<void>;
|
|
26
|
+
clear(conversationId: string, field: AnalyticsField<unknown>): Promise<void>;
|
|
27
|
+
get<TValue>(conversationId: string, field: AnalyticsField<TValue>): Promise<TValue | undefined>;
|
|
28
|
+
getSummary(conversationId: string): Promise<ConversationAnalyticsSummary | undefined>;
|
|
25
29
|
}
|
|
26
30
|
export declare class PostgresConversationRepository {
|
|
27
31
|
private readonly pool;
|
|
@@ -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,
|
|
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,EACT,cAAc,EACd,yBAAyB,EACzB,4BAA4B,EAC5B,yBAAyB,EAG5B,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;IACd,eAAe,CAAC,EAAE,MAAM,CAAA;CAC3B;AAID,MAAM,WAAW,eAAe;IAC5B,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;AAaD,qBAAa,iBAAkB,YAAW,SAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,SAAS;IAEhD,aAAa,CAAC,OAAO,SAAS,yBAAyB,EACnD,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,OAAO,GAChB,yBAAyB,CAAC,OAAO,CAAC;IAI/B,GAAG,CAAC,MAAM,EACZ,cAAc,EAAE,MAAM,EACtB,KAAK,EAAE,cAAc,CAAC,MAAM,CAAC,EAC7B,KAAK,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC;IAqBV,KAAK,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAc5E,GAAG,CAAC,MAAM,EACZ,cAAc,EAAE,MAAM,EACtB,KAAK,EAAE,cAAc,CAAC,MAAM,CAAC,GAC9B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAiBxB,UAAU,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,4BAA4B,GAAG,SAAS,CAAC;CAsB9F;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;YA4ClB,eAAe;CA0BhC;AAsED,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"}
|
|
@@ -11,70 +11,64 @@ class PostgresAnalytics {
|
|
|
11
11
|
constructor(database) {
|
|
12
12
|
this.database = database;
|
|
13
13
|
}
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
registerAgent(agentId, fields) {
|
|
15
|
+
return (0, analytics_1.registerAnalyticsFields)(agentId, fields);
|
|
16
|
+
}
|
|
17
|
+
async set(conversationId, field, value) {
|
|
18
|
+
const parsed = field.schema.parse(value);
|
|
19
|
+
(0, analytics_1.assertAnalyticsJsonValue)(parsed);
|
|
20
|
+
const serialized = JSON.stringify(parsed);
|
|
21
|
+
if (serialized === undefined) {
|
|
22
|
+
throw new Error(`Analytics value for '${field.key}' is not valid JSON`);
|
|
23
|
+
}
|
|
24
|
+
const result = await this.database.query(`UPDATE conversations
|
|
25
|
+
SET analytics = jsonb_set(analytics, ARRAY[$3::text], $4::jsonb, true),
|
|
26
|
+
updated_at = now()
|
|
27
|
+
WHERE conversation_id = $1
|
|
28
|
+
AND agent_id = $2`, [conversationId, field.agentId, field.key, serialized]);
|
|
29
|
+
assertConversationBelongsToAgent(result.rowCount, conversationId, field.agentId);
|
|
30
|
+
}
|
|
31
|
+
async clear(conversationId, field) {
|
|
32
|
+
const result = await this.database.query(`UPDATE conversations
|
|
33
|
+
SET analytics = analytics - $3::text,
|
|
34
|
+
updated_at = now()
|
|
35
|
+
WHERE conversation_id = $1
|
|
36
|
+
AND agent_id = $2`, [conversationId, field.agentId, field.key]);
|
|
37
|
+
assertConversationBelongsToAgent(result.rowCount, conversationId, field.agentId);
|
|
38
|
+
}
|
|
39
|
+
async get(conversationId, field) {
|
|
40
|
+
const result = await this.database.query(`SELECT analytics ? $3::text AS present,
|
|
41
|
+
analytics -> $3::text AS value
|
|
42
|
+
FROM conversations
|
|
43
|
+
WHERE conversation_id = $1
|
|
44
|
+
AND agent_id = $2`, [conversationId, field.agentId, field.key]);
|
|
45
|
+
if (result.rows.length === 0 || !result.rows[0].present) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
return field.schema.parse(result.rows[0].value);
|
|
49
|
+
}
|
|
50
|
+
async getSummary(conversationId) {
|
|
51
|
+
const result = await this.database.query(`SELECT conversation_id, agent_id, channel, started_at
|
|
52
|
+
FROM conversations
|
|
53
|
+
WHERE conversation_id = $1
|
|
54
|
+
AND agent_id IS NOT NULL
|
|
55
|
+
AND btrim(agent_id) <> ''`, [conversationId]);
|
|
56
|
+
const row = result.rows[0];
|
|
57
|
+
if (!row) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
16
60
|
return {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (serialized === undefined) {
|
|
22
|
-
throw new Error(`Analytics value for '${key.key}' is not valid JSON`);
|
|
23
|
-
}
|
|
24
|
-
const result = await database.query(`UPDATE conversations
|
|
25
|
-
SET analytics = jsonb_set(analytics, ARRAY[$2::text], $3::jsonb, true),
|
|
26
|
-
updated_at = now()
|
|
27
|
-
WHERE conversation_id = $1`, [conversationId, key.key, serialized]);
|
|
28
|
-
assertConversationExists(result.rowCount, conversationId);
|
|
29
|
-
},
|
|
30
|
-
async clear(key) {
|
|
31
|
-
const result = await database.query(`UPDATE conversations
|
|
32
|
-
SET analytics = analytics - $2::text,
|
|
33
|
-
updated_at = now()
|
|
34
|
-
WHERE conversation_id = $1`, [conversationId, key.key]);
|
|
35
|
-
assertConversationExists(result.rowCount, conversationId);
|
|
36
|
-
},
|
|
37
|
-
async get(key) {
|
|
38
|
-
const result = await database.query(`SELECT analytics ? $2::text AS present,
|
|
39
|
-
analytics -> $2::text AS value
|
|
40
|
-
FROM conversations
|
|
41
|
-
WHERE conversation_id = $1`, [conversationId, key.key]);
|
|
42
|
-
if (result.rows.length === 0 || !result.rows[0].present) {
|
|
43
|
-
return undefined;
|
|
44
|
-
}
|
|
45
|
-
return key.schema.parse(result.rows[0].value);
|
|
46
|
-
},
|
|
47
|
-
async getSummary() {
|
|
48
|
-
const result = await database.query(`SELECT conversation_id,
|
|
49
|
-
agent_id,
|
|
50
|
-
channel,
|
|
51
|
-
region,
|
|
52
|
-
started_at,
|
|
53
|
-
ended_at,
|
|
54
|
-
duration_seconds
|
|
55
|
-
FROM conversations
|
|
56
|
-
WHERE conversation_id = $1`, [conversationId]);
|
|
57
|
-
const row = result.rows[0];
|
|
58
|
-
if (!row) {
|
|
59
|
-
return undefined;
|
|
60
|
-
}
|
|
61
|
-
return {
|
|
62
|
-
conversationId: row.conversation_id,
|
|
63
|
-
agentId: row.agent_id ?? undefined,
|
|
64
|
-
channel: row.channel,
|
|
65
|
-
region: row.region ?? undefined,
|
|
66
|
-
startedAt: row.started_at,
|
|
67
|
-
endedAt: row.ended_at ?? undefined,
|
|
68
|
-
durationSeconds: row.duration_seconds ?? undefined,
|
|
69
|
-
};
|
|
70
|
-
},
|
|
61
|
+
conversationId: row.conversation_id,
|
|
62
|
+
agentId: row.agent_id,
|
|
63
|
+
channel: row.channel,
|
|
64
|
+
startedAt: row.started_at,
|
|
71
65
|
};
|
|
72
66
|
}
|
|
73
67
|
}
|
|
74
68
|
exports.PostgresAnalytics = PostgresAnalytics;
|
|
75
|
-
function
|
|
69
|
+
function assertConversationBelongsToAgent(rowCount, conversationId, agentId) {
|
|
76
70
|
if (rowCount !== 1) {
|
|
77
|
-
throw new Error(`Conversation '${conversationId}' does not
|
|
71
|
+
throw new Error(`Conversation '${conversationId}' does not belong to agent '${agentId}'`);
|
|
78
72
|
}
|
|
79
73
|
}
|
|
80
74
|
class PostgresConversationRepository {
|
|
@@ -101,7 +95,7 @@ class PostgresConversationRepository {
|
|
|
101
95
|
}
|
|
102
96
|
}
|
|
103
97
|
async upsertConversation(client, conversation) {
|
|
104
|
-
await client.query(`INSERT INTO conversations (
|
|
98
|
+
const result = await client.query(`INSERT INTO conversations (
|
|
105
99
|
conversation_id,
|
|
106
100
|
agent_id,
|
|
107
101
|
channel,
|
|
@@ -118,15 +112,18 @@ class PostgresConversationRepository {
|
|
|
118
112
|
conversations.duration_seconds,
|
|
119
113
|
EXCLUDED.duration_seconds
|
|
120
114
|
),
|
|
121
|
-
updated_at = now()
|
|
115
|
+
updated_at = now()
|
|
116
|
+
WHERE conversations.agent_id IS NULL
|
|
117
|
+
OR conversations.agent_id = EXCLUDED.agent_id`, [
|
|
122
118
|
conversation.conversationId,
|
|
123
|
-
conversation.agentId
|
|
119
|
+
conversation.agentId,
|
|
124
120
|
conversation.channel,
|
|
125
121
|
conversation.region ?? null,
|
|
126
122
|
conversation.startedAt,
|
|
127
123
|
conversation.endedAt ?? null,
|
|
128
124
|
conversation.durationSeconds ?? null,
|
|
129
125
|
]);
|
|
126
|
+
assertConversationBelongsToAgent(result.rowCount, conversation.conversationId, conversation.agentId);
|
|
130
127
|
}
|
|
131
128
|
async upsertPhoneCall(client, conversationId, phoneCall) {
|
|
132
129
|
await client.query(`INSERT INTO phone_calls (
|
|
@@ -199,6 +196,16 @@ const migrations = [
|
|
|
199
196
|
ON phone_calls (phone_number, conversation_id);
|
|
200
197
|
`,
|
|
201
198
|
},
|
|
199
|
+
{
|
|
200
|
+
version: 2,
|
|
201
|
+
sql: `
|
|
202
|
+
-- Analytics rationale: Enforce mandatory agent ownership for new and updated rows
|
|
203
|
+
-- without making deployment fail on historical conversations that have no agent ID.
|
|
204
|
+
ALTER TABLE conversations
|
|
205
|
+
ADD CONSTRAINT conversations_agent_id_required
|
|
206
|
+
CHECK (agent_id IS NOT NULL AND btrim(agent_id) <> '') NOT VALID;
|
|
207
|
+
`,
|
|
208
|
+
},
|
|
202
209
|
];
|
|
203
210
|
async function migrateAnalyticsDatabase(pool) {
|
|
204
211
|
const client = await pool.connect();
|