llmexer 0.3.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.
Files changed (60) hide show
  1. llmexer-0.3.0/LICENSE +21 -0
  2. llmexer-0.3.0/PKG-INFO +432 -0
  3. llmexer-0.3.0/README.md +404 -0
  4. llmexer-0.3.0/llmexer/__init__.py +0 -0
  5. llmexer-0.3.0/llmexer/base/__init__.py +1 -0
  6. llmexer-0.3.0/llmexer/base/dao.py +432 -0
  7. llmexer-0.3.0/llmexer/base/experiment.py +135 -0
  8. llmexer-0.3.0/llmexer/base/llm_core.py +22 -0
  9. llmexer-0.3.0/llmexer/base/llm_manager.py +288 -0
  10. llmexer-0.3.0/llmexer/base/llm_provider.py +278 -0
  11. llmexer-0.3.0/llmexer/base/papers.py +267 -0
  12. llmexer-0.3.0/llmexer/base/search.py +381 -0
  13. llmexer-0.3.0/llmexer/base/search_openalex.py +296 -0
  14. llmexer-0.3.0/llmexer/base/search_semantic_scholar.py +151 -0
  15. llmexer-0.3.0/llmexer/cli.py +148 -0
  16. llmexer-0.3.0/llmexer/commands/__init__.py +0 -0
  17. llmexer-0.3.0/llmexer/commands/experiment.py +734 -0
  18. llmexer-0.3.0/llmexer/commands/papers.py +481 -0
  19. llmexer-0.3.0/llmexer/commands/project.py +90 -0
  20. llmexer-0.3.0/llmexer/commands/search.py +858 -0
  21. llmexer-0.3.0/llmexer/commands/self.py +58 -0
  22. llmexer-0.3.0/llmexer/common.py +133 -0
  23. llmexer-0.3.0/llmexer/configs.py +33 -0
  24. llmexer-0.3.0/llmexer/constants.py +29 -0
  25. llmexer-0.3.0/llmexer/exceptions.py +42 -0
  26. llmexer-0.3.0/llmexer/logger.py +32 -0
  27. llmexer-0.3.0/llmexer/version.py +39 -0
  28. llmexer-0.3.0/llmexer.egg-info/PKG-INFO +432 -0
  29. llmexer-0.3.0/llmexer.egg-info/SOURCES.txt +58 -0
  30. llmexer-0.3.0/llmexer.egg-info/dependency_links.txt +1 -0
  31. llmexer-0.3.0/llmexer.egg-info/entry_points.txt +2 -0
  32. llmexer-0.3.0/llmexer.egg-info/requires.txt +13 -0
  33. llmexer-0.3.0/llmexer.egg-info/top_level.txt +1 -0
  34. llmexer-0.3.0/pyproject.toml +59 -0
  35. llmexer-0.3.0/setup.cfg +4 -0
  36. llmexer-0.3.0/tests/test_experiment_copy.py +220 -0
  37. llmexer-0.3.0/tests/test_experiment_generate.py +718 -0
  38. llmexer-0.3.0/tests/test_experiment_init.py +234 -0
  39. llmexer-0.3.0/tests/test_experiment_list.py +239 -0
  40. llmexer-0.3.0/tests/test_experiment_run.py +713 -0
  41. llmexer-0.3.0/tests/test_experiments_manager.py +417 -0
  42. llmexer-0.3.0/tests/test_papers_add.py +349 -0
  43. llmexer-0.3.0/tests/test_papers_download.py +631 -0
  44. llmexer-0.3.0/tests/test_papers_extract.py +468 -0
  45. llmexer-0.3.0/tests/test_project_create.py +57 -0
  46. llmexer-0.3.0/tests/test_project_current.py +72 -0
  47. llmexer-0.3.0/tests/test_project_rename.py +197 -0
  48. llmexer-0.3.0/tests/test_provider_base.py +205 -0
  49. llmexer-0.3.0/tests/test_provider_ollama.py +270 -0
  50. llmexer-0.3.0/tests/test_search_create.py +117 -0
  51. llmexer-0.3.0/tests/test_search_filter.py +319 -0
  52. llmexer-0.3.0/tests/test_search_list.py +135 -0
  53. llmexer-0.3.0/tests/test_search_merge.py +305 -0
  54. llmexer-0.3.0/tests/test_search_openalex_limit.py +131 -0
  55. llmexer-0.3.0/tests/test_search_openalex_query.py +35 -0
  56. llmexer-0.3.0/tests/test_search_pid.py +126 -0
  57. llmexer-0.3.0/tests/test_search_rename.py +177 -0
  58. llmexer-0.3.0/tests/test_search_run_file.py +319 -0
  59. llmexer-0.3.0/tests/test_search_stats.py +125 -0
  60. llmexer-0.3.0/tests/test_search_sync.py +298 -0
