@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.
@@ -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 interface AnalyticsKey<T> {
6
- readonly key: string;
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 type AnalyticsValue<TKey extends AnalyticsKey<unknown>> = TKey extends AnalyticsKey<infer TValue> ? TValue : never;
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?: string;
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
- getConversationAnalytics(conversationId: string): ConversationAnalytics;
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 analyticsKey: {
29
- boolean(key: string): AnalyticsKey<boolean>;
30
- number(key: string): AnalyticsKey<number>;
31
- string(key: string): AnalyticsKey<string>;
32
- enum<const TValues extends readonly [string, ...string[]]>(key: string, values: TValues): AnalyticsKey<TValues[number]>;
33
- json<T extends AnalyticsJsonValue>(key: string, schema: z.ZodType<T>): AnalyticsKey<T>;
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
@@ -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;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"}
1
+ {"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"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"}
@@ -1,65 +1,83 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.analyticsKey = void 0;
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 defineKey(key, schema) {
8
- if (!/^[A-Za-z0-9_.:-]{1,128}$/.test(key)) {
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.analyticsKey = {
14
- boolean(key) {
15
- return defineKey(key, zod_1.z.boolean());
11
+ exports.analyticsField = {
12
+ boolean() {
13
+ return defineField('boolean', zod_1.z.boolean());
16
14
  },
17
- number(key) {
18
- return defineKey(key, zod_1.z.number().finite());
15
+ number() {
16
+ return defineField('number', zod_1.z.number().finite());
19
17
  },
20
- string(key) {
21
- return defineKey(key, zod_1.z.string());
18
+ string() {
19
+ return defineField('string', zod_1.z.string());
22
20
  },
23
- enum(key, values) {
24
- return defineKey(key, zod_1.z.enum(values));
21
+ enum(values) {
22
+ return defineField('string', zod_1.z.enum(values));
25
23
  },
26
- json(key, schema) {
27
- return defineKey(key, schema);
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
- getConversationAnalytics(conversationId) {
35
- return {
36
- async set(key, value) {
37
- const parsed = key.schema.parse(value);
38
- assertAnalyticsJsonValue(parsed);
39
- const values = conversations.get(conversationId) ?? new Map();
40
- values.set(key.key, structuredClone(parsed));
41
- conversations.set(conversationId, values);
42
- },
43
- async clear(key) {
44
- const values = conversations.get(conversationId);
45
- values?.delete(key.key);
46
- if (values?.size === 0) {
47
- conversations.delete(conversationId);
48
- }
49
- },
50
- async get(key) {
51
- const values = conversations.get(conversationId);
52
- if (!values?.has(key.key)) {
53
- return undefined;
54
- }
55
- const value = values.get(key.key);
56
- return key.schema.parse(structuredClone(value));
57
- },
58
- async getSummary() {
59
- const summary = summariesById.get(conversationId);
60
- return summary ? structuredClone(summary) : undefined;
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?: string;
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 AnalyticsField {
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: AnalyticsField[];
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":"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"}
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 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
- }
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 || undefined,
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 = optionalString(query.agentId, '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
- if (agentId)
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, ConversationAnalytics } from './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?: string;
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
- getConversationAnalytics(conversationId: string): ConversationAnalytics;
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,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,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;YAqClB,eAAe;CA0BhC;AA4DD,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"}
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
- getConversationAnalytics(conversationId) {
15
- const database = this.database;
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
- async set(key, value) {
18
- const parsed = key.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 '${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 assertConversationExists(rowCount, conversationId) {
69
+ function assertConversationBelongsToAgent(rowCount, conversationId, agentId) {
76
70
  if (rowCount !== 1) {
77
- throw new Error(`Conversation '${conversationId}' does not exist`);
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 ?? null,
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@recombine-ai/platform",
3
- "version": "0.2.6-test.6",
3
+ "version": "0.2.6-test.8",
4
4
  "description": "Recombine AI: voice agent platform",
5
5
  "license": "EULA",
6
6
  "author": "recombine.ai",