@api-client/core 0.19.6 → 0.19.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.
@@ -42810,16 +42810,16 @@
42810
42810
  "@id": "#219"
42811
42811
  },
42812
42812
  {
42813
- "@id": "#210"
42813
+ "@id": "#219"
42814
42814
  },
42815
42815
  {
42816
- "@id": "#213"
42816
+ "@id": "#216"
42817
42817
  },
42818
42818
  {
42819
- "@id": "#216"
42819
+ "@id": "#210"
42820
42820
  },
42821
42821
  {
42822
- "@id": "#219"
42822
+ "@id": "#213"
42823
42823
  }
42824
42824
  ],
42825
42825
  "doc:root": false,
@@ -44232,7 +44232,7 @@
44232
44232
  "doc:ExternalDomainElement",
44233
44233
  "doc:DomainElement"
44234
44234
  ],
44235
- "doc:raw": "type: 'GENERAL'\ncountryDialCode : '+32'\nareaCode : '22'\nsubscriberNumber: '12.87.00'\nformatted: '+32-(0)22 000000'\n",
44235
+ "doc:raw": "-\n type: 'GENERAL'\n value: 'info@company.be'\n-\n type: 'IT_DEPT'\n value: 'it-service@company.be'\n",
44236
44236
  "core:mediaType": "application/yaml",
