@proteos/sdk 0.51.1 → 0.53.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proteos/sdk",
3
- "version": "0.51.1",
3
+ "version": "0.53.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "TypeScript SDK for the Proteos platform",
6
6
  "repository": {
@@ -66,8 +66,8 @@
66
66
  "tsup": "^8.3.0",
67
67
  "typescript": "^5.7.0",
68
68
  "vitest": "^3.0.0",
69
- "@proteos/biome-config": "0.0.0",
70
- "@proteos/tsconfig": "0.0.0"
69
+ "@proteos/tsconfig": "0.0.0",
70
+ "@proteos/biome-config": "0.0.0"
71
71
  },
72
72
  "scripts": {
73
73
  "build": "tsup",
@@ -33,6 +33,7 @@ import type {
33
33
  CreateGlossaryTermRequest,
34
34
  CreateSendingRuleRequest,
35
35
  CreateToneProfileSetupRequest,
36
+ CreateTranscriptionFromTextRequest,
36
37
  CreateTranscriptionRequest,
37
38
  DeleteConnectionQuery,
38
39
  DispatchMeetingBotRequest,
@@ -106,6 +107,7 @@ import type {
106
107
  UpdatePhoneNumberRequest,
107
108
  UpdateSendingRuleRequest,
108
109
  UpdateTranscriptionRequest,
110
+ UpsertToneProfileRequest,
109
111
  } from './types.js'
110
112
  import { type VoiceService, VoiceServiceImpl } from './voice.js'
111
113
 
@@ -1121,6 +1123,20 @@ export interface ToneProfileService {
1121
1123
  getProfile(id: string): Promise<ToneProfile>
1122
1124
  /** The single most-specific profile for a drafting context; 404 when none. */
1123
1125
  resolve(query: ResolveToneProfileQuery): Promise<ToneProfile>
1126
+ /**
1127
+ * Writes a HAND-AUTHORED profile, addressed by its scope tuple rather than an
1128
+ * id — so re-sending the same scope updates the row it created. The row lands
1129
+ * `source: 'manual'`, which locks that scope against synthesis forever after;
1130
+ * it also takes the scope over from a `model` row already there. Needs no
1131
+ * setup: a setup is the opt-in to AI synthesis, and writing a voice by hand is
1132
+ * the opposite of asking for one.
1133
+ */
1134
+ upsertProfile(request: UpsertToneProfileRequest): Promise<ToneProfile>
1135
+ /**
1136
+ * Removes one profile row, manual or model. A deleted model row comes back on
1137
+ * the next run; a deleted manual row hands its scope back to synthesis.
1138
+ */
1139
+ deleteProfile(id: string): Promise<void>
1124
1140
  }
1125
1141
 
1126
1142
  class ToneProfileServiceImpl implements ToneProfileService {
@@ -1170,6 +1186,17 @@ class ToneProfileServiceImpl implements ToneProfileService {
1170
1186
  query,
1171
1187
  )
1172
1188
  }
1189
+
1190
+ upsertProfile(request: UpsertToneProfileRequest): Promise<ToneProfile> {
1191
+ return this.client.request('PUT', `${CONVERSATION_BASE_PATH}/tone-profiles`, request)
1192
+ }
1193
+
1194
+ async deleteProfile(id: string): Promise<void> {
1195
+ await this.client.request(
1196
+ 'DELETE',
1197
+ `${CONVERSATION_BASE_PATH}/tone-profiles/${encodeURIComponent(id)}`,
1198
+ )
1199
+ }
1173
1200
  }
1174
1201
 
1175
1202
  /** Batch transcription of stored audio files + materialization. */
@@ -1180,6 +1207,14 @@ export interface TranscriptionService {
1180
1207
  * status is `completed` (or `failed`), then `materialize`.
1181
1208
  */
1182
1209
  createFromFile(request: CreateTranscriptionRequest): Promise<Transcription>
1210
+ /**
1211
+ * Lands an ALREADY WRITTEN transcript. Synchronous — no provider runs, and
1212
+ * the entity comes back `completed`, ready to `materialize` straight away.
1213
+ * The door for authoring a transcript (and so a conversation) from text with
1214
+ * nothing to upload. Same endpoint as `createFromFile`; the shape of the body
1215
+ * chooses the mode, and exactly one of the two is allowed.
1216
+ */
1217
+ createFromText(request: CreateTranscriptionFromTextRequest): Promise<Transcription>
1183
1218
  list(query?: ListTranscriptionsQuery): Promise<ListResponse<Transcription>>
1184
1219
  get(id: string): Promise<Transcription>
1185
1220
  /**
@@ -1204,6 +1239,10 @@ class TranscriptionServiceImpl implements TranscriptionService {
1204
1239
  return this.client.request('POST', `${CONVERSATION_BASE_PATH}/transcriptions`, request)
1205
1240
  }
1206
1241
 
1242
+ createFromText(request: CreateTranscriptionFromTextRequest): Promise<Transcription> {
1243
+ return this.client.request('POST', `${CONVERSATION_BASE_PATH}/transcriptions`, request)
1244
+ }
1245
+
1207
1246
  list(query: ListTranscriptionsQuery = {}): Promise<ListResponse<Transcription>> {
1208
1247
  return this.client.requestWithQuery('GET', `${CONVERSATION_BASE_PATH}/transcriptions`, query)
1209
1248
  }
@@ -914,6 +914,14 @@ export type ToneProfileSetupStatus = 'empty' | 'processing' | 'ready'
914
914
  /** A tone profile row's tier — derived, most specific wins at read time. */
915
915
  export type ToneProfileScope = 'user' | 'channel' | 'group' | 'contact'
916
916
 
917
+ /**
918
+ * Who wrote a tone profile, and therefore who may change it. `manual` rows are
919
+ * hand-authored and authoritative: synthesis never overwrites or prunes them,
920
+ * and deleting the user's setup (the AI opt-in) leaves them standing. `model`
921
+ * rows belong to the synthesis sweep, which owns them outright.
922
+ */
923
+ export type ToneProfileSource = 'manual' | 'model'
924
+
917
925
  /**
918
926
  * The per-user opt-in for tone-of-voice synthesis: only set-up users are
919
927
  * swept. Also carries the whole-user synthesis claim (status/started_at).
@@ -933,7 +941,8 @@ export interface ToneProfileSetup {
933
941
  }
934
942
 
935
943
  /**
936
- * One generated tone-of-voice instruction row. Rows form a specificity
944
+ * One tone-of-voice instruction row synthesized or hand-authored (`source`).
945
+ * Rows form a specificity
937
946
  * hierarchy — user aggregate (the constant voice), per-channel base, per
938
947
  * contact-group, per individual contact — and every row is SELF-CONTAINED: a
939
948
  * drafting consumer injects exactly one row's `instructions` verbatim (use
@@ -951,6 +960,8 @@ export interface ToneProfile {
951
960
  /** Scopes the row to one individual contact within the group. */
952
961
  contact_id?: string
953
962
  scope: ToneProfileScope
963
+ /** Who owns this row — see ToneProfileSource. */
964
+ source: ToneProfileSource
954
965
  /** COMPLETE markdown instruction set, served verbatim to a drafting model. */
955
966
  instructions: string
956
967
  /** Short delta vs the tier above — what a human scans; empty on root tiers. */
@@ -975,6 +986,30 @@ export interface ListToneProfilesQuery extends PaginationQuery {
975
986
  owned_by_id?: string
976
987
  channel?: Channel
977
988
  scope?: ToneProfileScope
989
+ /** Narrow to hand-authored (`manual`) or synthesized (`model`) rows. */
990
+ source?: ToneProfileSource
991
+ }
992
+
993
+ /**
994
+ * Writes ONE hand-authored tone profile. The identity is the scope tuple, not
995
+ * an id — re-sending the same scope updates the row it created. The row lands
996
+ * `source: 'manual'`, which locks that scope against synthesis; it also takes
997
+ * the scope over from a `model` row already sitting there.
998
+ *
999
+ * Scope grammar (mirrors the resolve precedence): omit `channel` for the
1000
+ * cross-channel base voice; `contact_group_key` needs a `channel`;
1001
+ * `contact_id` needs both, because resolution reaches a contact row only
1002
+ * through its group.
1003
+ */
1004
+ export interface UpsertToneProfileRequest {
1005
+ owned_by_id: string
1006
+ channel?: Channel
1007
+ contact_group_key?: string
1008
+ contact_id?: string
1009
+ /** COMPLETE markdown style guide for this tier; never a delta. */
1010
+ instructions: string
1011
+ /** Optional human-facing note on how this tier differs from the one above. */
1012
+ differences?: string
978
1013
  }
979
1014
 
980
1015
  /**
@@ -1229,6 +1264,11 @@ export type ContactAddressKind =
1229
1264
  | 'email'
1230
1265
  | 'phone'
1231
1266
  | 'slack'
1267
+ /** LinkedIn's stable profile id (ACoAA… / ACwAA… / AE…) — what sending needs. */
1268
+ | 'linkedin_id'
1269
+ /** The `/in/<slug>` public identifier — what humans and records hold. */
1270
+ | 'linkedin_public_identifier'
1271
+ /** Legacy alias of `linkedin_id` (rows written before the kind split); read-only. */
1232
1272
  | 'linkedin'
1233
1273
  | 'telegram'
1234
1274
  | 'instagram'
@@ -1466,10 +1506,25 @@ export interface AttachContactAddressRequest {
1466
1506
  name?: string
1467
1507
  }
1468
1508
 
1509
+ /** How a contact→record edge came to be: a user's explicit link, or the
1510
+ * binding maintained from the record's contact-address attributes (exactly one
1511
+ * per record; never unlinked by hand). */
1512
+ export type ContactRecordLinkSource = 'manual' | 'record'
1513
+
1514
+ /** One canonical address key of a contact (the dedup backbone). */
1515
+ export interface ContactAddressKey {
1516
+ kind: ContactAddressKind
1517
+ value: string
1518
+ scope?: string
1519
+ raw_value?: string
1520
+ }
1521
+
1469
1522
  /**
1470
1523
  * A directed edge from a contact to a business record (entity_slug +
1471
1524
  * record_id). Bare — no role; the relationship's semantics come from the
1472
- * target entity. Immutable once created.
1525
+ * target entity. A `manual` link is immutable once created; a `record`
1526
+ * binding repoints/refreshes as the record's addresses change and carries the
1527
+ * keys the record contributed (`address_keys`).
1473
1528
  */
1474
1529
  export interface ContactRecordLink {
1475
1530
  id: string
@@ -1477,8 +1532,12 @@ export interface ContactRecordLink {
1477
1532
  contact_id: string
1478
1533
  entity_slug: string
1479
1534
  record_id: string
1535
+ source: ContactRecordLinkSource
1536
+ address_keys?: ContactAddressKey[]
1480
1537
  created_at: string
1481
1538
  created_by: UserRef
1539
+ updated_at?: string
1540
+ updated_by?: UserRef
1482
1541
  }
1483
1542
 
1484
1543
  export interface CreateContactRecordLinkRequest {
@@ -1491,6 +1550,7 @@ export interface ListContactRecordLinksQuery extends PaginationQuery {
1491
1550
  contact_id?: string
1492
1551
  entity_slug?: string
1493
1552
  record_id?: string
1553
+ source?: ContactRecordLinkSource
1494
1554
  }
1495
1555
 
1496
1556
  export interface MergeContactsRequest {
@@ -1595,6 +1655,10 @@ export interface Transcription {
1595
1655
  updated_by: UserRef
1596
1656
  }
1597
1657
 
1658
+ /**
1659
+ * Transcribes a storage-service audio file. Asynchronous: the row lands
1660
+ * `processing` and the caller polls until `completed`.
1661
+ */
1598
1662
  export interface CreateTranscriptionRequest {
1599
1663
  file_id: string
1600
1664
  language?: string
@@ -1602,6 +1666,38 @@ export interface CreateTranscriptionRequest {
1602
1666
  is_diarized?: boolean
1603
1667
  }
1604
1668
 
1669
+ /**
1670
+ * Lands an ALREADY WRITTEN transcript — no provider runs and the row is
1671
+ * `completed` when the call returns. This is how a transcript, and through
1672
+ * `materialize` a conversation, is authored from text with nothing to upload.
1673
+ * `source_file_id` stays empty, which is how a reader tells the two kinds apart.
1674
+ *
1675
+ * Timings are optional: plain text has none, and materialization then sequences
1676
+ * the turns by their position instead of by `start_ms`.
1677
+ */
1678
+ /**
1679
+ * One turn of an authored transcript. Distinct from TranscriptTurn (the read
1680
+ * shape, always fully populated) because everything but who-said-what is
1681
+ * derived server-side: an unlabelled speaker becomes "Speaker N", and text with
1682
+ * no timings is sequenced by position at materialization.
1683
+ */
1684
+ export interface AuthoredTranscriptTurn {
1685
+ /** Speaker index — the same number means the same person. */
1686
+ speaker: number
1687
+ speaker_label?: string
1688
+ text: string
1689
+ start_ms?: number
1690
+ end_ms?: number
1691
+ confidence?: number
1692
+ }
1693
+
1694
+ export interface CreateTranscriptionFromTextRequest {
1695
+ turns: AuthoredTranscriptTurn[]
1696
+ language?: string
1697
+ /** Defaults to whether the turns name more than one speaker. */
1698
+ is_diarized?: boolean
1699
+ }
1700
+
1605
1701
  /**
1606
1702
  * Edits a COMPLETED transcription in place. `turns`, when present, replace the
1607
1703
  * diarized turns wholesale; `speaker_labels` maps diarized speaker indexes
@@ -1721,9 +1817,17 @@ export type SendingPeriod = 'rolling_24h' | 'rolling_7d' | 'rolling_30d'
1721
1817
  * ONE kind of act performed through a channel connection — shared by a
1722
1818
  * limit's `action` (what it counts), a channel action's `action_type` (what
1723
1819
  * was performed) and the eligibility check. `message` is the plain send
1724
- * (valid on a limit, never on a channel action row).
1820
+ * (valid on a limit, never on a channel action row). `profile_lookup` is the
1821
+ * silent twin of `profile_visit`: one provider read the platform performs to
1822
+ * learn a person's full identity (LinkedIn profile id + public identifier),
1823
+ * ledgered and limited like any other act.
1725
1824
  */
1726
- export type ChannelActionType = 'message' | 'invitation' | 'inmail' | 'profile_visit'
1825
+ export type ChannelActionType =
1826
+ | 'message'
1827
+ | 'invitation'
1828
+ | 'inmail'
1829
+ | 'profile_visit'
1830
+ | 'profile_lookup'
1727
1831
  export type Weekday =
1728
1832
  | 'monday'
1729
1833
  | 'tuesday'
package/src/data/index.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  import type { ProteosClient } from '../client.js'
2
2
  import { type QueryService, QueryServiceImpl } from './queries.js'
3
- import { type RecordService, RecordServiceImpl } from './records.js'
3
+ import {
4
+ type RecordDuplicateService,
5
+ RecordDuplicateServiceImpl,
6
+ type RecordService,
7
+ RecordServiceImpl,
8
+ } from './records.js'
4
9
 
5
10
  /**
6
11
  * Facade for the data-service (records + raw-SQL query API).
@@ -16,10 +21,13 @@ import { type RecordService, RecordServiceImpl } from './records.js'
16
21
  export class DataClient {
17
22
  readonly records: RecordService
18
23
  readonly queries: QueryService
24
+ /** Duplicate pairs of an entity's records (raised by contact binding). */
25
+ readonly recordDuplicates: RecordDuplicateService
19
26
 
20
27
  constructor(client: ProteosClient) {
21
28
  this.records = new RecordServiceImpl(client)
22
29
  this.queries = new QueryServiceImpl(client)
30
+ this.recordDuplicates = new RecordDuplicateServiceImpl(client)
23
31
  }
24
32
  }
25
33
 
@@ -33,15 +41,20 @@ export type {
33
41
  QueryValidateMeta,
34
42
  QueryValidateResponse,
35
43
  } from './queries.js'
36
- export type { RecordService } from './records.js'
44
+ export type { RecordDuplicateService, RecordService } from './records.js'
37
45
  export type {
38
46
  BatchTransactionError,
39
47
  BatchTransactionStatus,
40
48
  BatchUpsertRecordsResponse,
41
49
  BatchUpsertTransaction,
42
50
  BatchUpsertTransactionResult,
51
+ ListRecordDuplicatesQuery,
43
52
  ListRecordsOptions,
53
+ PublishContactObservationsResponse,
44
54
  RecordData,
55
+ RecordDuplicate,
56
+ RecordDuplicateSource,
57
+ RecordDuplicateStatus,
45
58
  } from './types.js'
46
59
 
47
60
  // Re-export Zod schemas
@@ -4,8 +4,11 @@ import type { ListResult } from '../types/common.js'
4
4
  import type {
5
5
  BatchUpsertRecordsResponse,
6
6
  BatchUpsertTransaction,
7
+ ListRecordDuplicatesQuery,
7
8
  ListRecordsOptions,
9
+ PublishContactObservationsResponse,
8
10
  RecordData,
11
+ RecordDuplicate,
9
12
  } from './types.js'
10
13
 
11
14
  const RECORDS_BASE_PATH = '/data/v1/records'
@@ -92,6 +95,26 @@ export interface RecordService {
92
95
  * (bare refs) on this path.
93
96
  */
94
97
  getPublic(orgId: string, entitySlug: string, id: string): Promise<RecordData>
98
+ /**
99
+ * Replays ONE page of the entity's records as record_contact_observation
100
+ * events (the backfill after an attribute became contact-address). Requires
101
+ * unscoped write on the entity; loop until `page + 1 >= pages_total`.
102
+ */
103
+ publishContactObservations(
104
+ entitySlug: string,
105
+ options?: { page?: number; page_size?: number },
106
+ ): Promise<PublishContactObservationsResponse>
107
+ }
108
+
109
+ /**
110
+ * Record duplicates — "these two records may be the same thing" pairs of one
111
+ * entity (see `RecordDuplicate`). Reading = entity read, dismissing = entity write.
112
+ */
113
+ export interface RecordDuplicateService {
114
+ list(entitySlug: string, query?: ListRecordDuplicatesQuery): Promise<ListResult<RecordDuplicate>>
115
+ get(entitySlug: string, id: string): Promise<RecordDuplicate>
116
+ /** Closes a pair as "not a duplicate" (409 `record_duplicate_not_open` when already closed). */
117
+ dismiss(entitySlug: string, id: string): Promise<RecordDuplicate>
95
118
  }
96
119
 
97
120
  /**
@@ -149,6 +172,17 @@ export class RecordServiceImpl implements RecordService {
149
172
  )
150
173
  }
151
174
 
175
+ async publishContactObservations(
176
+ entitySlug: string,
177
+ options: { page?: number; page_size?: number } = {},
178
+ ): Promise<PublishContactObservationsResponse> {
179
+ return this.client.requestWithQuery<PublishContactObservationsResponse>(
180
+ 'POST',
181
+ `${RECORDS_BASE_PATH}/${entitySlug}/contact-observations`,
182
+ options,
183
+ )
184
+ }
185
+
152
186
  listPublic(
153
187
  orgId: string,
154
188
  entitySlug: string,
@@ -180,3 +214,32 @@ export class RecordServiceImpl implements RecordService {
180
214
  )
181
215
  }
182
216
  }
217
+
218
+ export class RecordDuplicateServiceImpl implements RecordDuplicateService {
219
+ constructor(private readonly client: ProteosClient) {}
220
+
221
+ async list(
222
+ entitySlug: string,
223
+ query: ListRecordDuplicatesQuery = {},
224
+ ): Promise<ListResult<RecordDuplicate>> {
225
+ return this.client.requestWithQuery<ListResult<RecordDuplicate>>(
226
+ 'GET',
227
+ `${RECORDS_BASE_PATH}/${entitySlug}/duplicates`,
228
+ query,
229
+ )
230
+ }
231
+
232
+ async get(entitySlug: string, id: string): Promise<RecordDuplicate> {
233
+ return this.client.request<RecordDuplicate>(
234
+ 'GET',
235
+ `${RECORDS_BASE_PATH}/${entitySlug}/duplicates/${encodeURIComponent(id)}`,
236
+ )
237
+ }
238
+
239
+ async dismiss(entitySlug: string, id: string): Promise<RecordDuplicate> {
240
+ return this.client.request<RecordDuplicate>(
241
+ 'POST',
242
+ `${RECORDS_BASE_PATH}/${entitySlug}/duplicates/${encodeURIComponent(id)}/dismiss`,
243
+ )
244
+ }
245
+ }
package/src/data/types.ts CHANGED
@@ -96,3 +96,55 @@ export const BatchUpsertTransactionResultSchema = z.object({
96
96
  export const BatchUpsertRecordsResponseSchema = z.object({
97
97
  results: z.array(BatchUpsertTransactionResultSchema),
98
98
  })
99
+
100
+ // ── Record duplicates ─────────────────────────────────────────────────────────
101
+
102
+ /** How a duplicate pair was detected. `contact_binding` = two records bound to
103
+ * the same conversation contact through their contact-address attributes. */
104
+ export type RecordDuplicateSource = 'contact_binding' | (string & {})
105
+
106
+ export type RecordDuplicateStatus = 'open' | 'dismissed' | 'resolved'
107
+
108
+ /**
109
+ * "These two records of one entity may be the same thing." Asymmetric:
110
+ * `primary_record_id` is the record to keep, `secondary_record_id` the
111
+ * newcomer. A group of colliding records has ONE primary and one row per other
112
+ * member (four records = three rows). `evidence` is shaped by `source` — for
113
+ * `contact_binding`: `{ contact_id, address_keys: [{kind, value}] }`.
114
+ */
115
+ export interface RecordDuplicate {
116
+ id: string
117
+ org_id: string
118
+ entity_slug: string
119
+ primary_record_id: string
120
+ secondary_record_id: string
121
+ source: RecordDuplicateSource
122
+ evidence: Record<string, unknown>
123
+ status: RecordDuplicateStatus
124
+ resolved_by?: { type: string; id: string }
125
+ resolved_at?: string
126
+ created_at: string
127
+ created_by: { type: string; id: string }
128
+ updated_at: string
129
+ updated_by: { type: string; id: string }
130
+ }
131
+
132
+ export interface ListRecordDuplicatesQuery {
133
+ /** Only pairs this record takes part in (either side). */
134
+ record_id?: string
135
+ /** Defaults to `open` on the server. */
136
+ status?: RecordDuplicateStatus
137
+ page?: number
138
+ page_size?: number
139
+ }
140
+
141
+ /** ONE page of an entity's records replayed as contact observations. */
142
+ export interface PublishContactObservationsResponse {
143
+ entity_slug: string
144
+ page: number
145
+ page_size: number
146
+ pages_total: number
147
+ items_total: number
148
+ published: number
149
+ skipped: number
150
+ }