@columenlabs/lumen-ai 0.1.77 → 0.1.79
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 +47 -5
- package/dist/assistant/chatgpt.d.ts +61 -0
- package/dist/assistant/chatgpt.js +165 -0
- package/dist/assistant/engine.d.ts +63 -9
- package/dist/assistant/engine.js +424 -85
- package/dist/assistant/index.d.ts +3 -3
- package/dist/assistant/index.js +3 -3
- package/dist/assistant/models.d.ts +16 -2
- package/dist/assistant/models.js +46 -3
- package/dist/assistant/routes.js +50 -33
- package/dist/assistant/state.d.ts +45 -14
- package/dist/assistant/state.js +93 -23
- package/package.json +6 -5
package/dist/assistant/engine.js
CHANGED
|
@@ -8,15 +8,20 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Hand off: an `escalate` tool hands one task to a higher tier with the same tools, in its own
|
|
10
10
|
* section of the chat, and its memo goes back. Advice: Anthropic's advisor tool lets a higher tier
|
|
11
|
-
* read the chat and send a plan (it runs nothing), at most `ADVICE_MAX` an answer.
|
|
11
|
+
* read the chat and send a plan (it runs nothing), at most `ADVICE_MAX` an answer (Claude only).
|
|
12
|
+
*
|
|
13
|
+
* Providers: a tier runs a Claude model (Anthropic's Messages API) or an OpenAI model (the
|
|
14
|
+
* Responses API), each on its payer's key of that provider. A hand-off may cross providers.
|
|
12
15
|
*
|
|
13
16
|
* Spend: every call is priced at list price as it runs; past what its payer has left this month the
|
|
14
17
|
* model must answer from what it has.
|
|
15
18
|
*/
|
|
16
19
|
import Anthropic from '@anthropic-ai/sdk';
|
|
17
20
|
import { HAND_OFF_WHYS } from '@columenlabs/lumen-ui-types/assistant';
|
|
18
|
-
import
|
|
19
|
-
import {
|
|
21
|
+
import OpenAI from 'openai';
|
|
22
|
+
import { CHATGPT_REDIRECT, SigninError, finishChatgpt, startChatgpt, testChatgpt, } from './chatgpt.js';
|
|
23
|
+
import { ALL_MODELS, DEFAULT_OUT, Meter, PROVIDERS, effortParams, } from './models.js';
|
|
24
|
+
import { DEFAULT_CAP, State, keyDocName, keyState, waitText, } from './state.js';
|
|
20
25
|
import { Docs } from './store.js';
|
|
21
26
|
import { ToolError, rowCount, toolName, withDeadline, } from './tools.js';
|
|
22
27
|
import { scrapeqlTools, webFetchTools } from './web.js';
|
|
@@ -92,6 +97,10 @@ function untilStopped(work, signal) {
|
|
|
92
97
|
});
|
|
93
98
|
}
|
|
94
99
|
const newId = (prefix) => `${prefix}_${crypto.randomUUID().replaceAll('-', '').slice(0, 20)}`;
|
|
100
|
+
/** Every tool line in a chat, those in hand-off sections included. */
|
|
101
|
+
function stepsOf(items) {
|
|
102
|
+
return items.flatMap((i) => i.kind === 'step' ? [i] : i.kind === 'handoff' ? stepsOf(i.items) : []);
|
|
103
|
+
}
|
|
95
104
|
/** Every draft in a chat, those in hand-off sections included. */
|
|
96
105
|
function draftsOf(items) {
|
|
97
106
|
return items.flatMap((i) => i.kind === 'draft' ? [i] : i.kind === 'handoff' ? draftsOf(i.items) : []);
|
|
@@ -100,23 +109,27 @@ function draftsOf(items) {
|
|
|
100
109
|
function shownError(err) {
|
|
101
110
|
if (err instanceof AssistantError)
|
|
102
111
|
return { message: err.message, code: err.code };
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
112
|
+
const api = err instanceof Anthropic.APIError
|
|
113
|
+
? { who: 'Claude', status: err.status }
|
|
114
|
+
: err instanceof OpenAI.APIError
|
|
115
|
+
? { who: 'OpenAI', status: err.status }
|
|
116
|
+
: null;
|
|
117
|
+
if (api) {
|
|
118
|
+
const { who, status } = api;
|
|
119
|
+
if (status === 401)
|
|
120
|
+
return { message: `The ${who} key was refused.`, code: 'key_refused' };
|
|
121
|
+
if (status === 429)
|
|
107
122
|
return {
|
|
108
|
-
message:
|
|
123
|
+
message: `${who} is busy or the key hit its rate limit. Try again shortly.`,
|
|
109
124
|
code: 'rate_limited',
|
|
110
125
|
};
|
|
111
|
-
if (
|
|
112
|
-
return { message:
|
|
113
|
-
return {
|
|
114
|
-
message: `Claude refused the request (${err.status ?? 'error'}).`,
|
|
115
|
-
code: 'model_error',
|
|
116
|
-
};
|
|
126
|
+
if (status === 529 || (status ?? 0) >= 500)
|
|
127
|
+
return { message: `${who} is overloaded. Try again shortly.`, code: 'overloaded' };
|
|
128
|
+
return { message: `${who} refused the request (${status ?? 'error'}).`, code: 'model_error' };
|
|
117
129
|
}
|
|
118
130
|
return { message: 'Something went wrong while answering.', code: 'internal' };
|
|
119
131
|
}
|
|
132
|
+
const isApiError = (err) => err instanceof Anthropic.APIError || err instanceof OpenAI.APIError;
|
|
120
133
|
export class Assistant {
|
|
121
134
|
o;
|
|
122
135
|
name;
|
|
@@ -131,7 +144,7 @@ export class Assistant {
|
|
|
131
144
|
throw new Error('An assistant needs at least one tier');
|
|
132
145
|
this.name = o.name;
|
|
133
146
|
this.tiers = o.tiers;
|
|
134
|
-
this.models = o.models ??
|
|
147
|
+
this.models = o.models ?? ALL_MODELS;
|
|
135
148
|
this.tasks = o.tasks ?? [
|
|
136
149
|
{ id: 'chat', label: 'Chat', about: 'The side panel', tier: o.tiers[0].id },
|
|
137
150
|
];
|
|
@@ -140,12 +153,31 @@ export class Assistant {
|
|
|
140
153
|
timeZone: o.timeZone ?? 'UTC',
|
|
141
154
|
defaultCap: o.defaultCap ?? DEFAULT_CAP,
|
|
142
155
|
...(o.appKey ? { appKey: o.appKey } : {}),
|
|
156
|
+
...(o.fetch ? { fetch: o.fetch } : {}),
|
|
143
157
|
});
|
|
144
158
|
}
|
|
145
|
-
|
|
159
|
+
anthropic(apiKey) {
|
|
146
160
|
const f = this.o.fetch;
|
|
147
161
|
return new Anthropic({ apiKey, maxRetries: 1, ...(f ? { fetch: f } : {}) });
|
|
148
162
|
}
|
|
163
|
+
openai(apiKey, plan = false) {
|
|
164
|
+
const f = this.o.fetch;
|
|
165
|
+
// A plan sign-in's calls are not retried: a usage limit is final for now.
|
|
166
|
+
return new OpenAI({ apiKey, maxRetries: plan ? 0 : 1, ...(f ? { fetch: f } : {}) });
|
|
167
|
+
}
|
|
168
|
+
clients(paying) {
|
|
169
|
+
const { keys, plans } = paying;
|
|
170
|
+
return {
|
|
171
|
+
...(keys.anthropic ? { anthropic: this.anthropic(keys.anthropic) } : {}),
|
|
172
|
+
...(keys.openai
|
|
173
|
+
? { openai: this.openai(keys.openai, !!plans.openai), openaiPlan: !!plans.openai }
|
|
174
|
+
: {}),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/** The providers the app's models run on, in the order the models list them. */
|
|
178
|
+
get providers() {
|
|
179
|
+
return [...new Set(this.models.map((m) => m.provider))];
|
|
180
|
+
}
|
|
149
181
|
tierById(id) {
|
|
150
182
|
return this.tiers.find((t) => t.id === id);
|
|
151
183
|
}
|
|
@@ -177,20 +209,42 @@ export class Assistant {
|
|
|
177
209
|
return (this.tierById(task === 'chat' ? (p.tier ?? startId) : startId) ?? this.tiers[0]);
|
|
178
210
|
}
|
|
179
211
|
access(p, g) {
|
|
180
|
-
|
|
212
|
+
const a = p.access?.[g.id] ?? g.access ?? 'read';
|
|
213
|
+
return a === 'write' && !g.writes ? 'draft' : a;
|
|
214
|
+
}
|
|
215
|
+
groupById(id) {
|
|
216
|
+
return this.groups.find((g) => g.id === id);
|
|
181
217
|
}
|
|
182
218
|
// ---------------------------------------------------------------- status and settings
|
|
183
219
|
async status(caller) {
|
|
184
|
-
const
|
|
220
|
+
const main = await this.mainTier(caller);
|
|
221
|
+
const mainModel = (await this.pick(caller, main)).model;
|
|
222
|
+
const [p, providers, scrapeql, spend, appCap, ownCap, { paying, wait }] = await Promise.all([
|
|
185
223
|
this.state.person(caller.id),
|
|
186
|
-
this.
|
|
187
|
-
|
|
188
|
-
|
|
224
|
+
Promise.all(this.providers.map(async (id) => {
|
|
225
|
+
const [doc, key, own] = await Promise.all([
|
|
226
|
+
this.state.appKey(id),
|
|
227
|
+
this.state.appApiKey(id),
|
|
228
|
+
this.state.ownKey(caller.id, id),
|
|
229
|
+
]);
|
|
230
|
+
const app = caller.admin
|
|
231
|
+
? keyState(doc, (d) => d.api_key)
|
|
232
|
+
: { set: !!key };
|
|
233
|
+
return {
|
|
234
|
+
id,
|
|
235
|
+
label: PROVIDERS[id].label,
|
|
236
|
+
prefix: PROVIDERS[id].prefix,
|
|
237
|
+
keys_url: PROVIDERS[id].keysUrl,
|
|
238
|
+
...(PROVIDERS[id].signin ? { signin: PROVIDERS[id].signin } : {}),
|
|
239
|
+
app: { ...app, ...(key?.fromEnv ? { from_env: true } : {}) },
|
|
240
|
+
own: keyState(own, (d) => d.api_key),
|
|
241
|
+
};
|
|
242
|
+
})),
|
|
189
243
|
this.o.web?.scrapeql ? this.state.scrapeql() : Promise.resolve(null),
|
|
190
244
|
this.state.spend(caller.id),
|
|
191
245
|
this.state.appCap(),
|
|
192
|
-
this.state.
|
|
193
|
-
this.
|
|
246
|
+
this.state.ownCap(caller.id),
|
|
247
|
+
this.state.paying(caller.id, mainModel.provider),
|
|
194
248
|
]);
|
|
195
249
|
const tiers = await Promise.all(this.tiers.map(async (t) => {
|
|
196
250
|
const { model, effort } = await this.pick(caller, t);
|
|
@@ -200,19 +254,28 @@ export class Assistant {
|
|
|
200
254
|
about: t.about,
|
|
201
255
|
model: model.id,
|
|
202
256
|
model_label: model.label,
|
|
257
|
+
provider: model.provider,
|
|
203
258
|
effort,
|
|
204
259
|
};
|
|
205
260
|
}));
|
|
206
|
-
const appState = caller.admin
|
|
207
|
-
? keyState(appDoc, (d) => d.api_key)
|
|
208
|
-
: { set: !!appKey };
|
|
209
261
|
return {
|
|
210
262
|
name: this.name,
|
|
263
|
+
commands: (this.o.commands ?? []).map((c) => ({
|
|
264
|
+
name: c.name,
|
|
265
|
+
about: c.about,
|
|
266
|
+
...(c.args ? { args: c.args } : {}),
|
|
267
|
+
})),
|
|
211
268
|
wait,
|
|
269
|
+
...(wait ? { wait_provider: mainModel.provider } : {}),
|
|
212
270
|
admin: caller.admin,
|
|
213
271
|
tier: main.id,
|
|
214
272
|
tiers,
|
|
215
|
-
models: this.models.map((m) => ({
|
|
273
|
+
models: this.models.map((m) => ({
|
|
274
|
+
id: m.id,
|
|
275
|
+
label: m.label,
|
|
276
|
+
group: m.group,
|
|
277
|
+
provider: m.provider,
|
|
278
|
+
})),
|
|
216
279
|
tasks: this.tasks.map((t) => ({
|
|
217
280
|
id: t.id,
|
|
218
281
|
label: t.label,
|
|
@@ -225,6 +288,7 @@ export class Assistant {
|
|
|
225
288
|
about: g.about,
|
|
226
289
|
access: this.access(p, g),
|
|
227
290
|
drafts: true,
|
|
291
|
+
writes: !!g.writes,
|
|
228
292
|
})),
|
|
229
293
|
hand_off: p.hand_off ?? 'next',
|
|
230
294
|
advice: p.advice && (p.advice === 'off' || this.tierById(p.advice)) ? p.advice : 'off',
|
|
@@ -236,13 +300,8 @@ export class Assistant {
|
|
|
236
300
|
: { set: !!scrapeql }
|
|
237
301
|
: null,
|
|
238
302
|
},
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
own: {
|
|
242
|
-
...keyState(own, (d) => d.api_key),
|
|
243
|
-
cap: own?.cap_usd ?? this.o.defaultCap ?? DEFAULT_CAP,
|
|
244
|
-
},
|
|
245
|
-
},
|
|
303
|
+
providers,
|
|
304
|
+
caps: { app: appCap, own: ownCap },
|
|
246
305
|
pay: p.pay ?? 'app',
|
|
247
306
|
spend: {
|
|
248
307
|
month: this.state.month(),
|
|
@@ -317,12 +376,15 @@ export class Assistant {
|
|
|
317
376
|
const fns = new Map();
|
|
318
377
|
for (const o of opened)
|
|
319
378
|
for (const def of o.open.tools) {
|
|
320
|
-
if (!def.read && o.access !== 'draft')
|
|
379
|
+
if (!def.read && o.access !== 'draft' && o.access !== 'write')
|
|
321
380
|
continue;
|
|
381
|
+
// Read + write on a group that offers no writes drafts.
|
|
382
|
+
const write = !def.read && o.access === 'write' && !!this.groupById(o.group)?.writes;
|
|
322
383
|
fns.set(toolName(o.group, def.name), {
|
|
323
384
|
group: o.group,
|
|
324
385
|
def,
|
|
325
|
-
draft: !def.read,
|
|
386
|
+
draft: !def.read && !write,
|
|
387
|
+
write,
|
|
326
388
|
open: o.open,
|
|
327
389
|
});
|
|
328
390
|
}
|
|
@@ -335,7 +397,7 @@ export class Assistant {
|
|
|
335
397
|
toolsFor(fns) {
|
|
336
398
|
return [...fns.entries()].map(([name, f]) => ({
|
|
337
399
|
name,
|
|
338
|
-
description: `${f.draft ? "DRAFT ONLY: this call changes nothing. It puts a card on the person's screen that they Save or Dismiss. " : ''}${f.def.title ? `${f.def.title}. ` : ''}${f.def.description ?? ''}`.slice(0, 4000),
|
|
400
|
+
description: `${f.draft ? "DRAFT ONLY: this call changes nothing. It puts a card on the person's screen that they Save or Dismiss. " : f.write ? 'WRITES AT ONCE: this call makes the change now; the person can undo it. ' : ''}${f.def.title ? `${f.def.title}. ` : ''}${f.def.description ?? ''}`.slice(0, 4000),
|
|
339
401
|
input_schema: { type: 'object', ...f.def.inputSchema },
|
|
340
402
|
}));
|
|
341
403
|
}
|
|
@@ -391,6 +453,7 @@ export class Assistant {
|
|
|
391
453
|
history: [],
|
|
392
454
|
};
|
|
393
455
|
const compactOnly = message === '/compact';
|
|
456
|
+
const asked = await this.expand(caller, message);
|
|
394
457
|
if (!compactOnly)
|
|
395
458
|
doc.items.push({
|
|
396
459
|
kind: 'user',
|
|
@@ -402,20 +465,27 @@ export class Assistant {
|
|
|
402
465
|
});
|
|
403
466
|
const start = doc.items.length;
|
|
404
467
|
emit('chat', { id: chatId, title: doc.title });
|
|
405
|
-
|
|
468
|
+
// `tier` on a message: a higher one takes it as a task in its own section (handTo below); the
|
|
469
|
+
// same or a lower one answers it as the main tier, for this message only.
|
|
470
|
+
const picked = await this.mainTier(caller);
|
|
471
|
+
const lower = req.tier && !this.above(picked).some((t) => t.id === req.tier)
|
|
472
|
+
? this.tierById(req.tier)
|
|
473
|
+
: undefined;
|
|
474
|
+
const main = await this.session(caller, lower ?? picked);
|
|
475
|
+
const { paying, wait } = await this.state.paying(caller.id, main.model.provider);
|
|
406
476
|
if (!paying) {
|
|
407
|
-
const
|
|
408
|
-
|
|
477
|
+
const code = wait ?? 'not_connected';
|
|
478
|
+
const text = waitText(code, main.model.provider);
|
|
479
|
+
doc.items.push({ kind: 'error', text, code });
|
|
409
480
|
await this.writeChat(caller, chatId, doc);
|
|
410
|
-
emit('error', { message: text, code
|
|
481
|
+
emit('error', { message: text, code });
|
|
411
482
|
emit('done', { id: chatId, usage: doc.usage ?? NO_USAGE });
|
|
412
483
|
return;
|
|
413
484
|
}
|
|
414
485
|
const p = await this.state.person(caller.id);
|
|
415
|
-
const main = await this.session(caller, await this.mainTier(caller));
|
|
416
486
|
const policy = p.hand_off ?? 'next';
|
|
417
487
|
const advisor = await this.advisorFor(caller, p.advice, main);
|
|
418
|
-
const
|
|
488
|
+
const clients = this.clients(paying);
|
|
419
489
|
const live = [main.meter, ...(advisor ? [advisor.session.meter] : [])];
|
|
420
490
|
const before = doc.usage ?? NO_USAGE;
|
|
421
491
|
let context = before.context;
|
|
@@ -433,7 +503,8 @@ export class Assistant {
|
|
|
433
503
|
const handTo = req.tier ? this.above(main.tier).find((t) => t.id === req.tier) : undefined;
|
|
434
504
|
const tools = await this.openTools(caller, p);
|
|
435
505
|
const runs = [];
|
|
436
|
-
|
|
506
|
+
doc.writes ??= {};
|
|
507
|
+
const counts = { steps: 0, handoffs: 0, advice: 0, end: main.tier, writes: doc.writes };
|
|
437
508
|
let outcome = 'ok';
|
|
438
509
|
try {
|
|
439
510
|
const appSystem = typeof this.o.system === 'string' ? this.o.system : await this.o.system(caller);
|
|
@@ -451,7 +522,7 @@ export class Assistant {
|
|
|
451
522
|
},
|
|
452
523
|
];
|
|
453
524
|
if (compactOnly || (before.context ?? 0) > COMPACT_AT) {
|
|
454
|
-
const did = await this.compact(
|
|
525
|
+
const did = await this.compact(clients, main, doc, emit);
|
|
455
526
|
if (did)
|
|
456
527
|
context = undefined;
|
|
457
528
|
if (compactOnly) {
|
|
@@ -470,7 +541,7 @@ export class Assistant {
|
|
|
470
541
|
}
|
|
471
542
|
const page = this.cardNews(doc) +
|
|
472
543
|
(req.refs?.length
|
|
473
|
-
? `The person points at: ${JSON.stringify(req.refs).slice(0,
|
|
544
|
+
? `The person points at: ${JSON.stringify(req.refs).slice(0, 8000)}\n\n`
|
|
474
545
|
: '') +
|
|
475
546
|
(req.context
|
|
476
547
|
? `Page: ${req.context.label}${req.context.note === undefined ? '' : `\n${JSON.stringify(req.context.note).slice(0, 4000)}`}\n\n`
|
|
@@ -480,11 +551,11 @@ export class Assistant {
|
|
|
480
551
|
: '';
|
|
481
552
|
const messages = [
|
|
482
553
|
...doc.history.map((t) => ({ role: t.role, content: t.content })),
|
|
483
|
-
{ role: 'user', content: `${page}${lead}${
|
|
554
|
+
{ role: 'user', content: `${page}${lead}${asked}` },
|
|
484
555
|
];
|
|
485
556
|
const escalateTool = this.escalateTool(main.tier);
|
|
486
557
|
let answer = await this.loop({
|
|
487
|
-
|
|
558
|
+
clients,
|
|
488
559
|
session: main,
|
|
489
560
|
system,
|
|
490
561
|
tools: [...this.toolsFor(tools.fns), ...(escalateTool ? [escalateTool] : [])],
|
|
@@ -502,8 +573,8 @@ export class Assistant {
|
|
|
502
573
|
tool: (use) => use.name === 'escalate'
|
|
503
574
|
? this.handOff(use, {
|
|
504
575
|
caller,
|
|
505
|
-
|
|
506
|
-
message,
|
|
576
|
+
clients,
|
|
577
|
+
message: asked,
|
|
507
578
|
handTo,
|
|
508
579
|
page,
|
|
509
580
|
policy,
|
|
@@ -531,7 +602,7 @@ export class Assistant {
|
|
|
531
602
|
emit('text', { delta: answer });
|
|
532
603
|
emit('text_end', {});
|
|
533
604
|
}
|
|
534
|
-
doc.history.push({ role: 'user', content: `${page}${
|
|
605
|
+
doc.history.push({ role: 'user', content: `${page}${asked}` });
|
|
535
606
|
doc.history.push({ role: 'assistant', content: this.remember(answer, runs) });
|
|
536
607
|
}
|
|
537
608
|
catch (err) {
|
|
@@ -544,14 +615,14 @@ export class Assistant {
|
|
|
544
615
|
.map((i) => i.text)
|
|
545
616
|
.join('\n\n');
|
|
546
617
|
if (!compactOnly) {
|
|
547
|
-
doc.history.push({ role: 'user', content:
|
|
618
|
+
doc.history.push({ role: 'user', content: asked });
|
|
548
619
|
doc.history.push({ role: 'assistant', content: said || '(stopped before answering)' });
|
|
549
620
|
}
|
|
550
621
|
}
|
|
551
622
|
else {
|
|
552
623
|
outcome = 'error';
|
|
553
624
|
const shown = shownError(err);
|
|
554
|
-
if (!(err instanceof AssistantError) && !(err
|
|
625
|
+
if (!(err instanceof AssistantError) && !isApiError(err))
|
|
555
626
|
console.error(JSON.stringify({
|
|
556
627
|
event: 'assistant_error',
|
|
557
628
|
error: String(err?.stack ?? err).slice(0, 2000),
|
|
@@ -594,6 +665,7 @@ export class Assistant {
|
|
|
594
665
|
`You are ${this.name}. Today is ${new Date().toISOString().slice(0, 10)}.`,
|
|
595
666
|
'Answer in short plain sentences. Use the tools for facts; never guess a number a tool can give you. Call several tools at once when they do not depend on each other.',
|
|
596
667
|
'A tool marked DRAFT ONLY makes a card the person saves or dismisses; it changes nothing by itself. Say what the card will do; never claim it is done. The next message tells you how each card ended.',
|
|
668
|
+
'A tool marked WRITES AT ONCE makes its change when you call it; the person can undo it from the chat. Change only what they asked for, and say what you changed.',
|
|
597
669
|
];
|
|
598
670
|
if (!fns.size)
|
|
599
671
|
lines.push('No tools are on now: the person can turn them on in Settings ▸ Tools.');
|
|
@@ -615,10 +687,13 @@ export class Assistant {
|
|
|
615
687
|
/** The advisor for this answer, or null when off. Never weaker than the model it advises. */
|
|
616
688
|
async advisorFor(caller, setting, main) {
|
|
617
689
|
const tier = setting && setting !== 'off' ? this.tierById(setting) : undefined;
|
|
618
|
-
if (!tier)
|
|
690
|
+
if (!tier || main.model.provider !== 'anthropic')
|
|
619
691
|
return null;
|
|
620
692
|
const rank = (m) => this.models.indexOf(m);
|
|
621
693
|
let session = await this.session(caller, tier);
|
|
694
|
+
// Anthropic's advisor tool runs a Claude model: another provider's tier advises nothing.
|
|
695
|
+
if (session.model.provider !== 'anthropic')
|
|
696
|
+
session = { ...main };
|
|
622
697
|
if (rank(session.model) < rank(main.model))
|
|
623
698
|
session = { ...main, meter: new Meter(main.model, '') };
|
|
624
699
|
session.meter = new Meter(session.model, `${session.tier.label} · advice`);
|
|
@@ -645,6 +720,17 @@ export class Assistant {
|
|
|
645
720
|
},
|
|
646
721
|
};
|
|
647
722
|
}
|
|
723
|
+
/** What the model is asked: an app command's prompt, else the message as typed. */
|
|
724
|
+
async expand(caller, message) {
|
|
725
|
+
const m = /^\/([\w-]+)(?:\s+([\s\S]*))?$/.exec(message);
|
|
726
|
+
const c = m && this.o.commands?.find((x) => x.name === m[1]);
|
|
727
|
+
if (!c)
|
|
728
|
+
return message;
|
|
729
|
+
const args = (m?.[2] ?? '').trim();
|
|
730
|
+
return typeof c.prompt === 'function'
|
|
731
|
+
? c.prompt(args, caller)
|
|
732
|
+
: c.prompt.replaceAll('{args}', args);
|
|
733
|
+
}
|
|
648
734
|
/** The tiers the person's message asks for by name ("go deeper" is the next one up). */
|
|
649
735
|
askedFor(message, main) {
|
|
650
736
|
const reach = this.above(main);
|
|
@@ -682,6 +768,8 @@ export class Assistant {
|
|
|
682
768
|
if (c.over())
|
|
683
769
|
return reply('No money left for a higher tier this answer: answer yourself.', true);
|
|
684
770
|
const sub = await this.session(c.caller, tier);
|
|
771
|
+
if (!c.clients[sub.model.provider])
|
|
772
|
+
return reply(`${tier.label} runs on ${PROVIDERS[sub.model.provider].label} and no ${PROVIDERS[sub.model.provider].label} key is set for whoever pays this answer: answer yourself.`, true);
|
|
685
773
|
c.live.push(sub.meter);
|
|
686
774
|
c.counts.handoffs += 1;
|
|
687
775
|
if (this.tiers.indexOf(tier) > this.tiers.indexOf(c.counts.end))
|
|
@@ -707,7 +795,7 @@ export class Assistant {
|
|
|
707
795
|
let failed;
|
|
708
796
|
try {
|
|
709
797
|
memo = await this.loop({
|
|
710
|
-
|
|
798
|
+
clients: c.clients,
|
|
711
799
|
session: sub,
|
|
712
800
|
system: c.system,
|
|
713
801
|
tools: this.toolsFor(c.tools),
|
|
@@ -776,6 +864,8 @@ export class Assistant {
|
|
|
776
864
|
let rows;
|
|
777
865
|
try {
|
|
778
866
|
const result = await untilStopped(withDeadline(title, 60_000, f.open.call(f.def.name, use.input)), signal);
|
|
867
|
+
if (f.write && counts.writes)
|
|
868
|
+
counts.writes[id] = { group: f.group, tool: f.def.name, args: use.input, result };
|
|
779
869
|
rows = rowCount(result);
|
|
780
870
|
out = typeof result === 'string' ? result : JSON.stringify(result);
|
|
781
871
|
}
|
|
@@ -797,6 +887,9 @@ export class Assistant {
|
|
|
797
887
|
ms,
|
|
798
888
|
...(rows !== undefined ? { rows } : {}),
|
|
799
889
|
...(error ? { error } : {}),
|
|
890
|
+
...(f.write && ok
|
|
891
|
+
? { write: { undo: !!this.groupById(f.group)?.undo, status: 'done' } }
|
|
892
|
+
: {}),
|
|
800
893
|
};
|
|
801
894
|
sink.push(item);
|
|
802
895
|
emit('step', item);
|
|
@@ -816,7 +909,156 @@ export class Assistant {
|
|
|
816
909
|
return reply(cut, !ok);
|
|
817
910
|
}
|
|
818
911
|
/** Model calls and tool rounds until the model answers; returns its words. */
|
|
819
|
-
|
|
912
|
+
loop(o) {
|
|
913
|
+
const { provider } = o.session.model;
|
|
914
|
+
const label = PROVIDERS[provider].label;
|
|
915
|
+
if (provider === 'openai') {
|
|
916
|
+
if (!o.clients.openai)
|
|
917
|
+
throw new AssistantError(`No ${label} key is set for this answer.`, 'not_connected');
|
|
918
|
+
return this.loopOpenAI({ ...o, client: o.clients.openai, plan: !!o.clients.openaiPlan });
|
|
919
|
+
}
|
|
920
|
+
if (!o.clients.anthropic)
|
|
921
|
+
throw new AssistantError(`No ${label} key is set for this answer.`, 'not_connected');
|
|
922
|
+
return this.loopClaude({ ...o, client: o.clients.anthropic });
|
|
923
|
+
}
|
|
924
|
+
/** The loop on OpenAI's Responses API: the same tools, Stop, caps and rounds as Claude's. */
|
|
925
|
+
async loopOpenAI(o) {
|
|
926
|
+
const { client, session, tools, sink, emit, signal, plan } = o;
|
|
927
|
+
const instructions = o.system.map((b) => b.text).join('\n\n');
|
|
928
|
+
const fnTools = tools.map((t) => ({
|
|
929
|
+
type: 'function',
|
|
930
|
+
name: t.name,
|
|
931
|
+
description: t.description ?? null,
|
|
932
|
+
parameters: t.input_schema,
|
|
933
|
+
strict: false,
|
|
934
|
+
}));
|
|
935
|
+
// The chat so far is plain text turns; this answer's calls are appended as OpenAI items.
|
|
936
|
+
const input = o.messages.map((m) => ({
|
|
937
|
+
role: m.role,
|
|
938
|
+
content: typeof m.content === 'string'
|
|
939
|
+
? m.content
|
|
940
|
+
: m.content
|
|
941
|
+
.map((b) => (b.type === 'text' ? b.text : ''))
|
|
942
|
+
.filter(Boolean)
|
|
943
|
+
.join('\n'),
|
|
944
|
+
}));
|
|
945
|
+
let answer = '';
|
|
946
|
+
let force = o.first?.type === 'tool' ? { type: 'function', name: o.first.name } : undefined;
|
|
947
|
+
// A plan sign-in takes function tools only inside a namespace and no output cap.
|
|
948
|
+
const sent = (only) => {
|
|
949
|
+
const fns = only ? fnTools.filter((t) => t.name === only) : fnTools;
|
|
950
|
+
if (!plan)
|
|
951
|
+
return { tools: fns, max_output_tokens: session.tier.out ?? DEFAULT_OUT };
|
|
952
|
+
return {
|
|
953
|
+
tools: [
|
|
954
|
+
{
|
|
955
|
+
type: 'namespace',
|
|
956
|
+
name: 'app',
|
|
957
|
+
description: `${this.name}'s tools`,
|
|
958
|
+
tools: fns.map(({ strict: _, ...t }) => t),
|
|
959
|
+
},
|
|
960
|
+
],
|
|
961
|
+
};
|
|
962
|
+
};
|
|
963
|
+
for (let round = 0; round < MAX_ROUNDS; round++) {
|
|
964
|
+
signal?.throwIfAborted();
|
|
965
|
+
const effort = session.effort === 'default' ? undefined : session.effort;
|
|
966
|
+
// On a plan a forced call is the one tool offered, with any call required.
|
|
967
|
+
const only = plan && typeof force === 'object' ? force.name : undefined;
|
|
968
|
+
const choice = only ? 'required' : force;
|
|
969
|
+
const stream = client.responses.stream({
|
|
970
|
+
model: session.model.id,
|
|
971
|
+
instructions,
|
|
972
|
+
input,
|
|
973
|
+
...sent(only),
|
|
974
|
+
store: false,
|
|
975
|
+
stream: true,
|
|
976
|
+
include: ['reasoning.encrypted_content'],
|
|
977
|
+
...(effort ? { reasoning: { effort } } : {}),
|
|
978
|
+
...(choice ? { tool_choice: choice } : {}),
|
|
979
|
+
}, signal ? { signal } : {});
|
|
980
|
+
let partial = '';
|
|
981
|
+
stream.on('response.output_text.delta', (e) => {
|
|
982
|
+
partial += e.delta;
|
|
983
|
+
emit('text', { delta: e.delta });
|
|
984
|
+
});
|
|
985
|
+
const res = await stream.finalResponse().catch((err) => {
|
|
986
|
+
if (signal?.aborted && partial)
|
|
987
|
+
sink.push({ kind: 'text', text: partial });
|
|
988
|
+
throw err;
|
|
989
|
+
});
|
|
990
|
+
if (res.status === 'failed') {
|
|
991
|
+
if (partial)
|
|
992
|
+
sink.push({ kind: 'text', text: partial });
|
|
993
|
+
const code = res.error?.code ?? '';
|
|
994
|
+
throw new AssistantError(/usage_limit/.test(code)
|
|
995
|
+
? 'Your ChatGPT plan reached its usage limit for now: try again later, or pick another model.'
|
|
996
|
+
: 'OpenAI could not finish the answer.', 'upstream');
|
|
997
|
+
}
|
|
998
|
+
const u = res.usage;
|
|
999
|
+
if (u) {
|
|
1000
|
+
const read = u.input_tokens_details?.cached_tokens ?? 0;
|
|
1001
|
+
const written = u.input_tokens_details?.cache_write_tokens ?? 0;
|
|
1002
|
+
session.meter.add({
|
|
1003
|
+
input_tokens: Math.max(0, u.input_tokens - read - written),
|
|
1004
|
+
output_tokens: u.output_tokens,
|
|
1005
|
+
cache_read_input_tokens: read,
|
|
1006
|
+
cache_creation_input_tokens: written,
|
|
1007
|
+
}, plan);
|
|
1008
|
+
o.context?.(u.input_tokens + u.output_tokens);
|
|
1009
|
+
}
|
|
1010
|
+
o.called();
|
|
1011
|
+
const cut = res.incomplete_details?.reason === 'max_output_tokens' ? ' …(cut)' : '';
|
|
1012
|
+
if (cut)
|
|
1013
|
+
emit('text', { delta: cut });
|
|
1014
|
+
const text = (res.output_text ?? '') + cut;
|
|
1015
|
+
if (text) {
|
|
1016
|
+
answer += (answer ? '\n\n' : '') + text;
|
|
1017
|
+
sink.push({ kind: 'text', text });
|
|
1018
|
+
emit('text_end', {});
|
|
1019
|
+
}
|
|
1020
|
+
// Reasoning, words and calls go back as they came, so the model keeps its own thread.
|
|
1021
|
+
input.push(...res.output);
|
|
1022
|
+
const calls = res.output.filter((b) => b.type === 'function_call');
|
|
1023
|
+
if (calls.length === 0)
|
|
1024
|
+
break;
|
|
1025
|
+
const results = await Promise.all(calls.map((c) => {
|
|
1026
|
+
let args = {};
|
|
1027
|
+
try {
|
|
1028
|
+
args = c.arguments ? JSON.parse(c.arguments) : {};
|
|
1029
|
+
}
|
|
1030
|
+
catch { }
|
|
1031
|
+
const use = {
|
|
1032
|
+
type: 'tool_use',
|
|
1033
|
+
id: c.call_id,
|
|
1034
|
+
name: c.name,
|
|
1035
|
+
input: args,
|
|
1036
|
+
};
|
|
1037
|
+
return untilStopped(o.tool(use), signal);
|
|
1038
|
+
}));
|
|
1039
|
+
for (const r of results) {
|
|
1040
|
+
const body = typeof r.content === 'string' ? r.content : JSON.stringify(r.content ?? '');
|
|
1041
|
+
input.push({
|
|
1042
|
+
type: 'function_call_output',
|
|
1043
|
+
call_id: r.tool_use_id,
|
|
1044
|
+
output: r.is_error ? `Error: ${body}` : body,
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
force = undefined;
|
|
1048
|
+
const limit = o.over();
|
|
1049
|
+
const last = round === MAX_ROUNDS - 2;
|
|
1050
|
+
if (limit || last) {
|
|
1051
|
+
input.push({
|
|
1052
|
+
role: 'user',
|
|
1053
|
+
content: limit ?? 'No more tool calls: answer now from what you have.',
|
|
1054
|
+
});
|
|
1055
|
+
force = 'none';
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
return answer;
|
|
1059
|
+
}
|
|
1060
|
+
/** The loop on Anthropic's Messages API, with the advisor and prompt caching. */
|
|
1061
|
+
async loopClaude(o) {
|
|
820
1062
|
const { client, session, system, tools, messages, sink, emit, signal } = o;
|
|
821
1063
|
let answer = '';
|
|
822
1064
|
let force = o.first;
|
|
@@ -934,27 +1176,46 @@ export class Assistant {
|
|
|
934
1176
|
return answer;
|
|
935
1177
|
}
|
|
936
1178
|
/** Swaps the history for a short summary. False with nothing to compact. */
|
|
937
|
-
async compact(
|
|
1179
|
+
async compact(clients, s, doc, emit) {
|
|
938
1180
|
if (doc.history.length < 2)
|
|
939
1181
|
return false;
|
|
940
|
-
const
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1182
|
+
const system = `Summarise this chat between a person and ${this.name}, their assistant, so ${this.name} can carry on from the summary alone. Keep what the person wants, the facts and numbers found, decisions made and cards drafted or saved, and anything still open. Plain bullets, at most 400 words. No preamble.`;
|
|
1183
|
+
const chat = doc.history
|
|
1184
|
+
.map((t) => `${t.role === 'user' ? 'Person' : this.name}: ${t.content}`)
|
|
1185
|
+
.join('\n\n');
|
|
1186
|
+
let summary = '';
|
|
1187
|
+
if (s.model.provider === 'openai' && clients.openai) {
|
|
1188
|
+
const res = await clients.openai.responses.create({
|
|
1189
|
+
model: s.model.id,
|
|
1190
|
+
instructions: system,
|
|
1191
|
+
input: chat,
|
|
1192
|
+
store: false,
|
|
1193
|
+
max_output_tokens: 1500,
|
|
1194
|
+
});
|
|
1195
|
+
const u = res.usage;
|
|
1196
|
+
if (u)
|
|
1197
|
+
s.meter.add({
|
|
1198
|
+
input_tokens: u.input_tokens - (u.input_tokens_details?.cached_tokens ?? 0),
|
|
1199
|
+
output_tokens: u.output_tokens,
|
|
1200
|
+
cache_read_input_tokens: u.input_tokens_details?.cached_tokens ?? 0,
|
|
1201
|
+
});
|
|
1202
|
+
summary = res.output_text ?? '';
|
|
1203
|
+
}
|
|
1204
|
+
else if (clients.anthropic) {
|
|
1205
|
+
const msg = await clients.anthropic.messages.create({
|
|
1206
|
+
model: s.model.id,
|
|
1207
|
+
max_tokens: 1500,
|
|
1208
|
+
system,
|
|
1209
|
+
messages: [{ role: 'user', content: chat }],
|
|
1210
|
+
});
|
|
1211
|
+
s.meter.add(msg.usage);
|
|
1212
|
+
summary = msg.content
|
|
1213
|
+
.filter((b) => b.type === 'text')
|
|
1214
|
+
.map((b) => b.text)
|
|
1215
|
+
.join('');
|
|
1216
|
+
}
|
|
1217
|
+
else
|
|
1218
|
+
return false;
|
|
958
1219
|
doc.history = [
|
|
959
1220
|
{ role: 'user', content: `Summary of our chat so far (compacted):\n${summary}` },
|
|
960
1221
|
{ role: 'assistant', content: 'Noted. I carry on from this summary.' },
|
|
@@ -967,10 +1228,16 @@ export class Assistant {
|
|
|
967
1228
|
cardNews(doc) {
|
|
968
1229
|
const told = new Set(doc.told ?? []);
|
|
969
1230
|
const news = draftsOf(doc.items).filter((d) => d.status !== 'open' && !told.has(d.id));
|
|
970
|
-
|
|
1231
|
+
const undone = stepsOf(doc.items).filter((x) => x.write?.status === 'undone' && !told.has(x.id));
|
|
1232
|
+
if (!news.length && !undone.length)
|
|
971
1233
|
return '';
|
|
972
|
-
doc.told = [...told, ...news.map((d) => d.id)];
|
|
973
|
-
|
|
1234
|
+
doc.told = [...told, ...news.map((d) => d.id), ...undone.map((x) => x.id)];
|
|
1235
|
+
const back = undone.length
|
|
1236
|
+
? `The person undid these changes you made: ${undone.map((x) => `${x.title} ${JSON.stringify(doc.writes?.[x.id]?.args ?? {}).slice(0, 300)}`).join('; ')}. Do not make them again unless asked.\n\n`
|
|
1237
|
+
: '';
|
|
1238
|
+
if (!news.length)
|
|
1239
|
+
return back;
|
|
1240
|
+
return `${back}Cards since your last answer:\n${news
|
|
974
1241
|
.map((d) => `- ${d.title} ${JSON.stringify(d.args).slice(0, 300)}: ${d.status}${d.error ? ` (${d.error})` : ''}${d.result !== undefined ? ` → ${JSON.stringify(d.result).slice(0, 300)}` : ''}`)
|
|
975
1242
|
.join('\n')}\n\n`;
|
|
976
1243
|
}
|
|
@@ -990,13 +1257,61 @@ export class Assistant {
|
|
|
990
1257
|
return `${answer}\n\n[Tools I used and what they returned]\n${lines.join('\n')}`;
|
|
991
1258
|
}
|
|
992
1259
|
// ---------------------------------------------------------------- keys
|
|
993
|
-
/**
|
|
994
|
-
async
|
|
1260
|
+
/** Starts a person's ChatGPT plan sign-in: the address to open in a new tab. */
|
|
1261
|
+
async startSignin(caller, provider) {
|
|
1262
|
+
if (PROVIDERS[provider].signin !== 'chatgpt')
|
|
1263
|
+
throw new AssistantError(`${PROVIDERS[provider].label} has no sign-in.`, 'no_signin', 404);
|
|
1264
|
+
const name = `signin:${provider}`;
|
|
1265
|
+
const { url, reg } = await startChatgpt(await this.state.docs.read(caller.id, name), this.name);
|
|
1266
|
+
await this.state.docs.write(caller.id, name, reg);
|
|
1267
|
+
return { url, redirect: CHATGPT_REDIRECT };
|
|
1268
|
+
}
|
|
1269
|
+
/** Finishes it from the address it ended on; the plan then stands in for the person's own key. */
|
|
1270
|
+
async finishSignin(caller, provider, pasted) {
|
|
1271
|
+
const name = `signin:${provider}`;
|
|
1272
|
+
try {
|
|
1273
|
+
const { tokens, reg } = await finishChatgpt(await this.state.docs.read(caller.id, name), pasted, this.o.fetch ?? fetch);
|
|
1274
|
+
await this.state.docs.write(caller.id, name, reg);
|
|
1275
|
+
const test = await testChatgpt(tokens, this.o.fetch ?? fetch);
|
|
1276
|
+
await this.state.docs.write(caller.id, keyDocName(provider), {
|
|
1277
|
+
api_key: tokens.access_token,
|
|
1278
|
+
saved_at: Date.now(),
|
|
1279
|
+
test,
|
|
1280
|
+
chatgpt: tokens,
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
catch (err) {
|
|
1284
|
+
if (err instanceof SigninError)
|
|
1285
|
+
throw new AssistantError(err.message, 'signin_failed');
|
|
1286
|
+
throw err;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
/** Tests a person's own key, refreshing a plan sign-in first. */
|
|
1290
|
+
async testOwn(caller, provider) {
|
|
1291
|
+
const d = await this.state.ownKey(caller.id, provider);
|
|
1292
|
+
if (!d)
|
|
1293
|
+
return null;
|
|
1294
|
+
if (!d.chatgpt)
|
|
1295
|
+
return this.testKey(provider, d.api_key);
|
|
1296
|
+
const fresh = await this.state.ownApiKey(caller.id, provider);
|
|
1297
|
+
const now = await this.state.ownKey(caller.id, provider);
|
|
1298
|
+
if (!fresh || !now?.chatgpt)
|
|
1299
|
+
return now?.test ?? { ok: false, detail: 'Sign in again.', at: Date.now() };
|
|
1300
|
+
return testChatgpt(now.chatgpt, this.o.fetch ?? fetch);
|
|
1301
|
+
}
|
|
1302
|
+
/** One free call (reading a model) that says whether the provider takes the key. */
|
|
1303
|
+
async testKey(provider, apiKey) {
|
|
995
1304
|
const at = Date.now();
|
|
996
|
-
const model = this.
|
|
1305
|
+
const model = this.models.find((m) => m.provider === provider);
|
|
1306
|
+
const label = PROVIDERS[provider].label;
|
|
1307
|
+
if (!model)
|
|
1308
|
+
return { ok: false, detail: `This app runs no ${label} models.`, at };
|
|
997
1309
|
try {
|
|
998
|
-
|
|
999
|
-
|
|
1310
|
+
if (provider === 'openai')
|
|
1311
|
+
await this.openai(apiKey).models.retrieve(model.id);
|
|
1312
|
+
else
|
|
1313
|
+
await this.anthropic(apiKey).models.retrieve(model.id);
|
|
1314
|
+
return { ok: true, detail: `${label} took the key.`, at };
|
|
1000
1315
|
}
|
|
1001
1316
|
catch (err) {
|
|
1002
1317
|
return { ok: false, detail: shownError(err).message, at };
|
|
@@ -1036,9 +1351,9 @@ export class Assistant {
|
|
|
1036
1351
|
const tools = await this.openTools(caller, p);
|
|
1037
1352
|
try {
|
|
1038
1353
|
const f = tools.fns.get(d.tool);
|
|
1039
|
-
if (!f
|
|
1354
|
+
if (!f || f.def.read) {
|
|
1040
1355
|
d.status = 'failed';
|
|
1041
|
-
d.error = 'Its tools are no longer on Read + draft in Settings ▸ Tools.';
|
|
1356
|
+
d.error = 'Its tools are no longer on Read + draft or Read + write in Settings ▸ Tools.';
|
|
1042
1357
|
}
|
|
1043
1358
|
else {
|
|
1044
1359
|
try {
|
|
@@ -1058,6 +1373,30 @@ export class Assistant {
|
|
|
1058
1373
|
await this.writeChat(caller, chatId, doc);
|
|
1059
1374
|
return d;
|
|
1060
1375
|
}
|
|
1376
|
+
/** Takes back one change a write made at once, through its group's `undo`. */
|
|
1377
|
+
async undoWrite(caller, chatId, stepId) {
|
|
1378
|
+
const doc = await this.state.docs.read(caller.id, `chat:${chatId}`);
|
|
1379
|
+
if (!doc)
|
|
1380
|
+
throw new AssistantError('That chat is gone.', 'not_found', 404);
|
|
1381
|
+
const step = stepsOf(doc.items).find((x) => x.id === stepId);
|
|
1382
|
+
const w = doc.writes?.[stepId];
|
|
1383
|
+
if (!step?.write || !w)
|
|
1384
|
+
throw new AssistantError('That change is gone.', 'not_found', 404);
|
|
1385
|
+
if (step.write.status === 'undone')
|
|
1386
|
+
throw new AssistantError('That change is already undone.', 'settled', 409);
|
|
1387
|
+
const g = this.groupById(w.group);
|
|
1388
|
+
if (!g?.undo)
|
|
1389
|
+
throw new AssistantError('That change cannot be undone here.', 'no_undo', 400);
|
|
1390
|
+
try {
|
|
1391
|
+
await withDeadline(`Undo ${step.title}`, 60_000, g.undo(caller, w));
|
|
1392
|
+
step.write = { ...step.write, status: 'undone' };
|
|
1393
|
+
}
|
|
1394
|
+
catch (err) {
|
|
1395
|
+
throw new AssistantError(err instanceof Error ? err.message.slice(0, 300) : 'Undo failed.', 'undo_failed', 502);
|
|
1396
|
+
}
|
|
1397
|
+
await this.writeChat(caller, chatId, doc);
|
|
1398
|
+
return step;
|
|
1399
|
+
}
|
|
1061
1400
|
}
|
|
1062
1401
|
export function createAssistant(o) {
|
|
1063
1402
|
return new Assistant(o);
|