@api-client/core 0.19.7 → 0.19.9

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,7 +42810,7 @@
42810
42810
  "@id": "#219"
42811
42811
  },
42812
42812
  {
42813
- "@id": "#210"
42813
+ "@id": "#219"
42814
42814
  },
42815
42815
  {
42816
42816
  "@id": "#213"
@@ -42819,7 +42819,7 @@
42819
42819
  "@id": "#216"
42820
42820
  },
42821
42821
  {
42822
- "@id": "#219"
42822
+ "@id": "#210"
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": "type: \"GENERAL\"\nvalue: \"www.company.be\"\n",
44236
44236
  "core:mediaType": "application/yaml",
44237
44237
  "sourcemaps:sources": [
44238
44238
  {
@@ -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,7 +45116,7 @@
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)-(3,0)]"
45120
45120
  },
45121
45121
  {
45122
45122
  "@id": "#215/source-map/lexical/element_0",
@@ -45131,7 +45131,7 @@
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.7",
4
+ "version": "0.19.9",
5
5
  "license": "Apache-2.0",
6
6
  "exports": {
7
7
  "./browser.js": {
@@ -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') {
@@ -5,7 +5,7 @@ import type { DomainPropertyType } from '../DataFormat.js'
5
5
  import type { OnDeleteRule } from '../index.js'
6
6
  import { SemanticType } from '../Semantics.js'
7
7
  import type { AssociationTarget, PropertySchema } from '../types.js'
8
- import type { AiMessageSchema, AiModelMessage } from '../../models/AiMessage.js'
8
+ import type { AiModelMessageSchema, AiModelMessage, AiUserMessageSchema } from '../../models/AiMessage.js'
9
9
 
10
10
  /**
11
11
  * A copy of the `Type` enum from the `@google/genai` package.
@@ -206,11 +206,11 @@ export interface AiDomainDeltaResponse {
206
206
  * received from the AI generation endpoint.
207
207
  */
208
208
  export type AiStreamEvent =
209
- | { event: 'user-message'; data: AiMessageSchema }
210
- | { event: 'agent-message'; data: AiMessageSchema }
209
+ | { event: 'user-message'; data: AiUserMessageSchema }
210
+ | { event: 'agent-message'; data: AiModelMessageSchema }
211
211
  | { event: 'thought-chunk'; data: string }
212
212
  | { event: 'text-chunk'; data: string }
213
- | { event: 'done'; data: AiMessageSchema }
213
+ | { event: 'done'; data: AiModelMessageSchema }
214
214
  | { event: 'error'; data: Exception }
215
215
  | { event: 'session-updated'; data: AiSessionSchema }
216
216
 
@@ -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
package/src/sdk/AiSdk.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { ContextListOptions, ContextListResult } from '../events/BaseEvents.js'
2
2
  import { Exception } from '../exceptions/exception.js'
3
3
  import type { AiStreamEvent } from '../modeling/ai/types.js'
4
- import type { AiMessage, AiMessageSchema } from '../models/AiMessage.js'
4
+ import type { AiMessageSchema, AiModelMessageSchema, AiUserMessageSchema } from '../models/AiMessage.js'
5
5
  import type { AiSessionApp, AiSessionSchema } from '../models/AiSession.js'
6
6
  import { AiMessageKind, AiSessionKind } from '../models/kinds.js'
7
7
  import { RouteBuilder } from './RouteBuilder.js'
@@ -95,9 +95,10 @@ export class AiSdk extends SdkBase {
95
95
 
96
96
  switch (eventName) {
97
97
  case 'user-message':
98
+ yield { event: eventName, data: eventData as AiUserMessageSchema }
99
+ break
98
100
  case 'agent-message':
99
- // Assume the data is a full AiMessage
100
- yield { event: eventName, data: eventData as AiMessage }
101
+ yield { event: eventName, data: eventData as AiModelMessageSchema }
101
102
  break
102
103
  case 'thought-chunk':
103
104
  case 'text-chunk':
@@ -105,7 +106,7 @@ export class AiSdk extends SdkBase {
105
106
  yield { event: eventName, data: eventData as string }
106
107
  break
107
108
  case 'done':
108
- yield { event: eventName, data: eventData as AiMessage }
109
+ yield { event: eventName, data: eventData as AiModelMessageSchema }
109
110
  return
110
111
  case 'error':
111
112
  yield { event: eventName, data: Exception.fromRawException(eventData, 'Failed to generate results') }
@@ -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
- })