@ai-sdk/angular 2.0.45 → 2.0.47
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/CHANGELOG.md +13 -0
- package/package.json +6 -5
- package/src/index.ts +6 -0
- package/src/lib/chat.ng.test.ts +1361 -0
- package/src/lib/chat.ng.ts +77 -0
- package/src/lib/completion.ng.test.ts +170 -0
- package/src/lib/completion.ng.ts +117 -0
- package/src/lib/structured-object.ng.test.ts +255 -0
- package/src/lib/structured-object.ng.ts +242 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import {
|
|
2
|
+
generateId,
|
|
3
|
+
isAbortError,
|
|
4
|
+
safeValidateTypes,
|
|
5
|
+
type FetchFunction,
|
|
6
|
+
type InferSchema,
|
|
7
|
+
} from '@ai-sdk/provider-utils';
|
|
8
|
+
import { signal } from '@angular/core';
|
|
9
|
+
import {
|
|
10
|
+
FlexibleSchema,
|
|
11
|
+
asSchema,
|
|
12
|
+
isDeepEqualData,
|
|
13
|
+
parsePartialJson,
|
|
14
|
+
type DeepPartial,
|
|
15
|
+
} from 'ai';
|
|
16
|
+
|
|
17
|
+
export type StructuredObjectOptions<
|
|
18
|
+
SCHEMA extends FlexibleSchema,
|
|
19
|
+
RESULT = InferSchema<SCHEMA>,
|
|
20
|
+
> = {
|
|
21
|
+
/**
|
|
22
|
+
* The API endpoint. It should stream JSON that matches the schema as chunked text.
|
|
23
|
+
*/
|
|
24
|
+
api: string;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A schema that defines the shape of the complete object.
|
|
28
|
+
*/
|
|
29
|
+
schema: SCHEMA;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A unique identifier. If not provided, a random one will be
|
|
33
|
+
* generated. When provided, the `useObject` hook with the same `id` will
|
|
34
|
+
* have shared states across components.
|
|
35
|
+
*/
|
|
36
|
+
id?: string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* An optional value for the initial object.
|
|
40
|
+
*/
|
|
41
|
+
initialValue?: DeepPartial<RESULT>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Custom fetch implementation. You can use it as a middleware to intercept requests,
|
|
45
|
+
* or to provide a custom fetch implementation for e.g. testing.
|
|
46
|
+
*/
|
|
47
|
+
fetch?: FetchFunction;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Callback that is called when the stream has finished.
|
|
51
|
+
*/
|
|
52
|
+
onFinish?: (event: {
|
|
53
|
+
/**
|
|
54
|
+
* The generated object (typed according to the schema).
|
|
55
|
+
* Can be undefined if the final object does not match the schema.
|
|
56
|
+
*/
|
|
57
|
+
object: RESULT | undefined;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Optional error object. This is e.g. a TypeValidationError when the final object does not match the schema.
|
|
61
|
+
*/
|
|
62
|
+
error: Error | undefined;
|
|
63
|
+
}) => Promise<void> | void;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Callback function to be called when an error is encountered.
|
|
67
|
+
*/
|
|
68
|
+
onError?: (error: Error) => void;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Additional HTTP headers to be included in the request.
|
|
72
|
+
*/
|
|
73
|
+
headers?: Record<string, string> | Headers;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The credentials mode to be used for the fetch request.
|
|
77
|
+
* Possible values are: 'omit', 'same-origin', 'include'.
|
|
78
|
+
* Defaults to 'same-origin'.
|
|
79
|
+
*/
|
|
80
|
+
credentials?: RequestCredentials;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export class StructuredObject<
|
|
84
|
+
SCHEMA extends FlexibleSchema,
|
|
85
|
+
RESULT = InferSchema<SCHEMA>,
|
|
86
|
+
INPUT = unknown,
|
|
87
|
+
> {
|
|
88
|
+
readonly options: StructuredObjectOptions<SCHEMA, RESULT>;
|
|
89
|
+
readonly id: string;
|
|
90
|
+
#abortController: AbortController | undefined;
|
|
91
|
+
|
|
92
|
+
// Reactive state
|
|
93
|
+
readonly #object = signal<DeepPartial<RESULT> | undefined>(undefined);
|
|
94
|
+
readonly #loading = signal<boolean>(false);
|
|
95
|
+
readonly #error = signal<Error | undefined>(undefined);
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The current value for the generated object. Updated as the API streams JSON chunks.
|
|
99
|
+
*/
|
|
100
|
+
get object(): DeepPartial<RESULT> | undefined {
|
|
101
|
+
return this.#object();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The error object of the API request */
|
|
105
|
+
get error(): Error | undefined {
|
|
106
|
+
return this.#error();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Flag that indicates whether an API request is in progress.
|
|
111
|
+
*/
|
|
112
|
+
get loading(): boolean {
|
|
113
|
+
return this.#loading();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
constructor(options: StructuredObjectOptions<SCHEMA, RESULT>) {
|
|
117
|
+
this.options = options;
|
|
118
|
+
this.id = options.id ?? generateId();
|
|
119
|
+
this.#object.set(options.initialValue);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Abort the current request immediately, keep the current partial object if any.
|
|
124
|
+
*/
|
|
125
|
+
stop = () => {
|
|
126
|
+
try {
|
|
127
|
+
this.#abortController?.abort();
|
|
128
|
+
} catch {
|
|
129
|
+
// ignore
|
|
130
|
+
} finally {
|
|
131
|
+
this.#loading.set(false);
|
|
132
|
+
this.#abortController = undefined;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Calls the API with the provided input as JSON body.
|
|
138
|
+
*/
|
|
139
|
+
submit = async (input: INPUT) => {
|
|
140
|
+
try {
|
|
141
|
+
this.#clearObject();
|
|
142
|
+
|
|
143
|
+
this.#loading.set(true);
|
|
144
|
+
|
|
145
|
+
const abortController = new AbortController();
|
|
146
|
+
this.#abortController = abortController;
|
|
147
|
+
|
|
148
|
+
const actualFetch = this.options.fetch ?? fetch;
|
|
149
|
+
const response = await actualFetch(this.options.api, {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
headers: {
|
|
152
|
+
'Content-Type': 'application/json',
|
|
153
|
+
...this.options.headers,
|
|
154
|
+
},
|
|
155
|
+
credentials: this.options.credentials,
|
|
156
|
+
signal: abortController.signal,
|
|
157
|
+
body: JSON.stringify(input),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
if (!response.ok) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
(await response.text()) ?? 'Failed to fetch the response.',
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (response.body == null) {
|
|
167
|
+
throw new Error('The response body is empty.');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let accumulatedText = '';
|
|
171
|
+
let latestObject: DeepPartial<RESULT> | undefined = undefined;
|
|
172
|
+
|
|
173
|
+
await response.body.pipeThrough(new TextDecoderStream()).pipeTo(
|
|
174
|
+
new WritableStream<string>({
|
|
175
|
+
write: async chunk => {
|
|
176
|
+
if (abortController?.signal.aborted) {
|
|
177
|
+
throw new DOMException('Stream aborted', 'AbortError');
|
|
178
|
+
}
|
|
179
|
+
accumulatedText += chunk;
|
|
180
|
+
|
|
181
|
+
const { value } = await parsePartialJson(accumulatedText);
|
|
182
|
+
const currentObject = value as DeepPartial<RESULT>;
|
|
183
|
+
|
|
184
|
+
if (!isDeepEqualData(latestObject, currentObject)) {
|
|
185
|
+
latestObject = currentObject;
|
|
186
|
+
|
|
187
|
+
this.#object.set(currentObject);
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
close: async () => {
|
|
192
|
+
this.#loading.set(false);
|
|
193
|
+
this.#abortController = undefined;
|
|
194
|
+
|
|
195
|
+
if (this.options.onFinish != null) {
|
|
196
|
+
const validationResult = await safeValidateTypes({
|
|
197
|
+
value: latestObject,
|
|
198
|
+
schema: asSchema(this.options.schema),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (validationResult.success) {
|
|
202
|
+
this.options.onFinish({
|
|
203
|
+
object: validationResult.value,
|
|
204
|
+
error: undefined,
|
|
205
|
+
});
|
|
206
|
+
} else {
|
|
207
|
+
this.options.onFinish({
|
|
208
|
+
object: undefined,
|
|
209
|
+
error: (validationResult as { error: Error }).error,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
}),
|
|
215
|
+
);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (isAbortError(error)) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const coalescedError =
|
|
222
|
+
error instanceof Error ? error : new Error(String(error));
|
|
223
|
+
if (this.options.onError) {
|
|
224
|
+
this.options.onError(coalescedError);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
this.#loading.set(false);
|
|
228
|
+
this.#error.set(coalescedError);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
clear = () => {
|
|
233
|
+
this.stop();
|
|
234
|
+
this.#clearObject();
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
#clearObject = () => {
|
|
238
|
+
this.#object.set(undefined);
|
|
239
|
+
this.#error.set(undefined);
|
|
240
|
+
this.#loading.set(false);
|
|
241
|
+
};
|
|
242
|
+
}
|