@recombine-ai/platform 0.2.6-test.7 → 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,29 +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;
21
+ agentId: string;
12
22
  channel: string;
13
23
  startedAt: Date;
14
24
  }
15
25
  export interface Analytics {
16
- set<TKey extends AnalyticsKey<unknown>>(conversationId: string, key: TKey, value: AnalyticsValue<TKey>): Promise<void>;
17
- clear(conversationId: string, key: AnalyticsKey<unknown>): Promise<void>;
18
- get<TKey extends AnalyticsKey<unknown>>(conversationId: string, key: TKey): Promise<AnalyticsValue<TKey> | undefined>;
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>;
19
30
  getSummary(conversationId: string): Promise<ConversationAnalyticsSummary | undefined>;
20
31
  }
21
- export declare const analyticsKey: {
22
- boolean(key: string): AnalyticsKey<boolean>;
23
- number(key: string): AnalyticsKey<number>;
24
- string(key: string): AnalyticsKey<string>;
25
- enum<const TValues extends readonly [string, ...string[]]>(key: string, values: TValues): AnalyticsKey<TValues[number]>;
26
- 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>;
27
38
  };
39
+ export declare function registerAnalyticsFields<TFields extends AnalyticsFieldDefinitions>(agentId: string, fields: TFields): RegisteredAnalyticsFields<TFields>;
28
40
  export declare function createInMemoryAnalytics(summaries?: ConversationAnalyticsSummary[]): Analytics;
29
41
  export declare function assertAnalyticsJsonValue(value: unknown): asserts value is AnalyticsJsonValue;
30
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,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,IAAI,CAAA;CAClB;AAID,MAAM,WAAW,SAAS;IAGtB,GAAG,CAAC,IAAI,SAAS,YAAY,CAAC,OAAO,CAAC,EAClC,cAAc,EAAE,MAAM,EACtB,GAAG,EAAE,IAAI,EACT,KAAK,EAAE,cAAc,CAAC,IAAI,CAAC,GAC5B,OAAO,CAAC,IAAI,CAAC,CAAA;IAChB,KAAK,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACxE,GAAG,CAAC,IAAI,SAAS,YAAY,CAAC,OAAO,CAAC,EAClC,cAAc,EAAE,MAAM,EACtB,GAAG,EAAE,IAAI,GACV,OAAO,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,CAAA;IAC5C,UAAU,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,4BAA4B,GAAG,SAAS,CAAC,CAAA;CACxF;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,CA8CjG;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,57 +1,79 @@
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
- async set(conversationId, key, value) {
35
- const parsed = key.schema.parse(value);
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);
36
52
  assertAnalyticsJsonValue(parsed);
37
53
  const values = conversations.get(conversationId) ?? new Map();
38
- values.set(key.key, structuredClone(parsed));
54
+ values.set(field.key, structuredClone(parsed));
39
55
  conversations.set(conversationId, values);
56
+ conversationAgents.set(conversationId, field.agentId);
40
57
  },
