11import axios , { AxiosInstance } from 'axios'
22import { backOff } from 'exponential-backoff'
3+ import { createNanoEvents , Unsubscribe } from 'nanoevents'
34import { defaultModel , models } from './models'
45import { CognitiveRequest , CognitiveResponse , CognitiveStreamChunk , Model } from './types'
56
67export { CognitiveRequest , CognitiveResponse , CognitiveStreamChunk }
78
9+ export type BetaEvents = {
10+ request : ( req : { input : CognitiveRequest } ) => void
11+ response : ( req : { input : CognitiveRequest } , res : CognitiveResponse ) => void
12+ error : ( req : { input : CognitiveRequest } , error : any ) => void
13+ retry : ( req : { input : CognitiveRequest } , error : any ) => void
14+ }
15+
816type ClientProps = {
917 apiUrl ?: string
1018 timeout ?: number
@@ -31,6 +39,7 @@ export class CognitiveBeta {
3139 private readonly _withCredentials : boolean
3240 private readonly _headers : Record < string , string | string [ ] >
3341 private readonly _debug : boolean = false
42+ private _events = createNanoEvents < BetaEvents > ( )
3443
3544 public constructor ( props : ClientProps ) {
3645 this . _apiUrl = props . apiUrl || 'https://api.botpress.cloud'
@@ -68,17 +77,33 @@ export class CognitiveBeta {
6877 } )
6978 }
7079
80+ public on < K extends keyof BetaEvents > ( event : K , cb : BetaEvents [ K ] ) : Unsubscribe {
81+ return this . _events . on ( event , cb )
82+ }
83+
7184 public async generateText ( input : CognitiveRequest , options : RequestOptions = { } ) {
7285 const signal = options . signal ?? AbortSignal . timeout ( this . _timeout )
86+ const req = { input }
87+
88+ this . _events . emit ( 'request' , req )
89+
90+ try {
91+ const { data } = await this . _withServerRetry (
92+ ( ) =>
93+ this . _axiosClient . post < CognitiveResponse > ( '/v2/cognitive/generate-text' , input , {
94+ signal,
95+ timeout : options . timeout ?? this . _timeout ,
96+ } ) ,
97+ options ,
98+ req
99+ )
73100
74- const { data } = await this . _withServerRetry ( ( ) =>
75- this . _axiosClient . post < CognitiveResponse > ( '/v2/cognitive/generate-text' , input , {
76- signal,
77- timeout : options . timeout ?? this . _timeout ,
78- } )
79- )
80-
81- return data
101+ this . _events . emit ( 'response' , req , data )
102+ return data
103+ } catch ( error ) {
104+ this . _events . emit ( 'error' , req , error )
105+ throw error
106+ }
82107 }
83108
84109 public async listModels ( ) {
@@ -94,69 +119,102 @@ export class CognitiveBeta {
94119 options : RequestOptions = { }
95120 ) : AsyncGenerator < CognitiveStreamChunk , void , unknown > {
96121 const signal = options . signal ?? AbortSignal . timeout ( this . _timeout )
122+ const req = { input : request }
123+ const chunks : CognitiveStreamChunk [ ] = [ ]
124+ let lastChunk : CognitiveStreamChunk | undefined
125+
126+ this . _events . emit ( 'request' , req )
127+
128+ try {
129+ if ( isBrowser ( ) ) {
130+ const res = await fetch ( `${ this . _apiUrl } /v2/cognitive/generate-text-stream` , {
131+ method : 'POST' ,
132+ headers : {
133+ ...this . _headers ,
134+ 'Content-Type' : 'application/json' ,
135+ } ,
136+ credentials : this . _withCredentials ? 'include' : 'omit' ,
137+ body : JSON . stringify ( { ...request , stream : true } ) ,
138+ signal,
139+ } )
97140
98- if ( isBrowser ( ) ) {
99- const res = await fetch ( `${ this . _apiUrl } /v2/cognitive/generate-text-stream` , {
100- method : 'POST' ,
101- headers : {
102- ...this . _headers ,
103- 'Content-Type' : 'application/json' ,
104- } ,
105- credentials : this . _withCredentials ? 'include' : 'omit' ,
106- body : JSON . stringify ( { ...request , stream : true } ) ,
107- signal,
108- } )
109-
110- if ( ! res . ok ) {
111- const text = await res . text ( ) . catch ( ( ) => '' )
112- const err = new Error ( `HTTP ${ res . status } : ${ text || res . statusText } ` )
113- ; ( err as any ) . response = { status : res . status , data : text }
114- throw err
115- }
141+ if ( ! res . ok ) {
142+ const text = await res . text ( ) . catch ( ( ) => '' )
143+ const err = new Error ( `HTTP ${ res . status } : ${ text || res . statusText } ` )
144+ ; ( err as any ) . response = { status : res . status , data : text }
145+ throw err
146+ }
116147
117- const body = res . body
118- if ( ! body ) {
119- throw new Error ( 'No response body received for streaming request' )
120- }
148+ const body = res . body
149+ if ( ! body ) {
150+ throw new Error ( 'No response body received for streaming request' )
151+ }
121152
122- const reader = body . getReader ( )
123- const iterable = ( async function * ( ) {
124- for ( ; ; ) {
125- const { value, done } = await reader . read ( )
126- if ( done ) {
127- break
128- }
129- if ( value ) {
130- yield value
153+ const reader = body . getReader ( )
154+ const iterable = ( async function * ( ) {
155+ for ( ; ; ) {
156+ const { value, done } = await reader . read ( )
157+ if ( done ) {
158+ break
159+ }
160+ if ( value ) {
161+ yield value
162+ }
131163 }
164+ } ) ( )
165+
166+ for await ( const obj of this . _ndjson < CognitiveStreamChunk > ( iterable ) ) {
167+ chunks . push ( obj )
168+ lastChunk = obj
169+ yield obj
132170 }
133- } ) ( )
134171
135- for await ( const obj of this . _ndjson < CognitiveStreamChunk > ( iterable ) ) {
136- yield obj
172+ // Emit response event with the final chunk metadata
173+ if ( lastChunk ?. metadata ) {
174+ this . _events . emit ( 'response' , req , {
175+ output : chunks . map ( ( c ) => c . output || '' ) . join ( '' ) ,
176+ metadata : lastChunk . metadata ,
177+ } )
178+ }
179+ return
137180 }
138- return
139- }
140181
141- const res = await this . _withServerRetry ( ( ) =>
142- this . _axiosClient . post (
143- '/v2/cognitive/generate-text-stream' ,
144- { ...request , stream : true } ,
145- {
146- responseType : 'stream' ,
147- signal,
148- timeout : options . timeout ?? this . _timeout ,
149- }
182+ const res = await this . _withServerRetry (
183+ ( ) =>
184+ this . _axiosClient . post (
185+ '/v2/cognitive/generate-text-stream' ,
186+ { ...request , stream : true } ,
187+ {
188+ responseType : 'stream' ,
189+ signal,
190+ timeout : options . timeout ?? this . _timeout ,
191+ }
192+ ) ,
193+ options ,
194+ req
150195 )
151- )
152196
153- const nodeStream : AsyncIterable < Uint8Array > = res . data as any
154- if ( ! nodeStream ) {
155- throw new Error ( 'No response body received for streaming request' )
156- }
197+ const nodeStream : AsyncIterable < Uint8Array > = res . data as any
198+ if ( ! nodeStream ) {
199+ throw new Error ( 'No response body received for streaming request' )
200+ }
157201
158- for await ( const obj of this . _ndjson < CognitiveStreamChunk > ( nodeStream ) ) {
159- yield obj
202+ for await ( const obj of this . _ndjson < CognitiveStreamChunk > ( nodeStream ) ) {
203+ chunks . push ( obj )
204+ lastChunk = obj
205+ yield obj
206+ }
207+
208+ // Emit response event with the final chunk metadata
209+ if ( lastChunk ?. metadata ) {
210+ this . _events . emit ( 'response' , req , {
211+ output : chunks . map ( ( c ) => c . output || '' ) . join ( '' ) ,
212+ metadata : lastChunk . metadata ,
213+ } )
214+ }
215+ } catch ( error ) {
216+ this . _events . emit ( 'error' , req , error )
217+ throw error
160218 }
161219 }
162220
@@ -214,14 +272,34 @@ export class CognitiveBeta {
214272 return false
215273 }
216274
217- private async _withServerRetry < T > ( fn : ( ) => Promise < T > ) : Promise < T > {
218- return backOff ( fn , {
219- numOfAttempts : 3 ,
220- startingDelay : 300 ,
221- timeMultiple : 2 ,
222- jitter : 'full' ,
223- retry : ( e ) => this . _isRetryableServerError ( e ) ,
224- } )
275+ private async _withServerRetry < T > (
276+ fn : ( ) => Promise < T > ,
277+ options : RequestOptions = { } ,
278+ req ?: { input : CognitiveRequest }
279+ ) : Promise < T > {
280+ let attemptCount = 0
281+ return backOff (
282+ async ( ) => {
283+ try {
284+ const result = await fn ( )
285+ attemptCount = 0
286+ return result
287+ } catch ( error ) {
288+ if ( attemptCount > 0 && req ) {
289+ this . _events . emit ( 'retry' , req , error )
290+ }
291+ attemptCount ++
292+ throw error
293+ }
294+ } ,
295+ {
296+ numOfAttempts : 3 ,
297+ startingDelay : 300 ,
298+ timeMultiple : 2 ,
299+ jitter : 'full' ,
300+ retry : ( e ) => ! options . signal ?. aborted && this . _isRetryableServerError ( e ) ,
301+ }
302+ )
225303 }
226304}
227305
0 commit comments