@celljs/ai-core 3.2.0 → 3.3.0
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/README.md +107 -0
- package/lib/common/chat/message/assistant-message.d.ts +3 -2
- package/lib/common/chat/message/assistant-message.d.ts.map +1 -1
- package/lib/common/chat/message/assistant-message.js +2 -1
- package/lib/common/chat/message/assistant-message.js.map +1 -1
- package/lib/common/chat/message/index.d.ts +1 -0
- package/lib/common/chat/message/index.d.ts.map +1 -1
- package/lib/common/chat/message/index.js +1 -0
- package/lib/common/chat/message/index.js.map +1 -1
- package/lib/common/chat/message/message-protocol.d.ts +23 -1
- package/lib/common/chat/message/message-protocol.d.ts.map +1 -1
- package/lib/common/chat/message/message-protocol.js +1 -0
- package/lib/common/chat/message/message-protocol.js.map +1 -1
- package/lib/common/chat/message/tool-response-message.d.ts +32 -0
- package/lib/common/chat/message/tool-response-message.d.ts.map +1 -0
- package/lib/common/chat/message/tool-response-message.js +23 -0
- package/lib/common/chat/message/tool-response-message.js.map +1 -0
- package/lib/common/chat/metadata/chat-generation-metadata.d.ts +28 -0
- package/lib/common/chat/metadata/chat-generation-metadata.d.ts.map +1 -0
- package/lib/common/chat/metadata/chat-generation-metadata.js +21 -0
- package/lib/common/chat/metadata/chat-generation-metadata.js.map +1 -0
- package/lib/common/chat/metadata/index.d.ts +1 -0
- package/lib/common/chat/metadata/index.d.ts.map +1 -1
- package/lib/common/chat/metadata/index.js +1 -0
- package/lib/common/chat/metadata/index.js.map +1 -1
- package/lib/common/chat/metadata/metadata-protocol.d.ts +2 -0
- package/lib/common/chat/metadata/metadata-protocol.d.ts.map +1 -1
- package/lib/common/chat/metadata/metadata-protocol.js.map +1 -1
- package/lib/common/chat/prompt/index.d.ts +1 -0
- package/lib/common/chat/prompt/index.d.ts.map +1 -1
- package/lib/common/chat/prompt/index.js +1 -0
- package/lib/common/chat/prompt/index.js.map +1 -1
- package/lib/common/chat/prompt/prompt-protocol.d.ts +17 -16
- package/lib/common/chat/prompt/prompt-protocol.d.ts.map +1 -1
- package/lib/common/chat/prompt/prompt-protocol.js +2 -0
- package/lib/common/chat/prompt/prompt-protocol.js.map +1 -1
- package/lib/common/chat/prompt/prompt-template.d.ts +8 -0
- package/lib/common/chat/prompt/prompt-template.d.ts.map +1 -0
- package/lib/common/chat/prompt/prompt-template.js +34 -0
- package/lib/common/chat/prompt/prompt-template.js.map +1 -0
- package/lib/common/embedding/embedding-protocol.d.ts +11 -8
- package/lib/common/embedding/embedding-protocol.d.ts.map +1 -1
- package/lib/common/embedding/embedding-protocol.js +10 -1
- package/lib/common/embedding/embedding-protocol.js.map +1 -1
- package/lib/common/error/error.d.ts +5 -0
- package/lib/common/error/error.d.ts.map +1 -0
- package/lib/common/error/error.js +11 -0
- package/lib/common/error/error.js.map +1 -0
- package/lib/common/error/index.d.ts +2 -0
- package/lib/common/error/index.d.ts.map +1 -0
- package/lib/common/error/index.js +5 -0
- package/lib/common/error/index.js.map +1 -0
- package/lib/common/index.d.ts +2 -0
- package/lib/common/index.d.ts.map +1 -1
- package/lib/common/index.js +2 -0
- package/lib/common/index.js.map +1 -1
- package/lib/common/model/function/function-protocol.d.ts +2 -2
- package/lib/common/model/function/function-protocol.d.ts.map +1 -1
- package/lib/common/model/model-protocol.d.ts +2 -1
- package/lib/common/model/model-protocol.d.ts.map +1 -1
- package/lib/common/sse/index.d.ts +4 -0
- package/lib/common/sse/index.d.ts.map +1 -0
- package/lib/common/sse/index.js +7 -0
- package/lib/common/sse/index.js.map +1 -0
- package/lib/common/sse/line-decoder.d.ts +15 -0
- package/lib/common/sse/line-decoder.d.ts.map +1 -0
- package/lib/common/sse/line-decoder.js +54 -0
- package/lib/common/sse/line-decoder.js.map +1 -0
- package/lib/common/sse/sse-decoder.d.ts +27 -0
- package/lib/common/sse/sse-decoder.d.ts.map +1 -0
- package/lib/common/sse/sse-decoder.js +74 -0
- package/lib/common/sse/sse-decoder.js.map +1 -0
- package/lib/common/sse/sse-protocol.d.ts +23 -0
- package/lib/common/sse/sse-protocol.d.ts.map +1 -0
- package/lib/common/sse/sse-protocol.js +4 -0
- package/lib/common/sse/sse-protocol.js.map +1 -0
- package/lib/common/utils/index.d.ts +1 -0
- package/lib/common/utils/index.d.ts.map +1 -1
- package/lib/common/utils/index.js +1 -0
- package/lib/common/utils/index.js.map +1 -1
- package/lib/common/utils/sse-util.d.ts +10 -0
- package/lib/common/utils/sse-util.d.ts.map +1 -0
- package/lib/common/utils/sse-util.js +109 -0
- package/lib/common/utils/sse-util.js.map +1 -0
- package/package.json +5 -5
- package/src/common/chat/message/assistant-message.ts +5 -3
- package/src/common/chat/message/index.ts +1 -0
- package/src/common/chat/message/message-protocol.ts +27 -1
- package/src/common/chat/message/tool-response-message.ts +40 -0
- package/src/common/chat/metadata/chat-generation-metadata.ts +38 -0
- package/src/common/chat/metadata/index.ts +1 -0
- package/src/common/chat/metadata/metadata-protocol.ts +2 -0
- package/src/common/chat/prompt/index.ts +1 -0
- package/src/common/chat/prompt/prompt-protocol.ts +18 -28
- package/src/common/chat/prompt/prompt-template.ts +26 -0
- package/src/common/embedding/embedding-protocol.ts +17 -8
- package/src/common/error/error.ts +7 -0
- package/src/common/error/index.ts +1 -0
- package/src/common/index.ts +2 -0
- package/src/common/model/function/function-protocol.ts +2 -2
- package/src/common/model/model-protocol.ts +4 -2
- package/src/common/sse/index.ts +3 -0
- package/src/common/sse/line-decoder.ts +65 -0
- package/src/common/sse/sse-decoder.ts +88 -0
- package/src/common/sse/sse-protocol.ts +25 -0
- package/src/common/utils/index.ts +1 -0
- package/src/common/utils/sse-util.ts +111 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { AbstractMessage } from './abstract-message';
|
|
2
|
+
import { MessageType } from './message-protocol';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Represents a tool response.
|
|
6
|
+
*/
|
|
7
|
+
export interface ToolResponse {
|
|
8
|
+
/**
|
|
9
|
+
* The unique identifier for the tool.
|
|
10
|
+
*/
|
|
11
|
+
id: string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The name of the tool.
|
|
15
|
+
*/
|
|
16
|
+
name: string;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The response data from the tool.
|
|
20
|
+
*/
|
|
21
|
+
responseData: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The ToolResponseMessage class represents a message with a function content in a chat
|
|
26
|
+
* application.
|
|
27
|
+
*/
|
|
28
|
+
export class ToolResponseMessage extends AbstractMessage {
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Creates an instance of ToolResponseMessage.
|
|
32
|
+
* @param responses The list of tool responses.
|
|
33
|
+
* @param metadata Additional metadata.
|
|
34
|
+
*/
|
|
35
|
+
constructor(
|
|
36
|
+
readonly responses: ToolResponse[], metadata: Record<string, any> = {}) {
|
|
37
|
+
super(MessageType.TOOL, '', [], metadata);
|
|
38
|
+
this.responses = responses;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ResultMetadata } from '../../model/model-protocol';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Abstract Data Type (ADT) encapsulating information on the completion choices in the AI
|
|
5
|
+
* response.
|
|
6
|
+
*/
|
|
7
|
+
export interface ChatGenerationMetadata extends ResultMetadata {
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The reason this choice completed for the generation.
|
|
11
|
+
*/
|
|
12
|
+
readonly contentFilterMetadata?: any;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The reason this choice completed for the generation.
|
|
16
|
+
*/
|
|
17
|
+
readonly finishReason?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class ChatGenerationMetadata {
|
|
21
|
+
|
|
22
|
+
static EMPTY: ChatGenerationMetadata = ChatGenerationMetadata.from();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Factory method used to construct a new {@link ChatGenerationMetadata} from the
|
|
26
|
+
* given finish reason and content filter metadata.
|
|
27
|
+
*
|
|
28
|
+
* @param finishReason The reason for completion.
|
|
29
|
+
* @param contentFilterMetadata The content filter metadata.
|
|
30
|
+
* @returns A new instance of {@link ChatGenerationMetadata}.
|
|
31
|
+
*/
|
|
32
|
+
static from(finishReason?: string, contentFilterMetadata?: any): ChatGenerationMetadata {
|
|
33
|
+
return {
|
|
34
|
+
finishReason, contentFilterMetadata
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { ModelOptions, ModelRequest } from '../../model/model-protocol';
|
|
2
|
-
import {
|
|
2
|
+
import { Message, MessageType } from '../message/message-protocol';
|
|
3
|
+
|
|
4
|
+
export const PromptTemplate = Symbol('PromptTemplate');
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* The ChatOptions represent the common options, portable across different chat models.
|
|
6
8
|
*/
|
|
7
9
|
export interface ChatOptions extends ModelOptions {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
model?: string;
|
|
11
|
+
frequencyPenalty?: number;
|
|
12
|
+
maxTokens?: number;
|
|
13
|
+
presencePenalty?: number;
|
|
14
|
+
stopSequences?: string[];
|
|
15
|
+
temperature?: number;
|
|
16
|
+
topK?: number;
|
|
17
|
+
topP?: number;
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
export interface Prompt extends ModelRequest<Message[]> {
|
|
@@ -17,28 +22,13 @@ export interface Prompt extends ModelRequest<Message[]> {
|
|
|
17
22
|
copy(): Prompt;
|
|
18
23
|
}
|
|
19
24
|
|
|
20
|
-
export interface
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
export interface PromptTemplateContext {
|
|
26
|
+
variables?: Record<string, any>;
|
|
27
|
+
chatOptions?: ChatOptions;
|
|
28
|
+
messageType?: MessageType;
|
|
24
29
|
}
|
|
25
30
|
|
|
26
|
-
export interface
|
|
27
|
-
|
|
28
|
-
create(
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface PromptTemplateChatActions {
|
|
33
|
-
|
|
34
|
-
createMessages(): Message[];
|
|
35
|
-
createMessages(model: Map<String, Object>): Message[];
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface PromptTemplateMessageActions {
|
|
40
|
-
|
|
41
|
-
createMessage(mediaList: Media[]): Message;
|
|
42
|
-
createMessage(model: Map<String, Object>): Message;
|
|
43
|
-
|
|
31
|
+
export interface PromptTemplate {
|
|
32
|
+
render(template: string, ctx?: PromptTemplateContext): Promise<string>;
|
|
33
|
+
create(template: string, ctx?: PromptTemplateContext): Promise<Prompt>;
|
|
44
34
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Autowired, Component, ExpressionHandler, IllegalArgumentError } from '@celljs/core';
|
|
2
|
+
import { Prompt, PromptTemplate, PromptTemplateContext } from './prompt-protocol';
|
|
3
|
+
import { PromptImpl } from './prompt';
|
|
4
|
+
import { MessageType, SystemMessage } from '../message';
|
|
5
|
+
|
|
6
|
+
@Component(PromptTemplate)
|
|
7
|
+
export class PromptTemplateImpl implements PromptTemplate {
|
|
8
|
+
|
|
9
|
+
@Autowired(ExpressionHandler)
|
|
10
|
+
protected readonly expressionHandler: ExpressionHandler;
|
|
11
|
+
|
|
12
|
+
async render(template: string, ctx?: PromptTemplateContext): Promise<string> {
|
|
13
|
+
if (ctx?.variables) {
|
|
14
|
+
return this.expressionHandler.handle(template, ctx, { ignoreSpecialChar: true, bracketBegin: '{', bracketEnd: '}' });
|
|
15
|
+
}
|
|
16
|
+
return template;
|
|
17
|
+
}
|
|
18
|
+
async create(template: string, ctx?: PromptTemplateContext): Promise<Prompt> {
|
|
19
|
+
if (!ctx?.messageType || ctx?.messageType === MessageType.USER) {
|
|
20
|
+
return new PromptImpl(await this.render(template, ctx), ctx?.chatOptions);
|
|
21
|
+
} else if (ctx?.messageType === MessageType.SYSTEM) {
|
|
22
|
+
return new PromptImpl(new SystemMessage(await this.render(template, ctx)), ctx?.chatOptions);
|
|
23
|
+
}
|
|
24
|
+
throw new IllegalArgumentError(`Invalid message type: ${ctx?.messageType}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -4,6 +4,16 @@ import { Model, ModelOptions, ModelRequest, ModelResponse, ModelResult, Response
|
|
|
4
4
|
|
|
5
5
|
export const EmbeddingModel = Symbol('EmbeddingModel');
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Enum representing the modality type of the source data.
|
|
9
|
+
*/
|
|
10
|
+
export enum ModalityType {
|
|
11
|
+
TEXT = 'TEXT',
|
|
12
|
+
IMAGE = 'IMAGE',
|
|
13
|
+
AUDIO = 'AUDIO',
|
|
14
|
+
VIDEO = 'VIDEO'
|
|
15
|
+
}
|
|
16
|
+
|
|
7
17
|
/**
|
|
8
18
|
* Represents the metadata for an embedding result.
|
|
9
19
|
*/
|
|
@@ -29,14 +39,13 @@ export interface EmbeddingResultMetadata extends ResultMetadata {
|
|
|
29
39
|
documentData: any;
|
|
30
40
|
}
|
|
31
41
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
VIDEO = 'VIDEO'
|
|
42
|
+
export namespace EmbeddingResultMetadata {
|
|
43
|
+
export const EMPTY: EmbeddingResultMetadata = {
|
|
44
|
+
modalityType: ModalityType.TEXT,
|
|
45
|
+
documentId: '',
|
|
46
|
+
mimeType: MimeTypeUtils.TEXT_PLAIN,
|
|
47
|
+
documentData: undefined
|
|
48
|
+
};
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
/**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './error';
|
package/src/common/index.ts
CHANGED
|
@@ -25,12 +25,12 @@ export interface FunctionCallback {
|
|
|
25
25
|
/**
|
|
26
26
|
* Called when a model detects and triggers a function call. The model is responsible
|
|
27
27
|
* to pass the function arguments in the pre-configured JSON schema format.
|
|
28
|
-
* @param
|
|
28
|
+
* @param functionArguments JSON string with the function arguments to be passed to the
|
|
29
29
|
* function. The arguments are defined as JSON schema usually registered with the the
|
|
30
30
|
* model.
|
|
31
31
|
* @return String containing the function call response.
|
|
32
32
|
*/
|
|
33
|
-
call(
|
|
33
|
+
call(functionArguments: string): Promise<string>;
|
|
34
34
|
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -39,7 +39,9 @@ export interface ResultMetadata {}
|
|
|
39
39
|
* object within the core domain, enhancing the understanding and management of AI model
|
|
40
40
|
* responses in various applications.
|
|
41
41
|
*/
|
|
42
|
-
export interface ResponseMetadata {
|
|
42
|
+
export interface ResponseMetadata {
|
|
43
|
+
readonly extra: Record<string, any>;
|
|
44
|
+
}
|
|
43
45
|
|
|
44
46
|
/**
|
|
45
47
|
* This interface provides methods to access the main output of the AI model and the
|
|
@@ -182,6 +184,6 @@ export interface StreamingModel<TReq extends ModelRequest<any>, TResChunk extend
|
|
|
182
184
|
* @param request the request object to be sent to the AI model
|
|
183
185
|
* @return the streaming response from the AI model
|
|
184
186
|
*/
|
|
185
|
-
stream(request: TReq): Observable<TResChunk
|
|
187
|
+
stream(request: TReq): Promise<Observable<TResChunk>>;
|
|
186
188
|
|
|
187
189
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Bytes, ByteUtil } from '@celljs/core';
|
|
2
|
+
import { LineDecoder } from './sse-protocol';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Line decoder.
|
|
6
|
+
*/
|
|
7
|
+
export class LineDecoderImpl implements LineDecoder {
|
|
8
|
+
private buffer: string[];
|
|
9
|
+
private trailingCR: boolean;
|
|
10
|
+
|
|
11
|
+
static readonly NEWLINE_CHARS = new Set(['\n', '\r', '\x0b', '\x0c', '\x1c', '\x1d', '\x1e', '\x85', '\u2028', '\u2029']);
|
|
12
|
+
static readonly NEWLINE_REGEXP = /\r\n|[\n\r\x0b\x0c\x1c\x1d\x1e\x85\u2028\u2029]/g;
|
|
13
|
+
|
|
14
|
+
constructor() {
|
|
15
|
+
this.buffer = [];
|
|
16
|
+
this.trailingCR = false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
decode(chunk: Bytes): string[] {
|
|
20
|
+
let text = ByteUtil.decode(chunk);
|
|
21
|
+
|
|
22
|
+
if (this.trailingCR) {
|
|
23
|
+
text = '\r' + text;
|
|
24
|
+
this.trailingCR = false;
|
|
25
|
+
}
|
|
26
|
+
if (text.endsWith('\r')) {
|
|
27
|
+
this.trailingCR = true;
|
|
28
|
+
text = text.slice(0, -1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!text) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const trailingNewline = LineDecoderImpl.NEWLINE_CHARS.has(text[text.length - 1] || '');
|
|
36
|
+
let lines = text.split(LineDecoderImpl.NEWLINE_REGEXP);
|
|
37
|
+
|
|
38
|
+
if (lines.length === 1 && !trailingNewline) {
|
|
39
|
+
this.buffer.push(lines[0]!);
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (this.buffer.length > 0) {
|
|
44
|
+
lines = [this.buffer.join('') + lines[0], ...lines.slice(1)];
|
|
45
|
+
this.buffer = [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!trailingNewline) {
|
|
49
|
+
this.buffer = [lines.pop() || ''];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return lines;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
flush(): string[] {
|
|
56
|
+
if (this.buffer.length === 0 && !this.trailingCR) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const lines = [this.buffer.join('')];
|
|
61
|
+
this.buffer = [];
|
|
62
|
+
this.trailingCR = false;
|
|
63
|
+
return lines;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { EventDecoder, ServerSentEvent } from './sse-protocol';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Server-sent event.
|
|
5
|
+
*/
|
|
6
|
+
export class SSEDecoder implements EventDecoder {
|
|
7
|
+
private event?: string;
|
|
8
|
+
private data: string[];
|
|
9
|
+
private chunks: string[];
|
|
10
|
+
|
|
11
|
+
constructor() {
|
|
12
|
+
this.event = undefined;
|
|
13
|
+
this.data = [];
|
|
14
|
+
this.chunks = [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
protected buildServerSentEvent(): ServerSentEvent {
|
|
18
|
+
const sse: ServerSentEvent = {
|
|
19
|
+
event: this.event,
|
|
20
|
+
data: this.data.join('\n'),
|
|
21
|
+
raw: this.chunks,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
this.event = undefined;
|
|
25
|
+
this.data = [];
|
|
26
|
+
this.chunks = [];
|
|
27
|
+
|
|
28
|
+
return sse;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
decode(line: string): ServerSentEvent | undefined {
|
|
32
|
+
if (line.endsWith('\r')) {
|
|
33
|
+
line = line.substring(0, line.length - 1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!line) {
|
|
37
|
+
if (!this.event && this.data.length === 0) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return this.buildServerSentEvent();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.chunks.push(line);
|
|
45
|
+
|
|
46
|
+
if (line.startsWith(':')) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (line.startsWith('{') && line.endsWith('}')) {
|
|
51
|
+
this.data.push(line);
|
|
52
|
+
return this.buildServerSentEvent();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const [fieldName, , value] = this.partition(line, ':');
|
|
56
|
+
|
|
57
|
+
const trimmedValue = value.startsWith(' ') ? value.substring(1) : value;
|
|
58
|
+
|
|
59
|
+
if (fieldName === 'event') {
|
|
60
|
+
this.event = trimmedValue;
|
|
61
|
+
} else if (fieldName === 'data') {
|
|
62
|
+
this.data.push(trimmedValue);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Partitions a string into three parts based on a delimiter.
|
|
70
|
+
* @param str The string to partition.
|
|
71
|
+
* @param delimiter The delimiter to partition the string by.
|
|
72
|
+
* @returns A tuple containing the three parts of the partitioned string.
|
|
73
|
+
* The first part is the string before the delimiter, the second part is the delimiter itself,
|
|
74
|
+
* and the third part is the string after the delimiter.
|
|
75
|
+
* If the delimiter is not found in the string, the first part is the entire string, the second part is an empty string, and the third part is an empty string.
|
|
76
|
+
* @example
|
|
77
|
+
* partition('hello,world', ',') // ['hello', ',', 'world']
|
|
78
|
+
* partition('hello,world', '.') // ['hello,world', '', '']
|
|
79
|
+
* partition('hello,world', 'l') // ['he', 'l', 'lo,world']
|
|
80
|
+
*/
|
|
81
|
+
protected partition(str: string, delimiter: string): [string, string, string] {
|
|
82
|
+
const index = str.indexOf(delimiter);
|
|
83
|
+
if (index !== -1) {
|
|
84
|
+
return [str.substring(0, index), delimiter, str.substring(index + delimiter.length)];
|
|
85
|
+
}
|
|
86
|
+
return [str, '', ''];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Bytes } from '@celljs/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Server-sent event.
|
|
5
|
+
*/
|
|
6
|
+
export interface ServerSentEvent {
|
|
7
|
+
event?: string;
|
|
8
|
+
data: string;
|
|
9
|
+
raw: string[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Decoder for server-sent events.
|
|
14
|
+
*/
|
|
15
|
+
export interface EventDecoder {
|
|
16
|
+
decode(line: string): ServerSentEvent | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Decoder for line-based protocols.
|
|
21
|
+
*/
|
|
22
|
+
export interface LineDecoder {
|
|
23
|
+
decode(chunk: Bytes): string[];
|
|
24
|
+
flush(): string[];
|
|
25
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Observable, Subscriber } from 'rxjs';
|
|
2
|
+
import { ServerSentEvent, SSEDecoder, LineDecoderImpl } from '../sse';
|
|
3
|
+
import { Bytes } from '@celljs/core';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SSE utility class.
|
|
7
|
+
*/
|
|
8
|
+
export class SSEUtil {
|
|
9
|
+
|
|
10
|
+
static toObservable<Item>(readableStream: ReadableStream, controller?: AbortController): Observable<Item> {
|
|
11
|
+
return new Observable<Item>(subscriber => {
|
|
12
|
+
(async () => {
|
|
13
|
+
const decoder = new SSEDecoder();
|
|
14
|
+
const lineDecoder = new LineDecoderImpl();
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const iterable = this.readableStreamAsyncIterable<Bytes>(readableStream);
|
|
18
|
+
for await (const chunk of iterable) {
|
|
19
|
+
const lines = lineDecoder.decode(chunk);
|
|
20
|
+
for (const line of lines) {
|
|
21
|
+
const sse = decoder.decode(line);
|
|
22
|
+
this.handleSSE(sse, subscriber, controller);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const remainingLines = lineDecoder.flush();
|
|
27
|
+
for (const line of remainingLines) {
|
|
28
|
+
const sse = decoder.decode(line);
|
|
29
|
+
if (sse) {
|
|
30
|
+
this.handleSSE(sse, subscriber, controller);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
subscriber.complete();
|
|
35
|
+
} catch (e) {
|
|
36
|
+
if (e instanceof Error && e.name === 'AbortError') {
|
|
37
|
+
subscriber.complete();
|
|
38
|
+
} else {
|
|
39
|
+
subscriber.error(e);
|
|
40
|
+
}
|
|
41
|
+
} finally {
|
|
42
|
+
controller?.abort();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
})();
|
|
46
|
+
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private static handleSSE<Item>(sse: ServerSentEvent | undefined, subscriber: Subscriber<Item>, controller?: AbortController): void {
|
|
52
|
+
if (sse) {
|
|
53
|
+
if (sse.data.startsWith('[DONE]')) {
|
|
54
|
+
subscriber.complete();
|
|
55
|
+
controller?.abort();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!sse.event || sse.event === 'error') {
|
|
60
|
+
let data;
|
|
61
|
+
try {
|
|
62
|
+
data = JSON.parse(sse.data);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.error('Could not parse message into JSON:', sse.data);
|
|
65
|
+
console.error('From chunk:', sse.raw);
|
|
66
|
+
subscriber.error(e);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (data && data.error) {
|
|
71
|
+
subscriber.error(data.error);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
subscriber.next(data);
|
|
76
|
+
|
|
77
|
+
if (data.done === true) {
|
|
78
|
+
subscriber.complete();
|
|
79
|
+
controller?.abort();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private static readableStreamAsyncIterable<T>(stream: any): AsyncIterableIterator<T> {
|
|
86
|
+
if (stream[Symbol.asyncIterator]) {return stream; }
|
|
87
|
+
|
|
88
|
+
const reader = stream.getReader();
|
|
89
|
+
return {
|
|
90
|
+
async next() {
|
|
91
|
+
try {
|
|
92
|
+
const result = await reader.read();
|
|
93
|
+
if (result?.done) {reader.releaseLock(); }
|
|
94
|
+
return result;
|
|
95
|
+
} catch (e) {
|
|
96
|
+
reader.releaseLock();
|
|
97
|
+
throw e;
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
async return() {
|
|
101
|
+
const cancelPromise = reader.cancel();
|
|
102
|
+
reader.releaseLock();
|
|
103
|
+
await cancelPromise;
|
|
104
|
+
return { done: true, value: undefined };
|
|
105
|
+
},
|
|
106
|
+
[Symbol.asyncIterator]() {
|
|
107
|
+
return this;
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|