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 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.