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

WIP: templates #30

Merged
merged 17 commits into from
Jun 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ["myst_parser"]
myst_enable_extensions = ["colon_fence"]

# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ maxdepth: 3
---
setup
usage
templates
logging
contributing
changelog
Expand Down
1 change: 1 addition & 0 deletions docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Keys can be persisted in a file that is used by the tool. This file is called `k
```
llm keys path
```
On macOS this will be `~/Library/Application Support/io.datasette.llm/keys.json`. On Linux it may be something like `~/.config/io.datasette.llm/keys.json`.

Rather than editing this file directly, you can instead add keys to it using the `llm keys set` command.

Expand Down
147 changes: 147 additions & 0 deletions docs/templates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# Prompt templates

Prompt templates can be created to reuse useful prompts with different input data.

## Getting started

Here's a template for summarizing text:

```yaml
'Summarize this: $input'
```
This is a string of YAML - hence the single quotes. The `$input` token will be replaced by extra content provided to the template when it is executed.

To create this template with the name `summary` run the following:

```
llm templates edit summary
```
This will open the system default editor.

:::{tip}
You can control which editor will be used here using the `EDITOR` environment variable - for example, to use VS Code:

export EDITOR="code -w"

Add that to your `~/.zshrc` or `~/.bashrc` file depending on which shell you use (`zsh` is the default on macOS since macOS Catalina in 2019).
:::

You can also create a file called `summary.yaml` in the folder shown by running `llm templates path`, for example:
```bash
$ llm templates path
/Users/simon/Library/Application Support/io.datasette.llm/templates
```

## Using a template

You can execute a named template using the `-t/--template` option:

```bash
curl -s https://example.com/ | llm -t summary
```

This can be combined with the `-m` option to specify a different model:
```bash
curl -s https://llm.datasette.io/en/latest/ | \
llm -t summary -m gpt-3.5-turbo-16k
```
## Longer prompts

You can also represent this template as a YAML dictionary with a `prompt:` key, like this one:

```yaml
prompt: 'Summarize this: $input'
```
Or use YAML multi-line strings for longer inputs. I created this using `llm templates edit steampunk`:
```yaml
prompt: >
Summarize the following text.

Insert frequent satirical steampunk-themed illustrative anecdotes.
Really go wild with that.

Text to summarize: $input
```
The `prompt: >` causes the following indented text to be treated as a single string, with newlines collapsed to spaces. Use `prompt: |` to preserve newlines.

Running that with `llm -t steampunk` against GPT-4 (via [strip-tags](https://github.com/simonw/strip-tags) to remove HTML tags from the input and minify whitespace):
```bash
curl -s 'https://til.simonwillison.net/macos/imovie-slides-and-audio' | \
strip-tags -m | llm -t steampunk -m 4
```
Output:
> In a fantastical steampunk world, Simon Willison decided to merge an old MP3 recording with slides from the talk using iMovie. After exporting the slides as images and importing them into iMovie, he had to disable the default Ken Burns effect using the "Crop" tool. Then, Simon manually synchronized the audio by adjusting the duration of each image. Finally, he published the masterpiece to YouTube, with the whimsical magic of steampunk-infused illustrations leaving his viewers in awe.

## System templates

Templates are YAML files. A template can contain a single string, as shown above, which will then be treated as the prompt.

When working with models that support system prompts (such as `gpt-3.5-turbo` and `gpt-4`) you can instead set a system prompt using a `system:` key like so:

```yaml
system: Summarize this
```
If you specify only a system prompt you don't need to use the `$input` variable - `llm` will use the user's input as the whole of the regular prompt, which will then be processed using the instructions set in that system prompt.

You can combine system and regular prompts like so:

```yaml
system: You speak like an excitable Victorian adventurer
prompt: 'Summarize this: $input'
```

## Additional template variables

Templates that work against the user's normal input (content that is either piped to the tool via standard input or passed as a command-line argument) use just the `$input` variable.

You can use additional named variables. These will then need to be provided using the `-p/--param` option when executing the template.

Here's an example template called `recipe`, created using `llm templates edit recipe`:

```yaml
prompt: |
Suggest a recipe using ingredients: $ingredients

It should be based on cuisine from this country: $country
```
This can be executed like so:

```bash
llm -t recipe -p ingredients 'sausages, milk' -p country Germany
```
My output started like this:
> Recipe: German Sausage and Potato Soup
>
> Ingredients:
> - 4 German sausages
> - 2 cups whole milk

This example combines input piped to the tool with additional parameters. Call this `summarize`:

```yaml
system: Summarize this text in the voice of $voice
```
Then to run it:
```bash
curl -s 'https://til.simonwillison.net/macos/imovie-slides-and-audio' | \
strip-tags -m | llm -t summarize -p voice GlaDOS
```
I got this:

> My previous test subject seemed to have learned something new about iMovie. They exported keynote slides as individual images [...] Quite impressive for a human.

## Setting a default model for a template

Templates executed using `llm -t template-name` will execute using the default model that the user has configured for the tool - or `gpt-3.5-turbo` if they have not configured their own default.

You can specify a new default model for a template using the `model:` key in the associated YAML. Here's a template called `roast`:

```yaml
model: gpt-4
system: roast the user at every possible opportunity, be succinct
```
Example:
```bash
llm -t roast 'How are you today?'
```
> I'm doing great but with your boring questions, I must admit, I've seen more life in a cemetery.
48 changes: 48 additions & 0 deletions llm/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from pydantic import BaseModel
import string
from typing import Optional


class Template(BaseModel):
name: str
prompt: Optional[str]
system: Optional[str]
model: Optional[str]

class Config:
extra = "forbid"

class MissingVariables(Exception):
pass

def execute(self, input, params=None):
params = params or {}
params["input"] = input
if not self.prompt:
system = self.interpolate(self.system, params)
prompt = input
else:
prompt = self.interpolate(self.prompt, params)
system = self.interpolate(self.system, params)
return prompt, system

@classmethod
def interpolate(cls, text, params):
if not text:
return text
# Confirm all variables in text are provided
string_template = string.Template(text)
vars = cls.extract_vars(string_template)
missing = [p for p in vars if p not in params]
if missing:
raise cls.MissingVariables(
"Missing variables: {}".format(", ".join(missing))
)
return string_template.substitute(**params)

@staticmethod
def extract_vars(string_template):
return [
match.group("named")
for match in string_template.pattern.finditer(string_template.template)
]
Loading