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.
- llmexer-0.3.0/LICENSE +21 -0
- llmexer-0.3.0/PKG-INFO +432 -0
- llmexer-0.3.0/README.md +404 -0
- llmexer-0.3.0/llmexer/__init__.py +0 -0
- llmexer-0.3.0/llmexer/base/__init__.py +1 -0
- llmexer-0.3.0/llmexer/base/dao.py +432 -0
- llmexer-0.3.0/llmexer/base/experiment.py +135 -0
- llmexer-0.3.0/llmexer/base/llm_core.py +22 -0
- llmexer-0.3.0/llmexer/base/llm_manager.py +288 -0
- llmexer-0.3.0/llmexer/base/llm_provider.py +278 -0
- llmexer-0.3.0/llmexer/base/papers.py +267 -0
- llmexer-0.3.0/llmexer/base/search.py +381 -0
- llmexer-0.3.0/llmexer/base/search_openalex.py +296 -0
- llmexer-0.3.0/llmexer/base/search_semantic_scholar.py +151 -0
- llmexer-0.3.0/llmexer/cli.py +148 -0
- llmexer-0.3.0/llmexer/commands/__init__.py +0 -0
- llmexer-0.3.0/llmexer/commands/experiment.py +734 -0
- llmexer-0.3.0/llmexer/commands/papers.py +481 -0
- llmexer-0.3.0/llmexer/commands/project.py +90 -0
- llmexer-0.3.0/llmexer/commands/search.py +858 -0
- llmexer-0.3.0/llmexer/commands/self.py +58 -0
- llmexer-0.3.0/llmexer/common.py +133 -0
- llmexer-0.3.0/llmexer/configs.py +33 -0
- llmexer-0.3.0/llmexer/constants.py +29 -0
- llmexer-0.3.0/llmexer/exceptions.py +42 -0
- llmexer-0.3.0/llmexer/logger.py +32 -0
- llmexer-0.3.0/llmexer/version.py +39 -0
- llmexer-0.3.0/llmexer.egg-info/PKG-INFO +432 -0
- llmexer-0.3.0/llmexer.egg-info/SOURCES.txt +58 -0
- llmexer-0.3.0/llmexer.egg-info/dependency_links.txt +1 -0
- llmexer-0.3.0/llmexer.egg-info/entry_points.txt +2 -0
- llmexer-0.3.0/llmexer.egg-info/requires.txt +13 -0
- llmexer-0.3.0/llmexer.egg-info/top_level.txt +1 -0
- llmexer-0.3.0/pyproject.toml +59 -0
- llmexer-0.3.0/setup.cfg +4 -0
- llmexer-0.3.0/tests/test_experiment_copy.py +220 -0
- llmexer-0.3.0/tests/test_experiment_generate.py +718 -0
- llmexer-0.3.0/tests/test_experiment_init.py +234 -0
- llmexer-0.3.0/tests/test_experiment_list.py +239 -0
- llmexer-0.3.0/tests/test_experiment_run.py +713 -0
- llmexer-0.3.0/tests/test_experiments_manager.py +417 -0
- llmexer-0.3.0/tests/test_papers_add.py +349 -0
- llmexer-0.3.0/tests/test_papers_download.py +631 -0
- llmexer-0.3.0/tests/test_papers_extract.py +468 -0
- llmexer-0.3.0/tests/test_project_create.py +57 -0
- llmexer-0.3.0/tests/test_project_current.py +72 -0
- llmexer-0.3.0/tests/test_project_rename.py +197 -0
- llmexer-0.3.0/tests/test_provider_base.py +205 -0
- llmexer-0.3.0/tests/test_provider_ollama.py +270 -0
- llmexer-0.3.0/tests/test_search_create.py +117 -0
- llmexer-0.3.0/tests/test_search_filter.py +319 -0
- llmexer-0.3.0/tests/test_search_list.py +135 -0
- llmexer-0.3.0/tests/test_search_merge.py +305 -0
- llmexer-0.3.0/tests/test_search_openalex_limit.py +131 -0
- llmexer-0.3.0/tests/test_search_openalex_query.py +35 -0
- llmexer-0.3.0/tests/test_search_pid.py +126 -0
- llmexer-0.3.0/tests/test_search_rename.py +177 -0
- llmexer-0.3.0/tests/test_search_run_file.py +319 -0
- llmexer-0.3.0/tests/test_search_stats.py +125 -0
- 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
|
+

|
|
394
|
+
|
|
395
|
+
Checking the statistics of a performed search query directly in CLI:
|
|
396
|
+
```
|
|
397
|
+
llmexer search stats --file <filename>
|
|
398
|
+
```
|
|
399
|
+

|
|
400
|
+
|
|
401
|
+
List existing projects directly in CLI with the current project highlighted:
|
|
402
|
+
```
|
|
403
|
+
llmexer experiment list
|
|
404
|
+
```
|
|
405
|
+

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