ebb-ai 0.6.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.
@@ -0,0 +1,57 @@
1
+ # Dependencies
2
+ node_modules/
3
+ .pnpm-store/
4
+
5
+ # Build output
6
+ dist/
7
+ build/
8
+ *.tsbuildinfo
9
+ .next/
10
+ out/
11
+
12
+ # Python
13
+ __pycache__/
14
+ *.py[cod]
15
+ *$py.class
16
+ .venv/
17
+ venv/
18
+ *.egg-info/
19
+ .pytest_cache/
20
+ .ruff_cache/
21
+
22
+ # Environment
23
+ .env
24
+ .env.local
25
+ .env.*.local
26
+ !.env.example
27
+
28
+ # Logs
29
+ *.log
30
+ npm-debug.log*
31
+ pnpm-debug.log*
32
+
33
+ # IDE
34
+ .vscode/
35
+ .idea/
36
+ *.swp
37
+ *.swo
38
+
39
+ # OS
40
+ .DS_Store
41
+ Thumbs.db
42
+
43
+ # Local data (SQLite scheduler queue)
44
+ *.db
45
+ *.db-journal
46
+ *.sqlite
47
+ *.sqlite3
48
+ ebb-queue.db
49
+
50
+ # Coverage
51
+ coverage/
52
+ *.lcov
53
+
54
+ # Temporary
55
+ tmp/
56
+ .cache/
57
+ development/
ebb_ai-0.6.0/PKG-INFO ADDED
@@ -0,0 +1,399 @@
1
+ Metadata-Version: 2.4
2
+ Name: ebb-ai
3
+ Version: 0.6.0
4
+ Summary: Carbon-aware scheduling for agentic AI workflows.
5
+ Project-URL: Homepage, https://github.com/Vitalini/ebb-ai
6
+ Project-URL: Issues, https://github.com/Vitalini/ebb-ai/issues
7
+ Project-URL: Repository, https://github.com/Vitalini/ebb-ai
8
+ Author: Vitalii Borovyk
9
+ License: Apache-2.0
10
+ Keywords: agents,ai,carbon-aware,llm,mcp,scheduling,sustainability
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Framework :: AsyncIO
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.11
24
+ Requires-Dist: aiosqlite>=0.20
25
+ Requires-Dist: httpx>=0.27
26
+ Provides-Extra: anthropic
27
+ Requires-Dist: anthropic>=0.39; extra == 'anthropic'
28
+ Provides-Extra: dev
29
+ Requires-Dist: anthropic>=0.39; extra == 'dev'
30
+ Requires-Dist: openai>=1.50; extra == 'dev'
31
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
32
+ Requires-Dist: pytest>=8; extra == 'dev'
33
+ Requires-Dist: ruff>=0.7; extra == 'dev'
34
+ Provides-Extra: openai
35
+ Requires-Dist: openai>=1.50; extra == 'openai'
36
+ Description-Content-Type: text/markdown
37
+
38
+ # ebb-ai (Python)
39
+
40
+ **Carbon-aware scheduling for agentic AI workflows.**
41
+
42
+ `ebb-ai` defers non-urgent LLM calls to execution windows that are
43
+ simultaneously cleaner on the electricity grid, cheaper at the
44
+ provider, and friendlier to your hardware budget. The same agent code
45
+ that would have made a synchronous LLM call now hands the work to
46
+ `ebb-ai`, which picks the right time and the right route — and writes
47
+ a per-task carbon receipt you can audit.
48
+
49
+ This package is the Python port of [`@ebb-ai/core`](../core-ts/). The
50
+ two stay in lock-step on every public name; the only deliberate
51
+ asymmetry is that the Python port ships **SQLite-backed durable
52
+ persistence** from day one, while the TypeScript port is still
53
+ in-memory at v0.1. See [`ROADMAP.md`](../../ROADMAP.md) section 4.1.
54
+
55
+ > Status: v0.2 · 2026-05 · pre-PyPI, install from source.
56
+
57
+ ---
58
+
59
+ ## Why this exists
60
+
61
+ Modern AI agents call LLM APIs synchronously by default. Three costs follow:
62
+
63
+ - **Carbon.** Grid carbon intensity varies 30–60% inside a single day
64
+ across the major US ISOs. Inference at 2 p.m. on a hot day is
65
+ materially dirtier than the same call at 3 a.m.
66
+ - **Dollars.** Anthropic and OpenAI both offer batch APIs at a flat
67
+ 50% discount for tasks that can wait up to 24 hours. Almost no agent
68
+ code uses them by default because it requires rewriting the call
69
+ site.
70
+ - **Latency, honestly.** Off-peak *sync* execution is sometimes
71
+ faster because providers throttle and queue at peak. **Batch API
72
+ is *not* faster** — it trades latency (up to 24h SLA) for the
73
+ 50% discount.
74
+
75
+ `ebb-ai` fixes all three for any task that is not "answer me right now."
76
+
77
+ ---
78
+
79
+ ## Install
80
+
81
+ ```bash
82
+ # from source (until the first PyPI release)
83
+ pip install -e packages/core-py
84
+
85
+ # with vendor extras
86
+ pip install -e "packages/core-py[anthropic,openai]"
87
+
88
+ # dev install for contributors
89
+ pip install -e "packages/core-py[dev,anthropic,openai]"
90
+ ```
91
+
92
+ Python 3.11+. The core library only requires `httpx` and `aiosqlite`;
93
+ the Anthropic and OpenAI extras pull in the official vendor SDKs.
94
+
95
+ ---
96
+
97
+ ## Quick start
98
+
99
+ ### 1. The five-line version
100
+
101
+ ```python
102
+ import asyncio
103
+ from ebb_ai import defer
104
+
105
+ async def main():
106
+ result = await defer(
107
+ lambda: "do the work here",
108
+ deadline="2026-05-13T08:00:00-04:00", # "by 8am tomorrow my time"
109
+ carbon_budget_g=5, # optional max grams CO2e
110
+ region="US-CAL-CISO", # optional grid region
111
+ )
112
+ print(result)
113
+
114
+ asyncio.run(main())
115
+ ```
116
+
117
+ `defer()` returns whatever the callable returns. The task body can be
118
+ sync or async; both work.
119
+
120
+ ### 2. Hand-built scheduler with SQLite persistence
121
+
122
+ For long-running services you want a single, explicitly-constructed
123
+ scheduler with a durable queue:
124
+
125
+ ```python
126
+ import asyncio
127
+ from ebb_ai import Scheduler, DeferOptions
128
+
129
+ async def main():
130
+ async with Scheduler(db_path="/var/lib/ebb/queue.sqlite") as scheduler:
131
+ await scheduler.defer(
132
+ lambda: do_research_run(),
133
+ DeferOptions(
134
+ deadline="2026-05-13T08:00:00-04:00",
135
+ carbon_budget_g=5,
136
+ region="US-CAL-CISO",
137
+ task_id="user:42:weekly-research",
138
+ ),
139
+ )
140
+
141
+ asyncio.run(main())
142
+ ```
143
+
144
+ If the process restarts, the next `Scheduler(db_path=…)` will see the
145
+ completed receipts via `await scheduler.list_persisted_tasks()`.
146
+
147
+ ### 3. Anthropic Batch API
148
+
149
+ ```python
150
+ import asyncio
151
+ from ebb_ai import Scheduler, DeferOptions
152
+ from ebb_ai.providers import AnthropicAdapter
153
+
154
+ async def main():
155
+ adapter = AnthropicAdapter() # reads ANTHROPIC_API_KEY
156
+
157
+ async def work():
158
+ result = await adapter.dispatch(
159
+ "claude-sonnet-4-5",
160
+ "Summarize today's git commits.",
161
+ )
162
+ return result.text
163
+
164
+ async with Scheduler() as scheduler:
165
+ summary = await scheduler.defer(
166
+ work,
167
+ DeferOptions(
168
+ deadline="2026-05-13T08:00:00-04:00",
169
+ region="US-CAL-CISO",
170
+ ),
171
+ )
172
+ print(summary)
173
+
174
+ asyncio.run(main())
175
+ ```
176
+
177
+ For genuinely batched work (50+ prompts at once), use the Batch API
178
+ directly:
179
+
180
+ ```python
181
+ handle = await adapter.dispatch_batch(
182
+ "claude-sonnet-4-5",
183
+ ["prompt 1", "prompt 2", "prompt 3"],
184
+ )
185
+ print(handle.batch_id) # poll via the Anthropic SDK
186
+ ```
187
+
188
+ ### 4. OpenAI Batch API
189
+
190
+ ```python
191
+ import asyncio
192
+ from ebb_ai.providers import OpenAIAdapter
193
+
194
+ async def main():
195
+ adapter = OpenAIAdapter() # reads OPENAI_API_KEY
196
+ handle = await adapter.dispatch_batch(
197
+ "gpt-4.1-mini",
198
+ ["prompt 1", "prompt 2"],
199
+ )
200
+ print(handle.batch_id)
201
+ ```
202
+
203
+ `OpenAIAdapter` builds the required JSONL input file in memory,
204
+ uploads it via `files.create(purpose="batch")`, and submits the batch
205
+ job — the standard OpenAI batch flow.
206
+
207
+ ---
208
+
209
+ ## API reference
210
+
211
+ ### Top-level exports
212
+
213
+ | Name | Kind | Description |
214
+ |---|---|---|
215
+ | `defer(task, *, deadline, carbon_budget_g, region, task_id)` | async function | Defers on the process-wide default scheduler. |
216
+ | `Scheduler` | class | Explicit scheduler; supports SQLite persistence. |
217
+ | `mock_grid_feed()` | function | Deterministic synthetic feed for dev/tests. |
218
+ | `electricity_maps_feed(api_key=None)` | function | Electricity Maps free-tier API client. |
219
+ | `pick_best_window(entries, deadline)` | pure function | Picks the lowest-intensity entry inside `[now, deadline]`. |
220
+ | `normalize_deadline(d)` | function | Parses + validates a user-supplied deadline. |
221
+ | `CarbonBudgetExceededError` | exception | Raised when no candidate window meets the user budget. |
222
+ | `InvalidDeadlineError` | exception (`ValueError`) | Raised when the deadline is unparseable or in the past. |
223
+ | `TaskRecord`, `CarbonReceipt`, `GridForecast`, `GridForecastEntry`, `DeferOptions` | dataclasses | Public record types. |
224
+
225
+ ### `Scheduler`
226
+
227
+ ```python
228
+ Scheduler(*, feed: GridFeed | None = None,
229
+ default_region: str = "US-CAL-CISO",
230
+ db_path: str | None = None)
231
+ ```
232
+
233
+ Methods:
234
+
235
+ - `await scheduler.connect()` — opens the SQLite connection (no-op
236
+ when `db_path` is unset).
237
+ - `await scheduler.defer(task, opts)` — deferred await. Returns the
238
+ task's eventual result.
239
+ - `scheduler.enqueue(task, opts)` — fire-and-forget. Returns the
240
+ `TaskRecord` immediately with `status="queued"`.
241
+ - `scheduler.get_task(task_id)` / `scheduler.list_tasks()` — in-memory
242
+ snapshots.
243
+ - `await scheduler.load_persisted_task(task_id)` /
244
+ `await scheduler.list_persisted_tasks()` — reads from SQLite.
245
+ - `await scheduler.shutdown()` — cancels in-flight schedule tasks and
246
+ closes the DB. The class also supports `async with`.
247
+
248
+ ### Carbon-receipt schema
249
+
250
+ Every completed task has a `receipt` field:
251
+
252
+ ```python
253
+ @dataclass
254
+ class CarbonReceipt:
255
+ task_id: str
256
+ ran_at: str
257
+ region: str
258
+ estimated_carbon_g_co2: float
259
+ provider: str | None = None
260
+ model: str | None = None
261
+ duration_ms: float | None = None
262
+ ```
263
+
264
+ The `TaskRecord` also carries `intensity_source`: `"scored"` if the
265
+ receipt used the forecast entry the scheduler chose against,
266
+ `"current"` if we had to fall back to a freshly-fetched current-hour
267
+ intensity at dispatch time.
268
+
269
+ The receipt's `estimated_carbon_g_co2` is computed via
270
+ `estimate_energy_kwh(model=..., input_tokens=..., output_tokens=...)
271
+ * grid_intensity_g_co2_per_kwh`. As of v0.6 the energy module ships
272
+ per-model Wh/token coefficients from the published research
273
+ (Patterson et al. 2021; Luccioni, Jernite, Strubell 2024; Hugging
274
+ Face AI Energy Score). When no model is specified the function
275
+ returns the pre-v0.6 flat `0.0015` kWh estimate to preserve
276
+ backwards compatibility. See `ebb_ai.energy` for the table and
277
+ citation metadata.
278
+
279
+ ### Grid feeds
280
+
281
+ Both feeds implement `GridFeed.fetch_forecast(region, hours)`:
282
+
283
+ - `mock_grid_feed()` — deterministic intraday sinusoid with regional
284
+ floors. Useful for dev and CI without an API key.
285
+ - `electricity_maps_feed(api_key=None)` — hits
286
+ `api.electricitymap.org/v3/carbon-intensity/forecast` with a 5 s
287
+ hard timeout. Falls back to the mock feed (and logs a warning) on
288
+ any failure, mirroring the TypeScript port.
289
+
290
+ Supported region codes match Electricity Maps' zone codes
291
+ (`US-CAL-CISO`, `US-TEX-ERCO`, `US-NE-ISNE`, `US-NY-NYIS`,
292
+ `US-MIDA-PJM`, `US-MIDW-MISO`, `FR`, `DE`, `GB`, ...).
293
+
294
+ ### Provider adapters
295
+
296
+ ```python
297
+ from ebb_ai.providers import AnthropicAdapter, OpenAIAdapter, DispatchOptions
298
+ ```
299
+
300
+ Both adapters implement:
301
+
302
+ ```python
303
+ async def dispatch(model: str, prompt: str,
304
+ options: DispatchOptions | None = None) -> DispatchResult: ...
305
+
306
+ async def dispatch_batch(model: str, prompts: list[str],
307
+ options: DispatchOptions | None = None) -> BatchHandle: ...
308
+ ```
309
+
310
+ Both modules import cleanly even when the vendor SDK isn't installed —
311
+ construction is what raises a clear error. This means you can write
312
+ code that *references* the adapters without forcing a dependency on
313
+ both SDKs.
314
+
315
+ ---
316
+
317
+ ## Persistence (Python-only in v0.2)
318
+
319
+ The Python port ships SQLite-backed durability from day one. To enable
320
+ it, construct the `Scheduler` with a `db_path`:
321
+
322
+ ```python
323
+ async with Scheduler(db_path="/var/lib/ebb/queue.sqlite") as scheduler:
324
+ ...
325
+ ```
326
+
327
+ Schema:
328
+
329
+ ```sql
330
+ CREATE TABLE tasks (
331
+ task_id TEXT PRIMARY KEY,
332
+ status TEXT NOT NULL,
333
+ enqueued_at TEXT NOT NULL,
334
+ scheduled_for TEXT,
335
+ completed_at TEXT,
336
+ region TEXT NOT NULL,
337
+ carbon_budget_g REAL,
338
+ result_json TEXT,
339
+ error TEXT,
340
+ receipt_json TEXT,
341
+ intensity_source TEXT
342
+ );
343
+ ```
344
+
345
+ Receipts and results are stored as JSON. Non-JSON results (e.g. an
346
+ Anthropic `Message` object) are stored as `{"_repr": "<repr>"}` so
347
+ audit trails survive odd value types.
348
+
349
+ Multi-writer support (multiple schedulers pointed at one DB) requires
350
+ WAL mode and is on the v0.3 roadmap.
351
+
352
+ ---
353
+
354
+ ## Testing
355
+
356
+ ```bash
357
+ cd packages/core-py
358
+ python3 -m venv .venv
359
+ . .venv/bin/activate
360
+ pip install -e ".[dev,anthropic,openai]"
361
+ pytest -v
362
+ ```
363
+
364
+ The test suite covers:
365
+
366
+ - Mock grid feed shape and intraday curve.
367
+ - `pick_best_window` correctness (empty, past, in-window).
368
+ - Scheduler accounting: queued/scheduled/running/completed
369
+ transitions.
370
+ - Deadline validation: unparseable strings, past dates,
371
+ `datetime` instances.
372
+ - Carbon-budget enforcement (US-MIDW-MISO floor exceeds a 0.1 g cap).
373
+ - SQLite round-trips, including reopening the DB after shutdown.
374
+ - Provider adapter request shapes and response parsing for both
375
+ Anthropic and OpenAI (mocked SDKs — no live calls).
376
+
377
+ ---
378
+
379
+ ## Roadmap
380
+
381
+ - **v0.2 (this release)** — scheduler, grid feeds, SQLite persistence,
382
+ Anthropic + OpenAI adapters with Batch API.
383
+ - **v0.3** — per-model energy coefficients; WattTime marginal-emissions
384
+ feed; multi-writer DB with WAL mode; richer receipt fields (model,
385
+ provider, token counts) propagated by the scheduler itself.
386
+ - **v0.4** — Gemini adapter; local-Ollama route; MCP spec PRs for
387
+ `priority` / `deadline` / `carbon_budget` fields.
388
+
389
+ See [`ROADMAP.md`](../../ROADMAP.md) for the full 24-week plan.
390
+
391
+ ---
392
+
393
+ ## License
394
+
395
+ [Apache License 2.0](../../LICENSE) — patent grant included.
396
+
397
+ ---
398
+
399
+ *Built by [Vitalii Borovyk](https://github.com/Vitalini).*