llm-mr 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- llm_mr-0.1.0/PKG-INFO +719 -0
- llm_mr-0.1.0/README.md +694 -0
- llm_mr-0.1.0/llm_mr/__init__.py +1 -0
- llm_mr-0.1.0/llm_mr/hookspecs.py +54 -0
- llm_mr-0.1.0/llm_mr/io_plugins.py +168 -0
- llm_mr-0.1.0/llm_mr/plugin.py +29 -0
- llm_mr-0.1.0/llm_mr/processors.py +2208 -0
- llm_mr-0.1.0/llm_mr/py.typed +0 -0
- llm_mr-0.1.0/llm_mr/registries.py +145 -0
- llm_mr-0.1.0/pyproject.toml +49 -0
llm_mr-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: llm-mr
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Map-reduce spreadsheet operations powered by LLM prompts
|
|
5
|
+
Author: Jack Cushman
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Requires-Dist: click>=7.0
|
|
16
|
+
Requires-Dist: llm>=0.23
|
|
17
|
+
Requires-Dist: openpyxl>=2.4
|
|
18
|
+
Requires-Dist: pluggy>=0.9.0
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Project-URL: Homepage, https://github.com/jcushman/llm-mr
|
|
21
|
+
Project-URL: Repository, https://github.com/jcushman/llm-mr
|
|
22
|
+
Project-URL: Issues, https://github.com/jcushman/llm-mr/issues
|
|
23
|
+
Project-URL: Changelog, https://github.com/jcushman/llm-mr/blob/main/CHANGELOG.md
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# llm-mr
|
|
27
|
+
|
|
28
|
+
`llm-mr` is a plugin for the [`llm`](https://llm.datasette.io/) command line tool
|
|
29
|
+
that adds map/reduce/filter helpers for spreadsheet files. Use it when you have a large
|
|
30
|
+
file you want to process with an LLM, and need to break it down into smaller chunks
|
|
31
|
+
rather than processing the entire file at once.
|
|
32
|
+
|
|
33
|
+
The plugin supports CSV, JSONL, and XLSX input/output, and can be extended with third-party plugins
|
|
34
|
+
for other formats.
|
|
35
|
+
|
|
36
|
+
## Examples
|
|
37
|
+
|
|
38
|
+
Classify every row in a spreadsheet by sentiment:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
llm mr map "Classify sentiment as positive/negative/neutral" -p -i feedback.csv -c sentiment -o out.csv
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Summarize notes per department:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
llm mr reduce "Summarize key themes" -p -i employees.csv --group-by department -o summary.csv
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Filter a JSONL corpus to just the articles about a topic:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
llm mr filter "about climate policy" -p -i articles.jsonl -o climate.jsonl
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Pipe data through stdin/stdout — JSONL is the default streaming format:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
cat data.jsonl | llm mr filter "about climate" -p | llm mr map "summarize" -c summary -p > out.jsonl
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Expand each row into multiple output rows:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
llm mr map "List the five largest cities" -p -i countries.csv --multiple -c city -o cities.csv
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Bulk-rename columns with a Python expression — no LLM needed:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
llm mr map 'row["name"].upper()' -e -i data.csv -c name_upper -o clean.csv
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Or just describe what you want — interactive mode synthesizes the expression for you:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
$ llm mr map "uppercase the names" -i data.csv -c name_upper -o clean.csv
|
|
78
|
+
Use deterministic expression?
|
|
79
|
+
row["name"].upper() [Y/n]: Y
|
|
80
|
+
Using deterministic expression: row["name"].upper()
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Installation
|
|
84
|
+
|
|
85
|
+
If you already have [`llm`](https://llm.datasette.io/) installed:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
llm install llm-mr
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
New to `llm`? It's a command-line tool for interacting with language models.
|
|
92
|
+
Install both together:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pip install llm llm-mr
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Then [configure a model and API key](https://llm.datasette.io/en/stable/setup.html)
|
|
99
|
+
before continuing.
|
|
100
|
+
|
|
101
|
+
## Writing I/O format plugins
|
|
102
|
+
|
|
103
|
+
Extra tabular formats (beyond CSV, JSONL, and XLSX) ship as normal Python packages that
|
|
104
|
+
depend on `llm-mr` and register **input** and/or **output** plugins with [pluggy](https://pluggy.readthedocs.io/)
|
|
105
|
+
via the `llm_mr` entry-point group.
|
|
106
|
+
|
|
107
|
+
Declare the entry point in `pyproject.toml` (the value is an importable module that is
|
|
108
|
+
loaded for side effects; a package `__init__.py` works well):
|
|
109
|
+
|
|
110
|
+
```toml
|
|
111
|
+
[project.entry-points.llm_mr]
|
|
112
|
+
myformat = "llm_mr_myformat"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
In that module, use `mr_hookimpl` from `llm_mr.hookspecs` and implement
|
|
116
|
+
`register_mr_inputs` and/or `register_mr_outputs`. Each receives a `register` callback;
|
|
117
|
+
pass an instance of your plugin class.
|
|
118
|
+
|
|
119
|
+
Plugins must satisfy the `InputPlugin` and/or `OutputPlugin` protocols in
|
|
120
|
+
`llm_mr.registries`:
|
|
121
|
+
|
|
122
|
+
- **Input:** `name` (string id, e.g. `"parquet"`), `extensions` (e.g. `[".parquet"]`),
|
|
123
|
+
and `open(self, path: Path)` as a context manager yielding a `TableStream` (`rows`
|
|
124
|
+
iterable and optional `fieldnames`).
|
|
125
|
+
- **Output:** same `name` / `extensions`, plus `write(self, path: Path, rows, fieldnames)`
|
|
126
|
+
that writes the file.
|
|
127
|
+
|
|
128
|
+
This is all that's required — stdin/stdout piping works automatically via a
|
|
129
|
+
temp-file intermediary. For streaming without a temp file, also implement
|
|
130
|
+
`StreamableInput` (`open_stream(self, stream)`) and/or `StreamableOutput`
|
|
131
|
+
(`write_stream(self, stream, rows, fieldnames)`).
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
# llm_mr_myformat/__init__.py
|
|
135
|
+
from contextlib import contextmanager
|
|
136
|
+
from pathlib import Path
|
|
137
|
+
from typing import Iterator
|
|
138
|
+
|
|
139
|
+
from llm_mr.hookspecs import mr_hookimpl
|
|
140
|
+
from llm_mr.registries import TableStream
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class ParquetInputPlugin:
|
|
144
|
+
name = "parquet"
|
|
145
|
+
extensions = [".parquet"]
|
|
146
|
+
|
|
147
|
+
@contextmanager
|
|
148
|
+
def open(self, path: Path) -> Iterator[TableStream]:
|
|
149
|
+
rows = ... # load rows as Iterable[Row]
|
|
150
|
+
yield TableStream(rows=rows, fieldnames=[...])
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@mr_hookimpl
|
|
154
|
+
def register_mr_inputs(register):
|
|
155
|
+
register(ParquetInputPlugin())
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
After installation, `llm mr` discovers plugins through the same entry-point loading as
|
|
159
|
+
the main `llm` tool; users install your package with `pip` / `llm install` like any other
|
|
160
|
+
dependency.
|
|
161
|
+
|
|
162
|
+
## Tutorial: Getting Started
|
|
163
|
+
|
|
164
|
+
### Step 1: Install
|
|
165
|
+
|
|
166
|
+
Follow the [Installation](#installation) instructions above, then make sure you
|
|
167
|
+
have a model configured. If you already use `llm` with an OpenAI or Anthropic
|
|
168
|
+
key, you're all set — skip to Step 2.
|
|
169
|
+
|
|
170
|
+
If you'd rather use a free local model, [Ollama](https://ollama.com/) is the
|
|
171
|
+
quickest path:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
llm install llm-ollama
|
|
175
|
+
ollama pull llama3.2:1b
|
|
176
|
+
llm -m llama3.2:1b "Hello, world!"
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Step 2: Create Sample Data
|
|
180
|
+
|
|
181
|
+
Create `foods.csv`:
|
|
182
|
+
|
|
183
|
+
```csv
|
|
184
|
+
food,description
|
|
185
|
+
pizza,"Cheesy flatbread with tomato sauce and various toppings"
|
|
186
|
+
broccoli,"Green cruciferous vegetable, often steamed or roasted"
|
|
187
|
+
chocolate,"Sweet confection made from cocoa beans"
|
|
188
|
+
kale,"Dark leafy green vegetable, often used in salads"
|
|
189
|
+
ice_cream,"Frozen dairy dessert, sweet and creamy"
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Step 3: Map
|
|
193
|
+
|
|
194
|
+
The `map` command adds a new column to the input file.
|
|
195
|
+
Here we make a separate LLM request for each row to classify the food as a 'treat' or 'not treat'.
|
|
196
|
+
The `-p` flag means "use my instruction as a literal prompt" — the text you provide is sent
|
|
197
|
+
directly to the model for each row.
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
llm mr map \
|
|
201
|
+
"Based on the food description, classify this as 'treat' or 'not treat'" \
|
|
202
|
+
-p -i foods.csv -c tastiness -o foods_classified.csv
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
```csv
|
|
206
|
+
food,description,tastiness
|
|
207
|
+
pizza,"Cheesy flatbread with tomato sauce and various toppings",treat
|
|
208
|
+
broccoli,"Green cruciferous vegetable, often steamed or roasted",not treat
|
|
209
|
+
chocolate,"Sweet confection made from cocoa beans",treat
|
|
210
|
+
kale,"Dark leafy green vegetable, often used in salads",not treat
|
|
211
|
+
ice_cream,"Frozen dairy dessert, sweet and creamy",treat
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Because we used the `-p` flag, the LLM is asked to classify each row with the exact prompt we supplied.
|
|
215
|
+
Under the hood, the full prompt sent for the first row looks like this:
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
You are assisting with spreadsheet transformations.
|
|
219
|
+
<spreadsheet_rows>
|
|
220
|
+
<row_0>
|
|
221
|
+
{"food": "pizza", "description": "Cheesy flatbread with tomato sauce and various toppings"}
|
|
222
|
+
</row_0>
|
|
223
|
+
</spreadsheet_rows>
|
|
224
|
+
<user_instruction>
|
|
225
|
+
Based on the food description, classify this as 'treat' or 'not treat'
|
|
226
|
+
</user_instruction>
|
|
227
|
+
For each row, provide a single value for column 'tastiness' that answers the user_instruction.
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Without the `-p` flag, you get **interactive mode**: the tool asks a planning model to
|
|
231
|
+
figure out how to handle your instruction — either as a Python expression or as an
|
|
232
|
+
LLM prompt. It shows you what it came up with and waits for you to press `Y` (accept)
|
|
233
|
+
or `n` (reject) before anything runs.
|
|
234
|
+
|
|
235
|
+
In the example below, we reject the suggested Python expression and accept the
|
|
236
|
+
generated LLM prompt instead:
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
$ llm mr map "label which foods are treats" -i foods.csv -c tastiness -o foods_classified.csv
|
|
240
|
+
Use deterministic expression?
|
|
241
|
+
"treat" if any(w in row["description"].lower() for w in ("sweet", "dessert", "confection")) else "not treat" [Y/n]: n
|
|
242
|
+
Run as prompt per row?
|
|
243
|
+
Based on the food name and description, classify this food as 'treat' or 'not treat'. Return only one of those two labels. [Y/n]: Y
|
|
244
|
+
Processing 5 batches (parallel=1)
|
|
245
|
+
Completed batch 1/5
|
|
246
|
+
...
|
|
247
|
+
Wrote 5 rows to foods_classified.csv
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Finally we can provide a Python expression directly with `-e` — no LLM call at all:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
llm mr map '"sweet" in row["description"].lower()' -e -i foods.csv -c is_sweet -o sweets.csv
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
```csv
|
|
257
|
+
food,description,is_sweet
|
|
258
|
+
pizza,"Cheesy flatbread with tomato sauce and various toppings",False
|
|
259
|
+
broccoli,"Green cruciferous vegetable, often steamed or roasted",False
|
|
260
|
+
chocolate,"Sweet confection made from cocoa beans",True
|
|
261
|
+
kale,"Dark leafy green vegetable, often used in salads",False
|
|
262
|
+
ice_cream,"Frozen dairy dessert, sweet and creamy",True
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Step 4: Filter (Expression)
|
|
266
|
+
|
|
267
|
+
You can use the `filter` command to keep only rows matching a criterion.
|
|
268
|
+
Here we keep only the rows where the food is a 'treat'.
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
llm mr filter 'row["tastiness"] == "treat"' \
|
|
272
|
+
-e -i foods_classified.csv -o treats_only.csv
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
```csv
|
|
276
|
+
food,description,tastiness
|
|
277
|
+
pizza,"Cheesy flatbread with tomato sauce and various toppings",treat
|
|
278
|
+
chocolate,"Sweet confection made from cocoa beans",treat
|
|
279
|
+
ice_cream,"Frozen dairy dessert, sweet and creamy",treat
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Like the map step, we could have used the `-p` flag, or no flag for interactive mode.
|
|
283
|
+
|
|
284
|
+
### Step 5: Reduce
|
|
285
|
+
|
|
286
|
+
The `reduce` command groups rows by a given column and summarizes each group.
|
|
287
|
+
Output is a small table with two columns: the **group key** and the **reduced
|
|
288
|
+
value**. By default those columns are named `group` and `mr_result`; here we
|
|
289
|
+
rename them to match the grouping column and a clearer summary name using
|
|
290
|
+
`--group-key-column` and `-c` / `--column`.
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
llm mr reduce "What characteristics do these foods share?" \
|
|
294
|
+
-p -i foods_classified.csv --group-by tastiness \
|
|
295
|
+
--group-key-column tastiness -c summary -o food_analysis.csv
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
```csv
|
|
299
|
+
tastiness,summary
|
|
300
|
+
treat,"These foods are typically sweet and have a high sugar content"
|
|
301
|
+
not treat,"These foods are typically green and have a bitter taste"
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Again, we could have used `-e` to provide a Python expression directly (no LLM needed), or no flag for interactive mode.
|
|
305
|
+
|
|
306
|
+
### Step 6: Expand with --multiple
|
|
307
|
+
|
|
308
|
+
Sometimes you want the model to produce several values per input row — for
|
|
309
|
+
example, brainstorming related items or splitting a field into parts. The
|
|
310
|
+
`--multiple` flag tells `map` to expect a list from each LLM call and expand
|
|
311
|
+
each item into its own output row.
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
llm mr map "Come up with more foods matching this description" \
|
|
315
|
+
-p -i food_analysis.csv --column food --multiple -o more_foods.csv
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
(Use the `food_analysis.csv` from step 5 so the input columns are `tastiness`
|
|
319
|
+
and `summary`.)
|
|
320
|
+
|
|
321
|
+
```csv
|
|
322
|
+
tastiness,summary,food
|
|
323
|
+
treat,"These foods are typically sweet and have a high sugar content","cake"
|
|
324
|
+
treat,"These foods are typically sweet and have a high sugar content","candy_bar"
|
|
325
|
+
not treat,"These foods are typically green and have a bitter taste","spinach"
|
|
326
|
+
not treat,"These foods are typically green and have a bitter taste","swiss_chard"
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## Instruction Modes
|
|
330
|
+
|
|
331
|
+
`llm mr` has three commands: `map` to process each row, `reduce` to group rows, and `filter` to keep only rows matching a criterion.
|
|
332
|
+
|
|
333
|
+
Every command takes one positional argument — the **instruction** — plus a mode
|
|
334
|
+
flag. Input is read from `-i` (file) or stdin; output goes to `-o` (file) or
|
|
335
|
+
stdout.
|
|
336
|
+
|
|
337
|
+
| Flag | Mode | What happens |
|
|
338
|
+
|------|------|-------------|
|
|
339
|
+
| *(default)* | Interactive | LLM tries to synthesize a Python expression; falls back to writing a prompt to run per-row if it can't. Asks you to confirm before running. Requires `-i` (cannot read from stdin). |
|
|
340
|
+
| `-p` | Prompt | Treat the instruction as a literal LLM prompt used to process each row. |
|
|
341
|
+
| `-e` | Expression | Treat the instruction as a Python expression evaluated locally. No LLM calls at all. |
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
# Interactive — tool figures out the best execution strategy
|
|
345
|
+
llm mr map "uppercase the names" -i data.csv -c name_upper -o out.csv
|
|
346
|
+
|
|
347
|
+
# Prompt — send this exact prompt to the LLM for each row
|
|
348
|
+
llm mr map "Classify sentiment as positive/negative/neutral" -p -i data.csv -c sentiment -o out.csv
|
|
349
|
+
|
|
350
|
+
# Expression — Python expression, no LLM needed
|
|
351
|
+
llm mr map 'row["name"].upper()' -e -i data.csv -c name_upper -o out.csv
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Selecting Models
|
|
355
|
+
|
|
356
|
+
To choose a different model from the `llm` tool's default for both the planning and per-item work, use the `-m` flag:
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
llm mr map "classify sentiment" -p -i data.csv -m gpt-4o -c sentiment -o out.csv
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
In interactive mode the planning step and per-item work can use different models.
|
|
363
|
+
Either side can be overridden independently:
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
# Cheap worker, default planner
|
|
367
|
+
llm mr map "classify sentiment" -i data.csv --worker-model gpt-4o-mini -c sentiment -o out.csv
|
|
368
|
+
|
|
369
|
+
# Powerful planner, default worker
|
|
370
|
+
llm mr map "classify sentiment" -i data.csv --planning-model gpt-4o -c sentiment -o out.csv
|
|
371
|
+
|
|
372
|
+
# Override both
|
|
373
|
+
llm mr map "classify sentiment" -i data.csv --planning-model gpt-4o --worker-model gpt-4o-mini -c sentiment -o out.csv
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
In `-p` mode, only the worker model is used. In `-e` mode, no model is used.
|
|
377
|
+
|
|
378
|
+
## Recovering from Failures
|
|
379
|
+
|
|
380
|
+
Both `map` and `reduce` write a sidecar error file (`<output>.err`) when batches
|
|
381
|
+
or groups fail. Use `--repair` to retry only the failed items:
|
|
382
|
+
|
|
383
|
+
```bash
|
|
384
|
+
# Initial run — some batches may time out or fail
|
|
385
|
+
llm mr map "..." -p -i data.jsonl -c result -j 20 -o output.jsonl
|
|
386
|
+
# Warning: 3 batches failed; see output.jsonl.err — rerun with --repair
|
|
387
|
+
|
|
388
|
+
# Retry only the failed rows
|
|
389
|
+
llm mr map "..." -p -i data.jsonl -c result -j 4 -o output.jsonl --repair
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
The `--repair` flag is idempotent — if some retries still fail, they stay in
|
|
393
|
+
the `.err` file. Delete the output and `.err` files to start fresh.
|
|
394
|
+
|
|
395
|
+
When output goes to stdout (no `-o`), error records are written as JSONL lines
|
|
396
|
+
to stderr instead of a sidecar file. You can redirect them:
|
|
397
|
+
|
|
398
|
+
```bash
|
|
399
|
+
cat data.jsonl | llm mr map "..." -p 2>errors.jsonl > out.jsonl
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
`--repair` requires `-o` — it cannot be used with stdout output.
|
|
403
|
+
|
|
404
|
+
## Python Expressions (`-e`)
|
|
405
|
+
|
|
406
|
+
Expression mode (`-e`) evaluates a single Python expression in a restricted
|
|
407
|
+
sandbox — no imports, no file access, no side effects. **This is a lightweight
|
|
408
|
+
convenience restriction, not a security sandbox**. A determined user can escape
|
|
409
|
+
it. Do not rely on it to run untrusted expressions.
|
|
410
|
+
|
|
411
|
+
### Map expressions
|
|
412
|
+
|
|
413
|
+
The expression receives a single variable `row`, a dict mapping column names to
|
|
414
|
+
string values. It should return the value to store in the target column.
|
|
415
|
+
|
|
416
|
+
```bash
|
|
417
|
+
# row["name"] is available as a string
|
|
418
|
+
llm mr map 'row["name"].upper()' -e -i data.csv -c name_upper -o out.csv
|
|
419
|
+
|
|
420
|
+
# arithmetic on coerced values
|
|
421
|
+
llm mr map 'int(row["price"]) * 2' -e -i data.csv -c double_price -o out.csv
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
`--multiple` cannot be combined with `-e`.
|
|
425
|
+
|
|
426
|
+
### Filter expressions
|
|
427
|
+
|
|
428
|
+
Same as map: the expression receives `row` (a dict). Return a truthy value to
|
|
429
|
+
**keep** the row, falsy to discard it.
|
|
430
|
+
|
|
431
|
+
```bash
|
|
432
|
+
llm mr filter 'int(row["score"]) >= 10' -e -i data.csv -o filtered.csv
|
|
433
|
+
llm mr filter '"keyword" in row["text"].lower()' -e -i data.csv -o filtered.csv
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### Reduce expressions
|
|
437
|
+
|
|
438
|
+
The expression receives `rows`, a **list** of dicts (all rows in the current
|
|
439
|
+
group). It should return a single aggregate value. Output columns are still
|
|
440
|
+
`group` and `mr_result` by default (or whatever you pass with
|
|
441
|
+
`--group-key-column` / `-c`).
|
|
442
|
+
|
|
443
|
+
```bash
|
|
444
|
+
llm mr reduce 'sum(int(r["score"]) for r in rows)' -e -i data.csv --group-by team -o totals.csv
|
|
445
|
+
llm mr reduce 'len(rows)' -e -i data.csv --group-by department -o counts.csv
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Available builtins
|
|
449
|
+
|
|
450
|
+
All standard Python builtins are removed. Only the following are available:
|
|
451
|
+
|
|
452
|
+
`len`, `int`, `float`, `str`, `bool`, `min`, `max`, `abs`, `round`, `sorted`,
|
|
453
|
+
`list`, `tuple`, `set`, `dict`, `sum`, `any`, `all`, `enumerate`, `zip`, `map`,
|
|
454
|
+
`filter`
|
|
455
|
+
|
|
456
|
+
String methods (`.upper()`, `.lower()`, `.split()`, `.strip()`, `.startswith()`,
|
|
457
|
+
etc.) and dict methods (`.get()`, `.keys()`, `.values()`, `.items()`) work
|
|
458
|
+
normally since they are methods on the values, not builtins.
|
|
459
|
+
|
|
460
|
+
### What is NOT allowed
|
|
461
|
+
|
|
462
|
+
- **Imports** — `import`, `__import__()`, and the full `__builtins__` dict are
|
|
463
|
+
all removed.
|
|
464
|
+
- **Statements** — the expression must be a single expression, not a statement.
|
|
465
|
+
No `=`, `for` (except in comprehensions), `if` (except ternary), `def`,
|
|
466
|
+
`class`, etc.
|
|
467
|
+
- **I/O** — `open`, `print`, `input`, and similar are unavailable.
|
|
468
|
+
- **Arbitrary functions** — only the builtins listed above are in scope.
|
|
469
|
+
|
|
470
|
+
## Piping and Formats
|
|
471
|
+
|
|
472
|
+
All three commands support stdin/stdout piping alongside file-based I/O.
|
|
473
|
+
|
|
474
|
+
### Input and output
|
|
475
|
+
|
|
476
|
+
- `-i` / `--input` — read from a file. Omit to read from stdin.
|
|
477
|
+
- `-o` / `--output` — write to a file. Omit to write to stdout.
|
|
478
|
+
- `--in-place` — (map only) overwrite the input file. Requires `-i`.
|
|
479
|
+
|
|
480
|
+
```bash
|
|
481
|
+
# File to file
|
|
482
|
+
llm mr filter "about climate" -p -i data.csv -o out.csv
|
|
483
|
+
|
|
484
|
+
# Pipe in, pipe out (JSONL default)
|
|
485
|
+
cat data.jsonl | llm mr filter "about climate" -p > out.jsonl
|
|
486
|
+
|
|
487
|
+
# Pipe chain
|
|
488
|
+
cat data.jsonl | llm mr filter "about climate" -p | llm mr map "summarize" -c summary -p > out.jsonl
|
|
489
|
+
|
|
490
|
+
# File in, pipe out (output matches input format)
|
|
491
|
+
llm mr map "summarize" -c summary -p -i data.csv > out.csv
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
When reading from stdin, interactive mode is not available — use `-p` or `-e`.
|
|
495
|
+
If stdin is a TTY (nothing piped) and no `-i` is provided, the command errors
|
|
496
|
+
with a helpful message.
|
|
497
|
+
|
|
498
|
+
### Format detection
|
|
499
|
+
|
|
500
|
+
Format is detected automatically from file extensions. When piping (no file
|
|
501
|
+
extension), JSONL is the default. Three flags give explicit control:
|
|
502
|
+
|
|
503
|
+
- `-f` / `--format` — set the default format for both directions
|
|
504
|
+
- `--input-format` — override input format only
|
|
505
|
+
- `--output-format` — override output format only
|
|
506
|
+
|
|
507
|
+
Resolution cascade (applied independently for input and output):
|
|
508
|
+
|
|
509
|
+
1. Specific flag (`--input-format` / `--output-format`)
|
|
510
|
+
2. File extension on `-i` / `-o`
|
|
511
|
+
3. General `-f` flag
|
|
512
|
+
4. Match the other end
|
|
513
|
+
5. JSONL fallback
|
|
514
|
+
|
|
515
|
+
```bash
|
|
516
|
+
# Pipe CSV explicitly
|
|
517
|
+
cat data.csv | llm mr filter "about climate" -p -f csv > out.csv
|
|
518
|
+
|
|
519
|
+
# CSV input file, JSONL stdout output
|
|
520
|
+
llm mr filter "about climate" -p -i data.csv --output-format jsonl
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### Status messages
|
|
524
|
+
|
|
525
|
+
All progress and status messages go to stderr, keeping stdout clean for data.
|
|
526
|
+
This is true whether you use `-o` or pipe to stdout.
|
|
527
|
+
|
|
528
|
+
### Non-interactive use (`llm` stdin)
|
|
529
|
+
|
|
530
|
+
When stdin is not a TTY (for example in CI or some automation tools), the
|
|
531
|
+
underlying `llm` CLI may wait for input. If a command seems to hang, redirect
|
|
532
|
+
stdin, e.g. append `</dev/null` to the command.
|
|
533
|
+
|
|
534
|
+
## Command Reference
|
|
535
|
+
|
|
536
|
+
### Map
|
|
537
|
+
|
|
538
|
+
Apply a transformation to each row, producing a new column.
|
|
539
|
+
|
|
540
|
+
```bash
|
|
541
|
+
llm mr map "Return a short summary of the notes" -p -i data.csv -c summary -o output.csv
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
Options:
|
|
545
|
+
|
|
546
|
+
- `-i` / `--input` — input file (omit to read stdin)
|
|
547
|
+
- `-o` / `--output` or `--in-place` — where to write results (omit `-o` for stdout)
|
|
548
|
+
- `-c` / `--column` — target column name (default: `mr_result`)
|
|
549
|
+
- `-f` / `--format` — default format for both directions
|
|
550
|
+
- `--input-format` / `--output-format` — override format per direction
|
|
551
|
+
- `--where` — pre-filter rows (e.g. `status=active`, `score>=10`)
|
|
552
|
+
- `--few-shot N` — use N existing values as examples
|
|
553
|
+
- `--batch-size` / `--max-chars` — control batching
|
|
554
|
+
- `-j` / `--parallel` — concurrent LLM calls (default: 1)
|
|
555
|
+
- `--multiple` — model emits a list per row; each item becomes its own output row
|
|
556
|
+
- `-m` / `--model` — LLM model to use
|
|
557
|
+
- `--worker-model` — model for per-item work (defaults to `-m`)
|
|
558
|
+
- `--planning-model` — model for interactive planning (defaults to `-m`)
|
|
559
|
+
- `-n` / `--limit` — only process first N rows
|
|
560
|
+
- `--repair` — retry failed rows from the `.err` sidecar (requires `-o`)
|
|
561
|
+
- `--dry-run` — show a sample prompt and exit without making LLM calls
|
|
562
|
+
- `-v` / `--verbose` — print each prompt as it is sent
|
|
563
|
+
|
|
564
|
+
### Reduce
|
|
565
|
+
|
|
566
|
+
Group rows and summarize each group. Each output row has two fields: the group
|
|
567
|
+
key (default column name `group`) and the reduced value (default `mr_result`).
|
|
568
|
+
Use `--group-key-column` and `-c` to rename them.
|
|
569
|
+
|
|
570
|
+
```bash
|
|
571
|
+
llm mr reduce "Summarize performance" -p -i data.csv --group-by department -o summary.csv
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
With clearer column names:
|
|
575
|
+
|
|
576
|
+
```bash
|
|
577
|
+
llm mr reduce "Summarize performance" -p -i data.csv --group-by department \
|
|
578
|
+
--group-key-column department -c summary -o summary.csv
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
With `-e`, you can aggregate with plain Python — no LLM needed:
|
|
582
|
+
|
|
583
|
+
```bash
|
|
584
|
+
llm mr reduce 'sum(int(r["score"]) for r in rows)' -e -i data.csv --group-by team -o totals.csv
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
Options:
|
|
588
|
+
|
|
589
|
+
- `-i` / `--input` — input file (omit to read stdin)
|
|
590
|
+
- `-o` / `--output` — output path (omit for stdout)
|
|
591
|
+
- `--group-by` — column(s) to group by (required, repeatable)
|
|
592
|
+
- `--group-key-column` — name of the group-key column in output (default: `group`)
|
|
593
|
+
- `-c` / `--column` — result column name (default: `mr_result`; must differ from `--group-key-column`)
|
|
594
|
+
- `-f` / `--format` — default format for both directions
|
|
595
|
+
- `--input-format` / `--output-format` — override format per direction
|
|
596
|
+
- `--where` — pre-filter rows
|
|
597
|
+
- `--max-chars` — max characters per reduction prompt
|
|
598
|
+
- `-j` / `--parallel` — concurrent groups
|
|
599
|
+
- `-m` / `--model`, `--worker-model`, `--planning-model`
|
|
600
|
+
- `-n` / `--limit` — only process first N groups
|
|
601
|
+
- `--repair` — retry failed groups (requires `-o`; use the same `--group-key-column` and `-c` as the original run)
|
|
602
|
+
- `--dry-run` — show a sample prompt and exit without making LLM calls
|
|
603
|
+
- `-v` / `--verbose` — print each prompt as it is sent
|
|
604
|
+
|
|
605
|
+
### Filter
|
|
606
|
+
|
|
607
|
+
Keep only rows matching a criterion.
|
|
608
|
+
|
|
609
|
+
```bash
|
|
610
|
+
# Expression: Python filter, no LLM
|
|
611
|
+
llm mr filter 'int(row["score"]) >= 10' -e -i data.csv -o filtered.csv
|
|
612
|
+
|
|
613
|
+
# Prompt: LLM classifies each row
|
|
614
|
+
llm mr filter "about prediction markets" -p -i data.csv -m gpt-4o -o filtered.csv
|
|
615
|
+
|
|
616
|
+
# Interactive: tool tries to synthesize a filter expression
|
|
617
|
+
llm mr filter "articles from 2024" -i data.csv -o filtered.csv
|
|
618
|
+
|
|
619
|
+
# Pipe: stdin to stdout
|
|
620
|
+
cat data.jsonl | llm mr filter "about climate" -p > out.jsonl
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
Options:
|
|
624
|
+
|
|
625
|
+
- `-i` / `--input` — input file (omit to read stdin)
|
|
626
|
+
- `-o` / `--output` — output path (omit for stdout)
|
|
627
|
+
- `-f` / `--format` — default format for both directions
|
|
628
|
+
- `--input-format` / `--output-format` — override format per direction
|
|
629
|
+
- `--where` — pre-filter before instruction filter
|
|
630
|
+
- `--batch-size` / `--max-chars` — control batching for LLM mode
|
|
631
|
+
- `-j` / `--parallel` — concurrent batches
|
|
632
|
+
- `-m` / `--model`, `--worker-model`, `--planning-model`
|
|
633
|
+
- `-n` / `--limit` — only consider first N rows
|
|
634
|
+
- `--dry-run` — show a sample prompt and exit without making LLM calls
|
|
635
|
+
- `-v` / `--verbose` — print each prompt as it is sent
|
|
636
|
+
|
|
637
|
+
## Debugging and Cost Tracking
|
|
638
|
+
|
|
639
|
+
### Inspecting prompts
|
|
640
|
+
|
|
641
|
+
Use `--dry-run` to see the exact prompt and JSON schema that would be sent to
|
|
642
|
+
the model, without actually making any API calls:
|
|
643
|
+
|
|
644
|
+
```bash
|
|
645
|
+
llm mr map "Classify sentiment" -p -i data.csv -c sentiment -o out.csv --dry-run
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
This prints the first batch's prompt, the schema, and the total number of
|
|
649
|
+
batches that would be processed, then exits.
|
|
650
|
+
|
|
651
|
+
Use `--verbose` (or `-v`) to print every prompt as it is sent during a real
|
|
652
|
+
run:
|
|
653
|
+
|
|
654
|
+
```bash
|
|
655
|
+
llm mr map "Classify sentiment" -p -i data.csv -c sentiment -o out.csv --verbose
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
Both flags work with `map`, `reduce`, and `filter`.
|
|
659
|
+
|
|
660
|
+
### Cost tracking
|
|
661
|
+
|
|
662
|
+
The `llm` tool automatically logs every prompt and response to its SQLite
|
|
663
|
+
database. After any `llm mr` run, you'll see a line like:
|
|
664
|
+
|
|
665
|
+
```
|
|
666
|
+
Made 47 LLM calls; run 'llm logs -n 47' to review
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
Use that command to inspect the prompts, responses, and token counts from
|
|
670
|
+
your run. For more on the logs system, see the
|
|
671
|
+
[llm logs documentation](https://llm.datasette.io/en/stable/logging.html).
|
|
672
|
+
|
|
673
|
+
## Development
|
|
674
|
+
|
|
675
|
+
This project is managed with [`uv`](https://docs.astral.sh/uv/latest/) and [`just`](https://just.systems/):
|
|
676
|
+
|
|
677
|
+
```bash
|
|
678
|
+
uv sync # install dependencies
|
|
679
|
+
just test # run tests
|
|
680
|
+
just lint # check linting and formatting
|
|
681
|
+
just fix # auto-fix linting and formatting
|
|
682
|
+
just check # lint + test
|
|
683
|
+
just release # check, tag v{version}, push branch + tag (see docs/release.md)
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
Release notes live in
|
|
687
|
+
[CHANGELOG.md](CHANGELOG.md); maintainers can follow [docs/release.md](docs/release.md)
|
|
688
|
+
for versioning, tags, and PyPI.
|
|
689
|
+
|
|
690
|
+
## Future Work
|
|
691
|
+
|
|
692
|
+
- **Rate-limiting for `-j` / `--parallel`** — Currently `-j 20` fires all
|
|
693
|
+
requests concurrently with no throttling, which can trigger API rate limits
|
|
694
|
+
(HTTP 429). Failed batches land in the `.err` sidecar and can be retried
|
|
695
|
+
with `--repair`, but adding automatic retry with exponential backoff would
|
|
696
|
+
make high-parallelism runs more robust.
|
|
697
|
+
|
|
698
|
+
- **Token-limit awareness** — The `--max-chars` flag uses character counts as
|
|
699
|
+
a proxy for token limits. Actual token counts are model-specific and the
|
|
700
|
+
`llm` library does not expose a tokenizer API, so precise per-model token
|
|
701
|
+
budgeting is not feasible in the general case. The current heuristic
|
|
702
|
+
(roughly 4 characters per token for English text) works in practice, and
|
|
703
|
+
context-window errors are caught by the `.err` / `--repair` mechanism.
|
|
704
|
+
|
|
705
|
+
## See Also
|
|
706
|
+
|
|
707
|
+
Some other tools offering "run an LLM prompt against every row" features
|
|
708
|
+
with different trade-offs:
|
|
709
|
+
|
|
710
|
+
- [smelt-ai](https://github.com/Cydra-Tech/smelt-ai) — Python library that
|
|
711
|
+
batch-processes `list[dict]` through LLMs with Pydantic-typed outputs, concurrency,
|
|
712
|
+
and retry.
|
|
713
|
+
- [Cellm](https://github.com/getcellm/cellm) — `=PROMPT()` formula for Excel.
|
|
714
|
+
- [sheets-llm](https://github.com/nicucalcea/sheets-llm) — `=LLM()` custom
|
|
715
|
+
function for Google Sheets.
|
|
716
|
+
- [Datablist](https://www.datablist.com/enrichments/run-chatgpt-bulk) — web app
|
|
717
|
+
that runs ChatGPT prompts per CSV row.
|
|
718
|
+
- [batch-llm.com](https://batch-llm.com/) — SaaS for uploading CSVs and running
|
|
719
|
+
prompt templates per row via OpenAI, Anthropic, or Google models.
|