llmexer-0.3.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 vdmitriyev, ellariel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
llmexer-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,432 @@
1
+ Metadata-Version: 2.4
2
+ Name: llmexer
3
+ Version: 0.3.0
4
+ Summary: A framework and CLI utility to curate datasets and orchestrate (design, run, evaluate) various LLM experiments
5
+ Author: vdmitriyev, ellariel
6
+ Maintainer: vdmitriyev
7
+ Project-URL: homepage, https://github.com/vdmitriyev/llmexer
8
+ Project-URL: changelog, https://github.com/vdmitriyev/llmexer/blob/main/HISTORY.rst
9
+ Project-URL: repository, https://github.com/vdmitriyev/llmexer.git
10
+ Project-URL: documentation, https://vdmitriyev.github.io/llmexer/
11
+ Requires-Python: >=3.11
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: typer>=0.9.0
15
+ Requires-Dist: rich>=13.7.0
16
+ Requires-Dist: python-dotenv>=1.0.0
17
+ Requires-Dist: Jinja2>=3.1.6
18
+ Requires-Dist: typing-extensions>=4.0.0
19
+ Requires-Dist: pandas>=3.0.1
20
+ Requires-Dist: requests>=2.33.0
21
+ Requires-Dist: pypdf>=6.10.2
22
+ Requires-Dist: langdetect>=1.0.9
23
+ Requires-Dist: pydantic>=2.12.5
24
+ Requires-Dist: openai>=2.30.0
25
+ Requires-Dist: PyYAML>=6.0
26
+ Requires-Dist: SQLAlchemy>=2.0.36
27
+ Dynamic: license-file
28
+
29
+ ## ๐Ÿ”ฐAbout
30
+
31
+ `llmexer` is a framework and CLI utility to create and curate datasets (e.g., publications, metadata, LLM prompts, etc.) and orchestrate (design, run, evaluate) various LLM experiments on them
32
+
33
+ > ๐Ÿช„ The philosophy of the tool is: `everything` is a `file`. Projects, experiments, searches, configs, and further items will be saved as files. The CLI helps you to modify most of the files, but the same files could be modified manually (e.g., adding a new LLM model, modification of search search or paper as PDFs could be manually added, further more, a SQLite database with generated experiments could be inspected and edited etc.).
34
+
35
+ ## ๐Ÿ“ฆ Installation
36
+
37
+ * Install using `pip`
38
+ ```
39
+ pip install --upgrade llmexer
40
+ ```
41
+ * Install using `uv`
42
+ ```
43
+ uv pip install --upgrade llmexer
44
+ ```
45
+ * Check `configuration` section afterward
46
+
47
+ ## โš™๏ธ Configuration
48
+
49
+ This tool requires access to local or remote running LLMs. It uses a `.env` file to securely load your API credentials, and also set the current project.
50
+
51
+ 1. Create a `.env` file in the root of the project
52
+ 2. Set the current project ID (optional):
53
+ ```env
54
+ PROJECT_ID=20260330-3a9adf70
55
+ ```
56
+ 3. Configure the docling backend (optional, used by `papers extract --processor docling`):
57
+ ```env
58
+ DOCLING_URL=http://localhost:5001/
59
+ DOCLING_USER=myuser
60
+ DOCLING_PASSWORD=mypassword
61
+ ```
62
+ 4. Set the API key for LLM providers (optional, used by `experiment run` for ollama, vllm, OpenAI, Gemini):
63
+ ```env
64
+ # Base URL for a specific provider
65
+ PROVIDER_OLLAMA_URL=http://localhost:11434/v1
66
+ PROVIDER_VLLM_URL=http://localhost:8000/v1
67
+
68
+ # The API key for a specific provider (takes precedence over LLM_API_KEY)
69
+ PROVIDER_OPENAI_KEY=sk-...
70
+ ```
71
+ The pattern is `PROVIDER_<PROVIDER_UPPER>_URL` and `PROVIDER_<PROVIDER_UPPER>_KEY` where `<PROVIDER_UPPER>` is the provider name in uppercase (e.g. `OLLAMA`, `VLLM`, `OPENAI`, `GEMINI`).
72
+ 5. Enable the OpenAlex search engine (optional, used by `search run` as a second source after Semantic Scholar; skipped if unset):
73
+ ```env
74
+ OPENALEX_API_KEY=...
75
+ # Optional: cap on OpenAlex results processed per query (default 5000)
76
+ MAX_OPEN_ALEX_RESPONSES=5000
77
+ # Optional: used for DOI downloads via Unpaywall and also as the OpenAlex polite-pool mailto
78
+ UNPAYWALL_EMAIL=you@example.com
79
+ ```
80
+ 6. If you would like to change the envs based on the project run (e.g., just test a LLM provider for a particular project, via stats of a search, etc.), you could also pass a custom file as `.env` to the CLI:
81
+ ```
82
+ llmexer --env-file custom.env
83
+ ```
84
+
85
+ ## Documentation
86
+
87
+ [Documentation](https://vdmitriyev.github.io/llmexer/)
88
+
89
+ ## ๐Ÿš€ Getting Started
90
+
91
+ A typical end-to-end workflow for collecting and processing papers inside a project:
92
+
93
+ **1. Create a new project**
94
+ ```bash
95
+ llmexer project create
96
+ # Output: created project '20260402-a1b2c3d4'
97
+ ```
98
+
99
+ **2. Give it a meaningful name**
100
+ ```bash
101
+ llmexer project rename --old-id 20260402-a1b2c3d4 --new-id llm-survey-2026
102
+ ```
103
+
104
+ **3. Initialise the project structure**
105
+
106
+ Scaffold a standard `experiment/` subfolder with template CSVs and a prompt file:
107
+ ```bash
108
+ llmexer experiment init --pid llm-survey-2026
109
+ ```
110
+
111
+ <details>
112
+ Initialization of the project creates following files (inside <PROJECT_NAME> folder):
113
+
114
+ - `experiment/llm-models.csv` - list of models to use (name, provider, notes); pre-filled with `gemma4:31b`, `phi4:14b`
115
+ - `experiment/data.csv` - input data rows (ID, Title, Abstract)
116
+ - `experiment/mapping.csv` - maps data IDs to prompt IDs; pre-filled with `D01;prompt01` and `D02;prompt01`
117
+ - `experiment/prompts/prompt01.txt` - a starter Jinja2 prompt template using `{{title}}` and `{{abstract}}`
118
+ - `experiment/llm-params.csv` - LLM hyperparameter profiles;
119
+ - identity columns: `provider`, `model_name`, `profile_name`;
120
+ - universal columns: `temperature`, `top_p`, `max_tokens`;
121
+ - provider-grouped columns: `ollama_context_window`, `ollama_repeat_penalty` (ollama), `vllm_min_p`, `vllm_best_of` (vllm), `openai_seed` (openai), `gemini_thinking_level` (gemini); pre-filled with example profiles for `ollama`, `openai`, `vllm`, and `gemini`
122
+ </details>
123
+
124
+ **4. Generate the full experiment database**
125
+
126
+ After filling in `experiment/llm-models.csv`, `experiment/data.csv`, `experiment/mapping.csv`, `experiment/llm-params.csv`, and your Jinja2 prompt templates:
127
+ ```bash
128
+ llmexer experiment generate --pid llm-survey-2026
129
+ ```
130
+
131
+ <details>
132
+ This renders every (data row ร— prompt ร— LLM models ร— LLM parameters) combination and writes a self-contained SQLite database `experiment/experiment_<YYYYMMDD>_<NN>.db` (`<NN>` is a zero-padded counter starting at `01`). Each LLM provider gets its own table (e.g. `experiment_ollama`, `experiment_openai`) holding the rendered prompt, model identity, that provider's hyperparameter columns, and the SHA-256 hashes โ€” plus the result columns that `experiment run` fills in later. The `code` field encodes each combination as `DATAID_PROMPTID_MODELNAME_PROFILENAME`. Use `--dry-run` to preview the row count without writing:
133
+ ```bash
134
+ llmexer --dry-run experiment generate --pid llm-survey-2026
135
+ ```
136
+ </details>
137
+
138
+ > ๐Ÿ’ก **Hint โ€” external tools:** the generated experiment store is a SQLite database
139
+ > (`experiment/experiment_*.db`). Beyond the CLI, you can open and edit it directly with any
140
+ > external SQLite tool โ€” for example [DBeaver](https://dbeaver.io/).
141
+
142
+ **5. Run the experiment - call LLMs and collect results**
143
+
144
+ Once `experiment generate` has produced the database, run all combinations:
145
+ ```bash
146
+ llmexer experiment run --pid llm-survey-2026 --file experiment_<SAMPLE>.db
147
+ ```
148
+
149
+ <details>
150
+
151
+ This reads every row from the generated database (`experiment_*.db`, which already contains all param columns) and calls the appropriate LLM. With no `--file` it uses the newest `experiment_*.db`. Results are written **back into the same database in place** โ€” each row's response, status, token usage, and timestamps are updated on its provider table, so the database stays the single source of truth (no separate results file). Re-running skips rows that already finished successfully and updates the rest. Each individual call is also saved as a JSON file under `experiment/responses/`. Both the per-call JSON and the database `response_json` column include the **complete raw backend response** under `raw_response` (all provider fields โ€” e.g. `finish_reason`, per-token `usage`, and ollama extras like `eval_count` / `*_duration`), not just the response text and total token count.
152
+
153
+ Use `--dry-run` to preview the row count without making any LLM calls:
154
+ ```bash
155
+ llmexer --dry-run experiment run --pid llm-survey-2026
156
+ ```
157
+
158
+ Run only a specific provider's rows (e.g. when only ollama is available):
159
+ ```bash
160
+ llmexer experiment run --pid llm-survey-2026 --filter-provider ollama
161
+ ```
162
+
163
+ Run a single combination by its `ID` (or `code`):
164
+ ```bash
165
+ llmexer experiment run --pid llm-survey-2026 \
166
+ --file experiment_<SAMPLE>.db --id 1
167
+ ```
168
+
169
+ </details>
170
+
171
+ **6. Inspect experiment statistics**
172
+
173
+ Get aggregate statistics (total, finished, running, errors, total tokens, and per-provider / per-model breakdowns). The per-model table reports, for each model, `requests`, `finished`, `open` (pending/unrun), `time total` (HH:MM:SS elapsed over finished requests), `average time` (HH:MM:SS mean elapsed per finished request), and `tokens` (summed over finished requests). With no `--file` it reads the project's single `experiment_*.db` (pass `--file` if several exist):
174
+ ```bash
175
+ llmexer experiment stats --pid llm-survey-2026
176
+ ```
177
+ Pass `--file` to inspect a specific database instead:
178
+ ```bash
179
+ llmexer experiment stats --pid llm-survey-2026 --file experiment_<SAMPLE>.db
180
+ ```
181
+
182
+ The API key is read from `.env` (pattern -> `PROVIDER_<PROVIDER_UPPER>_KEY`).
183
+
184
+ P.S.: CLI interfaces could become very complex with the time, thus refer to the `--help` to get options and parameters of the utility:
185
+ ```bash
186
+ llmexer --help
187
+ ```
188
+
189
+ ## ๐Ÿ“ข Scenario 1: Gathering data for projects by adding papers
190
+
191
+ **1. Add papers to the project** - local file
192
+
193
+ From a local file:
194
+ ```bash
195
+ llmexer papers add --pid llm-survey-2026 --file ~/Downloads/attention-is-all-you-need.pdf
196
+ ```
197
+ **2. Add papers to the project** - from directory
198
+
199
+ From a directory of PDFs:
200
+ ```bash
201
+ llmexer papers add --pid llm-survey-2026 --directory ~/Downloads/papers/
202
+ ```
203
+
204
+ **3. Add papers to the project** - from url
205
+ From a URL:
206
+ ```bash
207
+ llmexer papers add --pid llm-survey-2026 --url https://arxiv.org/pdf/1706.03762
208
+ ```
209
+
210
+ ## ๐Ÿ“ข Scenario 2: Gathering data for projects by running search
211
+
212
+ **1. Run a literature search**
213
+
214
+ Create a search configuration and run it:
215
+ ```bash
216
+ llmexer search create --pid llm-survey-2026 --query "large language models"
217
+ llmexer search list --pid llm-survey-2026
218
+ llmexer search run --pid llm-survey-2026 --file 20260401-bfdd863d.yaml
219
+ ```
220
+
221
+ Or run directly from a query string:
222
+ ```bash
223
+ llmexer search run --pid llm-survey-2026 --query "large language models" --limit 500
224
+ ```
225
+
226
+ Results are saved as `<ID>__results.csv` in `searches/` (the raw JSON is saved as `<ID>__results_raw.json` in `searches/jsons/`).
227
+
228
+ **2. Filter search results by excluding rows (optional)**
229
+
230
+ `filter` **excludes** rows by one or more criteria and writes `<ID>__filtered.csv`. Filters chain: each run reads the existing `__filtered.csv` (or the `__results.csv` if none) and rewrites it. Combine `--language`, `--source`, `--doi`, `--downloaded` in one run:
231
+ ```bash
232
+ # drop German rows and rows still not downloaded
233
+ llmexer search filter --pid llm-survey-2026 --file 20260401-bfdd863d.yaml --language de --downloaded
234
+ ```
235
+ `--file` is optional โ€” omit it to apply the same filters to **every** search in the project. Every applied filter (per search) is recorded in `searches/logs/filters-applied.log`.
236
+
237
+ ## ๐Ÿ“ข Scenario 3: Gathering data by downloading papers and extracting text
238
+
239
+ **1. Download open-access papers by DOI via Unpaywall**
240
+
241
+ Download by DOI (one or more):
242
+ ```bash
243
+ llmexer papers download --pid llm-survey-2026 --doi 10.1038/nature12373 --email you@example.com
244
+ ```
245
+ Download from a full search result CSV (downloads all papers with a DOI, names each file `YEAR_AUTHOR_TITLE_DOI.pdf`):
246
+ ```bash
247
+ llmexer papers download --pid llm-survey-2026 --search-file 20260401-bfdd863d__results.csv
248
+ ```
249
+ Or from a filtered CSV to download only the papers that passed the language filter:
250
+ ```bash
251
+ llmexer papers download --pid llm-survey-2026 --search-file 20260401-bfdd863d__filtered.csv
252
+ ```
253
+ Failed downloads are saved automatically as `20260401-bfdd863d__results_download_failed.csv` (columns: `doi`, `url`, `title`, `desired_filename`, `downloaded`) in the `searches/logs/` folder. After a `--search-file` download completes, the search is automatically synced against the `papers/` folder in **existing-only** mode โ€” it updates `pdf_downloaded` (and text/markdown companions) for the listed rows but does not add new rows for unrelated PDFs.
254
+
255
+ **2. Extract text from all added papers** - pypdf
256
+
257
+ Using the default `pypdf` backend (saves `.txt` files):
258
+ ```bash
259
+ llmexer papers extract --pid llm-survey-2026
260
+ ```
261
+
262
+ **3. Extract text from all added papers** - docling
263
+
264
+ Using the `docling` backend for richer Markdown output (saves `.md` files), reading connection details from `.env`:
265
+ ```bash
266
+ llmexer papers extract --pid llm-survey-2026 --processor docling
267
+ ```
268
+
269
+ Override `.env` connection settings at runtime:
270
+ ```bash
271
+ llmexer papers extract --pid llm-survey-2026 --processor docling \
272
+ --docling-url http://myserver:5001/ \
273
+ --docling-user admin \
274
+ --docling-password secret
275
+ ```
276
+
277
+ By default, papers that already have an extracted file are skipped. Use `--rewrite` to force re-extraction:
278
+ ```bash
279
+ llmexer papers extract --pid llm-survey-2026 --rewrite
280
+ ```
281
+
282
+ #### Using Current Project ID
283
+
284
+ Many commands support the `--pid` parameter to specify which project to work with. If you set `PROJECT_ID` in your `.env` file, you can omit this parameter and the commands will use the current project automatically:
285
+
286
+ ```bash
287
+ # Set in .env
288
+ PROJECT_ID=my-project
289
+
290
+ # These commands will use my-project automatically
291
+ llmexer search run --query "machine learning"
292
+ ```
293
+
294
+ You can still override the current project by explicitly providing `--pid`:
295
+ ```bash
296
+ llmexer search run --pid different-project --query "deep learning"
297
+ ```
298
+
299
+ ## ๐Ÿ—‚๏ธ CLI category: **project**
300
+
301
+ The `project` (alias: `proj`) category provides commands for managing LLM projects (the top-level container for experiments, papers, and searches):
302
+
303
+ | Command | Description | Command Example |
304
+ |-----------|-------------|-----------------|
305
+ | `create` | Create a new project folder under `.projects/` using format `YYYYMMDD-GUID`. Accepts an optional custom ID. | `llmexer project create --id my-project` |
306
+ | `current` | Display the current project ID loaded from `.env`. | `llmexer project current` |
307
+ | `rename` | Rename an existing project. Uses `PROJECT_ID` from `.env` if `--old-id` is omitted. | `llmexer project rename --old-id old-name --new-id new-name` |
308
+
309
+ ## ๐Ÿงช CLI category: **experiment**
310
+
311
+ The `experiment` (alias: `exp`) category provides commands for initialising, generating, and running LLM experiments inside a project:
312
+
313
+ | Command | Description | Command Example |
314
+ |-----------|-------------|-----------------|
315
+ | `init` | Initialise an existing project with a standard folder structure (`experiment/`, `experiment/prompts/`) and template files: `llm-models.csv` (pre-filled with example ollama models), `data.csv`, `mapping.csv` (pre-filled with D01 and D02 rows), `prompts/prompt01.txt` (Jinja2 template using `{{title}}` and `{{abstract}}`), and `llm-params.csv` (hyperparameter profiles; universal: `temperature`, `top_p`, `max_tokens`; ollama: `ollama_context_window`, `ollama_repeat_penalty`; vllm: `vllm_min_p`, `vllm_best_of`; openai: `openai_seed`; gemini: `gemini_thinking_level`). Raises an error if already initialised. | `llmexer experiment init --pid my-project` |
316
+ | `copy-papers` | Copy parsed papers (`.md`/`.txt`) from the project's `papers/` folder into `experiment/data.csv` as rows `ID;filename;content`, with IDs `P01`, `P02`, โ€ฆ ordered alphabetically by filename (`.md` preferred over `.txt` when both exist). An existing `data.csv` is backed up to `data_backup_<YYYYMMDD>_<NN>.csv` first. | `llmexer experiment copy-papers --pid my-project` |
317
+ | `copy-search` | Copy a search results CSV (`--file`, absolute or relative to the project's `searches/` folder) into `experiment/data.csv` as rows `ID;Title;Abstract;doi;authors`, with IDs `S01`, `S02`, โ€ฆ preserving the source file's row order. An existing `data.csv` is backed up to `data_backup_<YYYYMMDD>_<NN>.csv` first. | `llmexer experiment copy-search --pid my-project --file <SEARCH_ID>__results.csv` |
318
+ | `generate` | Render all (data row ร— prompt ร— LLM models ร— LLM parameters) combinations and write a self-contained SQLite database `experiment/experiment_<YYYYMMDD>_<NN>.db` (`<NN>` is a zero-padded counter starting at `01`). Each LLM provider gets its own table (e.g. `experiment_ollama`) with columns `ID`, `code` (`DATAID_PROMPTID_MODELNAME_PROFILENAME`), `prompt`, `tokens_estimate`, `original_data`, `model_name`, `provider_name`, that provider's param columns from `llm-params.csv` (`profile_name`, `temperature`, `top_p`, `max_tokens`, plus the provider-specific ones, e.g. `ollama_context_window`, `ollama_repeat_penalty`), the `prompt_hash` / `original_data_hash` columns, and the result columns filled in by `run`. Rows are sorted by model order from `llm-models.csv`. Supports `--dry-run`. | `llmexer experiment generate --pid my-project` |
319
+ | `run` | Execute every row in the generated database `experiment_*.db` (no separate params file needed โ€” all columns are embedded). Calls each LLM via the OpenAI SDK (supports ollama, vllm, openai, gemini) and writes results **back into the same database in place** (response, status, token usage, timestamps, plus the complete raw backend response under `raw_response`); re-runs skip rows that already finished successfully and update the rest. Individual JSON responses are saved under `experiment/responses/`. Supports `--dry-run`, `--file` (choose a specific `.db`, defaults to the newest), `--filter-provider` (only run rows for a specific provider), `--id` (run a single combination by its `ID` or `code`). API key read from `LLM_API_KEY` or `PROVIDER_<PROVIDER_UPPER>_KEY` env vars; URL from `PROVIDER_<PROVIDER_UPPER>_URL` or built-in defaults. Requires `openai` package (`pip install openai`). | `llmexer experiment run --pid my-project --filter-provider ollama` |
320
+ | `stats` | Show aggregate statistics from a project's experiment database: totals (total, finished, running, errors), total tokens, and per-provider / per-model breakdowns rendered as Rich tables. The Models table has per-model columns `requests`, `finished`, `open` (pending/unrun), `time total` (HH:MM:SS over finished requests), `average time` (HH:MM:SS mean per finished request), and `tokens` (summed over finished requests). With no `--file` it reads the project's single `experiment_*.db` (pass `--file` to choose one when several exist). | `llmexer experiment stats --pid my-project` |
321
+ | `list` | List all projects with their initialization state and generated experiment databases, with optional sorting by name or date. | `llmexer experiment list --sort-by date --desc` |
322
+
323
+ ## ๐Ÿ“‘ CLI category: **papers**
324
+
325
+ The `papers` category provides commands for managing PDF papers within a project:
326
+
327
+ | Command | Description | Command Example |
328
+ |-----------|-------------|-----------------|
329
+ | `add --file` | Copy a single PDF into the project's `papers/` folder. | `llmexer papers add --file /path/to/paper.pdf` |
330
+ | `add --directory` | Recursively copy all PDFs from a directory. Already-existing papers are skipped. | `llmexer papers add --directory /path/to/folder` |
331
+ | `add --url` | Download a PDF from a URL into the project's `papers/` folder. | `llmexer papers add --url https://example.com/paper.pdf` |
332
+ | `download --doi` | Download one or more open-access PDFs by DOI using the Unpaywall API. Email required via `--email` or `UNPAYWALL_EMAIL` env var. | `llmexer papers download --doi 10.1038/nature12373 --email you@example.com` |
333
+ | `download --search-file` | Download all papers from a search result CSV (inside `searches/`), including filtered CSVs (`__filtered.csv`). Files are named `YEAR_AUTHOR_TITLE_DOI.pdf`. On completion, auto-runs `search sync` to reconcile the search against `papers/` (updates `pdf_downloaded`, txt/markdown). Failures saved as `<stem>_download_failed.csv` in `searches/logs/`. | `llmexer papers download --search-file 20260401-abc123__filtered.csv` |
334
+ | `extract` | Extract text from all PDFs in `papers/`. Default `pypdf` backend saves `.txt`; `docling` backend sends PDFs to a remote docling-serve instance and saves `.md`. Connection details (`DOCLING_URL`, `DOCLING_USER`, `DOCLING_PASSWORD`) read from `.env`; overridable via `--docling-url`, `--docling-user`, `--docling-password`. Already-extracted files are skipped unless `--rewrite` is passed. Pass `--skip-if-md` (pypdf only) to skip PDFs that already have a `.md` extraction. Reports counts for `Extracted`, `Skipped`, `Existing`, and `Error`. | `llmexer papers extract --pid my-project --processor docling` |
335
+
336
+ ## ๐Ÿ” CLI category: **search**
337
+
338
+ The `search` category provides commands for managing and running literature searches. `search run` queries the Semantic Scholar bulk API and, when `OPENALEX_API_KEY` is set, additionally queries the OpenAlex Works API as a second engine โ€” keeping only publications not already found by Semantic Scholar (matched by DOI, falling back to title). OpenAlex is skipped if the key is unset. OpenAlex rows are marked `entry_source="OpenAlex"`.
339
+
340
+ | Command | Description | Command Example |
341
+ |-----------|-------------|-----------------|
342
+ | `create` | Create a search configuration YAML file in the project's `searches/` folder. | `llmexer search create --query "machine learning"` |
343
+ | `list` | List all search YAML configs in the project's `searches/` folder as a table (columns: `#`, `Name`, `Query`, `Year`, `Created`, `Results`). Prints a next-step hint referencing the latest search file. | `llmexer search list --pid my-project` |
344
+ | `rename` | Rename a search ID and all its associated files (`<id>.yaml`, `<id>__results.csv`, `<id>__filtered.csv`, `<id>__results_raw.json` under `searches/jsons/`, and `<id>__results_download_failed.csv` under `searches/logs/`). Accepts a full `.yaml` filename for `--old-id`. | `llmexer search rename --old-id 20260401-abc123 --new-id my-search` |
345
+ | `run --query` | Run a search directly from a query string. Saves `<ID>__results.csv` to `searches/` and `<ID>__results_raw.json` to `searches/jsons/`. CSV columns include: `sem_scholar_paper_id`, `year`, `title`, `authors`, `abstract`, `isOpenAccess`, `doi`, `language`, `referenceCount`, `citationCount`, `entry_source`, `pdf_filename`, `txt_filename`, `markdown_filename`, `pdf_downloaded`. Raw JSON also contains `fieldsOfStudy`, `citationStyles`, `publicationTypes`. | `llmexer search run --query "neural networks" --limit 200` |
346
+ | `run --file` | Run a search loading parameters from an existing YAML config. Use `--rewrite` to overwrite existing result files. | `llmexer search run --file 20260401-abc123.yaml` |
347
+ | `stats` | Display statistics for a completed search: papers per year and a stats breakdown (open access, language, downloaded, entry source, txt/markdown presence), stacked for results and filtered CSVs. Without `--file`, falls back to the merged file(s) (`<pid>__merged_results.csv` / `<pid>__merged_filtered.csv`) if present. | `llmexer search stats --file 20260401-abc123.yaml` |
348
+ | `filter` | **Exclude** rows from a search and rewrite `<ID>__filtered.csv`. Reads the existing `__filtered.csv` if present (filters chain), else `__results.csv`. `--file` is optional โ€” omit it to filter every search in the project. Combinable criteria, each applied in order and logged: `--language <code>` / `--source <value>` / `--doi <value>` drop rows equal to the value; `--downloaded` drops rows not yet downloaded. Each applied filter appends a line to `searches/logs/filters-applied.log`. | `llmexer search filter --file 20260401-abc123.yaml --language de --downloaded` |
349
+ | `merge` | Merge the project's search CSVs into two deduplicated files: `<pid>__merged_results.csv` (from `*__results.csv`) and `<pid>__merged_filtered.csv` (from `*__filtered.csv`). Deduplicates by DOI (falling back to title); adds a `0/1` column per search (named after its YAML id) and a `duplicates_counter` column (number of duplicate occurrences, i.e. searches found in minus one). Rows are sorted by year (newest first; blank years last). Use `--rewrite` to overwrite; respects `--dry-run`. | `llmexer search merge --pid my-project` |
350
+ | `sync` | Reconcile `<ID>__results.csv` (and `<ID>__filtered.csv` if present) against the project's `papers/` folder. Updates `pdf_downloaded`, `txt_filename`, and `markdown_filename` for existing rows. By default only files listed in existing rows are updated; pass `--add-local-extra-pdfs` to also append new rows for PDFs in `papers/` not yet listed (marked `entry_source="manually added"`). `--file` is optional: with it a single search is synced, without it every search in the project is synced (in that case `--add-local-extra-pdfs` is not applied). Respects `--dry-run`. | `llmexer search sync --file 20260401-abc123.yaml` |
351
+
352
+ Semantic Scholar API Documentation: [Paper bulk search](https://api.semanticscholar.org/api-docs/#tag/Paper-Data/operation/get_graph_paper_bulk_search) -> this can be used to formulate more sophisticated query string
353
+
354
+ OpenAlex API Documentation: [Works](https://docs.openalex.org/api-entities/works) -> queried as a second engine when `OPENALEX_API_KEY` is set
355
+
356
+ ## ๐Ÿ”Ž CLI category: **self**
357
+
358
+ The `self` category provides introspection commands for the llmexer CLI itself:
359
+
360
+ | Command | Description | Command Example |
361
+ |-----------|-------------|-----------------|
362
+ | `version` | Print the current llmexer package version. | `llmexer self version` |
363
+ | `envs` | Display all llmexer-relevant environment variables as a table. `PROJECT_ID` is highlighted in bold cyan; `DOCLING_PASSWORD` is masked as `********` when set. | `llmexer self envs` |
364
+
365
+
366
+ ## ๐Ÿ“„ Additional: Renaming PDFs with `pdf-renamer` tool
367
+
368
+ Before adding papers to a project, you can automatically rename them by their bibliographic metadata (year, journal, authors, title) using the external [`pdf-renamer`](https://github.com/MicheleCotrufo/pdf-renamer) tool.
369
+
370
+ No installation is needed โ€” run it directly with `uvx`.
371
+
372
+ Rename using custom format: year - authors (et al.) - title:
373
+ ```bash
374
+ uvx --from pdf-renamer pdfrenamer -f "{YYYY}_{A3etal}_{T}" /path/to/pdfs
375
+ ```
376
+ Rename recursively (include subdirectories)
377
+ ```bash
378
+ uvx --from pdf-renamer pdfrenamer /path/to/pdfs -sf
379
+ ```
380
+
381
+ There is also a possibility to extract BiBTeX of a publication as follows
382
+ ```bash
383
+ uvx --from pdf2bib pdf2bib -s bibtex.bib /path/to/pdfs
384
+ ```
385
+
386
+ ## CLI UI
387
+
388
+ CLI feature overview:
389
+ ```
390
+ llmexer --help
391
+ ```
392
+
393
+ ![help CLI](https://raw.githubusercontent.com/vdmitriyev/llmexer/refs/heads/main/docs/cli-ui.png)
394
+
395
+ Checking the statistics of a performed search query directly in CLI:
396
+ ```
397
+ llmexer search stats --file <filename>
398
+ ```
399
+ ![search CLI](https://raw.githubusercontent.com/vdmitriyev/llmexer/refs/heads/main/docs/cli-ui-search-stats.png)
400
+
401
+ List existing projects directly in CLI with the current project highlighted:
402
+ ```
403
+ llmexer experiment list
404
+ ```
405
+ ![experiment CLI](https://raw.githubusercontent.com/vdmitriyev/llmexer/refs/heads/main/docs/cli-ui-experiment-list.png)
406
+
407
+
408
+ ## ๐Ÿงฉ Development Setup
409
+
410
+ This guide walks through setting up the project for local development using `uv`.
411
+
412
+ 1. Create a new virtual environment in a `.venv` directory and activates it.
413
+ ```bash
414
+ uv venv
415
+ ```
416
+ 1. Activate the environment (macOS/Linux):
417
+ ```
418
+ source .venv/bin/activate
419
+ ```
420
+ 1. Activate the environment (Windows):
421
+ ```
422
+ call .venv/Scripts/activate.bat
423
+ ```
424
+ 1. Install package in **editable mode** with **dev** dependencies
425
+ Installing the package in **editable mode** (`-e`) is the key to development. It links the `llmexer` command in your environment directly to your source code.
426
+ ```bash
427
+ uv pip install -e . --group dev
428
+ ```
429
+
430
+ ## License
431
+
432
+ [MIT](https://github.com/vdmitriyev/llmexer/blob/main/LICENSE)