41
- async clear(conversationId, key) {
58
+ async clear(conversationId, field) {
59
+ if (conversationAgents.get(conversationId) !== field.agentId) {
60
+ return;
61
+ }
42
62
  const values = conversations.get(conversationId);
43
- values?.delete(key.key);
63
+ values?.delete(field.key);
44
64
  if (values?.size === 0) {
45
65
  conversations.delete(conversationId);
46
66
  }
47
67
  },
48
- async get(conversationId, key) {
68
+ async get(conversationId, field) {
69
+ if (conversationAgents.get(conversationId) !== field.agentId) {
70
+ return undefined;
71
+ }
49
72
  const values = conversations.get(conversationId);
50
- if (!values?.has(key.key)) {
73
+ if (!values?.has(field.key)) {
51
74
  return undefined;
52
75
  }
53
- const value = values.get(key.key);
54
- return key.schema.parse(structuredClone(value));
76
+ return field.schema.parse(structuredClone(values.get(field.key)));
55
77
  },
56
78
  async getSummary(conversationId) {
57
79
  const summary = summariesById.get(conversationId);
@@ -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, AnalyticsKey, AnalyticsValue, ConversationAnalyticsSummary } 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,9 +21,10 @@ interface Queryable {
21
21
  export declare class PostgresAnalytics implements Analytics {
22
22
  private readonly database;
23
23
  constructor(database: Queryable);
24
- set<TKey extends AnalyticsKey<unknown>>(conversationId: string, key: TKey, value: AnalyticsValue<TKey>): Promise<void>;
25
- clear(conversationId: string, key: AnalyticsKey<unknown>): Promise<void>;
26
- get<TKey extends AnalyticsKey<unknown>>(conversationId: string, key: TKey): Promise<AnalyticsValue<TKey> | undefined>;
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>;
27
28
  getSummary(conversationId: string): Promise<ConversationAnalyticsSummary | undefined>;
28
29
  }
29
30
  export declare class PostgresConversationRepository {
@@ -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,EACT,YAAY,EACZ,cAAc,EACd,4BAA4B,EAE/B,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,6BAA6B,EAAE,MAAM,4BAA4B,CAAA;AAI1E,MAAM,WAAW,kBAAkB;IAC/B,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,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;AAYD,qBAAa,iBAAkB,YAAW,SAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,SAAS;IAE1C,GAAG,CAAC,IAAI,SAAS,YAAY,CAAC,OAAO,CAAC,EACxC,cAAc,EAAE,MAAM,EACtB,GAAG,EAAE,IAAI,EACT,KAAK,EAAE,cAAc,CAAC,IAAI,CAAC,GAC5B,OAAO,CAAC,IAAI,CAAC;IAoBV,KAAK,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAaxE,GAAG,CAAC,IAAI,SAAS,YAAY,CAAC,OAAO,CAAC,EACxC,cAAc,EAAE,MAAM,EACtB,GAAG,EAAE,IAAI,GACV,OAAO,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC;IAgBtC,UAAU,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,4BAA4B,GAAG,SAAS,CAAC;CAmB9F;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,55 +11,64 @@ class PostgresAnalytics {
11
11
  constructor(database) {
12
12
  this.database = database;
13
13
  }
14
- async set(conversationId, key, value) {
15
- const parsed = key.schema.parse(value);
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);
16
19
  (0, analytics_1.assertAnalyticsJsonValue)(parsed);
17
20
  const serialized = JSON.stringify(parsed);
18
21
  if (serialized === undefined) {
19
- throw new Error(`Analytics value for '${key.key}' is not valid JSON`);
22
+ throw new Error(`Analytics value for '${field.key}' is not valid JSON`);
20
23
  }
21
24
  const result = await this.database.query(`UPDATE conversations
22
- SET analytics = jsonb_set(analytics, ARRAY[$2::text], $3::jsonb, true),
25
+ SET analytics = jsonb_set(analytics, ARRAY[$3::text], $4::jsonb, true),
23
26
  updated_at = now()
24
- WHERE conversation_id = $1`, [conversationId, key.key, serialized]);
25
- assertConversationExists(result.rowCount, conversationId);
27
+ WHERE conversation_id = $1
28
+ AND agent_id = $2`, [conversationId, field.agentId, field.key, serialized]);
29
+ assertConversationBelongsToAgent(result.rowCount, conversationId, field.agentId);
26
30
  }
27
- async clear(conversationId, key) {
31
+ async clear(conversationId, field) {
28
32
  const result = await this.database.query(`UPDATE conversations
29
- SET analytics = analytics - $2::text,
33
+ SET analytics = analytics - $3::text,
30
34
  updated_at = now()
31
- WHERE conversation_id = $1`, [conversationId, key.key]);
32
- assertConversationExists(result.rowCount, conversationId);
35
+ WHERE conversation_id = $1
36
+ AND agent_id = $2`, [conversationId, field.agentId, field.key]);
37
+ assertConversationBelongsToAgent(result.rowCount, conversationId, field.agentId);
33
38
  }
34
- async get(conversationId, key) {
35
- const result = await this.database.query(`SELECT analytics ? $2::text AS present,
36
- analytics -> $2::text AS value
39
+ async get(conversationId, field) {
40
+ const result = await this.database.query(`SELECT analytics ? $3::text AS present,
41
+ analytics -> $3::text AS value
37
42
  FROM conversations
38
- WHERE conversation_id = $1`, [conversationId, key.key]);
43
+ WHERE conversation_id = $1
44
+ AND agent_id = $2`, [conversationId, field.agentId, field.key]);
39
45
  if (result.rows.length === 0 || !result.rows[0].present) {
40
46
  return undefined;
41
47
  }
42
- return key.schema.parse(result.rows[0].value);
48
+ return field.schema.parse(result.rows[0].value);
43
49
  }
44
50
  async getSummary(conversationId) {
45
- const result = await this.database.query(`SELECT conversation_id, channel, started_at
51
+ const result = await this.database.query(`SELECT conversation_id, agent_id, channel, started_at
46
52
  FROM conversations
47
- WHERE conversation_id = $1`, [conversationId]);
53
+ WHERE conversation_id = $1
54
+ AND agent_id IS NOT NULL
55
+ AND btrim(agent_id) <> ''`, [conversationId]);
48
56
  const row = result.rows[0];
49
57
  if (!row) {
50
58
  return undefined;
51
59
  }
52
60
  return {
53
61
  conversationId: row.conversation_id,
62
+ agentId: row.agent_id,
54
63
  channel: row.channel,
55
64
  startedAt: row.started_at,
56
65
  };
57
66
  }
58
67
  }
59
68
  exports.PostgresAnalytics = PostgresAnalytics;
60
- function assertConversationExists(rowCount, conversationId) {
69
+ function assertConversationBelongsToAgent(rowCount, conversationId, agentId) {
61
70
  if (rowCount !== 1) {
62
- throw new Error(`Conversation '${conversationId}' does not exist`);
71
+ throw new Error(`Conversation '${conversationId}' does not belong to agent '${agentId}'`);
63
72
  }
64
73
  }
65
74
  class PostgresConversationRepository {
@@ -86,7 +95,7 @@ class PostgresConversationRepository {
86
95
  }
87
96
  }
88
97
  async upsertConversation(client, conversation) {
89
- await client.query(`INSERT INTO conversations (
98
+ const result = await client.query(`INSERT INTO conversations (
90
99
  conversation_id,
91
100
  agent_id,
92
101
  channel,
@@ -103,15 +112,18 @@ class PostgresConversationRepository {
103
112
  conversations.duration_seconds,
104
113
  EXCLUDED.duration_seconds
105
114
  ),
106
- updated_at = now()`, [
115
+ updated_at = now()
116
+ WHERE conversations.agent_id IS NULL
117
+ OR conversations.agent_id = EXCLUDED.agent_id`, [
107
118
  conversation.conversationId,
108
- conversation.agentId ?? null,
119
+ conversation.agentId,
109
120
  conversation.channel,
110
121
  conversation.region ?? null,
111
122
  conversation.startedAt,
112
123
  conversation.endedAt ?? null,
113
124
  conversation.durationSeconds ?? null,
114
125
  ]);
126
+ assertConversationBelongsToAgent(result.rowCount, conversation.conversationId, conversation.agentId);
115
127
  }
116
128
  async upsertPhoneCall(client, conversationId, phoneCall) {
117
129
  await client.query(`INSERT INTO phone_calls (
@@ -184,6 +196,16 @@ const migrations = [
184
196
  ON phone_calls (phone_number, conversation_id);
185
197
  `,
186
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
+ },
187
209
  ];
188
210
  async function migrateAnalyticsDatabase(pool) {
189
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.7",
3
+ "version": "0.2.6-test.8",
4
4
  "description": "Recombine AI: voice agent platform",
5
5
  "license": "EULA",
6
6
  "author": "recombine.ai",