Skip to content

Commit 526ad65

Browse files
authored
feat(zai): answer operation (botpress#14448)
1 parent 301dd3b commit 526ad65

28 files changed

Lines changed: 4970 additions & 1050 deletions

packages/cognitive/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@botpress/cognitive",
3-
"version": "0.1.50",
3+
"version": "0.2.0",
44
"description": "Wrapper around the Botpress Client to call LLMs",
55
"main": "./dist/index.cjs",
66
"module": "./dist/index.mjs",

packages/cognitive/src/client.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,27 @@ export class Cognitive {
184184
}
185185

186186
const betaClient = new CognitiveBeta(this._client.config)
187-
const response = await betaClient.generateText(input as any)
187+
const props: Request = { input }
188188

189-
return {
189+
// Forward beta client events to main client events
190+
betaClient.on('request', () => {
191+
this._events.emit('request', props)
192+
})
193+
194+
betaClient.on('error', (_req, error) => {
195+
this._events.emit('error', props, error)
196+
})
197+
198+
betaClient.on('retry', (_req, error) => {
199+
this._events.emit('retry', props, error)
200+
})
201+
202+
const response = await betaClient.generateText(input as any, {
203+
signal: input.signal,
204+
timeout: this._timeoutMs,
205+
})
206+
207+
const result: Response = {
190208
output: {
191209
id: 'beta-output',
192210
provider: response.metadata.provider,
@@ -224,6 +242,11 @@ export class Cognitive {
224242
},
225243
},
226244
}
245+
246+
// Emit final response event with actual data
247+
this._events.emit('response', props, result)
248+
249+
return result
227250
}
228251

229252
private async _generateContent(input: InputProps): Promise<Response> {

packages/cognitive/src/cognitive-v2/index.ts

Lines changed: 146 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
import axios, { AxiosInstance } from 'axios'
22
import { backOff } from 'exponential-backoff'
3+
import { createNanoEvents, Unsubscribe } from 'nanoevents'
34
import { defaultModel, models } from './models'
45
import { CognitiveRequest, CognitiveResponse, CognitiveStreamChunk, Model } from './types'
56

67
export { 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+
816
type 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

packages/cognitive/src/schemas.gen.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export type GenerateContentInput = {
3131
type: 'text' | 'image'
3232
/** Indicates the MIME type of the content. If not provided it will be detected from the content-type header of the provided URL. */
3333
mimeType?: string
34-
/** Required if part type is "text" */
34+
/** Required if part type is "text" */
3535
text?: string
3636
/** Required if part type is "image" */
3737
url?: string
@@ -103,7 +103,7 @@ export type GenerateContentOutput = {
103103
type: 'text' | 'image'
104104
/** Indicates the MIME type of the content. If not provided it will be detected from the content-type header of the provided URL. */
105105
mimeType?: string
106-
/** Required if part type is "text" */
106+
/** Required if part type is "text" */
107107
text?: string
108108
/** Required if part type is "image" */
109109
url?: string

packages/llmz/examples/01_chat_basic/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,10 @@
1414
* - Conversation history management
1515
*/
1616

17+
import { CLIChat } from '../utils/cli-chat'
1718
import { Client } from '@botpress/client'
1819
import { execute } from 'llmz'
1920

20-
import { CLIChat } from '../utils/cli-chat'
21-
2221
// Initialize the Botpress Client for LLM interactions
2322
// This client handles authentication and communication with language models
2423
const client = new Client({

packages/llmz/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
},
7171
"peerDependencies": {
7272
"@botpress/client": "1.27.0",
73-
"@botpress/cognitive": "0.1.50",
73+
"@botpress/cognitive": "0.2.0",
7474
"@bpinternal/thicktoken": "^1.0.5",
7575
"@bpinternal/zui": "1.2.1"
7676
},

0 commit comments

Comments
 (0)