Skip to content

Commit

Permalink
Merge pull request #68 from justyns/template-options
Browse files Browse the repository at this point in the history
Additional insertAt options, add support for postprocessors to templated prompts
  • Loading branch information
justyns authored Sep 9, 2024
2 parents 65eaa1b + 8179126 commit 47a1e42
Show file tree
Hide file tree
Showing 20 changed files with 480 additions and 27 deletions.
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.
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;
});
```
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() {
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;
}

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);
}
Loading

0 comments on commit 47a1e42

Please sign in to comment.