@kognitivedev/cloud-web-search 0.2.28

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/src/index.ts ADDED
@@ -0,0 +1,33 @@
1
+ export { KognitiveCloudWebSearchClient, readCloudWebSearchEventStream } from "./client";
2
+ export { CloudWebSearchStreamProtocolError, CloudWebSearchValidationError } from "./errors";
3
+ export { toCloudWebSearchRichEvents } from "./rich-events";
4
+ export { readSSEStream } from "./sse";
5
+ export type { ParsedSSEEvent } from "./sse";
6
+ export type {
7
+ CloudWebSearchClientConfig,
8
+ CloudWebSearchDecodeOptions,
9
+ CloudWebSearchExecutionParameters,
10
+ CloudWebSearchJobEventRecord,
11
+ CloudWebSearchJobRecord,
12
+ CloudWebSearchJobResultEnvelope,
13
+ CloudWebSearchJobStatus,
14
+ CloudWebSearchJobWaitResult,
15
+ CloudWebSearchMode,
16
+ CloudWebSearchOutputParser,
17
+ CloudWebSearchProductAnswer,
18
+ CloudWebSearchProgressEntry,
19
+ CloudWebSearchRichEvent,
20
+ CloudWebSearchRichSource,
21
+ CloudWebSearchResearchJobInput,
22
+ CloudWebSearchResult,
23
+ CloudWebSearchSearchJobInput,
24
+ CloudWebSearchSource,
25
+ CloudWebSearchStreamEvent,
26
+ CloudWebSearchSubscribeOptions,
27
+ CloudWebSearchTimeRange,
28
+ CreateCloudWebSearchJobInput,
29
+ LogLevel,
30
+ ResearchPlan,
31
+ ResearchPlanTodo,
32
+ WaitForCompletionOptions,
33
+ } from "./types";
@@ -0,0 +1,256 @@
1
+ import type {
2
+ CloudWebSearchJobEventRecord,
3
+ CloudWebSearchProductAnswer,
4
+ CloudWebSearchRichEvent,
5
+ CloudWebSearchRichSource,
6
+ CloudWebSearchStreamEvent,
7
+ } from "./types";
8
+ import { cloneUnknown, isPlainObject } from "./validation";
9
+
10
+ function asString(value: unknown): string | null {
11
+ return typeof value === "string" && value.trim() ? value : null;
12
+ }
13
+
14
+ function asNumber(value: unknown): number | null {
15
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
16
+ }
17
+
18
+ function asIndex(value: unknown): number | null {
19
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : null;
20
+ }
21
+
22
+ function toSource(value: unknown): CloudWebSearchRichSource | null {
23
+ if (!isPlainObject(value)) return null;
24
+ const url = asString(value.url);
25
+ const title = asString(value.title);
26
+ const keyFacts = Array.isArray(value.keyFacts)
27
+ ? value.keyFacts.filter((fact): fact is string => typeof fact === "string")
28
+ : undefined;
29
+
30
+ return {
31
+ title,
32
+ url,
33
+ snippet: asString(value.snippet),
34
+ score: asNumber(value.score),
35
+ confidence: asNumber(value.confidence),
36
+ faviconUrl: asString(value.faviconUrl),
37
+ host: asString(value.host),
38
+ relevantContent: asString(value.relevantContent),
39
+ ...(keyFacts ? { keyFacts } : {}),
40
+ };
41
+ }
42
+
43
+ function sourceFromPayload(payload: Record<string, unknown>): CloudWebSearchRichSource | null {
44
+ if (isPlainObject(payload.source)) {
45
+ return toSource(payload.source);
46
+ }
47
+ return toSource(payload);
48
+ }
49
+
50
+ function sourcesFromPayload(payload: Record<string, unknown>): CloudWebSearchRichSource[] {
51
+ const values = Array.isArray(payload.results)
52
+ ? payload.results
53
+ : Array.isArray(payload.sources)
54
+ ? payload.sources
55
+ : [];
56
+ return values.map((source) => toSource(source)).filter((source): source is CloudWebSearchRichSource => Boolean(source));
57
+ }
58
+
59
+ function payloadOf(record: CloudWebSearchJobEventRecord): Record<string, unknown> {
60
+ return isPlainObject(record.payload) ? record.payload : {};
61
+ }
62
+
63
+ function answerFromResult(value: unknown): CloudWebSearchProductAnswer | null {
64
+ if (!isPlainObject(value)) return null;
65
+ if (isPlainObject(value.output)) {
66
+ return cloneUnknown(value.output) as CloudWebSearchProductAnswer;
67
+ }
68
+ return null;
69
+ }
70
+
71
+ function fromRecord<TOutput>(
72
+ record: CloudWebSearchJobEventRecord,
73
+ ): CloudWebSearchRichEvent<TOutput>[] {
74
+ const payload = payloadOf(record);
75
+ const base = {
76
+ jobId: record.jobId,
77
+ eventId: record.id,
78
+ raw: cloneUnknown(record),
79
+ };
80
+
81
+ switch (record.eventType) {
82
+ case "websearch.research.plan.started":
83
+ return [{
84
+ type: "plan.started",
85
+ ...base,
86
+ message: record.message,
87
+ }];
88
+ case "websearch.research.plan.completed":
89
+ return [{
90
+ type: "plan.completed",
91
+ ...base,
92
+ plan: isPlainObject(record.payload) ? cloneUnknown(record.payload as any) : null,
93
+ }];
94
+ case "websearch.research.todo.generated":
95
+ return [{
96
+ type: "todo.generated",
97
+ ...base,
98
+ todo: isPlainObject(record.payload) ? cloneUnknown(record.payload as any) : null,
99
+ }];
100
+ case "websearch.search.started":
101
+ return [{
102
+ type: "search.started",
103
+ ...base,
104
+ query: asString(payload.query),
105
+ input: payload.input,
106
+ }];
107
+ case "websearch.search.completed":
108
+ return [{
109
+ type: "search.completed",
110
+ ...base,
111
+ query: asString(payload.query),
112
+ provider: asString(payload.provider),
113
+ results: sourcesFromPayload(payload),
114
+ }];
115
+ case "websearch.fetch.started":
116
+ case "websearch.source.opening":
117
+ return [{
118
+ type: "source.opening",
119
+ ...base,
120
+ source: sourceFromPayload(payload),
121
+ index: asIndex(payload.index),
122
+ }];
123
+ case "websearch.extract.completed":
124
+ case "websearch.source.extracted":
125
+ return [{
126
+ type: "source.extracted",
127
+ ...base,
128
+ source: sourceFromPayload(payload),
129
+ snippet: asString(payload.snippet),
130
+ confidence: asNumber(payload.confidence),
131
+ index: asIndex(payload.index),
132
+ }];
133
+ case "websearch.fetch.failed":
134
+ case "websearch.source.failed":
135
+ return [{
136
+ type: "source.failed",
137
+ ...base,
138
+ source: sourceFromPayload(payload),
139
+ errorMessage: asString(payload.error) ?? asString(payload.errorMessage),
140
+ index: asIndex(payload.index),
141
+ }];
142
+ case "websearch.research.reasoning":
143
+ return asString(payload.delta)
144
+ ? [{
145
+ type: "reasoning.delta",
146
+ ...base,
147
+ delta: asString(payload.delta)!,
148
+ }]
149
+ : [];
150
+ case "websearch.research.answer.delta":
151
+ return asString(payload.delta)
152
+ ? [{
153
+ type: "answer.delta",
154
+ ...base,
155
+ delta: asString(payload.delta)!,
156
+ }]
157
+ : [];
158
+ case "websearch.tool.sources.reviewing":
159
+ case "websearch.tool.synthesis.started":
160
+ return [{
161
+ type: "sources.reviewing",
162
+ ...base,
163
+ totalExtractions: asNumber(payload.totalExtractions),
164
+ message: record.message,
165
+ }];
166
+ case "websearch.tool.sources.filtered":
167
+ return [{
168
+ type: "sources.filtered",
169
+ ...base,
170
+ totalSources: asNumber(payload.totalSources),
171
+ credibleSources: asNumber(payload.credibleSources),
172
+ filteredOut: asNumber(payload.filteredOut),
173
+ message: record.message,
174
+ }];
175
+ case "websearch.tool.retrieval.completed":
176
+ case "websearch.tool.synthesis.completed":
177
+ return [{
178
+ type: "sources.reviewed",
179
+ ...base,
180
+ sources: sourcesFromPayload(payload),
181
+ credibleSourceCount: asNumber(payload.credibleSourceCount),
182
+ }];
183
+ case "websearch.research.synthesis.started":
184
+ case "websearch.synthesis.started":
185
+ return [{
186
+ type: "synthesis.started",
187
+ ...base,
188
+ totalExtractions: asNumber(payload.totalExtractions),
189
+ }];
190
+ case "websearch.research.synthesis.completed":
191
+ return [{
192
+ type: "synthesis.completed",
193
+ ...base,
194
+ }];
195
+ case "websearch.research.answer.completed":
196
+ return [{
197
+ type: "answer.completed",
198
+ ...base,
199
+ }];
200
+ default:
201
+ return [];
202
+ }
203
+ }
204
+
205
+ export function toCloudWebSearchRichEvents<TOutput = unknown>(
206
+ event: CloudWebSearchStreamEvent<TOutput>,
207
+ ): CloudWebSearchRichEvent<TOutput>[] {
208
+ if (event.type === "progress" && event.raw) {
209
+ return fromRecord<TOutput>(event.raw);
210
+ }
211
+
212
+ if (event.type === "event") {
213
+ return fromRecord<TOutput>(event.event);
214
+ }
215
+
216
+ if (event.type === "completed") {
217
+ return [{
218
+ type: "answer.completed",
219
+ jobId: event.jobId,
220
+ result: event.result,
221
+ answer: answerFromResult(event.result),
222
+ ...(event.eventId ? { eventId: event.eventId } : {}),
223
+ ...(event.raw ? { raw: event.raw } : {}),
224
+ }, {
225
+ type: "completed",
226
+ jobId: event.jobId,
227
+ job: event.job,
228
+ result: event.result,
229
+ ...(event.eventId ? { eventId: event.eventId } : {}),
230
+ ...(event.raw ? { raw: event.raw } : {}),
231
+ }];
232
+ }
233
+
234
+ if (event.type === "error") {
235
+ return [{
236
+ type: "error",
237
+ jobId: event.jobId,
238
+ job: event.job,
239
+ errorMessage: event.errorMessage,
240
+ ...(event.eventId ? { eventId: event.eventId } : {}),
241
+ ...(event.raw ? { raw: event.raw } : {}),
242
+ }];
243
+ }
244
+
245
+ if (event.type === "cancelled") {
246
+ return [{
247
+ type: "cancelled",
248
+ jobId: event.jobId,
249
+ job: event.job,
250
+ ...(event.eventId ? { eventId: event.eventId } : {}),
251
+ ...(event.raw ? { raw: event.raw } : {}),
252
+ }];
253
+ }
254
+
255
+ return [];
256
+ }
package/src/sse.ts ADDED
@@ -0,0 +1,173 @@
1
+ export type ParsedSSEEvent<T = unknown> = {
2
+ event: string;
3
+ data: T;
4
+ dataText: string;
5
+ id?: string;
6
+ retry?: number;
7
+ };
8
+
9
+ function parseEventData<T>(dataText: string): T {
10
+ try {
11
+ return JSON.parse(dataText) as T;
12
+ } catch {
13
+ return dataText as T;
14
+ }
15
+ }
16
+
17
+ function finalizeEvent<T>(
18
+ eventName: string,
19
+ dataLines: string[],
20
+ lastEventId: string | undefined,
21
+ retry: number | undefined,
22
+ ): ParsedSSEEvent<T> | null {
23
+ if (dataLines.length === 0) {
24
+ return null;
25
+ }
26
+
27
+ const dataText = dataLines.join("\n");
28
+ return {
29
+ event: eventName || "message",
30
+ data: parseEventData<T>(dataText),
31
+ dataText,
32
+ ...(lastEventId === undefined ? {} : { id: lastEventId }),
33
+ ...(retry === undefined ? {} : { retry }),
34
+ };
35
+ }
36
+
37
+ function findNextLineBreak(buffer: string): { index: number; length: number } | null {
38
+ for (let index = 0; index < buffer.length; index += 1) {
39
+ const code = buffer.charCodeAt(index);
40
+ if (code === 0x0a) {
41
+ return { index, length: 1 };
42
+ }
43
+ if (code === 0x0d) {
44
+ return {
45
+ index,
46
+ length: buffer.charCodeAt(index + 1) === 0x0a ? 2 : 1,
47
+ };
48
+ }
49
+ }
50
+
51
+ return null;
52
+ }
53
+
54
+ function parseField(line: string): { field: string; value: string } {
55
+ const separator = line.indexOf(":");
56
+ if (separator < 0) {
57
+ return { field: line, value: "" };
58
+ }
59
+
60
+ let value = line.slice(separator + 1);
61
+ if (value.startsWith(" ")) {
62
+ value = value.slice(1);
63
+ }
64
+
65
+ return {
66
+ field: line.slice(0, separator),
67
+ value,
68
+ };
69
+ }
70
+
71
+ export async function* readSSEStream<T = unknown>(
72
+ stream: ReadableStream<Uint8Array>,
73
+ ): AsyncGenerator<ParsedSSEEvent<T>> {
74
+ const reader = stream.getReader();
75
+ const decoder = new TextDecoder();
76
+
77
+ let buffer = "";
78
+ let eventName = "message";
79
+ let dataLines: string[] = [];
80
+ let lastEventId: string | undefined;
81
+ let pendingEventId: string | undefined;
82
+ let pendingRetry: number | undefined;
83
+
84
+ const processLine = (line: string): ParsedSSEEvent<T> | null => {
85
+ if (line === "") {
86
+ if (pendingEventId !== undefined) {
87
+ lastEventId = pendingEventId;
88
+ }
89
+
90
+ const event = finalizeEvent<T>(eventName, dataLines, lastEventId, pendingRetry);
91
+ eventName = "message";
92
+ dataLines = [];
93
+ pendingEventId = undefined;
94
+ pendingRetry = undefined;
95
+ return event;
96
+ }
97
+
98
+ if (line.startsWith(":")) {
99
+ return null;
100
+ }
101
+
102
+ const { field, value } = parseField(line);
103
+ switch (field) {
104
+ case "event":
105
+ eventName = value || "message";
106
+ break;
107
+ case "data":
108
+ dataLines.push(value);
109
+ break;
110
+ case "id":
111
+ if (!value.includes("\0")) {
112
+ pendingEventId = value;
113
+ }
114
+ break;
115
+ case "retry":
116
+ if (/^\d+$/.test(value)) {
117
+ pendingRetry = Number.parseInt(value, 10);
118
+ }
119
+ break;
120
+ default:
121
+ break;
122
+ }
123
+
124
+ return null;
125
+ };
126
+
127
+ try {
128
+ let isDone = false;
129
+
130
+ while (!isDone) {
131
+ const { done, value } = await reader.read();
132
+ isDone = done;
133
+ buffer += decoder.decode(value ?? new Uint8Array(), { stream: !done });
134
+
135
+ let nextLine = findNextLineBreak(buffer);
136
+ while (nextLine) {
137
+ const line = buffer.slice(0, nextLine.index);
138
+ buffer = buffer.slice(nextLine.index + nextLine.length);
139
+
140
+ const event = processLine(line);
141
+ if (event) {
142
+ yield event;
143
+ }
144
+
145
+ nextLine = findNextLineBreak(buffer);
146
+ }
147
+ }
148
+
149
+ buffer += decoder.decode();
150
+ if (buffer.length > 0) {
151
+ const event = processLine(buffer);
152
+ if (event) {
153
+ yield event;
154
+ }
155
+ }
156
+
157
+ if (pendingEventId !== undefined) {
158
+ lastEventId = pendingEventId;
159
+ }
160
+
161
+ const trailingEvent = finalizeEvent<T>(eventName, dataLines, lastEventId, pendingRetry);
162
+ if (trailingEvent) {
163
+ yield trailingEvent;
164
+ }
165
+ } finally {
166
+ try {
167
+ await reader.cancel();
168
+ } catch {
169
+ // Best effort cleanup for partially consumed streams.
170
+ }
171
+ reader.releaseLock();
172
+ }
173
+ }