Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Additional insertAt options, add support for postprocessors to templated prompts #68

Merged
merged 8 commits into from
Sep 9, 2024
Merged
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ test-space/
.DS_Store
silverbullet-ai.plug.js
docs/_public
docs/Library/Core
!docs/_plug
SECRETS.md
cov_profile
Expand Down
10 changes: 9 additions & 1 deletion docs/Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,19 @@ For the full changelog, please refer to the individual release notes on https://
This page is a brief overview of each version.

---
## Unreleased
## 0.4.0 (Unreleased)
- Use a separate queue for indexing embeddings and summaries, to prevent blocking the main SB indexing thread
- Refactor to use JSR for most Silverbullet imports, and lots of related changes
- Reduced bundle size
- Add support for [space-config](https://silverbullet.md/Space%20Config)
- Add support for [[Templated Prompts|Post Processor]] functions in [[Templated Prompts]].
- AICore Library: Updated all library files to have the meta tag.
- AICore Library: Add space-script functions to be used as post processors:
- **indentOneLevel** - Indent entire response one level deeper than the previous line.
- **removeDuplicateStart** - Remove the first line from the response if it matches the line before the response started.
- **convertToBulletList** - Convert response to a markdown list.
- **convertToTaskList** - Convert response to a markdown list of tasks.
- AICore Library: Add `aiSplitTodo` slash command and [[^Library/AICore/AIPrompt/AI Split Task]] templated prompt to split a task into smaller subtasks.

---
## 0.3.2
Expand Down
2 changes: 1 addition & 1 deletion docs/Library/AICore/AIPrompt/AI Create Space Script.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ aiprompt:

SilverBullet space script documentation:

[[!silverbullet.md/Space%20Script]]
[Space%20Script](https://silverbullet.md/Space%20Script)

Using the above documentation, please create a space-script following the users description in the note below. Output only valid markdown with a code block using space-script. No explanations, code in a markdown space-script block only. Must contain **silverbullet.registerFunction** or **silverbullet.registerCommand**. Use syscalls where available, but only if you know for sure they exist.

Expand Down
32 changes: 32 additions & 0 deletions docs/Library/AICore/AIPrompt/AI Split Task.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
tags:
- template
- aiPrompt
- meta

description: "Split current todo into smaller manageable chunks."
aiprompt:
description: "Split current todo into smaller manageable chunks."
slashCommand: aiSplitTodo
chat: true
enrichMessages: true
insertAt: new-line-below
postProcessors:
- convertToBulletList
- convertToTaskList
- removeDuplicateStart
- indentOneLevel
---

**user**: [enrich:false] I’ll provide the note contents, and instructions.
**assistant**: What is the note title?
**user**: [enrich:true] {{@page.name}}
**assistant**: What are the note contents?
**user**: [enrich:true]
{{@currentPageText}}
**assistant**: What is the parent item the user is looking at?
**user**: [enrich:true] {{@parentItemText}}
**assistant**: What is the current item the user is looking at? Include the parent task if appropriate.
**user**: [enrich:true] {{@currentItemText}}
**assistant**: What are the instructions?
**user**: [enrich:false] Split the current task into smaller, more manageable, and well-defined tasks. Return one task per line. Keep the list of new tasks small. DO NOT return any existing items.
justyns marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion docs/Library/AICore/New Page/AI New Chat.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ frontmatter:

**assistant**: Hello, how can I help you?

**user**: |^|
**user**: |^|
1 change: 1 addition & 0 deletions docs/Library/AICore/Space Script/AI Query LLM.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
tags:
- spacescript
- meta

description: >
This space script allows you to use `{{queryAI(userPrompt, systemPrompt)}}` inside of a template.
Expand Down
1 change: 1 addition & 0 deletions docs/Library/AICore/Space Script/AI Search Embeddings.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
tags:
- spacescript
- meta

description: >
This space script allows you to use `{{searchEmbeddings(query)}}` inside of a template. A string
Expand Down
34 changes: 34 additions & 0 deletions docs/Library/AICore/Space Script/Convert to bullets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
tags:
- spacescript
- meta

description: >
This space script allows takes a string and converts each line to a bullet item in a list, if it is not already.
---


```space-script
silverbullet.registerFunction({ name: "convertToBulletList" }, async (data) => {
const { response, lineBefore, lineAfter } = data;
const lines = response.split('\n');

// Get the indentation level of the line before
const indentationMatch = lineBefore.match(/^\s*/);
const indentation = indentationMatch ? indentationMatch[0] : '';

const bulletLines = lines.map(line => {
// Trim the line and add the indentation back
const trimmedLine = `${indentation}${line.trim()}`;

// Add a bullet if the line doesn't already start with one
if (!trimmedLine.trim().startsWith('- ')) {
return `- ${trimmedLine.trim()}`;
}
return trimmedLine;
});

const result = bulletLines.join('\n');
return result;
});
```
28 changes: 28 additions & 0 deletions docs/Library/AICore/Space Script/Convert to task list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
tags:
- spacescript
- meta

description: >
This space script takes a string, and makes sure each line is a markdown task.
---

```space-script
silverbullet.registerFunction({ name: "convertToTaskList" }, async (data) => {
const { response } = data;
const lines = response.split('\n');
const result = lines.map(line => {
if (/^\s*-\s*\[\s*[xX]?\s*\]/.test(line)) {
// Already a task
return line.trim();
}
if (/^\s*-/.test(line)) {
// bullet, but not a task
return `- [ ] ${line.slice(1).trim()}`;
}
// everything else, should be a non list item
return `- [ ] ${line.trim()}`;
}).join('\n');
return result;
});
```
36 changes: 36 additions & 0 deletions docs/Library/AICore/Space Script/Indent lines.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
tags:
- spacescript
- meta

description: >
This space script allows takes a string and indents each line one level, compared to the lineBefore.
---

```space-script
silverbullet.registerFunction({ name: "indentOneLevel" }, async (data) => {
const { response, lineBefore, lineCurrent } = data;
console.log(data);

// Function to determine the indentation of a line
const getIndentation = (line) => line.match(/^\s*/)[0];

// Determine the maximum indentation of lineBefore and lineCurrent
const maxIndentation = getIndentation(lineBefore).length > getIndentation(lineCurrent).length
? getIndentation(lineBefore)
: getIndentation(lineCurrent);

// Define additional indentation level
const additionalIndentation = ' ';

// Compute new indentation
const newIndentation = maxIndentation + additionalIndentation;

// Apply new indentation to all lines in the response
const indentedLines = response.split('\n').map(line => `${newIndentation}${line.trim()}`).join('\n');

console.log("indentedLines:", indentedLines);

return indentedLines;
});
Comment on lines +10 to +35
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well-implemented function with clear documentation.

The indentOneLevel function is well-documented and logically structured. It effectively uses regular expressions and string manipulation to indent lines based on the maximum indentation of two reference lines.

Suggestion for Improvement:
Consider adding error handling for potential edge cases, such as null or undefined inputs, to enhance robustness.

```
25 changes: 25 additions & 0 deletions docs/Library/AICore/Space Script/Remove Duplicate Start.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
tags:
- spacescript
- meta

description: >
This space script checks lineBefore against the first line of the response and deletes it if its a duplicate.
---


```space-script
silverbullet.registerFunction({ name: "removeDuplicateStart" }, async (data) => {
console.log(data);
const { response, lineBefore, lineCurrent } = data;
const lines = response.split('\n');

// Check if the first line matches either the previous or current line, and remove it if it does
if ((lines[0].trim() == lineBefore.trim()) || (lines[0].trim() == lineCurrent.trim())) {
lines.shift();
}
console.log(lines);

return lines.join('\n');
});
```
78 changes: 76 additions & 2 deletions docs/Templated Prompts.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ To be a templated prompt, the note must have the following frontmatter:
- Optionally, `aiprompt.systemPrompt` can be specified to override the system prompt
- Optionally, `aiprompt.chat` can be specified to treat the template as a multi-turn chat instead of single message
- Optionally, `aiprompt.enrichMessages` can be set to true to enrich each chat message
-
- Optionally, `aiprompt.postProcessors` can be set to a list of space-script function names to manipulate text returned by the llm

For example, here is a templated prompt to summarize the current note and insert the summary at the cursor:

Expand Down Expand Up @@ -65,6 +65,23 @@ Everything below is the content of the note:
{{readPage(@page.ref)}}
```


## Template Metadata

As of version 0.4.0, the following global metadata is available for use inside of an aiPrompt template:

* **`page`**: Metadata about the current page.
* **`currentItemBounds`**: Start and end positions of the current item. An item may be a bullet point or task.
* **`currentItemText`**: Full text of the current item.
* **`currentLineNumber`**: Line number of the current cursor position.
* **`lineStartPos`**: Starting character position of the current line.
* **`lineEndPos`**: Ending character position of the current line.
* **`currentPageText`**: Entire text of the current page.
* **`parentItemBounds`**: Start and end positions of the parent item.
* **`parentItemText`**: Full text of the parent item. A parent item may contain child items.

All of these can be accessed by prefixing the variable name with `@`, like `@lineEndPos` or `@currentLineNumber`.

## Chat-style prompts

As of version 0.3.0, `aiprompt.chat` can be set to true in the template frontmatter to treat the template similar to a page using [[Commands/AI: Chat on current page]].
Expand Down Expand Up @@ -96,4 +113,61 @@ Everything below is the content of the note:

These messages will be parsed into multiple chat messages when calling the LLM’s api. Only the response from the LLM will be included in the note where the template is triggered from.

The `enrich` attribute can also be toggled on or off per message. By default it is either disabled or goes off of the `aiPrompt.enrichMessages` frontmatter attribute. Assistant and system messages are never enriched.
The `enrich` attribute can also be toggled on or off per message. By default it is either disabled or goes off of the `aiPrompt.enrichMessages` frontmatter attribute. Assistant and system messages are never enriched.

## Post Processors

As of version 0.4.0, `aiPrompt.postProcessors` can be set to a list of space-script function names like in the example below. Once the LLM finishes streaming its response, the entire response will be sent to each post processor function in order.

Each function must accept a single data parameter. Currently, the parameter follows this typing:

```javascript
export type PostProcessorData = {
// The full response text
response: string;
// The line before where the response was inserted
lineBefore: string;
// The line after where the response was inserted
lineAfter: string;
// The line where the cursor was before the response was inserted
lineCurrent: string;
};
```

A simple post processing function looks like this:

```javascript
silverbullet.registerFunction({ name: "aiFooBar" }, async (data) => {

// Extract variables from PostProcessorData
const { response, lineBefore, lineCurrent, lineAfter } = data;

// Put the current response between FOO and BAR and return it
const newResponse = `FOO ${response} BAR`;
return newResponse
}
```

This function could be used in a template prompt like this:

```yaml
---
tags:
- template
- aiPrompt
- meta

description: "Generate a random pet name"
aiprompt:
description: "Generate a random pet name."
slashCommand: aiGeneratePetName
insertAt: cursor
postProcessors:
- aiFooBar
---

Generate a random name for a pet. Only generate a single name. Return nothing but that name.
```

Running this prompt, the LLM may return `Henry` as the name and then aiFooBar will transform it into `FOO Henry BAR` which is what will ultimately be placed in the note the templated was executed from.

4 changes: 4 additions & 0 deletions scripts/create-test-space.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ cd "$spacedir"/_plug
ln -sv ../../silverbullet-ai.plug.js* .
cd -

cd "$spacedir"
ln -sv ../docs/Library .
cd -

# This is a local file outside of the sbai directory
cp -v ../test-spaces/SECRETS.md "$spacedir"/
cp -v ../test-spaces/SETTINGS.md "$spacedir"/
41 changes: 37 additions & 4 deletions src/editorUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { editor } from "@silverbulletmd/silverbullet/syscalls";

async function getSelectedText() {
export async function getSelectedText() {
const selectedRange = await editor.getSelection();
let selectedText = "";
if (selectedRange.from === selectedRange.to) {
Expand All @@ -17,7 +17,7 @@ async function getSelectedText() {
};
}

async function getSelectedTextOrNote() {
export async function getSelectedTextOrNote() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logical extension of text selection functionality.

The getSelectedTextOrNote function effectively extends the getSelectedText function by adding the capability to determine if the entire note is selected. This is useful for features that need to differentiate between partial and full note selections.

Suggestion for Improvement:
Consider simplifying the condition for isWholeNote to enhance readability and maintainability.

const selectedTextInfo = await getSelectedText();
const pageText = await editor.getText();
if (selectedTextInfo.text === "") {
Expand All @@ -36,9 +36,42 @@ async function getSelectedTextOrNote() {
};
}

async function getPageLength() {
export async function getPageLength() {
const pageText = await editor.getText();
return pageText.length;
}

export { getPageLength, getSelectedText, getSelectedTextOrNote };
export function getLineNumberAtPos(text: string, pos: number): number {
const lines = text.split("\n");
let currentPos = 0;
for (let i = 0; i < lines.length; i++) {
if (currentPos <= pos && pos < currentPos + lines[i].length + 1) {
return i;
}
currentPos += lines[i].length + 1; // +1 for the newline character
}
return -1;
}
Comment on lines +44 to +54
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Efficient function for calculating line numbers.

The getLineNumberAtPos function is well-implemented and efficiently calculates the line number corresponding to a given position in the text. This function is crucial for features that need to manipulate text based on line numbers.

Suggestion for Improvement:
Consider optimizing the loop to reduce the number of iterations for large texts, potentially using a binary search approach if the lines are of relatively uniform length.


export function getLine(text: string, lineNumber: number): string {
const lines = text.split("\n");
if (lineNumber < 0 || lineNumber >= lines.length) {
return "";
}
return lines[lineNumber];
}

export function getLineOfPos(text: string, pos: number): string {
const lineNumber = getLineNumberAtPos(text, pos);
return getLine(text, lineNumber);
}

export function getLineBefore(text: string, pos: number): string {
const lineNumber = getLineNumberAtPos(text, pos);
return getLine(text, lineNumber - 1);
}

export function getLineAfter(text: string, pos: number): string {
const lineNumber = getLineNumberAtPos(text, pos);
return getLine(text, lineNumber + 1);
}
Comment on lines +69 to +77
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Useful functions for navigating text based on position.

The getLineBefore and getLineAfter functions effectively leverage getLineNumberAtPos and getLine to retrieve the lines before and after a specific position. These functions are crucial for features that need to navigate text based on its position within the editor.

Suggestion for Improvement:
Consider adding error handling for cases where the position is at the start or end of the text to prevent potential out-of-range errors.

Loading