@llumiverse/core 0.22.0 → 0.23.0-dev-20251118

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.
Files changed (42) hide show
  1. package/README.md +1 -1
  2. package/lib/cjs/CompletionStream.js +89 -20
  3. package/lib/cjs/CompletionStream.js.map +1 -1
  4. package/lib/cjs/Driver.js +23 -5
  5. package/lib/cjs/Driver.js.map +1 -1
  6. package/lib/cjs/async.js.map +1 -1
  7. package/lib/cjs/json.js +3 -174
  8. package/lib/cjs/json.js.map +1 -1
  9. package/lib/cjs/stream.js +16 -10
  10. package/lib/cjs/stream.js.map +1 -1
  11. package/lib/cjs/validation.js +31 -7
  12. package/lib/cjs/validation.js.map +1 -1
  13. package/lib/esm/CompletionStream.js +89 -20
  14. package/lib/esm/CompletionStream.js.map +1 -1
  15. package/lib/esm/Driver.js +23 -5
  16. package/lib/esm/Driver.js.map +1 -1
  17. package/lib/esm/async.js.map +1 -1
  18. package/lib/esm/json.js +3 -172
  19. package/lib/esm/json.js.map +1 -1
  20. package/lib/esm/stream.js +16 -10
  21. package/lib/esm/stream.js.map +1 -1
  22. package/lib/esm/validation.js +31 -7
  23. package/lib/esm/validation.js.map +1 -1
  24. package/lib/tsconfig.tsbuildinfo +1 -0
  25. package/lib/types/CompletionStream.d.ts +2 -2
  26. package/lib/types/CompletionStream.d.ts.map +1 -1
  27. package/lib/types/Driver.d.ts +3 -3
  28. package/lib/types/Driver.d.ts.map +1 -1
  29. package/lib/types/async.d.ts +2 -2
  30. package/lib/types/async.d.ts.map +1 -1
  31. package/lib/types/json.d.ts +0 -13
  32. package/lib/types/json.d.ts.map +1 -1
  33. package/lib/types/stream.d.ts.map +1 -1
  34. package/lib/types/validation.d.ts +2 -2
  35. package/lib/types/validation.d.ts.map +1 -1
  36. package/package.json +87 -86
  37. package/src/CompletionStream.ts +88 -21
  38. package/src/Driver.ts +29 -13
  39. package/src/async.ts +4 -4
  40. package/src/json.ts +3 -168
  41. package/src/stream.ts +19 -11
  42. package/src/validation.ts +33 -10
@@ -3,39 +3,36 @@ import { AbstractDriver } from "./Driver.js";
3
3
 
