Skip to content

Commit e1f82ea

Browse files
committed
docs / type fixes
1 parent 49145eb commit e1f82ea

File tree

5 files changed

+214
-44
lines changed

5 files changed

+214
-44
lines changed

docs/pages/docs/ai/reference.mdx

Lines changed: 175 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ type AIExtensionOptions = {
2121
stream?: boolean;
2222
/**
2323
* The default data format to use for LLM calls
24-
* "html" is recommended, the other formats are experimental
25-
* @default html
24+
* html format is recommended, the other formats are experimental
25+
* @default llmFormats.html
2626
*/
2727
dataFormat?: "html" | "json" | "markdown";
2828
/**
@@ -56,30 +56,46 @@ class AIExtension {
5656
/**
5757
* Execute a call to an LLM and apply the result to the editor
5858
*/
59-
callLLM(opts: CallSpecificCallLLMOptions): Promise<void>;
59+
callLLM(
60+
opts: MakeOptional<LLMRequestOptions, "model">,
61+
): Promise<LLMResponse | undefined>;
6062
/**
6163
* Returns a read-only zustand store with the state of the AI Menu
6264
*/
6365
get store(): ReadonlyStoreApi<{
6466
aiMenuState:
65-
| {
67+
| ({
68+
/**
69+
* The ID of the block that the AI menu is open at
70+
* this changes as the AI is making changes to the document
71+
*/
6672
blockId: string;
67-
status:
68-
| "user-input"
69-
| "thinking"
70-
| "ai-writing"
71-
| "error"
72-
| "user-reviewing";
73-
}
73+
} & (
74+
| {
75+
status: "error";
76+
error: any;
77+
}
78+
| {
79+
status:
80+
| "user-input"
81+
| "thinking"
82+
| "ai-writing"
83+
| "user-reviewing";
84+
}
85+
))
7486
| "closed";
87+
/**
88+
* The previous response from the LLM, used for multi-step LLM calls
89+
*/
90+
llmResponse?: LLMResponse;
7591
}>;
7692
/**
7793
* Returns a zustand store with the global configuration of the AI Extension,
7894
* these options are used as default across all LLM calls when calling {@link callLLM}
7995
*/
8096
readonly options: StoreApi<{
8197
model: LanguageModel;
82-
dataFormat: "html" | "json" | "markdown";
98+
dataFormat: LLMFormat;
8399
stream: boolean;
84100
promptBuilder?: PromptBuilder;
85101
}>;
@@ -100,6 +116,12 @@ class AIExtension {
100116
* Reject the changes made by the LLM
101117
*/
102118
rejectChanges(): void;
119+
/**
120+
* Retry the previous LLM call.
121+
*
122+
* Only valid if the current status is "error"
123+
*/
124+
retry(): Promise<void>;
103125
/**
104126
* Update the status of a call to an LLM
105127
*
@@ -112,24 +134,44 @@ class AIExtension {
112134
| "user-input"
113135
| "thinking"
114136
| "ai-writing"
115-
| "error"
116-
| "user-reviewing",
137+
| "user-reviewing"
138+
| {
139+
status: "error";
140+
error: any;
141+
},
117142
): void;
118143
}
119144
```
120145

121-
## `callLLM`
146+
### `LLMRequestOptions`
147+
148+
Requests an LLM are made by calling `callLLM` on the `AIExtension` object.
122149

123150
```typescript
124-
type CallLLMOptions = {
151+
type LLMRequestOptions = {
125152
/**
126153
* The language model to use for the LLM call (AI SDK)
154+
*
155+
* (when invoking `callLLM` via the `AIExtension` this will default to the
156+
* model set in the `AIExtension` options)
127157
*/
128158
model: LanguageModelV1;
129159
/**
130160
* The user prompt to use for the LLM call
131161
*/
132162
userPrompt: string;
163+
/**
164+
* Previous response from the LLM, used for multi-step LLM calls
165+
*
166+
* (populated automatically when invoking `callLLM` via the `AIExtension` class)
167+
*/
168+
previousResponse?: LLMResponse;
169+
/**
170+
* The default data format to use for LLM calls
171+
* "html" is recommended, the other formats are experimental
172+
* @default html format (`llm.html`)
173+
*/
174+
dataFormat?: LLMFormat;
133175
/**
134176
* The `PromptBuilder` to use for the LLM call
135177
*
@@ -199,14 +241,129 @@ type CallLLMOptions = {
199241
*/
200242
withDelays?: boolean;
201243
/**
202-
* Additional options to pass to the `generateObject` function
244+
* Additional options to pass to the AI SDK `generateObject` function
203245
* (only used when `stream` is `false`)
204246
*/
205247
_generateObjectOptions?: Partial<Parameters<typeof generateObject<any>>[0]>;
206248
/**
207-
* Additional options to pass to the `streamObject` function
249+
* Additional options to pass to the AI SDK `streamObject` function
208250
* (only used when `stream` is `true`)
209251
*/
210252
_streamObjectOptions?: Partial<Parameters<typeof streamObject<any>>[0]>;
211253
};
212254
```
255+
256+
## (advanced) `doLLMRequest`
257+
258+
The `CallLLM` function automatically passes the default options set in the `AIExtension` to the LLM request.
259+
It also handles the LLM response and the AI menu state
260+
261+
For advanced use cases, you can also directly use the `doLLMRequest` to issue an LLM request.
262+
In this case, you will need to manually handle the response.
263+
264+
```typescript
265+
/**
266+
* Execute an LLM call
267+
*
268+
* @param editor - The BlockNoteEditor the LLM should operate on
269+
* @param opts - The options for the LLM call (@link {CallLLMOptions})
270+
* @returns A `LLMResponse` object containing the LLM response which can be applied to the editor
271+
*/
272+
function doLLMRequest(
273+
editor: BlockNoteEditor<any, any, any>,
274+
opts: LLMRequestOptions,
275+
): Promise<LLMResponse>;
276+
```
277+
278+
Call `execute` on the `LLMResponse` object to apply the changes to the editor.
279+
280+
## PromptBuilder (advanced)
281+
282+
The `PromptBuilder` is a function that takes a BlockNoteEditor and details about the user's prompt
283+
and turns it into an array of CoreMessage objects (AI SDK) to be passed to the LLM.
284+
285+
Providing a custom `PromptBuilder` allows fine-grained control over the instructions sent to the LLM.
286+
To implement a custom `PromptBuilder`, you can use the `promptHelpers` provided by the LLM format.
287+
We recommend looking at the [default PromptBuilder](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-ai/src/api/formats/html-blocks/defaultHTMLPromptBuilder.ts) implementations as a starting point to implement your own.
288+
289+
```typescript
290+
/**
291+
* The input passed to a PromptBuilder
292+
*/
293+
type PromptBuilderInput = {
294+
/**
295+
* The user's prompt
296+
*/
297+
userPrompt: string;
298+
/**
299+
* The selection of the editor which the LLM should operate on
300+
*/
301+
selectedBlocks?: Block<any, any, any>[];
302+
/**
303+
* The ids of blocks that should be excluded from the prompt
304+
* (e.g.: if `deleteEmptyCursorBlock` is true in the LLMRequest,
305+
* this will be the id of the block that should be ignored)
306+
*/
307+
excludeBlockIds?: string[];
308+
/**
309+
* When following a multi-step conversation, or repairing a previous error,
310+
* the previous messages that have been sent to the LLM
311+
*/
312+
previousMessages?: Array<CoreMessage>;
313+
};
314+
315+
/**
316+
* A PromptBuilder is a function that takes a BlockNoteEditor and details about the user's promot
317+
* and turns it into an array of CoreMessage (AI SDK) to be passed to the LLM.
318+
*/
319+
type PromptBuilder = (
320+
editor: BlockNoteEditor<any, any, any>,
321+
opts: PromptBuilderInput,
322+
) => Promise<Array<CoreMessage>>;
323+
```
324+
325+
## Formats
326+
327+
When a LLM is called, the LLM needs to interpret the document, and invoke operations to modify the document.
328+
Different models might be able to understand different data formats better.
329+
By default, BlockNote and LLM models interoperate using a HTML based format. We also provide experimental JSON and Markdown based formats.
330+
331+
```typescript
332+
type LLMFormat = {
333+
/**
334+
* Function to get th format specific stream tools that the LLM can choose to invoke
335+
*/
336+
getStreamTools: (
337+
editor: BlockNoteEditor<any, any, any>,
338+
withDelays: boolean,
339+
defaultStreamTools?: {
340+
add?: boolean;
341+
update?: boolean;
342+
delete?: boolean;
343+
},
344+
selectionInfo?: {
345+
from: number;
346+
to: number;
347+
},
348+
onBlockUpdate?: (blockId: string) => void,
349+
) => StreamTool<any>[];
350+
/**
351+
* The default PromptBuilder that determines how a userPrompt is converted to an array of
352+
* LLM Messages (CoreMessage[])
353+
*/
354+
defaultPromptBuilder: PromptBuilder;
355+
/**
356+
* Helper functions which can be used when implementing a custom PromptBuilder.
357+
* The signature depends on the specific format
358+
*/
359+
promptHelpers: any;
360+
};
361+
362+
// The default LLMFormats are exported under `llmFormats`:
363+
364+
export const llmFormats = {
365+
_experimental_json,
366+
_experimental_markdown,
367+
html,
368+
};
369+
```

packages/xl-ai/src/AIExtension.ts

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,18 @@ type ReadonlyStoreApi<T> = Pick<
2626
>;
2727

2828
type AIPluginState = {
29-
/**
30-
* zustand design considerations:
31-
* - moved this to a nested object to have better typescript typing
32-
* - if we'd do this without a nested object, then we could easily set "wrong" values,
33-
* because "setState" takes a partial object (unless the second parameter "replace" = true),
34-
* and thus we'd lose typescript's typing help
35-
*
36-
*/
29+
// zustand design considerations:
30+
// - moved this to a nested object to have better typescript typing
31+
// - if we'd do this without a nested object, then we could easily set "wrong" values,
32+
// because "setState" takes a partial object (unless the second parameter "replace" = true),
33+
// and thus we'd lose typescript's typing help
34+
3735
aiMenuState:
3836
| ({
37+
/**
38+
* The ID of the block that the AI menu is open at
39+
* this changes as the AI is making changes to the document
40+
*/
3941
blockId: string;
4042
} & (
4143
| {
@@ -64,8 +66,8 @@ type GlobalLLMRequestOptions = {
6466
model: LanguageModel;
6567
/**
6668
* The default data format to use for LLM calls
67-
* "html" is recommended, the other formats are experimental
68-
* @default html
69+
* html format is recommended, the other formats are experimental
70+
* @default llmFormats.html
6971
*/
7072
dataFormat?: LLMFormat;
7173
/**
@@ -80,12 +82,10 @@ type GlobalLLMRequestOptions = {
8082
promptBuilder?: PromptBuilder;
8183
};
8284

83-
type CallSpecificLLMRequestOptions = MakeOptional<LLMRequestOptions, "model">;
84-
8585
const PLUGIN_KEY = new PluginKey(`blocknote-ai-plugin`);
8686

8787
export class AIExtension extends BlockNoteExtension {
88-
private previousRequestOptions: CallSpecificLLMRequestOptions | undefined;
88+
private previousRequestOptions: LLMRequestOptions | undefined;
8989

9090
public static name(): string {
9191
return "ai";
@@ -297,9 +297,9 @@ export class AIExtension extends BlockNoteExtension {
297297
/**
298298
* Execute a call to an LLM and apply the result to the editor
299299
*/
300-
public async callLLM(opts: CallSpecificLLMRequestOptions) {
300+
public async callLLM(opts: MakeOptional<LLMRequestOptions, "model">) {
301301
this.setAIResponseStatus("thinking");
302-
302+
let ret: LLMResponse | undefined;
303303
try {
304304
const requestOptions = {
305305
...this.options.getState(),
@@ -308,10 +308,11 @@ export class AIExtension extends BlockNoteExtension {
308308
};
309309
this.previousRequestOptions = requestOptions;
310310

311-
const ret = await doLLMRequest(this.editor, {
311+
ret = await doLLMRequest(this.editor, {
312312
...requestOptions,
313313
onStart: () => {
314314
this.setAIResponseStatus("ai-writing");
315+
opts.onStart?.();
315316
},
316317
onBlockUpdate: (blockId: string) => {
317318
// NOTE: does this setState with an anon object trigger unnecessary re-renders?
@@ -321,6 +322,7 @@ export class AIExtension extends BlockNoteExtension {
321322
status: "ai-writing",
322323
},
323324
});
325+
opts.onBlockUpdate?.(blockId);
324326
},
325327
});
326328

@@ -339,6 +341,7 @@ export class AIExtension extends BlockNoteExtension {
339341
// eslint-disable-next-line no-console
340342
console.warn("Error calling LLM", e);
341343
}
344+
return ret;
342345
}
343346
}
344347

packages/xl-ai/src/api/LLMRequest.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import { LLMFormat } from "./index.js";
1313
export type LLMRequestOptions = {
1414
/**
1515
* The language model to use for the LLM call (AI SDK)
16+
*
17+
* (when invoking `callLLM` via the `AIExtension` this will default to the
18+
* model set in the `AIExtension` options)
1619
*/
1720
model: LanguageModelV1;
1821
/**
@@ -21,6 +24,8 @@ export type LLMRequestOptions = {
2124
userPrompt: string;
2225
/**
2326
* Previous response from the LLM, used for multi-step LLM calls
27+
*
28+
* (populated automatically when invoking `callLLM` via the `AIExtension` class)
2429
*/
2530
previousResponse?: LLMResponse;
2631
/**
@@ -98,19 +103,19 @@ export type LLMRequestOptions = {
98103
*/
99104
withDelays?: boolean;
100105
/**
101-
* Additional options to pass to the `generateObject` function
106+
* Additional options to pass to the AI SDK `generateObject` function
102107
* (only used when `stream` is `false`)
103108
*/
104109
_generateObjectOptions?: Partial<Parameters<typeof generateObject<any>>[0]>;
105110
/**
106-
* Additional options to pass to the `streamObject` function
111+
* Additional options to pass to the AI SDK `streamObject` function
107112
* (only used when `stream` is `true`)
108113
*/
109114
_streamObjectOptions?: Partial<Parameters<typeof streamObject<any>>[0]>;
110115
};
111116

112117
/**
113-
* Execute an LLM call and apply the result to the editor
118+
* Execute an LLM call
114119
*
115120
* @param editor - The BlockNoteEditor the LLM should operate on
116121
* @param opts - The options for the LLM call (@link {CallLLMOptions})

0 commit comments

Comments
 (0)