44237
44237
  "sourcemaps:sources": [
44238
44238
  {
@@ -44253,7 +44253,7 @@
44253
44253
  "doc:ExternalDomainElement",
44254
44254
  "doc:DomainElement"
44255
44255
  ],
44256
- "doc:raw": "type: 'GENERAL'\ncountryDialCode : '+32'\nareaCode : '21'\nsubscriberNumber: '12.87.00'\nformatted: '+32-(0)21 302099'\n",
44256
+ "doc:raw": "type: \"GENERAL\"\nvalue: \"www.company.be\"\n",
44257
44257
  "core:mediaType": "application/yaml",
44258
44258
  "sourcemaps:sources": [
44259
44259
  {
@@ -44274,7 +44274,7 @@
44274
44274
  "doc:ExternalDomainElement",
44275
44275
  "doc:DomainElement"
44276
44276
  ],
44277
- "doc:raw": "-\n type: 'GENERAL'\n value: 'info@company.be'\n-\n type: 'IT_DEPT'\n value: 'it-service@company.be'\n",
44277
+ "doc:raw": "type: 'GENERAL'\ncountryDialCode : '+32'\nareaCode : '21'\nsubscriberNumber: '12.87.00'\nformatted: '+32-(0)21 302099'\n",
44278
44278
  "core:mediaType": "application/yaml",
44279
44279
  "sourcemaps:sources": [
44280
44280
  {
@@ -44295,7 +44295,7 @@
44295
44295
  "doc:ExternalDomainElement",
44296
44296
  "doc:DomainElement"
44297
44297
  ],
44298
- "doc:raw": "type: \"GENERAL\"\nvalue: \"www.company.be\"\n",
44298
+ "doc:raw": "type: 'GENERAL'\ncountryDialCode : '+32'\nareaCode : '22'\nsubscriberNumber: '12.87.00'\nformatted: '+32-(0)22 000000'\n",
44299
44299
  "core:mediaType": "application/yaml",
44300
44300
  "sourcemaps:sources": [
44301
44301
  {
@@ -45116,22 +45116,22 @@
45116
45116
  {
45117
45117
  "@id": "#212/source-map/lexical/element_0",
45118
45118
  "sourcemaps:element": "amf://id#212",
45119
- "sourcemaps:value": "[(1,0)-(6,0)]"
45119
+ "sourcemaps:value": "[(1,0)-(7,0)]"
45120
45120
  },
45121
45121
  {
45122
45122
  "@id": "#215/source-map/lexical/element_0",
45123
45123
  "sourcemaps:element": "amf://id#215",
45124
- "sourcemaps:value": "[(1,0)-(6,0)]"
45124
+ "sourcemaps:value": "[(1,0)-(3,0)]"
45125
45125
  },
45126
45126
  {
45127
45127
  "@id": "#218/source-map/lexical/element_0",
45128
45128
  "sourcemaps:element": "amf://id#218",
45129
- "sourcemaps:value": "[(1,0)-(7,0)]"
45129
+ "sourcemaps:value": "[(1,0)-(6,0)]"
45130
45130
  },
45131
45131
  {
45132
45132
  "@id": "#221/source-map/lexical/element_0",
45133
45133
  "sourcemaps:element": "amf://id#221",
45134
- "sourcemaps:value": "[(1,0)-(3,0)]"
45134
+ "sourcemaps:value": "[(1,0)-(6,0)]"
45135
45135
  },
45136
45136
  {
45137
45137
  "@id": "#338/source-map/synthesized-field/element_1",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@api-client/core",
3
3
  "description": "The API Client's core client library. Works in NodeJS and in a ES enabled browser.",
4
- "version": "0.19.6",
4
+ "version": "0.19.8",
5
5
  "license": "Apache-2.0",
6
6
  "exports": {
7
7
  "./browser.js": {
@@ -542,6 +542,17 @@ export class DomainProperty extends DomainElement {
542
542
  return object.schema as PropertyWebBindings
543
543
  }
544
544
 
545
+ /**
546
+ * Reads the web binding definition, if any.
547
+ * Useful for reading binding data without mutating the model when the binding
548
+ * is missing (unlike `getWebBinding()` which creates the binding when missing).
549
+ * @returns The web binding definition, if any
550
+ */
551
+ readWebBinding(): PropertyWebBindings | undefined {
552
+ const object = this.bindings.find((i) => i.type === 'web') as PropertyBinding | undefined
553
+ return object?.schema as PropertyWebBindings | undefined
554
+ }
555
+
545
556
  /**
546
557
  * Returns the schema value of the binding, if any was created.
547
558
  * @param type The type of the binding to read.
@@ -12,6 +12,7 @@ import { DataDomainDelta } from './DataDomainDelta.js'
12
12
  *
13
13
  * @param msg The generic AI chat message.
14
14
  * @returns The parsed in-memory wrapper context message.
15
+ * @deprecated Use `AiModelMessage.processMessageSafe` instead.
15
16
  */
16
17
  export function parseAiChatMessage(msg: AiMessageSchema): AiUserMessage | AiModelMessageWithDelta {
17
18
  if (msg.role === 'user') {
@@ -3,6 +3,7 @@ import { AiMessageKind } from './kinds.js'
3
3
  import { nanoid } from '../nanoid.js'
4
4
  import type { AiDomainDelta } from '../modeling/ai/types.js'
5
5
  import { DataDomainDelta } from '../modeling/ai/DataDomainDelta.js'
6
+ import { detectReasoning } from '../modeling/ai/message_parser.js'
6
7
 
7
8
  export type AiMessageRole = 'user' | 'model'
8
9
  export type AiMessageState = 'loading' | 'complete' | 'error' | 'terminated'
@@ -210,21 +211,39 @@ export class AiUserMessage extends AiMessageBase<AiUserMessageSchema> implements
210
211
  }
211
212
  }
212
213
 
213
- export class AiModelMessage extends AiMessageBase<AiModelMessageSchema> implements AiModelMessageSchema {
214
+ /**
215
+ * Model messages have the `text` that most likely is a structured JSON
216
+ * with the `delta` and `reasoning` properties. When an `AiModelMessage` is instantiated,
217
+ * it will try to parse the `text` into the `delta` and `reasoning` properties, leaving the
218
+ * `text` property unchanged.
219
+ *
220
+ * The delta object will be different for different models and different use cases.
221
+ * It is up to the model to define the delta object. However, the delta object should
222
+ * be normalized by each child class.
223
+ *
224
+ * @template D The type of the delta.
225
+ */
226
+ export abstract class AiModelMessage<D = unknown>
227
+ extends AiMessageBase<AiModelMessageSchema>
228
+ implements AiModelMessageSchema
229
+ {
214
230
  state: AiMessageState
215
231
  applied?: boolean
216
232
  thoughts: string
217
233
 
218
234
  /**
219
- * The delta of the message, if any.
235
+ * The extracted delta from the message.
220
236
  */
221
- delta?: AiDomainDelta
237
+ delta?: D
238
+
222
239
  /**
223
- * The reasoning of the delta.
240
+ * The extracted reasoning from the message.
224
241
  * Format in markdown.
225
242
  */
226
243
  reasoning?: string
227
244
 
245
+ #reasoningEnded = false
246
+
228
247
  static createSchema(input: Partial<AiModelMessageSchema>): AiModelMessageSchema {
229
248
  const base = AiMessageBase.createBaseSchema(input)
230
249
  return {
@@ -241,6 +260,7 @@ export class AiModelMessage extends AiMessageBase<AiModelMessageSchema> implemen
241
260
  this.state = input.state || 'loading'
242
261
  this.applied = input.applied
243
262
  this.thoughts = input.thoughts || ''
263
+ this.processMessageSafe()
244
264
  }
245
265
 
246
266
  override toJSON(): AiModelMessageSchema {
@@ -268,6 +288,8 @@ export class AiModelMessage extends AiMessageBase<AiModelMessageSchema> implemen
268
288
 
269
289
  /**
270
290
  * Adds a text chunk to the message. Useful when streaming.
291
+ *
292
+ * Note, this function progressively detects the reasoning on each chunk.
271
293
  * @param text The text chunk to add.
272
294
  */
273
295
  addText(text?: string): void {
@@ -275,6 +297,15 @@ export class AiModelMessage extends AiMessageBase<AiModelMessageSchema> implemen
275
297
  return
276
298
  }
277
299
  this.text += text
300
+ if (this.#reasoningEnded) {
301
+ return
302
+ }
303
+ const before = this.reasoning
304
+ const after = detectReasoning(this.text)
305
+ if (after && after === before) {
306
+ this.#reasoningEnded = true
307
+ }
308
+ this.reasoning = after
278
309
  }
279
310
 
280
311
  /**
@@ -283,12 +314,13 @@ export class AiModelMessage extends AiMessageBase<AiModelMessageSchema> implemen
283
314
  * It sets the delta and reasoning fields, if found in the message.
284
315
  */
285
316
  processMessage(): void {
286
- if (!this.text) {
317
+ const { text } = this
318
+ if (!text) {
287
319
  return
288
320
  }
289
- const parsed = JSON.parse(this.text)
321
+ const parsed = JSON.parse(text)
290
322
  if (parsed.delta) {
291
- this.delta = DataDomainDelta.normalize(parsed.delta)
323
+ this.delta = this.normalizeDelta(parsed.delta)
292
324
  }
293
325
  if (parsed.reasoning) {
294
326
  this.reasoning = parsed.reasoning
@@ -307,29 +339,44 @@ export class AiModelMessage extends AiMessageBase<AiModelMessageSchema> implemen
307
339
  }
308
340
  }
309
341
 
342
+ /**
343
+ * Normalizes the delta to the final shape required by the system.
344
+ *
345
+ * Probabilistic models can produce deltas that are not always what the author expected.
346
+ * For example, the DataDomain delta has `modifiedEntities` property that sometimes is
347
+ * populated multiple times by the same entity. This should be normalized to a single
348
+ * operation so that the UI can correctly render the changes, without an expensive lookup
349
+ * operations. The author of the message model should predict these situations and handle
350
+ * them accordingly.
351
+ *
352
+ * @param delta The delta to normalize.
353
+ * @returns The normalized delta.
354
+ */
355
+ abstract normalizeDelta(delta: D): D
356
+ }
357
+
358
+ /**
359
+ * A model message that contains a delta of type `AiDomainDelta`.
360
+ */
361
+ export class AiModelDataDomainMessage extends AiModelMessage<AiDomainDelta> {
310
362
  /**
311
363
  * Creates an empty model message. Useful when initializing LLM flow.
312
364
  * @param session The session key.
313
365
  * @returns An empty model message.
314
366
  */
315
- static createEmpty(session: string): AiModelMessage {
316
- return new AiModelMessage({
367
+ static createEmpty(session: string): AiModelDataDomainMessage {
368
+ return new AiModelDataDomainMessage({
317
369
  session,
318
370
  state: 'loading',
319
371
  })
320
372
  }
373
+
374
+ override normalizeDelta(delta: AiDomainDelta): AiDomainDelta {
375
+ return DataDomainDelta.normalize(delta)
376
+ }
321
377
  }
322
378
 
323
379
  /**
324
380
  * A union type representing any valid chat message in the AI session history.
325
381
  */
326
- export type AiMessage = AiUserMessage | AiModelMessage
327
-
328
- export const AiMessageFactory = {
329
- fromSchema(schema: Partial<AiMessageSchema>): AiMessage {
330
- if (schema.role === 'model') {
331
- return new AiModelMessage(schema as Partial<AiModelMessageSchema>)
332
- }
333
- return new AiUserMessage(schema as Partial<AiUserMessageSchema>)
334
- },
335
- }
382
+ export type AiMessage = AiUserMessage | AiModelMessage | AiModelDataDomainMessage
@@ -771,6 +771,44 @@ test.group('DomainProperty.readBinding()', () => {
771
771
  })
772
772
  })
773
773
 
774
+ test.group('DomainProperty.readWebBinding()', () => {
775
+ test('returns undefined if no web binding exists', ({ assert }) => {
776
+ const dataDomain = new DataDomain()
777
+ const property = new DomainProperty(dataDomain, 'test-parent')
778
+ const binding = property.readWebBinding()
779
+ assert.isUndefined(binding)
780
+ })
781
+
782
+ test('returns the web binding schema if it exists', ({ assert }) => {
783
+ const dataDomain = new DataDomain()
784
+ const property = new DomainProperty(dataDomain, 'test-parent', {
785
+ bindings: [{ type: 'web', schema: { format: 'float' } }],
786
+ })
787
+ const webBindings = property.readWebBinding()
788
+ assert.deepEqual(webBindings, { format: 'float' })
789
+ })
790
+
791
+ test('returns undefined if the web binding exists but has no schema', ({ assert }) => {
792
+ const dataDomain = new DataDomain()
793
+ const property = new DomainProperty(dataDomain, 'test-parent', {
794
+ // @ts-expect-error Testing undefined value
795
+ bindings: [{ type: 'web', schema: undefined }],
796
+ })
797
+ const webBindings = property.readWebBinding()
798
+ assert.isUndefined(webBindings)
799
+ })
800
+
801
+ test('returns undefined if the web binding exists but the schema is null', ({ assert }) => {
802
+ const dataDomain = new DataDomain()
803
+ const property = new DomainProperty(dataDomain, 'test-parent', {
804
+ // @ts-expect-error Testing null value
805
+ bindings: [{ type: 'web', schema: null }],
806
+ })
807
+ const webBindings = property.readWebBinding()
808
+ assert.isNull(webBindings)
809
+ })
810
+ })
811
+
774
812
  test.group('DomainProperty.toApiShape()', () => {
775
813
  test('returns an object', ({ assert }) => {
776
814
  const dataDomain = new DataDomain()
@@ -1,5 +1,5 @@
1
1
  import { test } from '@japa/runner'
2
- import { AiUserMessage, AiModelMessage, AiMessageFactory } from '../../../src/models/AiMessage.js'
2
+ import { AiUserMessage, AiModelDataDomainMessage } from '../../../src/models/AiMessage.js'
3
3
  import { AiMessageKind } from '../../../src/models/kinds.js'
4
4
 
5
5
  test.group('AiMessage | AiUserMessage', () => {
@@ -72,13 +72,13 @@ test.group('AiMessage | AiUserMessage', () => {
72
72
  })
73
73
  })
74
74
 
75
- test.group('AiMessage | AiModelMessage', () => {
75
+ test.group('AiMessage | AiModelDataDomainMessage', () => {
76
76
  test('createSchema validates required session', ({ assert }) => {
77
- assert.throws(() => AiModelMessage.createSchema({}), 'Session is required to create an AiMessage.')
77
+ assert.throws(() => AiModelDataDomainMessage.createSchema({}), 'Session is required to create an AiMessage.')
78
78
  })
79
79
 
80
80
  test('createSchema builds valid model schema', ({ assert }) => {
81
- const schema = AiModelMessage.createSchema({ session: 'session1', text: 'Response' })
81
+ const schema = AiModelDataDomainMessage.createSchema({ session: 'session1', text: 'Response' })
82
82
  assert.isString(schema.key)
83
83
  assert.isNotEmpty(schema.key)
84
84
  assert.equal(schema.kind, AiMessageKind)
@@ -93,7 +93,7 @@ test.group('AiMessage | AiModelMessage', () => {
93
93
  })
94
94
 
95
95
  test('constructor builds AiModelMessage instance', ({ assert }) => {
96
- const msg = new AiModelMessage({
96
+ const msg = new AiModelDataDomainMessage({
97
97
  key: 'msg1',
98
98
  session: 'sess1',
99
99
  text: 'Model utterance',
@@ -117,7 +117,7 @@ test.group('AiMessage | AiModelMessage', () => {
117
117
  })
118
118
 
119
119
  test('toJSON correctly serializes an AiModelMessage', ({ assert }) => {
120
- const msg = new AiModelMessage({
120
+ const msg = new AiModelDataDomainMessage({
121
121
  key: 'msg1',
122
122
  session: 'sess1',
123
123
  text: 'Message Data',
@@ -148,7 +148,7 @@ test.group('AiMessage | AiModelMessage', () => {
148
148
  })
149
149
 
150
150
  test('addThought safely appends thoughts', ({ assert }) => {
151
- const msg = new AiModelMessage({ session: 's1' })
151
+ const msg = new AiModelDataDomainMessage({ session: 's1' })
152
152
  msg.addThought('Hello ')
153
153
  msg.addThought('World!')
154
154
  msg.addThought()
@@ -156,20 +156,40 @@ test.group('AiMessage | AiModelMessage', () => {
156
156
  })
157
157
 
158
158
  test('addText safely appends text', ({ assert }) => {
159
- const msg = new AiModelMessage({ session: 's1' })
159
+ const msg = new AiModelDataDomainMessage({ session: 's1' })
160
160
  msg.addText('Hello ')
161
161
  msg.addText('World!')
162
162
  msg.addText()
163
163
  assert.equal(msg.text, 'Hello World!')
164
164
  })
165
165
 
166
+ test('addText progressively detects reasoning and stops when reasoning ends', ({ assert }) => {
167
+ const msg = new AiModelDataDomainMessage({ session: 's1' })
168
+
169
+ msg.addText('{"reason')
170
+ assert.isUndefined(msg.reasoning)
171
+
172
+ msg.addText('ing": "I ')
173
+ assert.equal(msg.reasoning, 'I ')
174
+
175
+ msg.addText('think')
176
+ assert.equal(msg.reasoning, 'I think')
177
+
178
+ msg.addText(' therefore "')
179
+ assert.equal(msg.reasoning, 'I think therefore ')
180
+
181
+ // This triggers after === before, freezing reasoning detection.
182
+ msg.addText(', "delta": {')
183
+ assert.equal(msg.reasoning, 'I think therefore ')
184
+ })
185
+
166
186
  test('processMessage throws on invalid JSON', ({ assert }) => {
167
- const msg = new AiModelMessage({ session: 's1', text: 'NOT JSON' })
187
+ const msg = new AiModelDataDomainMessage({ session: 's1', text: 'NOT JSON' })
168
188
  assert.throws(() => msg.processMessage(), SyntaxError)
169
189
  })
170
190
 
171
191
  test('processMessage parses delta and reasoning', ({ assert }) => {
172
- const msg = new AiModelMessage({
192
+ const msg = new AiModelDataDomainMessage({
173
193
  session: 's1',
174
194
  text: JSON.stringify({
175
195
  reasoning: 'I need to make models',
@@ -183,34 +203,16 @@ test.group('AiMessage | AiModelMessage', () => {
183
203
  })
184
204
 
185
205
  test('processMessageSafe swallows errors', ({ assert }) => {
186
- const msg = new AiModelMessage({ session: 's1', text: 'NOT JSON' })
206
+ const msg = new AiModelDataDomainMessage({ session: 's1', text: 'NOT JSON' })
187
207
  msg.processMessageSafe() // should not throw
188
208
  assert.isUndefined(msg.delta)
189
209
  assert.isUndefined(msg.reasoning)
190
210
  })
191
211
 
192
212
  test('createEmpty generates loading stub', ({ assert }) => {
193
- const msg = AiModelMessage.createEmpty('sess1')
213
+ const msg = AiModelDataDomainMessage.createEmpty('sess1')
194
214
  assert.equal(msg.session, 'sess1')
195
215
  assert.equal(msg.state, 'loading')
196
216
  assert.equal(msg.role, 'model')
197
217
  })
198
218
  })
199
-
200
- test.group('AiMessage | AiMessageFactory', () => {
201
- test('fromSchema generates AiUserMessage when role=user', ({ assert }) => {
202
- const schema = AiUserMessage.createSchema({ session: 'sess', text: 'Hello User' })
203
- const msg = AiMessageFactory.fromSchema(schema)
204
- assert.instanceOf(msg, AiUserMessage)
205
- assert.equal(msg.role, 'user')
206
- assert.equal(msg.text, 'Hello User')
207
- })
208
-
209
- test('fromSchema generates AiModelMessage when role=model', ({ assert }) => {
210
- const schema = AiModelMessage.createSchema({ session: 'sess', text: 'Hello Model' })
211
- const msg = AiMessageFactory.fromSchema(schema)
212
- assert.instanceOf(msg, AiModelMessage)
213
- assert.equal(msg.role, 'model')
214
- assert.equal(msg.text, 'Hello Model')
215
- })
216
- })