llm_batch_helper 0.3.1__py3-none-any.whl → 0.3.3__py3-none-any.whl
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_batch_helper/__init__.py +4 -1
- llm_batch_helper/exceptions.py +8 -0
- llm_batch_helper/input_handlers.py +27 -3
- llm_batch_helper/providers.py +31 -14
- {llm_batch_helper-0.3.1.dist-info → llm_batch_helper-0.3.3.dist-info}/METADATA +77 -43
- llm_batch_helper-0.3.3.dist-info/RECORD +10 -0
- llm_batch_helper-0.3.1.dist-info/RECORD +0 -10
- {llm_batch_helper-0.3.1.dist-info → llm_batch_helper-0.3.3.dist-info}/LICENSE +0 -0
- {llm_batch_helper-0.3.1.dist-info → llm_batch_helper-0.3.3.dist-info}/WHEEL +0 -0
llm_batch_helper/__init__.py
CHANGED
@@ -1,13 +1,16 @@
|
|
1
1
|
from .cache import LLMCache
|
2
2
|
from .config import LLMConfig
|
3
|
+
from .exceptions import InvalidPromptFormatError, VerificationFailedError
|
3
4
|
from .input_handlers import get_prompts, read_prompt_files, read_prompt_list
|
4
5
|
from .providers import process_prompts_batch, process_prompts_batch_async
|
5
6
|
|
6
|
-
__version__ = "0.3.
|
7
|
+
__version__ = "0.3.3"
|
7
8
|
|
8
9
|
__all__ = [
|
9
10
|
"LLMCache",
|
10
11
|
"LLMConfig",
|
12
|
+
"InvalidPromptFormatError",
|
13
|
+
"VerificationFailedError",
|
11
14
|
"get_prompts",
|
12
15
|
"process_prompts_batch",
|
13
16
|
"process_prompts_batch_async", # For backward compatibility
|
llm_batch_helper/exceptions.py
CHANGED
@@ -5,3 +5,11 @@ class VerificationFailedError(Exception):
|
|
5
5
|
super().__init__(message)
|
6
6
|
self.prompt_id = prompt_id
|
7
7
|
self.llm_response_data = llm_response_data
|
8
|
+
|
9
|
+
|
10
|
+
class InvalidPromptFormatError(Exception):
|
11
|
+
"""Custom exception for invalid prompt format."""
|
12
|
+
|
13
|
+
def __init__(self, message, invalid_item=None):
|
14
|
+
super().__init__(message)
|
15
|
+
self.invalid_item = invalid_item
|
@@ -2,6 +2,8 @@ import hashlib
|
|
2
2
|
from pathlib import Path
|
3
3
|
from typing import Any, Dict, List, Tuple, Union
|
4
4
|
|
5
|
+
from .exceptions import InvalidPromptFormatError
|
6
|
+
|
5
7
|
|
6
8
|
def read_prompt_files(input_dir: str) -> List[Tuple[str, str]]:
|
7
9
|
"""Read all text files from input directory and return as (filename, content) pairs.
|
@@ -53,12 +55,34 @@ def read_prompt_list(
|
|
53
55
|
elif isinstance(item, tuple) and len(item) == 2:
|
54
56
|
# Tuple format: (prompt_id, prompt_text)
|
55
57
|
prompt_id, prompt_text = item
|
56
|
-
elif isinstance(item, dict)
|
57
|
-
# Dict format:
|
58
|
+
elif isinstance(item, dict):
|
59
|
+
# Dict format: must have both "id" and "text" keys
|
60
|
+
if "id" not in item:
|
61
|
+
raise InvalidPromptFormatError(
|
62
|
+
f"Dictionary prompt is missing required 'id' key. "
|
63
|
+
f"Dictionary format must be: {{'id': 'prompt_id', 'text': 'prompt_text'}}. "
|
64
|
+
f"Got: {item}",
|
65
|
+
invalid_item=item
|
66
|
+
)
|
67
|
+
if "text" not in item:
|
68
|
+
raise InvalidPromptFormatError(
|
69
|
+
f"Dictionary prompt is missing required 'text' key. "
|
70
|
+
f"Dictionary format must be: {{'id': 'prompt_id', 'text': 'prompt_text'}}. "
|
71
|
+
f"Got: {item}",
|
72
|
+
invalid_item=item
|
73
|
+
)
|
58
74
|
prompt_id = item["id"]
|
59
75
|
prompt_text = item["text"]
|
60
76
|
else:
|
61
|
-
raise
|
77
|
+
raise InvalidPromptFormatError(
|
78
|
+
f"Invalid prompt format. Expected str, tuple, or dict, got {type(item).__name__}. "
|
79
|
+
f"Valid formats: "
|
80
|
+
f"- str: 'prompt text' "
|
81
|
+
f"- tuple: ('prompt_id', 'prompt_text') "
|
82
|
+
f"- dict: {{'id': 'prompt_id', 'text': 'prompt_text'}}. "
|
83
|
+
f"Got: {item}",
|
84
|
+
invalid_item=item
|
85
|
+
)
|
62
86
|
prompts.append((prompt_id, prompt_text))
|
63
87
|
return prompts
|
64
88
|
|
llm_batch_helper/providers.py
CHANGED
@@ -288,10 +288,11 @@ async def process_prompts_batch_async(
|
|
288
288
|
force: If True, force regeneration even if cached response exists
|
289
289
|
|
290
290
|
Returns:
|
291
|
-
Dict mapping prompt IDs to their responses
|
291
|
+
Dict mapping prompt IDs to their responses, ordered by input sequence
|
292
292
|
|
293
293
|
Note:
|
294
294
|
Either prompts or input_dir must be provided, but not both.
|
295
|
+
Results are returned in the same order as the input prompts.
|
295
296
|
"""
|
296
297
|
if prompts is None and input_dir is None:
|
297
298
|
raise ValueError("Either prompts or input_dir must be provided")
|
@@ -309,6 +310,9 @@ async def process_prompts_batch_async(
|
|
309
310
|
|
310
311
|
# Process prompts
|
311
312
|
results = {}
|
313
|
+
# Keep track of original order for sorting results
|
314
|
+
prompt_order = {prompt_id: idx for idx, (prompt_id, _) in enumerate(prompts)}
|
315
|
+
|
312
316
|
tasks = [
|
313
317
|
_process_single_prompt_attempt_with_verification(
|
314
318
|
prompt_id, prompt_text, config, provider, semaphore, cache_dir, force
|
@@ -320,7 +324,14 @@ async def process_prompts_batch_async(
|
|
320
324
|
prompt_id, response_data = await future
|
321
325
|
results[prompt_id] = response_data
|
322
326
|
|
323
|
-
|
327
|
+
# Sort results by original input order to maintain input sequence
|
328
|
+
# Note: Python 3.7+ guarantees dict insertion order, we explicitly sort
|
329
|
+
# to ensure results match the original prompt order regardless of completion order
|
330
|
+
ordered_results = {}
|
331
|
+
for prompt_id in sorted(results.keys(), key=lambda pid: prompt_order[pid]):
|
332
|
+
ordered_results[prompt_id] = results[prompt_id]
|
333
|
+
|
334
|
+
return ordered_results
|
324
335
|
|
325
336
|
|
326
337
|
def process_prompts_batch(
|
@@ -348,10 +359,11 @@ def process_prompts_batch(
|
|
348
359
|
force: If True, force regeneration even if cached response exists
|
349
360
|
|
350
361
|
Returns:
|
351
|
-
Dict mapping prompt IDs to their responses
|
362
|
+
Dict mapping prompt IDs to their responses, ordered by input sequence
|
352
363
|
|
353
364
|
Note:
|
354
365
|
Either prompts or input_dir must be provided, but not both.
|
366
|
+
Results are returned in the same order as the input prompts.
|
355
367
|
|
356
368
|
Example:
|
357
369
|
>>> from llm_batch_helper import LLMConfig, process_prompts_batch
|
@@ -361,6 +373,7 @@ def process_prompts_batch(
|
|
361
373
|
... config=config,
|
362
374
|
... provider="openai"
|
363
375
|
... )
|
376
|
+
>>> # Results will be in the same order as input prompts
|
364
377
|
"""
|
365
378
|
return _run_async_function(
|
366
379
|
process_prompts_batch_async,
|
@@ -390,18 +403,22 @@ async def _process_single_prompt_attempt_with_verification(
|
|
390
403
|
cache = LLMCache(cache_dir)
|
391
404
|
cached_response = cache.get_cached_response(prompt_id)
|
392
405
|
if cached_response is not None:
|
393
|
-
# Verify response if callback provided
|
394
406
|
cached_response_data = cached_response["llm_response"]
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
407
|
+
|
408
|
+
# If no verification callback, use cached response directly
|
409
|
+
if config.verification_callback is None:
|
410
|
+
return prompt_id, {**cached_response_data, "from_cache": True}
|
411
|
+
|
412
|
+
# Verify response if callback provided
|
413
|
+
verified = await asyncio.to_thread(
|
414
|
+
config.verification_callback,
|
415
|
+
prompt_id,
|
416
|
+
cached_response_data,
|
417
|
+
prompt_text,
|
418
|
+
**config.verification_callback_args,
|
419
|
+
)
|
420
|
+
if verified:
|
421
|
+
return prompt_id, {**cached_response_data, "from_cache": True}
|
405
422
|
|
406
423
|
# Process the prompt
|
407
424
|
last_exception_details = None
|
@@ -1,20 +1,20 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: llm_batch_helper
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.3
|
4
4
|
Summary: A Python package that enables batch submission of prompts to LLM APIs, with simplified interface and built-in async capabilities handled implicitly.
|
5
5
|
License: MIT
|
6
6
|
Keywords: llm,openai,together,openrouter,batch,async,ai,nlp,api
|
7
7
|
Author: Tianyi Peng
|
8
8
|
Author-email: tianyipeng95@gmail.com
|
9
|
-
Requires-Python: >=3.
|
9
|
+
Requires-Python: >=3.10,<4.0
|
10
10
|
Classifier: Development Status :: 4 - Beta
|
11
11
|
Classifier: Intended Audience :: Developers
|
12
12
|
Classifier: License :: OSI Approved :: MIT License
|
13
13
|
Classifier: Programming Language :: Python :: 3
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
14
15
|
Classifier: Programming Language :: Python :: 3.11
|
15
16
|
Classifier: Programming Language :: Python :: 3.12
|
16
17
|
Classifier: Programming Language :: Python :: 3.13
|
17
|
-
Classifier: Programming Language :: Python :: 3.10
|
18
18
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
19
19
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
20
20
|
Requires-Dist: httpx (>=0.24.0,<2.0.0)
|
@@ -39,9 +39,9 @@ A Python package that enables batch submission of prompts to LLM APIs, with buil
|
|
39
39
|
|
40
40
|
## Why we designed this package
|
41
41
|
|
42
|
-
|
42
|
+
Imagine you have 5000 prompts you need to send to an LLM. Running them sequentially can be painfully slow—sometimes taking hours or even days. Worse, if the process fails midway, you’re forced to start all over again. We’ve struggled with this exact frustration, which is why we built this package, to directly tackle these pain points:
|
43
43
|
|
44
|
-
1. **Efficient Batch Processing**: How do you run LLM calls in batches efficiently? Our async implementation is 3X-100X faster than multi-thread/multi-process approaches.
|
44
|
+
1. **Efficient Batch Processing**: How do you run LLM calls in batches efficiently? Our async implementation is 3X-100X faster than multi-thread/multi-process approaches. In my own experience, it reduces the time from 24 hours to 10min.
|
45
45
|
|
46
46
|
2. **API Reliability**: LLM APIs can be unstable, so we need robust retry mechanisms when calls get interrupted.
|
47
47
|
|
@@ -53,46 +53,31 @@ This package is designed to solve these exact pain points with async processing,
|
|
53
53
|
|
54
54
|
## Features
|
55
55
|
|
56
|
-
-
|
57
|
-
-
|
58
|
-
-
|
59
|
-
-
|
60
|
-
-
|
61
|
-
-
|
62
|
-
-
|
63
|
-
-
|
64
|
-
-
|
56
|
+
- **🚀 Dramatic Speed Improvements**: **10-100x faster** than sequential processing ([see demo](https://github.com/TianyiPeng/LLM_batch_helper/blob/main/tutorials/performance_comparison_tutorial.ipynb))
|
57
|
+
- **⚡ Async Processing**: Submit multiple prompts concurrently for maximum throughput
|
58
|
+
- **💾 Smart Caching**: Automatically cache responses and resume interrupted work seamlessly
|
59
|
+
- **📝 Multiple Input Formats**: Support for strings, tuples, dictionaries, and file-based prompts
|
60
|
+
- **🌐 Multi-Provider Support**: Works with OpenAI (all models), OpenRouter (100+ models), and Together.ai
|
61
|
+
- **🔄 Intelligent Retry Logic**: Built-in retry mechanism with exponential backoff and detailed logging
|
62
|
+
- **✅ Quality Control**: Custom verification callbacks for response validation
|
63
|
+
- **📊 Progress Tracking**: Real-time progress bars and comprehensive statistics
|
64
|
+
- **🎯 Simplified API**: No async/await complexity - works seamlessly in Jupyter notebooks (v0.3.0+)
|
65
|
+
- **🔧 Tunable Performance**: Adjust concurrency on-the-fly for optimal speed vs rate limits
|
65
66
|
|
66
67
|
## Installation
|
67
68
|
|
68
|
-
### For Users (Recommended)
|
69
|
-
|
70
69
|
```bash
|
71
70
|
# Install from PyPI
|
72
71
|
pip install llm_batch_helper
|
73
72
|
```
|
74
73
|
|
75
|
-
### For Development
|
76
|
-
|
77
|
-
```bash
|
78
|
-
# Clone the repository
|
79
|
-
git clone https://github.com/TianyiPeng/LLM_batch_helper.git
|
80
|
-
cd llm_batch_helper
|
81
|
-
|
82
|
-
# Install with Poetry
|
83
|
-
poetry install
|
84
|
-
|
85
|
-
# Activate the virtual environment
|
86
|
-
poetry shell
|
87
|
-
```
|
88
|
-
|
89
74
|
## Quick Start
|
90
75
|
|
91
76
|
### 1. Set up environment variables
|
92
77
|
|
93
78
|
**Option A: Environment Variables**
|
94
79
|
```bash
|
95
|
-
# For OpenAI (all models including GPT-5)
|
80
|
+
# For OpenAI (all OpenAI models including GPT-5)
|
96
81
|
export OPENAI_API_KEY="your-openai-api-key"
|
97
82
|
|
98
83
|
# For OpenRouter (100+ models - Recommended)
|
@@ -103,6 +88,11 @@ export TOGETHER_API_KEY="your-together-api-key"
|
|
103
88
|
```
|
104
89
|
|
105
90
|
**Option B: .env File (Recommended for Development)**
|
91
|
+
Create a `.env` file in your project:
|
92
|
+
```
|
93
|
+
OPENAI_API_KEY=your-openai-api-key
|
94
|
+
```
|
95
|
+
|
106
96
|
```python
|
107
97
|
# In your script, before importing llm_batch_helper
|
108
98
|
from dotenv import load_dotenv
|
@@ -112,17 +102,17 @@ load_dotenv() # Load from .env file
|
|
112
102
|
from llm_batch_helper import LLMConfig, process_prompts_batch
|
113
103
|
```
|
114
104
|
|
115
|
-
|
116
|
-
```
|
117
|
-
OPENAI_API_KEY=your-openai-api-key
|
118
|
-
TOGETHER_API_KEY=your-together-api-key
|
119
|
-
```
|
120
|
-
|
121
|
-
### 2. Interactive Tutorial (Recommended)
|
105
|
+
### 2. Interactive Tutorials (Recommended)
|
122
106
|
|
123
|
-
|
107
|
+
**🎯 NEW: Performance Comparison Tutorial**
|
108
|
+
See the dramatic speed improvements! Our [Performance Comparison Tutorial](https://github.com/TianyiPeng/LLM_batch_helper/blob/main/tutorials/performance_comparison_tutorial.ipynb) demonstrates:
|
109
|
+
- **10-100x speedup** vs naive sequential processing
|
110
|
+
- Processing **5,000 prompts** in minutes instead of hours
|
111
|
+
- **Smart caching** that lets you resume interrupted work
|
112
|
+
- **Tunable concurrency** for optimal performance
|
124
113
|
|
125
|
-
|
114
|
+
**📚 Complete Feature Tutorial**
|
115
|
+
Check out the comprehensive [main tutorial](https://github.com/TianyiPeng/LLM_batch_helper/blob/main/tutorials/llm_batch_helper_tutorial.ipynb) covering all features with interactive examples!
|
126
116
|
|
127
117
|
### 3. Basic usage
|
128
118
|
|
@@ -138,10 +128,10 @@ config = LLMConfig(
|
|
138
128
|
model_name="gpt-4o-mini",
|
139
129
|
temperature=1.0,
|
140
130
|
max_completion_tokens=100,
|
141
|
-
max_concurrent_requests=
|
131
|
+
max_concurrent_requests=100 # number of concurrent requests with asyncIO, this number decides how fast your pipeline can run. We suggest a number that is as large as possible (e.g., 300) while making sure you are not over the rate limit constrained by the LLM APIs.
|
142
132
|
)
|
143
133
|
|
144
|
-
# Process prompts
|
134
|
+
# Process prompts
|
145
135
|
prompts = [
|
146
136
|
"What is the capital of France?",
|
147
137
|
"What is 2+2?",
|
@@ -162,6 +152,49 @@ for prompt_id, response in results.items():
|
|
162
152
|
|
163
153
|
**🎉 New in v0.3.0**: `process_prompts_batch` now handles async operations **implicitly** - no more async/await syntax needed! Works seamlessly in Jupyter notebooks.
|
164
154
|
|
155
|
+
### 4. Multiple Input Formats
|
156
|
+
|
157
|
+
The package supports three different input formats for maximum flexibility:
|
158
|
+
|
159
|
+
```python
|
160
|
+
from llm_batch_helper import LLMConfig, process_prompts_batch
|
161
|
+
|
162
|
+
config = LLMConfig(
|
163
|
+
model_name="gpt-4o-mini",
|
164
|
+
temperature=1.0,
|
165
|
+
max_completion_tokens=100
|
166
|
+
)
|
167
|
+
|
168
|
+
# Mix different input formats in the same list
|
169
|
+
prompts = [
|
170
|
+
# String format - ID will be auto-generated from hash
|
171
|
+
"What is the capital of France?",
|
172
|
+
|
173
|
+
# Tuple format - (custom_id, prompt_text)
|
174
|
+
("custom_id_1", "What is 2+2?"),
|
175
|
+
|
176
|
+
# Dictionary format - {"id": custom_id, "text": prompt_text}
|
177
|
+
{"id": "shakespeare_q", "text": "Who wrote 'Hamlet'?"},
|
178
|
+
{"id": "science_q", "text": "Explain photosynthesis briefly."}
|
179
|
+
]
|
180
|
+
|
181
|
+
results = process_prompts_batch(
|
182
|
+
config=config,
|
183
|
+
provider="openai",
|
184
|
+
prompts=prompts,
|
185
|
+
cache_dir="cache"
|
186
|
+
)
|
187
|
+
|
188
|
+
# Print results with custom IDs
|
189
|
+
for prompt_id, response in results.items():
|
190
|
+
print(f"{prompt_id}: {response['response_text']}")
|
191
|
+
```
|
192
|
+
|
193
|
+
**Input Format Requirements:**
|
194
|
+
- **String**: Plain text prompt (ID auto-generated)
|
195
|
+
- **Tuple**: `(prompt_id, prompt_text)` - both elements required
|
196
|
+
- **Dictionary**: `{"id": "prompt_id", "text": "prompt_text"}` - both keys required
|
197
|
+
|
165
198
|
### 🔄 Backward Compatibility
|
166
199
|
|
167
200
|
For users who prefer the async version or have existing code, the async API is still available:
|
@@ -352,7 +385,8 @@ llm_batch_helper/
|
|
352
385
|
│ ├── prompts/ # Sample prompt files
|
353
386
|
│ └── llm_cache/ # Example cache directory
|
354
387
|
└── tutorials/ # Interactive tutorials
|
355
|
-
|
388
|
+
├── llm_batch_helper_tutorial.ipynb # Comprehensive feature tutorial
|
389
|
+
└── performance_comparison_tutorial.ipynb # Performance demo (NEW!)
|
356
390
|
```
|
357
391
|
|
358
392
|
## Supported Models
|
@@ -0,0 +1,10 @@
|
|
1
|
+
llm_batch_helper/__init__.py,sha256=dFEq-gZrOkRpvRWib8RiAP9h9Ww9NrRVGe4lLAxH7H0,579
|
2
|
+
llm_batch_helper/cache.py,sha256=QUODQ1tPCvFThO3yvVOTcorcOrmN2dP5HLF1Y2O1bTQ,1276
|
3
|
+
llm_batch_helper/config.py,sha256=vM3YEeLYkN9qfY1fTPXN27wAlTeaVXXCfcFw7nvbIqw,1372
|
4
|
+
llm_batch_helper/exceptions.py,sha256=FvcBV5gpSAmk_XwoBecGGqFeVDTawgeV6txw6Tob-z0,523
|
5
|
+
llm_batch_helper/input_handlers.py,sha256=KeEwWQZc6c9QYQwOCPEVb_njM5WVZF3d2Staomd2KjA,3945
|
6
|
+
llm_batch_helper/providers.py,sha256=o0gGp9uHOVKJQ8ZK2KYHDocjqPpiABMisVApvG35eaM,17765
|
7
|
+
llm_batch_helper-0.3.3.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
8
|
+
llm_batch_helper-0.3.3.dist-info/METADATA,sha256=nRaotV4SD8v6RufE4JKDGuRPZnBMrf0wJtEdfefJzi4,16740
|
9
|
+
llm_batch_helper-0.3.3.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
10
|
+
llm_batch_helper-0.3.3.dist-info/RECORD,,
|
@@ -1,10 +0,0 @@
|
|
1
|
-
llm_batch_helper/__init__.py,sha256=sCQ-eJVy3cCs80ASvcww10om_E9CHDctCfpIGLPIcy8,442
|
2
|
-
llm_batch_helper/cache.py,sha256=QUODQ1tPCvFThO3yvVOTcorcOrmN2dP5HLF1Y2O1bTQ,1276
|
3
|
-
llm_batch_helper/config.py,sha256=vM3YEeLYkN9qfY1fTPXN27wAlTeaVXXCfcFw7nvbIqw,1372
|
4
|
-
llm_batch_helper/exceptions.py,sha256=59_f3jINUhKFble6HTp8pmtLSFE2MYLHWGclwaQKs28,296
|
5
|
-
llm_batch_helper/input_handlers.py,sha256=IadA732F1Rw0zcBok5hjZr32RUm8eTUOpvLsRuMvaE4,2877
|
6
|
-
llm_batch_helper/providers.py,sha256=zv7dCiKZtSOcdV-4kvd3WKhClOv1jio9neZqGcYskm8,16794
|
7
|
-
llm_batch_helper-0.3.1.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
8
|
-
llm_batch_helper-0.3.1.dist-info/METADATA,sha256=AAzYnTi_1DhVn3subd9XT3LLFbKP63MCn1cptZTgMAc,14505
|
9
|
-
llm_batch_helper-0.3.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
10
|
-
llm_batch_helper-0.3.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|