@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.
- package/build/analytics.d.ts +30 -27
- package/build/analytics.d.ts.map +1 -1
- package/build/analytics.js +112 -60
- package/build/context-cache.js +6 -2
- package/build/index.d.ts +7 -5
- package/build/index.d.ts.map +1 -1
- package/build/index.js +23 -5
- package/build/interfaces/platform.d.ts +31 -12
- package/build/interfaces/platform.d.ts.map +1 -1
- package/build/interfaces/platform.js +20 -2
- 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 +392 -0
- package/build/postgres-analytics.d.ts +45 -0
- package/build/postgres-analytics.d.ts.map +1 -0
- package/build/postgres-analytics.js +295 -0
- 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 +5 -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
|
@@ -0,0 +1,295 @@
|
|
|
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 {
|
|
10
|
+
database;
|
|
11
|
+
constructor(database) {
|
|
12
|
+
this.database = database;
|
|
13
|
+
}
|
|
14
|
+
async set(conversationId, agentId, fieldName, value) {
|
|
15
|
+
(0, analytics_1.assertAnalyticsScalar)(value);
|
|
16
|
+
const serialized = JSON.stringify(value);
|
|
17
|
+
const result = await this.database.query(`UPDATE conversations
|
|
18
|
+
SET analytics = jsonb_set(analytics, ARRAY[$3::text], $4::jsonb, true),
|
|
19
|
+
updated_at = now()
|
|
20
|
+
WHERE conversation_id = $1
|
|
21
|
+
AND agent_id = $2`, [conversationId, agentId, fieldName, serialized]);
|
|
22
|
+
assertConversationBelongsToAgent(result.rowCount, conversationId, agentId);
|
|
23
|
+
}
|
|
24
|
+
async clear(conversationId, agentId, fieldName) {
|
|
25
|
+
const result = await this.database.query(`UPDATE conversations
|
|
26
|
+
SET analytics = analytics - $3::text,
|
|
27
|
+
updated_at = now()
|
|
28
|
+
WHERE conversation_id = $1
|
|
29
|
+
AND agent_id = $2`, [conversationId, agentId, fieldName]);
|
|
30
|
+
assertConversationBelongsToAgent(result.rowCount, conversationId, agentId);
|
|
31
|
+
}
|
|
32
|
+
async get(conversationId, agentId, fieldName) {
|
|
33
|
+
const result = await this.database.query(`SELECT analytics ? $3::text AS present,
|
|
34
|
+
analytics -> $3::text AS value
|
|
35
|
+
FROM conversations
|
|
36
|
+
WHERE conversation_id = $1
|
|
37
|
+
AND agent_id = $2`, [conversationId, agentId, fieldName]);
|
|
38
|
+
if (result.rows.length === 0 || !result.rows[0].present) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
return result.rows[0].value;
|
|
42
|
+
}
|
|
43
|
+
async getSummary(conversationId, agentId) {
|
|
44
|
+
const result = await this.database.query(`SELECT conversation_id, agent_id, channel, started_at
|
|
45
|
+
FROM conversations
|
|
46
|
+
WHERE conversation_id = $1
|
|
47
|
+
AND agent_id = $2`, [conversationId, agentId]);
|
|
48
|
+
const row = result.rows[0];
|
|
49
|
+
if (!row) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
conversationId: row.conversation_id,
|
|
54
|
+
agentId: row.agent_id,
|
|
55
|
+
channel: row.channel,
|
|
56
|
+
startedAt: row.started_at,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
exports.PostgresAnalytics = PostgresAnalytics;
|
|
61
|
+
function assertConversationBelongsToAgent(rowCount, conversationId, agentId) {
|
|
62
|
+
if (rowCount !== 1) {
|
|
63
|
+
throw new Error(`Conversation '${conversationId}' does not belong to agent '${agentId}'`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
class PostgresConversationRepository {
|
|
67
|
+
pool;
|
|
68
|
+
constructor(pool) {
|
|
69
|
+
this.pool = pool;
|
|
70
|
+
}
|
|
71
|
+
async saveConversation(conversation, phoneCall) {
|
|
72
|
+
const client = await this.pool.connect();
|
|
73
|
+
try {
|
|
74
|
+
await client.query('BEGIN');
|
|
75
|
+
await this.upsertConversation(client, conversation);
|
|
76
|
+
if (phoneCall) {
|
|
77
|
+
await this.upsertPhoneCall(client, conversation.conversationId, phoneCall);
|
|
78
|
+
}
|
|
79
|
+
await client.query('COMMIT');
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
await client.query('ROLLBACK');
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
client.release();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async upsertConversation(client, conversation) {
|
|
90
|
+
const result = await client.query(`INSERT INTO conversations (
|
|
91
|
+
conversation_id,
|
|
92
|
+
agent_id,
|
|
93
|
+
channel,
|
|
94
|
+
region,
|
|
95
|
+
started_at,
|
|
96
|
+
ended_at
|
|
97
|
+
) VALUES ($1, $2, $3, $4, $5, $6)
|
|
98
|
+
ON CONFLICT (conversation_id) DO UPDATE SET
|
|
99
|
+
agent_id = COALESCE(conversations.agent_id, EXCLUDED.agent_id),
|
|
100
|
+
region = COALESCE(conversations.region, EXCLUDED.region),
|
|
101
|
+
ended_at = COALESCE(conversations.ended_at, EXCLUDED.ended_at),
|
|
102
|
+
updated_at = now()
|
|
103
|
+
WHERE conversations.agent_id IS NULL
|
|
104
|
+
OR conversations.agent_id = EXCLUDED.agent_id`, [
|
|
105
|
+
conversation.conversationId,
|
|
106
|
+
conversation.agentId,
|
|
107
|
+
conversation.channel,
|
|
108
|
+
conversation.region ?? null,
|
|
109
|
+
conversation.startedAt,
|
|
110
|
+
conversation.endedAt ?? null,
|
|
111
|
+
]);
|
|
112
|
+
assertConversationBelongsToAgent(result.rowCount, conversation.conversationId, conversation.agentId);
|
|
113
|
+
}
|
|
114
|
+
async upsertPhoneCall(client, conversationId, phoneCall) {
|
|
115
|
+
await client.query(`INSERT INTO phone_calls (
|
|
116
|
+
conversation_id,
|
|
117
|
+
phone_number,
|
|
118
|
+
recording_url,
|
|
119
|
+
ended_reason,
|
|
120
|
+
duration_seconds
|
|
121
|
+
) VALUES ($1, $2, $3, $4, $5)
|
|
122
|
+
ON CONFLICT (conversation_id) DO UPDATE SET
|
|
123
|
+
phone_number = COALESCE(phone_calls.phone_number, EXCLUDED.phone_number),
|
|
124
|
+
recording_url = COALESCE(phone_calls.recording_url, EXCLUDED.recording_url),
|
|
125
|
+
ended_reason = COALESCE(phone_calls.ended_reason, EXCLUDED.ended_reason),
|
|
126
|
+
duration_seconds = COALESCE(
|
|
127
|
+
phone_calls.duration_seconds,
|
|
128
|
+
EXCLUDED.duration_seconds
|
|
129
|
+
)`, [
|
|
130
|
+
conversationId,
|
|
131
|
+
phoneCall.phoneNumber ?? null,
|
|
132
|
+
phoneCall.recordingUrl ?? null,
|
|
133
|
+
phoneCall.endedReason ?? null,
|
|
134
|
+
phoneCall.durationSeconds ?? null,
|
|
135
|
+
]);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
exports.PostgresConversationRepository = PostgresConversationRepository;
|
|
139
|
+
const migrations = [
|
|
140
|
+
{
|
|
141
|
+
version: 1,
|
|
142
|
+
sql: `
|
|
143
|
+
-- Analytics rationale: This is the channel-neutral parent row and top-level JSONB map.
|
|
144
|
+
-- Removing it leaves typed values with no lifecycle or common dimensions to attach to.
|
|
145
|
+
CREATE TABLE conversations (
|
|
146
|
+
conversation_id text PRIMARY KEY,
|
|
147
|
+
agent_id text,
|
|
148
|
+
channel text NOT NULL,
|
|
149
|
+
region text,
|
|
150
|
+
started_at timestamptz NOT NULL,
|
|
151
|
+
ended_at timestamptz,
|
|
152
|
+
duration_seconds double precision CHECK (
|
|
153
|
+
duration_seconds IS NULL OR duration_seconds >= 0
|
|
154
|
+
),
|
|
155
|
+
analytics jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (
|
|
156
|
+
jsonb_typeof(analytics) = 'object'
|
|
157
|
+
),
|
|
158
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
159
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
-- Analytics rationale: Phone-only data stays in a one-to-one child so text conversations
|
|
163
|
+
-- do not acquire call-specific columns or assumptions.
|
|
164
|
+
CREATE TABLE phone_calls (
|
|
165
|
+
conversation_id text PRIMARY KEY REFERENCES conversations(conversation_id)
|
|
166
|
+
ON DELETE CASCADE,
|
|
167
|
+
phone_number text,
|
|
168
|
+
recording_url text,
|
|
169
|
+
ended_reason text
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
-- Analytics rationale: These indexes cover time-ordered listing and its common equality
|
|
173
|
+
-- filters; without them dashboards and Telescope lists degrade to full table scans.
|
|
174
|
+
CREATE INDEX conversations_started_at_idx
|
|
175
|
+
ON conversations (started_at DESC, conversation_id DESC);
|
|
176
|
+
CREATE INDEX conversations_agent_started_at_idx
|
|
177
|
+
ON conversations (agent_id, started_at DESC, conversation_id DESC);
|
|
178
|
+
CREATE INDEX conversations_region_started_at_idx
|
|
179
|
+
ON conversations (region, started_at DESC, conversation_id DESC);
|
|
180
|
+
CREATE INDEX conversations_channel_started_at_idx
|
|
181
|
+
ON conversations (channel, started_at DESC, conversation_id DESC);
|
|
182
|
+
|
|
183
|
+
-- Analytics rationale: GIN accelerates JSONB containment filters while the phone index
|
|
184
|
+
-- supports exact caller lookups joined through the conversation ID.
|
|
185
|
+
CREATE INDEX conversations_analytics_idx
|
|
186
|
+
ON conversations USING gin (analytics);
|
|
187
|
+
CREATE INDEX phone_calls_phone_number_idx
|
|
188
|
+
ON phone_calls (phone_number, conversation_id);
|
|
189
|
+
`,
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
version: 2,
|
|
193
|
+
sql: `
|
|
194
|
+
-- Analytics rationale: Enforce mandatory agent ownership for new and updated rows
|
|
195
|
+
-- without making deployment fail on historical conversations that have no agent ID.
|
|
196
|
+
ALTER TABLE conversations
|
|
197
|
+
ADD CONSTRAINT conversations_agent_id_required
|
|
198
|
+
CHECK (agent_id IS NOT NULL AND btrim(agent_id) <> '') NOT VALID;
|
|
199
|
+
`,
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
version: 3,
|
|
203
|
+
sql: `
|
|
204
|
+
-- Analytics rationale: Duration describes a phone call, not every conversation. Move
|
|
205
|
+
-- existing call durations before removing the phone-specific column from the parent.
|
|
206
|
+
ALTER TABLE phone_calls
|
|
207
|
+
ADD COLUMN duration_seconds double precision CHECK (
|
|
208
|
+
duration_seconds IS NULL OR duration_seconds >= 0
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
UPDATE phone_calls AS p
|
|
212
|
+
SET duration_seconds = c.duration_seconds
|
|
213
|
+
FROM conversations AS c
|
|
214
|
+
WHERE p.conversation_id = c.conversation_id
|
|
215
|
+
AND p.duration_seconds IS NULL
|
|
216
|
+
AND c.duration_seconds IS NOT NULL;
|
|
217
|
+
|
|
218
|
+
ALTER TABLE conversations
|
|
219
|
+
DROP COLUMN duration_seconds;
|
|
220
|
+
`,
|
|
221
|
+
},
|
|
222
|
+
];
|
|
223
|
+
async function migrateAnalyticsDatabase(pool) {
|
|
224
|
+
const client = await pool.connect();
|
|
225
|
+
let lockAcquired = false;
|
|
226
|
+
let migrationFailed = false;
|
|
227
|
+
try {
|
|
228
|
+
await client.query("SELECT pg_advisory_lock(hashtext('recombine_analytics_migrations'))");
|
|
229
|
+
lockAcquired = true;
|
|
230
|
+
await client.query(`
|
|
231
|
+
CREATE TABLE IF NOT EXISTS analytics_schema_migrations (
|
|
232
|
+
version integer PRIMARY KEY,
|
|
233
|
+
applied_at timestamptz NOT NULL DEFAULT now()
|
|
234
|
+
)
|
|
235
|
+
`);
|
|
236
|
+
for (const migration of migrations) {
|
|
237
|
+
const applied = await client.query(`SELECT EXISTS (
|
|
238
|
+
SELECT 1 FROM analytics_schema_migrations WHERE version = $1
|
|
239
|
+
) AS exists`, [migration.version]);
|
|
240
|
+
if (applied.rows[0].exists) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
await client.query('BEGIN');
|
|
244
|
+
try {
|
|
245
|
+
await client.query(migration.sql);
|
|
246
|
+
await client.query('INSERT INTO analytics_schema_migrations (version) VALUES ($1)', [migration.version]);
|
|
247
|
+
await client.query('COMMIT');
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
await client.query('ROLLBACK');
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
migrationFailed = true;
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
await releaseAnalyticsMigrationClient(client, lockAcquired, migrationFailed);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
async function releaseAnalyticsMigrationClient(client, lockAcquired, migrationFailed) {
|
|
264
|
+
let discardClient = false;
|
|
265
|
+
try {
|
|
266
|
+
if (lockAcquired) {
|
|
267
|
+
await client.query("SELECT pg_advisory_unlock(hashtext('recombine_analytics_migrations'))");
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
discardClient = true;
|
|
272
|
+
if (!migrationFailed) {
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
finally {
|
|
277
|
+
client.release(discardClient);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async function createPostgresAnalyticsServices(config) {
|
|
281
|
+
const pool = new pg_1.Pool(config);
|
|
282
|
+
try {
|
|
283
|
+
await migrateAnalyticsDatabase(pool);
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
await pool.end();
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
analytics: new PostgresAnalytics(pool),
|
|
291
|
+
conversations: new PostgresConversationRepository(pool),
|
|
292
|
+
queries: new postgres_analytics_query_1.PostgresAnalyticsQueryService(pool),
|
|
293
|
+
close: () => pool.end(),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
@@ -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.10",
|
|
4
4
|
"description": "Recombine AI: voice agent platform",
|
|
5
5
|
"license": "EULA",
|
|
6
6
|
"author": "recombine.ai",
|
|
@@ -12,21 +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
|
+
"@types/pg": "8.15.5",
|
|
21
|
+
"pg": "8.16.3",
|
|
20
22
|
"redis": "5.11.0",
|
|
21
23
|
"zod": "^4.4.3"
|
|
22
24
|
},
|
|
23
|
-
"exports": {
|
|
24
|
-
".": {
|
|
25
|
-
"types": "./build/index.d.ts",
|
|
26
|
-
"import": "./build/index.js",
|
|
27
|
-
"default": "./build/index.js"
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
25
|
"publishConfig": {
|
|
31
26
|
"registry": "https://registry.npmjs.org"
|
|
32
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
|
-
});
|