@@ -4,21 +4,161 @@ import { isObject } from 'es-toolkit/compat'
44
55import type { RefDoc } from './types.js'
66
7+ export interface SSEEvent {
8+ data : string
9+ event ?: string
10+ }
11+
12+ export interface SSEConsumeResult {
13+ event ?: string
14+ events : SSEEvent [ ]
15+ remainder : string
16+ }
17+
18+ const IGNORED_SSE_EVENT_TYPES =
19+ / (?: ^ | [ . _ - ] ) (?: m e t a d a t a | t r a c e | u p d a t e s ? ) (?: $ | [ . _ - ] ) / i
20+
21+ const STREAM_TEXT_KEYS = [ 'delta' , 'text' , 'content' , 'answer' , 'message' ]
22+
723export const unicodeToString = ( unicodeStr : string ) =>
824 unicodeStr . replace ( / \\ u ( [ 0 - 9 a - f A - F ] { 4 } ) / g, ( _match , p1 : string ) =>
925 String . fromCharCode ( Number . parseInt ( p1 , 16 ) ) ,
1026 )
1127
28+ const isRecord = ( value : unknown ) : value is Record < string , unknown > =>
29+ isObject ( value ) && ! Array . isArray ( value )
30+
1231export const removePrefix = (
1332 prefix : string | null | undefined ,
1433 text : string ,
1534) => ( prefix && text . startsWith ( prefix ) ? text . slice ( prefix . length ) : text )
1635
36+ const parseSSEFieldValue = ( line : string , field : string ) => {
37+ const value = line . slice ( field . length + 1 )
38+ return value . startsWith ( ' ' ) ? value . slice ( 1 ) : value
39+ }
40+
41+ const getStringValue = ( value : unknown ) : string | undefined => {
42+ if ( typeof value === 'string' ) {
43+ return value
44+ }
45+
46+ if ( Array . isArray ( value ) ) {
47+ const text = value . map ( ( item ) => getStringValue ( item ) || '' ) . join ( '' )
48+ return text || undefined
49+ }
50+
51+ if ( ! isRecord ( value ) ) {
52+ return
53+ }
54+
55+ for ( const key of STREAM_TEXT_KEYS ) {
56+ const text = getStringValue ( value [ key ] )
57+ if ( text ) {
58+ return text
59+ }
60+ }
61+ }
62+
63+ const getChoicesDelta = ( choices : unknown ) : string | undefined => {
64+ if ( ! Array . isArray ( choices ) ) {
65+ return
66+ }
67+
68+ const text = choices . map ( ( choice ) => getStringValue ( choice ) || '' ) . join ( '' )
69+
70+ return text || undefined
71+ }
72+
73+ export const consumeSSEEvents = (
74+ buffer : string ,
75+ event ?: string ,
76+ ) : SSEConsumeResult => {
77+ const events : SSEEvent [ ] = [ ]
78+
79+ let remaining = buffer
80+ let currentEvent = event
81+
82+ let lineBoundary = remaining . match ( / \r ? \n / )
83+
84+ while ( lineBoundary ?. index != null ) {
85+ const line = remaining . slice ( 0 , lineBoundary . index )
86+ remaining = remaining . slice ( lineBoundary . index + lineBoundary [ 0 ] . length )
87+
88+ if ( ! line ) {
89+ currentEvent = undefined
90+ } else if ( line . startsWith ( ':' ) ) {
91+ //
92+ } else if ( line . startsWith ( 'event:' ) ) {
93+ currentEvent = parseSSEFieldValue ( line , 'event' )
94+ } else if ( line . startsWith ( 'data:' ) ) {
95+ events . push ( {
96+ event : currentEvent ,
97+ data : parseSSEFieldValue ( line , 'data' ) ,
98+ } )
99+ }
100+
101+ lineBoundary = remaining . match ( / \r ? \n / )
102+ }
103+
104+ return {
105+ event : currentEvent ,
106+ events,
107+ remainder : remaining ,
108+ }
109+ }
110+
111+ export const getAnswerDelta = ( { event, data } : SSEEvent ) => {
112+ if ( ! data || data === '[DONE]' ) {
113+ return ''
114+ }
115+
116+ if ( event && IGNORED_SSE_EVENT_TYPES . test ( event ) ) {
117+ return ''
118+ }
119+
120+ try {
121+ const payload = JSON . parse ( data )
122+
123+ if ( typeof payload === 'string' ) {
124+ return payload
125+ }
126+
127+ if ( ! isRecord ( payload ) ) {
128+ return ''
129+ }
130+
131+ const type = typeof payload . type === 'string' ? payload . type : undefined
132+
133+ if ( type && IGNORED_SSE_EVENT_TYPES . test ( type ) ) {
134+ return ''
135+ }
136+
137+ const choiceDelta = getChoicesDelta ( payload . choices )
138+
139+ if ( choiceDelta ) {
140+ return choiceDelta
141+ }
142+
143+ for ( const key of STREAM_TEXT_KEYS ) {
144+ const text = getStringValue ( payload [ key ] )
145+ if ( text ) {
146+ return text
147+ }
148+ }
149+
150+ return ''
151+ } catch {
152+ return data
153+ }
154+ }
155+
17156export function parseStreamContent ( text : string ) {
18- const matchDocs = text . match ( / < d o c s > ( [ \s \S ] * ?) < \/ d o c s > / )
19- const docsReferences = matchDocs ? .length
20- ? unicodeToString ( matchDocs [ 1 ] )
157+ const docsMatches = [ ... text . matchAll ( / < d o c s > ( [ \s \S ] * ?) < \/ d o c s > / g ) ]
158+ const docsReferences = docsMatches . length
159+ ? unicodeToString ( docsMatches . at ( - 1 ) ! [ 1 ] )
21160 : '[]'
161+
22162 let refDocs : RefDoc [ ] = [ ]
23163 try {
24164 refDocs = ( JSON . parse ( docsReferences ) as RefDoc [ ] ) . filter ( ( r ) =>
@@ -28,19 +168,23 @@ export function parseStreamContent(text: string) {
28168 //
29169 }
30170
31- const matchThinks = text . match ( / < t h i n k > [ \s \S ] * ?< \/ t h i n k > | ^ [ \s \S ] * ?< \/ t h i n k > / g)
32- const firstMatched = matchThinks ?. [ 0 ]
33- let matched = firstMatched || matchThinks ?. [ 1 ]
34- if ( matched ) {
35- matched = removePrefix ( firstMatched || '' , matched )
171+ const thinkMatches = [ ...text . matchAll ( / < t h i n k > ( [ \s \S ] * ?) < \/ t h i n k > / g) ]
172+ let thinkingProcess = thinkMatches . at ( - 1 ) ?. [ 1 ]
173+
174+ if ( ! thinkingProcess ) {
175+ const openThinkMatch = text . match ( / < t h i n k > ( [ \s \S ] * ) $ / )
176+ thinkingProcess = openThinkMatch ?. [ 1 ]
36177 }
37- const thinkingProcess = matchThinks && matched
178+
179+ const content = text
180+ . replace ( / < d o c s > [ \s \S ] * ?< \/ d o c s > / g, '' )
181+ . replace ( / < t h i n k > [ \s \S ] * ?< \/ t h i n k > / g, '' )
182+ . replace ( / < d o c s > [ \s \S ] * $ / g, '' )
183+ . replace ( / < t h i n k > [ \s \S ] * $ / g, '' )
38184
39185 return {
40186 refDocs,
41- thinkingProcess,
42- content : unicodeToString (
43- removePrefix ( thinkingProcess , removePrefix ( matchDocs ?. [ 0 ] , text ) ) ,
44- ) ,
187+ thinkingProcess : thinkingProcess && unicodeToString ( thinkingProcess ) ,
188+ content : unicodeToString ( content ) ,
45189 }
46190}
0 commit comments