4
4
  export class DefaultCompletionStream<PromptT = any> implements CompletionStream<PromptT> {
5
5
 
6
- chunks: string[];
6
+ chunks: number; // Counter for number of chunks instead of storing strings
7
7
  completion: ExecutionResponse<PromptT> | undefined;
8
8
 
9
9
  constructor(public driver: AbstractDriver<DriverOptions, PromptT>,
10
10
  public prompt: PromptT,
11
11
  public options: ExecutionOptions) {
12
- this.chunks = [];
12
+ this.chunks = 0;
13
13
  }
14
14
 
15
15
  async *[Symbol.asyncIterator]() {
16
16
  // reset state
17
17
  this.completion = undefined;
18
- if (this.chunks.length > 0) {
19
- this.chunks = [];
20
- }
21
- const chunks = this.chunks;
18
+ this.chunks = 0;
19
+ const accumulatedResults: any[] = []; // Accumulate CompletionResult[] from chunks
22
20
 
23
21
  this.driver.logger.debug(
24
22
  `[${this.driver.provider}] Streaming Execution of ${this.options.model} with prompt`,
25
23
  );
26
24
 
27
25
  const start = Date.now();
28
- let stream;
29
26
  let finish_reason: string | undefined = undefined;
30
27
  let promptTokens: number = 0;
31
28
  let resultTokens: number | undefined = undefined;
32
29
 
33
30
  try {
34
- stream = await this.driver.requestTextCompletionStream(this.prompt, this.options);
31
+ const stream = await this.driver.requestTextCompletionStream(this.prompt, this.options);
35
32
  for await (const chunk of stream) {
36
33
  if (chunk) {
37
34
  if (typeof chunk === 'string') {
38
- chunks.push(chunk);
35
+ this.chunks++;
39
36
  yield chunk;
40
37
  } else {
41
38
  if (chunk.finish_reason) { //Do not replace non-null values with null values
@@ -48,9 +45,65 @@ export class DefaultCompletionStream<PromptT = any> implements CompletionStream<
48
45
  promptTokens = Math.max(promptTokens, chunk.token_usage.prompt ?? 0);
49
46
  resultTokens = Math.max(resultTokens ?? 0, chunk.token_usage.result ?? 0);
50
47
  }
51
- if (chunk.result) {
52
- chunks.push(chunk.result);
53
- yield chunk.result;
48
+ if (Array.isArray(chunk.result) && chunk.result.length > 0) {
49
+ // Process each result in the chunk, combining consecutive text/JSON
50
+ for (const result of chunk.result) {
51
+ // Check if we can combine with the last accumulated result
52
+ const lastResult = accumulatedResults[accumulatedResults.length - 1];
53
+
54
+ if (lastResult &&
55
+ ((lastResult.type === 'text' && result.type === 'text') ||
56
+ (lastResult.type === 'json' && result.type === 'json'))) {
57
+ // Combine consecutive text or JSON results
58
+ if (result.type === 'text') {
59
+ lastResult.value += result.value;
60
+ } else if (result.type === 'json') {
61
+ // For JSON, combine the parsed objects directly
62
+ try {
63
+ const lastParsed = lastResult.value;
64
+ const currentParsed = result.value;
65
+ if (lastParsed !== null && typeof lastParsed === 'object' &&
66
+ currentParsed !== null && typeof currentParsed === 'object') {
67
+ const combined = { ...lastParsed, ...currentParsed };
68
+ lastResult.value = combined;
69
+ } else {
70
+ // If not objects, convert to string and concatenate
71
+ const lastStr = typeof lastParsed === 'string' ? lastParsed : JSON.stringify(lastParsed);
72
+ const currentStr = typeof currentParsed === 'string' ? currentParsed : JSON.stringify(currentParsed);
73
+ lastResult.value = lastStr + currentStr;
74
+ }
75
+ } catch {
76
+ // If anything fails, just concatenate string representations
77
+ lastResult.value = String(lastResult.value) + String(result.value);
78
+ }
79
+ }
80
+ } else {
81
+ // Add as new result
82
+ accumulatedResults.push(result);
83
+ }
84
+ }
85
+
86
+ // Convert CompletionResult[] to string for streaming
87
+ // Only yield if we have results to show
88
+ const resultText = chunk.result.map(r => {
89
+ switch (r.type) {
90
+ case 'text':
91
+ return r.value;
92
+ case 'json':
93
+ return JSON.stringify(r.value);
94
+ case 'image':
95
+ // Show truncated image placeholder for streaming
96
+ const truncatedValue = typeof r.value === 'string' ? r.value.slice(0, 10) : String(r.value).slice(0, 10);
97
+ return `\n[Image: ${truncatedValue}...]\n`;
98
+ default:
99
+ return String((r as any).value || '');
100
+ }
101
+ }).join('');
102
+
103
+ if (resultText) {
104
+ this.chunks++;
105
+ yield resultText;
106
+ }
54
107
  }
55
108
  }
56
109
  }
@@ -60,24 +113,24 @@ export class DefaultCompletionStream<PromptT = any> implements CompletionStream<
60
113
  throw error;
61
114
  }
62
115
 
63
- const content = chunks.join('');
64
-
65
116
  // Return undefined for the ExecutionTokenUsage object if there is nothing to fill it with.
66
- // Allows for checking for truthyness on token_usage, rather than it's internals. For testing and downstream usage.
67
- let tokens: ExecutionTokenUsage | undefined = resultTokens ?
117
+ // Allows for checking for truthy-ness on token_usage, rather than it's internals. For testing and downstream usage.
118
+ const tokens: ExecutionTokenUsage | undefined = resultTokens ?
68
119
  { prompt: promptTokens, result: resultTokens, total: resultTokens + promptTokens, } : undefined
69
120
 
70
121
  this.completion = {
71
- result: content,
122
+ result: accumulatedResults, // Return the accumulated CompletionResult[] instead of text
72
123
  prompt: this.prompt,
73
124
  execution_time: Date.now() - start,
74
125
  token_usage: tokens,
75
126
  finish_reason: finish_reason,
76
- chunks: chunks.length,
127
+ chunks: this.chunks,
77
128
  }
78
129
 
79
130
  try {
80
- this.driver.validateResult(this.completion, this.options);
131
+ if (this.completion) {
132
+ this.driver.validateResult(this.completion, this.options);
133
+ }
81
134
  } catch (error: any) {
82
135
  error.prompt = this.prompt;
83
136
  throw error;
@@ -103,9 +156,23 @@ export class FallbackCompletionStream<PromptT = any> implements CompletionStream
103
156
  );
104
157
  try {
105
158
  const completion = await this.driver._execute(this.prompt, this.options);
106
- const content = typeof completion.result === 'string' ? completion.result : JSON.stringify(completion.result);
159
+ // For fallback streaming, yield the text content but keep the original completion
160
+ const content = completion.result.map(r => {
161
+ switch (r.type) {
162
+ case 'text':
163
+ return r.value;
164
+ case 'json':
165
+ return JSON.stringify(r.value);
166
+ case 'image':
167
+ // Show truncated image placeholder for streaming
168
+ const truncatedValue = typeof r.value === 'string' ? r.value.slice(0, 10) : String(r.value).slice(0, 10);
169
+ return `[Image: ${truncatedValue}...]`;
170
+ default:
171
+ return String((r as any).value || '');
172
+ }
173
+ }).join('');
107
174
  yield content;
108
- this.completion = completion;
175
+ this.completion = completion; // Return the original completion with untouched CompletionResult[]
109
176
  } catch (error: any) {
110
177
  error.prompt = this.prompt;
111
178
  throw error;
package/src/Driver.ts CHANGED
@@ -9,7 +9,7 @@ import { formatTextPrompt } from "./formatters/index.js";
9
9
  import {
10
10
  AIModel,
11
11
  Completion,
12
- CompletionChunk,
12
+ CompletionChunkObject,
13
13
  CompletionStream,
14
14
  DataSource,
15
15
  DriverOptions,
@@ -17,7 +17,6 @@ import {
17
17
  EmbeddingsResult,
18
18
  ExecutionOptions,
19
19
  ExecutionResponse,
20
- ImageGeneration,
21
20
  Logger,
22
21
  Modalities,
23
22
  ModelSearchPayload,
@@ -29,19 +28,35 @@ import {
29
28
  } from "@llumiverse/common";
30
29
  import { validateResult } from "./validation.js";
31
30
 
31
+ // Helper to create logger methods that support both message-only and object-first signatures
32
+ function createConsoleLoggerMethod(consoleMethod: (...args: unknown[]) => void): Logger['info'] {
33
+ return ((objOrMsg: any, msgOrNever?: any, ...args: (string | number | boolean)[]) => {
34
+ if (typeof objOrMsg === 'string') {
35
+ // Message-only: logger.info("message", ...args)
36
+ consoleMethod(objOrMsg, msgOrNever, ...args);
37
+ } else if (msgOrNever !== undefined) {
38
+ // Object-first: logger.info({ obj }, "message", ...args)
39
+ consoleMethod(msgOrNever, objOrMsg, ...args);
40
+ } else {
41
+ // Object-only: logger.info({ obj })
42
+ consoleMethod(objOrMsg, ...args);
43
+ }
44
+ }) as Logger['info'];
45
+ }
46
+
32
47
  const ConsoleLogger: Logger = {
33
- debug: console.debug,
34
- info: console.info,
35
- warn: console.warn,
36
- error: console.error,
48
+ debug: createConsoleLoggerMethod(console.debug.bind(console)),
49
+ info: createConsoleLoggerMethod(console.info.bind(console)),
50
+ warn: createConsoleLoggerMethod(console.warn.bind(console)),
51
+ error: createConsoleLoggerMethod(console.error.bind(console)),
37
52
  }
38
53
 
39
54
  const noop = () => void 0;
40
55
  const NoopLogger: Logger = {
41
- debug: noop,
42
- info: noop,
43
- warn: noop,
44
- error: noop,
56
+ debug: noop as Logger['debug'],
57
+ info: noop as Logger['info'],
58
+ warn: noop as Logger['warn'],
59
+ error: noop as Logger['error'],
45
60
  }
46
61
 
47
62
  export function createLogger(logger: Logger | "console" | undefined) {
@@ -131,7 +146,8 @@ export abstract class AbstractDriver<OptionsT extends DriverOptions = DriverOpti
131
146
  try {
132
147
  result.result = validateResult(result.result, options.result_schema);
133
148
  } catch (error: any) {
134
- this.logger?.error({ err: error, data: result.result }, `[${this.provider}] [${options.model}] ${error.code ? '[' + error.code + '] ' : ''}Result validation error: ${error.message}`);
149
+ const errorMessage = `[${this.provider}] [${options.model}] ${error.code ? '[' + error.code + '] ' : ''}Result validation error: ${error.message}`;
150
+ this.logger.error({ err: error, data: result.result }, errorMessage);
135
151
  result.error = {
136
152
  code: error.code || error.name,
137
153
  message: error.message,
@@ -225,9 +241,9 @@ export abstract class AbstractDriver<OptionsT extends DriverOptions = DriverOpti
225
241
 
226
242
  abstract requestTextCompletion(prompt: PromptT, options: ExecutionOptions): Promise<Completion>;
227
243
 
228
- abstract requestTextCompletionStream(prompt: PromptT, options: ExecutionOptions): Promise<AsyncIterable<CompletionChunk>>;
244
+ abstract requestTextCompletionStream(prompt: PromptT, options: ExecutionOptions): Promise<AsyncIterable<CompletionChunkObject>>;
229
245
 
230
- async requestImageGeneration(_prompt: PromptT, _options: ExecutionOptions): Promise<Completion<ImageGeneration>> {
246
+ async requestImageGeneration(_prompt: PromptT, _options: ExecutionOptions): Promise<Completion> {
231
247
  throw new Error("Image generation not implemented.");
232
248
  //Cannot be made abstract, as abstract methods are required in the derived class
233
249
  }
package/src/async.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { ServerSentEvent } from "@vertesia/api-fetch-client"
2
- import { CompletionChunk } from "@llumiverse/common";
2
+ import { CompletionChunkObject } from "@llumiverse/common";
3
3
 
4
4
  export async function* asyncMap<T, R>(asyncIterable: AsyncIterable<T>, callback: (value: T, index: number) => R) {
5
5
  let i = 0;
@@ -18,9 +18,9 @@ export function oneAsyncIterator<T>(value: T): AsyncIterable<T> {
18
18
  /**
19
19
  * Given a ReadableStream of server sent events, tran
20
20
  */
21
- export function transformSSEStream(stream: ReadableStream<ServerSentEvent>, transform: (data: string) => CompletionChunk): ReadableStream<CompletionChunk> & AsyncIterable<CompletionChunk> {
21
+ export function transformSSEStream(stream: ReadableStream<ServerSentEvent>, transform: (data: string) => CompletionChunkObject): ReadableStream<CompletionChunkObject> & AsyncIterable<CompletionChunkObject> {
22
22
  // on node and bun the ReadableStream is an async iterable
23
- return stream.pipeThrough(new TransformStream<ServerSentEvent, CompletionChunk>({
23
+ return stream.pipeThrough(new TransformStream<ServerSentEvent, CompletionChunkObject>({
24
24
  transform(event: ServerSentEvent, controller) {
25
25
  if (event.type === 'event' && event.data && event.data !== '[DONE]') {
26
26
  try {
@@ -32,7 +32,7 @@ export function transformSSEStream(stream: ReadableStream<ServerSentEvent>, tran
32
32
  }
33
33
  }
34
34
  }
35
- })) as ReadableStream<CompletionChunk> & AsyncIterable<CompletionChunk>;
35
+ })) satisfies ReadableStream<CompletionChunkObject> & AsyncIterable<CompletionChunkObject>;
36
36
  }
37
37
 
38
38
  export class EventStream<T, ReturnT = any> implements AsyncIterable<T> {
package/src/json.ts CHANGED
@@ -1,181 +1,16 @@
1
1
  import { JSONValue } from "@llumiverse/common";
2
+ import { jsonrepair } from 'jsonrepair';
2
3
 
3
4
  function extractJsonFromText(text: string): string {
4
5
  const start = text.indexOf("{");
5
6
  const end = text.lastIndexOf("}");
6
- text = text.substring(start, end + 1);
7
- return text.replace(/\\n/g, "");
7
+ return text.substring(start, end + 1);
8
8
  }
9
9
 
10
10
  export function extractAndParseJSON(text: string): JSONValue {
11
11
  return parseJSON(extractJsonFromText(text));
12
12
  }
13
13
 
14
- const RX_DQUOTE = /^"([^"\\]|\\.)*"/us;
15
- const RX_SQUOTE = /^'([^'\\]|\\.)*'/us;
16
- const RX_NUMBER = /^-?\d+(\.\d+)?/;
17
- const RX_BOOLEAN = /^true|false/;
18
- const RX_NULL = /^null/;
19
- const RX_KEY = /^[$_a-zA-Z][$_a-zA-Z0-9]*/;
20
- const RX_PUNCTUATION = /^\s*([\[\]{}:,])\s*/;
21
-
22
- function fixText(value: string) {
23
- return value.replaceAll('\n', '\\n').replaceAll('\r', '\\r');
24
- }
25
-
26
- function decodeSingleQuotedString(value: string) {
27
- return JSON.parse('"' + value.slice(1, -1).replaceAll(/(?<!\\)"/g, '\\"') + '"');
28
- }
29
-
30
- export class JsonParser {
31
- pos: number = 0;
32
-
33
- constructor(public text: string) { }
34
-
35
- skip(n: number) {
36
- this.text = this.text.substring(n);
37
- this.pos += n;
38
- }
39
-
40
- tryReadPunctuation() {
41
- const m = RX_PUNCTUATION.exec(this.text);
42
- if (m) {
43
- this.skip(m[0].length);
44
- return m[1];
45
- }
46
- }
47
-
48
- readKey() {
49
- const first = this.text.charCodeAt(0);
50
- if (first === 34) { // "
51
- const m = RX_DQUOTE.exec(this.text);
52
- if (m) {
53
- this.skip(m[0].length);
54
- return JSON.parse(m[0]);
55
- }
56
- } else if (first === 39) { // '
57
- const m = RX_SQUOTE.exec(this.text);
58
- if (m) {
59
- this.skip(m[0].length);
60
- return decodeSingleQuotedString(m[0]);
61
- }
62
- } else {
63
- const m = RX_KEY.exec(this.text);
64
- if (m) {
65
- this.skip(m[0].length);
66
- return m[0];
67
- }
68
- }
69
- throw new Error('Expected a key at position ' + this.pos + ' but found ' + this.text);
70
- }
71
-
72
- readScalar() {
73
- const first = this.text.charCodeAt(0);
74
- if (first === 34) { // "
75
- const m = RX_DQUOTE.exec(this.text);
76
- if (m) {
77
- this.skip(m[0].length);
78
- return JSON.parse(fixText(m[0]));
79
- }
80
- } else if (first === 39) { // '
81
- const m = RX_SQUOTE.exec(this.text);
82
- if (m) {
83
- this.skip(m[0].length);
84
- return decodeSingleQuotedString(fixText(m[0]));
85
- }
86
- } else {
87
- let m = RX_NUMBER.exec(this.text);
88
- if (m) {
89
- this.skip(m[0].length);
90
- return parseFloat(m[0]);
91
- }
92
- m = RX_BOOLEAN.exec(this.text);
93
- if (m) {
94
- this.skip(m[0].length);
95
- return m[0] === 'true';
96
- }
97
- m = RX_NULL.exec(this.text);
98
- if (m) {
99
- this.skip(m[0].length);
100
- return null;
101
- }
102
- }
103
- throw new Error('Expected a value at position ' + this.pos + ' but found ' + this.text);
104
- }
105
-
106
- readObject() {
107
- let key: string | undefined;
108
- const obj: any = {};
109
- while (true) {
110
- if (!key) { // read key
111
- const p = this.tryReadPunctuation();
112
- if (p === '}') {
113
- return obj;
114
- } else if (p === ',') {
115
- continue;
116
- } else if (p) {
117
- throw new Error('Expected a key at position ' + this.pos + ' but found ' + this.text);
118
- }
119
- key = this.readKey();
120
- if (!key) {
121
- throw new Error('Expected a key at position ' + this.pos + ' but found ' + this.text);
122
- }
123
- if (this.tryReadPunctuation() !== ':') {
124
- throw new Error('Expected a colon at position ' + this.pos + ' but found ' + this.text);
125
- };
126
- } else { // read value
127
- const value = this.readValue();
128
- if (value === undefined) {
129
- throw new Error('Expected a value at position ' + this.pos + ' but found ' + this.text);
130
- }
131
- obj[key] = value;
132
- key = undefined;
133
- }
134
- }
135
- }
136
-
137
- readArray() {
138
- const ar: any[] = [];
139
- while (true) {
140
- const p = this.tryReadPunctuation();
141
- if (p === ',') {
142
- continue;
143
- } else if (p === ']') {
144
- return ar;
145
- } else if (p === '[') {
146
- ar.push(this.readArray());
147
- } else if (p === '{') {
148
- ar.push(this.readObject());
149
- } else if (!p) {
150
- ar.push(this.readScalar());
151
- } else {
152
- throw new Error('Expected a value at position ' + this.pos + ' but found ' + this.text);
153
- }
154
- }
155
- }
156
-
157
- readValue() {
158
- const p = this.tryReadPunctuation();
159
- if (p === '{') {
160
- return this.readObject();
161
- } else if (p === '[') {
162
- return this.readArray();
163
- } else if (!p) {
164
- return this.readScalar();
165
- }
166
- }
167
-
168
- static parse(text: string) {
169
- const parser = new JsonParser(text);
170
- const r = parser.readValue();
171
- if (r === undefined) {
172
- throw new Error('Not a valid JSON');
173
- }
174
- return r;
175
- }
176
- }
177
-
178
-
179
14
  export function parseJSON(text: string): JSONValue {
180
15
  text = text.trim();
181
16
  try {
@@ -183,7 +18,7 @@ export function parseJSON(text: string): JSONValue {
183
18
  } catch (err: any) {
184
19
  // use a relaxed parser
185
20
  try {
186
- return JsonParser.parse(text);
21
+ return JSON.parse(jsonrepair(text));
187
22
  } catch (err2: any) { // throw the original error
188
23
  throw err;
189
24
  }
package/src/stream.ts CHANGED
@@ -1,22 +1,30 @@
1
1
 
2
2
  export async function readStreamAsBase64(stream: ReadableStream): Promise<string> {
3
- return (await _readStreamAsBuffer(stream)).toString('base64');
3
+ const uint8Array = await readStreamAsUint8Array(stream);
4
+ return Buffer.from(uint8Array).toString('base64');
4
5
  }
5
6
 
6
7
  export async function readStreamAsString(stream: ReadableStream): Promise<string> {
7
- return (await _readStreamAsBuffer(stream)).toString();
8
+ const uint8Array = await readStreamAsUint8Array(stream);
9
+ return Buffer.from(uint8Array).toString();
8
10
  }
9
11
 
10
12
  export async function readStreamAsUint8Array(stream: ReadableStream): Promise<Uint8Array> {
11
- // We return a Uint8Array for strict type checking, even though the buffer extends Uint8Array.
12
- const buffer = await _readStreamAsBuffer(stream);
13
- return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
14
- }
15
-
16
- async function _readStreamAsBuffer(stream: ReadableStream): Promise<Buffer> {
17
- const out: Buffer[] = [];
13
+ const chunks: Uint8Array[] = [];
14
+ let totalLength = 0;
15
+
18
16
  for await (const chunk of stream) {
19
- out.push(Buffer.from(chunk));
17
+ const uint8Chunk = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
18
+ chunks.push(uint8Chunk);
19
+ totalLength += uint8Chunk.length;
20
+ }
21
+
22
+ const combined = new Uint8Array(totalLength);
23
+ let offset = 0;
24
+ for (const chunk of chunks) {
25
+ combined.set(chunk, offset);
26
+ offset += chunk.length;
20
27
  }
21
- return Buffer.concat(out);
28
+
29
+ return combined;
22
30
  }
package/src/validation.ts CHANGED
@@ -1,8 +1,8 @@
1
+ import { CompletionResult, ResultValidationError } from "@llumiverse/common";
1
2
  import { Ajv } from 'ajv';
2
3
  import addFormats from 'ajv-formats';
3
4
  import { extractAndParseJSON } from "./json.js";
4
5
  import { resolveField } from './resolver.js';
5
- import { ResultValidationError } from "@llumiverse/common";
6
6
 
7
7
 
8
8
  const ajv = new Ajv({
@@ -28,17 +28,40 @@ export class ValidationError extends Error implements ResultValidationError {
28
28
  }
29
29
  }
30
30
 
31
- export function validateResult(data: any, schema: Object) {
32
- let json;
31
+ function parseCompletionAsJson(data: CompletionResult[]) {
32
+ let lastError: ValidationError | undefined;
33
+ for (const part of data) {
34
+ if (part.type === "text") {
35
+ const text = part.value.trim();
36
+ try {
37
+ return extractAndParseJSON(text);
38
+ } catch (error: any) {
39
+ lastError = new ValidationError("json_error", error.message);
40
+ }
41
+ }
42
+ }
43
+ if (!lastError) {
44
+ lastError = new ValidationError("json_error", "No JSON compatible response found in completion result");
45
+ }
46
+ throw lastError;
47
+ }
48
+
33
49
 
34
- if (typeof data === "string") {
35
- try {
36
- json = extractAndParseJSON(data);
37
- } catch (error: any) {
38
- throw new ValidationError("json_error", error.message)
50
+ export function validateResult(data: CompletionResult[], schema: Object): CompletionResult[] {
51
+ let json;
52
+ if (Array.isArray(data)) {
53
+ const jsonResults = data.filter(r => r.type === "json");
54
+ if (jsonResults.length > 0) {
55
+ json = jsonResults[0].value;
56
+ } else {
57
+ try {
58
+ json = parseCompletionAsJson(data);
59
+ } catch (error: any) {
60
+ throw new ValidationError("json_error", error.message)
61
+ }
39
62
  }
40
63
  } else {
41
- json = data;
64
+ throw new Error("Data to validate must be an array")
42
65
  }
43
66
 
44
67
  const validate = ajv.compile(schema);
@@ -71,5 +94,5 @@ export function validateResult(data: any, schema: Object) {
71
94
  }
72
95
  }
73
96
 
74
- return json;
97
+ return [{ type: "json", value: json }];
75
98
  }