llm logs --prompts option (#737)

Closes #736
This commit is contained in:
Simon Willison 2025-02-02 12:03:01 -08:00 committed by GitHub
parent 21df241443
commit 41d64a8f12
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 90 additions and 3 deletions

View file

@ -302,6 +302,7 @@ Options:
-t, --truncate Truncate long strings in output
-u, --usage Include token usage
-r, --response Just output the last response
--prompts Output prompts, end-truncated if necessary
-x, --extract Extract first fenced code block
--xl, --extract-last Extract last fenced code block
-c, --current Show logs from the current conversation

View file

@ -89,6 +89,22 @@ You can truncate the display of the prompts and responses using the `-t/--trunca
```bash
llm logs -n 5 -t --json
```
Or use `--prompts` to see just the truncated prompts:
```bash
llm logs -n 2 --prompts
```
Example output:
```
- model: deepseek-reasoner
datetime: 2025-02-02T06:39:53
conversation: 01jk2pk05xq3d0vgk0202zrsg1
prompt: H01 There are five huts. H02 The Scotsman lives in the purple hut. H03 The Welshman owns the parrot. H04 Kombucha is...
- model: o3-mini
datetime: 2025-02-02T19:03:05
conversation: 01jk40qkxetedzpf1zd8k9bgww
system: Formatting re-enabled. Write a detailed README with extensive usage examples.
prompt: <documents> <document index="1"> <source>./Cargo.toml</source> <document_content> [package] name = "py-limbo" version...
```
(logs-conversation)=
### Logs for a conversation

View file

@ -4,6 +4,7 @@ from click_default_group import DefaultGroup
from dataclasses import asdict
import io
import json
import re
from llm import (
Attachment,
AsyncResponse,
@ -874,6 +875,9 @@ order by prompt_attachments."order"
@click.option("-t", "--truncate", is_flag=True, help="Truncate long strings in output")
@click.option("-u", "--usage", is_flag=True, help="Include token usage")
@click.option("-r", "--response", is_flag=True, help="Just output the last response")
@click.option(
"--prompts", is_flag=True, help="Output prompts, end-truncated if necessary"
)
@click.option("-x", "--extract", is_flag=True, help="Extract first fenced code block")
@click.option(
"extract_last",
@ -910,6 +914,7 @@ def logs_list(
truncate,
usage,
response,
prompts,
extract,
extract_last,
current_conversation,
@ -923,6 +928,18 @@ def logs_list(
db = sqlite_utils.Database(path)
migrate(db)
if prompts and (json_output or response):
invalid = " or ".join(
[
flag[0]
for flag in (("--json", json_output), ("--response", response))
if flag[1]
]
)
raise click.ClickException(
"Cannot use --prompts and {} together".format(invalid)
)
if response and not current_conversation and not conversation_id:
current_conversation = True
@ -1035,6 +1052,27 @@ def logs_list(
current_system = None
should_show_conversation = True
for row in rows:
if prompts:
system = _truncate_string(row["system"], 120, end=True)
prompt = _truncate_string(row["prompt"], 120, end=True)
cid = row["conversation_id"]
attachments = attachments_by_id.get(row["id"])
lines = [
"- model: {}".format(row["model"]),
" datetime: {}".format(row["datetime_utc"]).split(".")[0],
" conversation: {}".format(cid),
]
if system:
lines.append(" system: {}".format(system))
if prompt:
lines.append(" prompt: {}".format(prompt))
if attachments:
lines.append(" attachments:")
for attachment in attachments:
path = attachment["path"] or attachment["url"]
lines.append(" - {}: {}".format(attachment["type"], path))
click.echo("\n".join(lines))
continue
click.echo(
"# {}{}\n{}".format(
row["datetime_utc"].split(".")[0],
@ -1897,10 +1935,17 @@ def template_dir():
return path
def _truncate_string(s, max_length=100):
if len(s) > max_length:
def _truncate_string(s, max_length=100, end=False):
if not s:
return s
if end:
s = re.sub(r"\s+", " ", s)
if len(s) <= max_length:
return s
return s[: max_length - 3] + "..."
return s
if len(s) <= max_length:
return s
return s[: max_length - 3] + "..."
def logs_db_path():

View file

@ -164,6 +164,31 @@ def test_logs_extract_last_code(args, log_path):
assert result.output == 'print("hello word")\n\n'
def test_logs_prompts(log_path):
runner = CliRunner()
result = runner.invoke(cli, ["logs", "--prompts", "-p", str(log_path)])
assert result.exit_code == 0
output = datetime_re.sub("YYYY-MM-DDTHH:MM:SS", result.output)
expected = (
"- model: davinci\n"
" datetime: YYYY-MM-DDTHH:MM:SS\n"
" conversation: abc123\n"
" system: system\n"
" prompt: prompt\n"
"- model: davinci\n"
" datetime: YYYY-MM-DDTHH:MM:SS\n"
" conversation: abc123\n"
" system: system\n"
" prompt: prompt\n"
"- model: davinci\n"
" datetime: YYYY-MM-DDTHH:MM:SS\n"
" conversation: abc123\n"
" system: system\n"
" prompt: prompt\n"
)
assert output == expected
@pytest.mark.xfail(sys.platform == "win32", reason="Expected to fail on Windows")
@pytest.mark.parametrize("env", ({}, {"LLM_USER_PATH": "/tmp/llm-user-path"}))
def test_logs_path(monkeypatch, env, user_path):