Support fragments in chat through -f, --sf, and !fragments (#1048)

* add vscode and uv artifacts to gitignore
* !fragment feature
* Update fragment command to accept multiple fragments
* Add support for multiple fragments in !fragment command, factor out fragment processing
* Add support for fragment/system fragment arguments in chat command
* update docs

---------

Co-authored-by: Simon Willison <swillison@gmail.com>
This commit is contained in:
Dan Turkel 2025-05-24 02:31:16 -04:00 committed by GitHub
parent 4281fd5101
commit 88d3e11c65
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 177 additions and 23 deletions

2
.gitignore vendored
View file

@ -8,3 +8,5 @@ venv
*.egg-info *.egg-info
.DS_Store .DS_Store
.idea/ .idea/
.vscode/
uv.lock

View file

@ -39,6 +39,42 @@ This will read the contents of `setup.py` from standard input and use it as a fr
Fragments can also be used as part of your system prompt. Use `--sf value` or `--system-fragment value` instead of `-f`. Fragments can also be used as part of your system prompt. Use `--sf value` or `--system-fragment value` instead of `-f`.
## Using fragments in chat
The `chat` command also supports the `-f` and `--sf` arguments to start a chat with fragments.
```bash
llm chat -f my_doc.txt
Chatting with gpt-4
Type 'exit' or 'quit' to exit
Type '!multi' to enter multiple lines, then '!end' to finish
Type '!edit' to open your default editor and modify the prompt.
Type '!fragment <my_fragment> [<another_fragment> ...]' to insert one or more fragments
> Explain this document to me
```
Fragments can also be added *during* a chat conversation using the `!fragment <my_fragment>` command.
```bash
Chatting with gpt-4
Type 'exit' or 'quit' to exit
Type '!multi' to enter multiple lines, then '!end' to finish
Type '!edit' to open your default editor and modify the prompt.
Type '!fragment <my_fragment> [<another_fragment> ...]' to insert one or more fragments
> !fragment https://llm.datasette.io/en/stable/fragments.html
```
This can be combined with `!multi`:
```bash
> !multi
Explain the difference between fragments and templates to me
!fragment https://llm.datasette.io/en/stable/fragments.html https://llm.datasette.io/en/stable/templates.html
!end
```
Additionally, any `!fragment` lines found in a prompt created with `!edit` will also be parsed.
(fragments-browsing)= (fragments-browsing)=
## Browsing fragments ## Browsing fragments

View file

@ -163,24 +163,27 @@ Usage: llm chat [OPTIONS]
Hold an ongoing chat with a model. Hold an ongoing chat with a model.
Options: Options:
-s, --system TEXT System prompt to use -s, --system TEXT System prompt to use
-m, --model TEXT Model to use -m, --model TEXT Model to use
-c, --continue Continue the most recent conversation. -c, --continue Continue the most recent conversation.
--cid, --conversation TEXT Continue the conversation with the given ID. --cid, --conversation TEXT Continue the conversation with the given ID.
-t, --template TEXT Template to use -f, --fragment TEXT Fragment (alias, URL, hash or file path) to add
-p, --param <TEXT TEXT>... Parameters for template to the prompt
-o, --option <TEXT TEXT>... key/value options for the model --sf, --system-fragment TEXT Fragment to add to system prompt
-d, --database FILE Path to log database -t, --template TEXT Template to use
--no-stream Do not stream output -p, --param <TEXT TEXT>... Parameters for template
--key TEXT API key to use -o, --option <TEXT TEXT>... key/value options for the model
-T, --tool TEXT Name of a tool to make available to the model -d, --database FILE Path to log database
--functions TEXT Python code block or file path defining functions --no-stream Do not stream output
to register as tools --key TEXT API key to use
--td, --tools-debug Show full details of tool executions -T, --tool TEXT Name of a tool to make available to the model
--ta, --tools-approve Manually approve every tool execution --functions TEXT Python code block or file path defining
--cl, --chain-limit INTEGER How many chained tool responses to allow, default functions to register as tools
5, set 0 for unlimited --td, --tools-debug Show full details of tool executions
--help Show this message and exit. --ta, --tools-approve Manually approve every tool execution
--cl, --chain-limit INTEGER How many chained tool responses to allow,
default 5, set 0 for unlimited
--help Show this message and exit.
``` ```
(help-keys)= (help-keys)=

View file

@ -76,6 +76,7 @@ Chatting with gpt-4o
Type 'exit' or 'quit' to exit Type 'exit' or 'quit' to exit
Type '!multi' to enter multiple lines, then '!end' to finish Type '!multi' to enter multiple lines, then '!end' to finish
Type '!edit' to open your default editor and modify the prompt. Type '!edit' to open your default editor and modify the prompt.
Type '!fragment <my_fragment> [<another_fragment> ...]' to insert one or more fragments
> Tell me a joke about a pelican > Tell me a joke about a pelican
Why don't pelicans like to tip waiters? Why don't pelicans like to tip waiters?

View file

@ -405,6 +405,7 @@ Chatting with gpt-4
Type 'exit' or 'quit' to exit Type 'exit' or 'quit' to exit
Type '!multi' to enter multiple lines, then '!end' to finish Type '!multi' to enter multiple lines, then '!end' to finish
Type '!edit' to open your default editor and modify the prompt Type '!edit' to open your default editor and modify the prompt
Type '!fragment <my_fragment> [<another_fragment> ...]' to insert one or more fragments
> who are you? > who are you?
I am a sentient cheesecake, meaning I am an artificial I am a sentient cheesecake, meaning I am an artificial
intelligence embodied in a dessert form, specifically a intelligence embodied in a dessert form, specifically a
@ -426,6 +427,7 @@ Chatting with gpt-4
Type 'exit' or 'quit' to exit Type 'exit' or 'quit' to exit
Type '!multi' to enter multiple lines, then '!end' to finish Type '!multi' to enter multiple lines, then '!end' to finish
Type '!edit' to open your default editor and modify the prompt. Type '!edit' to open your default editor and modify the prompt.
Type '!fragment <my_fragment> [<another_fragment> ...]' to insert one or more fragments
> !multi custom-end > !multi custom-end
Explain this error: Explain this error:
@ -445,6 +447,7 @@ Chatting with gpt-4
Type 'exit' or 'quit' to exit Type 'exit' or 'quit' to exit
Type '!multi' to enter multiple lines, then '!end' to finish Type '!multi' to enter multiple lines, then '!end' to finish
Type '!edit' to open your default editor and modify the prompt. Type '!edit' to open your default editor and modify the prompt.
Type '!fragment <my_fragment> [<another_fragment> ...]' to insert one or more fragments
> !edit > !edit
``` ```

View file

@ -163,6 +163,39 @@ def resolve_fragments(
return resolved return resolved
def process_fragments_in_chat(
db: sqlite_utils.Database, prompt: str
) -> tuple[str, list[Fragment], list[Attachment]]:
"""
Process any !fragment commands in a chat prompt and return the modified prompt plus resolved fragments and attachments.
"""
prompt_lines = []
fragments = []
attachments = []
for line in prompt.splitlines():
if line.startswith("!fragment "):
try:
fragment_strs = line.strip().removeprefix("!fragment ").split()
fragments_and_attachments = resolve_fragments(
db, fragments=fragment_strs, allow_attachments=True
)
fragments += [
fragment
for fragment in fragments_and_attachments
if isinstance(fragment, Fragment)
]
attachments += [
attachment
for attachment in fragments_and_attachments
if isinstance(attachment, Attachment)
]
except FragmentNotFound as ex:
raise click.ClickException(str(ex))
else:
prompt_lines.append(line)
return "\n".join(prompt_lines), fragments, attachments
class AttachmentError(Exception): class AttachmentError(Exception):
"""Exception raised for errors in attachment resolution.""" """Exception raised for errors in attachment resolution."""
@ -888,6 +921,20 @@ def prompt(
"--conversation", "--conversation",
help="Continue the conversation with the given ID.", help="Continue the conversation with the given ID.",
) )
@click.option(
"fragments",
"-f",
"--fragment",
multiple=True,
help="Fragment (alias, URL, hash or file path) to add to the prompt",
)
@click.option(
"system_fragments",
"--sf",
"--system-fragment",
multiple=True,
help="Fragment to add to system prompt",
)
@click.option("-t", "--template", help="Template to use") @click.option("-t", "--template", help="Template to use")
@click.option( @click.option(
"-p", "-p",
@ -953,6 +1000,8 @@ def chat(
model_id, model_id,
_continue, _continue,
conversation_id, conversation_id,
fragments,
system_fragments,
template, template,
param, param,
options, options,
@ -1054,15 +1103,48 @@ def chat(
if key and isinstance(model, KeyModel): if key and isinstance(model, KeyModel):
kwargs["key"] = key kwargs["key"] = key
try:
fragments_and_attachments = resolve_fragments(
db, fragments, allow_attachments=True
)
argument_fragments = [
fragment
for fragment in fragments_and_attachments
if isinstance(fragment, Fragment)
]
argument_attachments = [
attachment
for attachment in fragments_and_attachments
if isinstance(attachment, Attachment)
]
argument_system_fragments = resolve_fragments(db, system_fragments)
except FragmentNotFound as ex:
raise click.ClickException(str(ex))
click.echo("Chatting with {}".format(model.model_id)) click.echo("Chatting with {}".format(model.model_id))
click.echo("Type 'exit' or 'quit' to exit") click.echo("Type 'exit' or 'quit' to exit")
click.echo("Type '!multi' to enter multiple lines, then '!end' to finish") click.echo("Type '!multi' to enter multiple lines, then '!end' to finish")
click.echo("Type '!edit' to open your default editor and modify the prompt") click.echo("Type '!edit' to open your default editor and modify the prompt")
click.echo(
"Type '!fragment <my_fragment> [<another_fragment> ...]' to insert one or more fragments"
)
in_multi = False in_multi = False
accumulated = [] accumulated = []
accumulated_fragments = []
accumulated_attachments = []
end_token = "!end" end_token = "!end"
while True: while True:
prompt = click.prompt("", prompt_suffix="> " if not in_multi else "") prompt = click.prompt("", prompt_suffix="> " if not in_multi else "")
fragments = []
attachments = []
if argument_fragments:
fragments += argument_fragments
# fragments from --fragments will get added to the first message only
argument_fragments = []
if argument_attachments:
attachments = argument_attachments
argument_attachments = []
if prompt.strip().startswith("!multi"): if prompt.strip().startswith("!multi"):
in_multi = True in_multi = True
bits = prompt.strip().split() bits = prompt.strip().split()
@ -1074,17 +1156,28 @@ def chat(
if edited_prompt is None: if edited_prompt is None:
click.echo("Editor closed without saving.", err=True) click.echo("Editor closed without saving.", err=True)
continue continue
prompt = edited_prompt.strip() prompt, fragments, attachments = process_fragments_in_chat(
if not prompt: db, edited_prompt.strip()
)
if not prompt and not fragments and not attachments:
continue continue
click.echo(prompt) if prompt.strip().startswith("!fragment "):
prompt, fragments, attachments = process_fragments_in_chat(db, prompt)
if in_multi: if in_multi:
if prompt.strip() == end_token: if prompt.strip() == end_token:
prompt = "\n".join(accumulated) prompt = "\n".join(accumulated)
fragments = accumulated_fragments
attachments = accumulated_attachments
in_multi = False in_multi = False
accumulated = [] accumulated = []
accumulated_fragments = []
accumulated_attachments = []
else: else:
accumulated.append(prompt) if prompt:
accumulated.append(prompt)
accumulated_fragments += fragments
accumulated_attachments += attachments
continue continue
if template_obj: if template_obj:
try: try:
@ -1100,9 +1193,21 @@ def chat(
prompt = new_prompt prompt = new_prompt
if prompt.strip() in ("exit", "quit"): if prompt.strip() in ("exit", "quit"):
break break
response = conversation.chain(prompt, system=system, **kwargs)
response = conversation.chain(
prompt,
fragments=[str(fragment) for fragment in fragments],
system_fragments=[
str(system_fragment) for system_fragment in argument_system_fragments
],
attachments=attachments,
system=system,
**kwargs,
)
# System prompt only sent for the first message: # System prompt only sent for the first message:
system = None system = None
system_fragments = []
for chunk in response: for chunk in response:
print(chunk, end="") print(chunk, end="")
sys.stdout.flush() sys.stdout.flush()

View file

@ -25,6 +25,7 @@ def test_chat_basic(mock_model, logs_db):
"\nType 'exit' or 'quit' to exit" "\nType 'exit' or 'quit' to exit"
"\nType '!multi' to enter multiple lines, then '!end' to finish" "\nType '!multi' to enter multiple lines, then '!end' to finish"
"\nType '!edit' to open your default editor and modify the prompt" "\nType '!edit' to open your default editor and modify the prompt"
"\nType '!fragment <my_fragment> [<another_fragment> ...]' to insert one or more fragments"
"\n> Hi" "\n> Hi"
"\none world" "\none world"
"\n> Hi two" "\n> Hi two"
@ -91,6 +92,7 @@ def test_chat_basic(mock_model, logs_db):
"\nType 'exit' or 'quit' to exit" "\nType 'exit' or 'quit' to exit"
"\nType '!multi' to enter multiple lines, then '!end' to finish" "\nType '!multi' to enter multiple lines, then '!end' to finish"
"\nType '!edit' to open your default editor and modify the prompt" "\nType '!edit' to open your default editor and modify the prompt"
"\nType '!fragment <my_fragment> [<another_fragment> ...]' to insert one or more fragments"
"\n> Continue" "\n> Continue"
"\ncontinued" "\ncontinued"
"\n> quit" "\n> quit"
@ -140,6 +142,7 @@ def test_chat_system(mock_model, logs_db):
"\nType 'exit' or 'quit' to exit" "\nType 'exit' or 'quit' to exit"
"\nType '!multi' to enter multiple lines, then '!end' to finish" "\nType '!multi' to enter multiple lines, then '!end' to finish"
"\nType '!edit' to open your default editor and modify the prompt" "\nType '!edit' to open your default editor and modify the prompt"
"\nType '!fragment <my_fragment> [<another_fragment> ...]' to insert one or more fragments"
"\n> Hi" "\n> Hi"
"\nI am mean" "\nI am mean"
"\n> quit" "\n> quit"
@ -303,6 +306,7 @@ def test_chat_tools(logs_db):
"Type 'exit' or 'quit' to exit\n" "Type 'exit' or 'quit' to exit\n"
"Type '!multi' to enter multiple lines, then '!end' to finish\n" "Type '!multi' to enter multiple lines, then '!end' to finish\n"
"Type '!edit' to open your default editor and modify the prompt\n" "Type '!edit' to open your default editor and modify the prompt\n"
"Type '!fragment <my_fragment> [<another_fragment> ...]' to insert one or more fragments\n"
'> {"prompt": "Convert hello to uppercase", "tool_calls": [{"name": "upper", ' '> {"prompt": "Convert hello to uppercase", "tool_calls": [{"name": "upper", '
'"arguments": {"text": "hello"}}]}\n' '"arguments": {"text": "hello"}}]}\n'
"{\n" "{\n"