runtime-narrative 0.1.0__tar.gz → 0.2.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.
- {runtime_narrative-0.1.0 → runtime_narrative-0.2.0}/LICENSE +21 -21
- runtime_narrative-0.2.0/PKG-INFO +408 -0
- runtime_narrative-0.2.0/README.MD +373 -0
- {runtime_narrative-0.1.0 → runtime_narrative-0.2.0}/pyproject.toml +68 -58
- runtime_narrative-0.2.0/runtime_narrative/__init__.py +31 -0
- {runtime_narrative-0.1.0 → runtime_narrative-0.2.0}/runtime_narrative/analyzers/__init__.py +3 -3
- {runtime_narrative-0.1.0 → runtime_narrative-0.2.0}/runtime_narrative/analyzers/ollama.py +220 -179
- {runtime_narrative-0.1.0 → runtime_narrative-0.2.0}/runtime_narrative/context.py +11 -11
- {runtime_narrative-0.1.0 → runtime_narrative-0.2.0}/runtime_narrative/decorators.py +87 -69
- runtime_narrative-0.2.0/runtime_narrative/diagnostics.py +389 -0
- {runtime_narrative-0.1.0 → runtime_narrative-0.2.0}/runtime_narrative/events.py +84 -66
- {runtime_narrative-0.1.0 → runtime_narrative-0.2.0}/runtime_narrative/failure.py +63 -63
- runtime_narrative-0.2.0/runtime_narrative/middleware.py +96 -0
- {runtime_narrative-0.1.0 → runtime_narrative-0.2.0}/runtime_narrative/renderer/__init__.py +11 -11
- {runtime_narrative-0.1.0 → runtime_narrative-0.2.0}/runtime_narrative/renderer/console.py +289 -218
- runtime_narrative-0.2.0/runtime_narrative/renderer/json_renderer.py +174 -0
- {runtime_narrative-0.1.0 → runtime_narrative-0.2.0}/runtime_narrative/stage.py +93 -58
- runtime_narrative-0.2.0/runtime_narrative/story.py +436 -0
- runtime_narrative-0.2.0/runtime_narrative.egg-info/PKG-INFO +408 -0
- {runtime_narrative-0.1.0 → runtime_narrative-0.2.0}/runtime_narrative.egg-info/SOURCES.txt +10 -1
- {runtime_narrative-0.1.0 → runtime_narrative-0.2.0}/setup.cfg +4 -4
- runtime_narrative-0.2.0/tests/test_async_renderer.py +28 -0
- runtime_narrative-0.2.0/tests/test_decorators.py +35 -0
- runtime_narrative-0.2.0/tests/test_diagnostics.py +206 -0
- runtime_narrative-0.2.0/tests/test_failure.py +19 -0
- runtime_narrative-0.2.0/tests/test_json_renderer.py +50 -0
- runtime_narrative-0.2.0/tests/test_middleware.py +64 -0
- runtime_narrative-0.2.0/tests/test_stage.py +24 -0
- runtime_narrative-0.2.0/tests/test_story.py +120 -0
- runtime_narrative-0.1.0/PKG-INFO +0 -195
- runtime_narrative-0.1.0/README.MD +0 -160
- runtime_narrative-0.1.0/runtime_narrative/__init__.py +0 -23
- runtime_narrative-0.1.0/runtime_narrative/middleware.py +0 -58
- runtime_narrative-0.1.0/runtime_narrative/renderer/json_renderer.py +0 -90
- runtime_narrative-0.1.0/runtime_narrative/story.py +0 -162
- runtime_narrative-0.1.0/runtime_narrative.egg-info/PKG-INFO +0 -195
- {runtime_narrative-0.1.0 → runtime_narrative-0.2.0}/runtime_narrative.egg-info/dependency_links.txt +0 -0
- {runtime_narrative-0.1.0 → runtime_narrative-0.2.0}/runtime_narrative.egg-info/requires.txt +0 -0
- {runtime_narrative-0.1.0 → runtime_narrative-0.2.0}/runtime_narrative.egg-info/top_level.txt +0 -0
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2024 Shashank Raj
|
|
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.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Shashank Raj
|
|
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.
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: runtime-narrative
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Model execution as human-readable stories with lean/rich failure diagnostics and optional LLM analysis
|
|
5
|
+
Author-email: Shashank Raj <shashank.raj28@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/sraj0501/runtime_narrative
|
|
8
|
+
Project-URL: Repository, https://github.com/sraj0501/runtime_narrative
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/sraj0501/runtime_narrative/issues
|
|
10
|
+
Keywords: logging,observability,tracing,fastapi,debugging,diagnostics,runtime_narrative
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Topic :: System :: Logging
|
|
22
|
+
Classifier: Topic :: System :: Monitoring
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Provides-Extra: console
|
|
28
|
+
Requires-Dist: typer>=0.9.0; extra == "console"
|
|
29
|
+
Provides-Extra: fastapi
|
|
30
|
+
Requires-Dist: starlette>=0.27.0; extra == "fastapi"
|
|
31
|
+
Provides-Extra: all
|
|
32
|
+
Requires-Dist: typer>=0.9.0; extra == "all"
|
|
33
|
+
Requires-Dist: starlette>=0.27.0; extra == "all"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# runtime-narrative
|
|
37
|
+
|
|
38
|
+
**Turn any Python application into a traceable story. Get minimal logs when everything works — and surgical, LLM-powered diagnostics the moment something breaks.**
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## The idea
|
|
43
|
+
|
|
44
|
+
Most logging tells you *that* something failed. `runtime-narrative` tells you *why* — with full awareness of every step that succeeded before the failure, what was supposed to happen next, and (optionally) a plain-English suggestion for how to fix it.
|
|
45
|
+
|
|
46
|
+
You model your application's execution as a **story** made up of **stages**. Each function or logical unit of work becomes a stage. The library watches everything:
|
|
47
|
+
|
|
48
|
+
- **When a stage passes:** one line — `✔ Stage completed: Validate Input (0.003s)`. No noise.
|
|
49
|
+
- **When anything fails:** a structured failure report with the exact file, line number, failing statement, the full timeline of what succeeded before it, and — if you plug in an LLM — a concrete logical fix suggestion.
|
|
50
|
+
|
|
51
|
+
This combines debugging and logging into a single mechanism: logs are minimal until something breaks, then they are explicit and actionable.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
Zero dependencies at the core:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install runtime-narrative
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Optional extras:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install "runtime-narrative[console]" # colored terminal output (typer)
|
|
67
|
+
pip install "runtime-narrative[fastapi]" # FastAPI/Starlette middleware
|
|
68
|
+
pip install "runtime-narrative[all]" # everything
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Quick start
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from runtime_narrative import story, stage
|
|
77
|
+
|
|
78
|
+
with story("Import Customers"):
|
|
79
|
+
with stage("Load CSV"):
|
|
80
|
+
rows = load_csv("customers.csv")
|
|
81
|
+
|
|
82
|
+
with stage("Validate Data"):
|
|
83
|
+
validate(rows)
|
|
84
|
+
|
|
85
|
+
with stage("Insert Records"):
|
|
86
|
+
db.insert(rows)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Everything works — minimal output:**
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
▶ Story started: Import Customers
|
|
93
|
+
✔ Stage completed: Load CSV (0.012s)
|
|
94
|
+
✔ Stage completed: Validate Data (0.004s)
|
|
95
|
+
✔ Stage completed: Insert Records (0.089s)
|
|
96
|
+
▶ Story ended: SUCCESS
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Something fails — full context, no guessing:**
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
▶ Story started: Import Customers
|
|
103
|
+
✔ Stage completed: Load CSV (0.012s)
|
|
104
|
+
✔ Stage completed: Validate Data (0.004s)
|
|
105
|
+
|
|
106
|
+
❌ Failure detected
|
|
107
|
+
Story: Import Customers
|
|
108
|
+
Stage: Insert Records
|
|
109
|
+
Error: ValueError - duplicate customer id
|
|
110
|
+
Location: app/db.py:47 (insert_row)
|
|
111
|
+
Code: raise ValueError("duplicate customer id")
|
|
112
|
+
Recent stages: Load CSV=completed (0.012s) | Validate Data=completed (0.004s) | Insert Records=failed (0.001s)
|
|
113
|
+
Progress: 66% (2 / 3)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The library knows what succeeded before the failure. That context is always part of the report.
|
|
117
|
+
|
|
118
|
+
Async code uses identical syntax with `async with`:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
async with story("Import Customers"):
|
|
122
|
+
async with stage("Load CSV"):
|
|
123
|
+
rows = await load_csv("customers.csv")
|
|
124
|
+
|
|
125
|
+
async with stage("Insert Records"):
|
|
126
|
+
await db.insert(rows)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## LLM-powered failure analysis (optional)
|
|
132
|
+
|
|
133
|
+
Plug in any local or remote LLM. When a failure occurs, the library packages the story name, stage name, error type, exact failing line, exception chain, and traceback — and asks the LLM for a targeted diagnostic.
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from runtime_narrative import story, stage, OllamaFailureAnalyzer
|
|
137
|
+
|
|
138
|
+
analyzer = OllamaFailureAnalyzer(model="llama3")
|
|
139
|
+
|
|
140
|
+
with story("Import Customers", failure_analyzer=analyzer):
|
|
141
|
+
with stage("Load CSV"):
|
|
142
|
+
rows = load_csv("customers.csv")
|
|
143
|
+
with stage("Insert Records"):
|
|
144
|
+
db.insert(rows)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The LLM response is structured and rendered inline:
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
+-- LLM Debug -----------------------------------------------------------+
|
|
151
|
+
| Exact Why |
|
|
152
|
+
| The INSERT fails because customer_id already exists in the customers |
|
|
153
|
+
| table (UNIQUE constraint). The error is raised at db.py:47. |
|
|
154
|
+
| |
|
|
155
|
+
| Evidence |
|
|
156
|
+
| ValueError: duplicate customer id — raised after catching a |
|
|
157
|
+
| sqlite3.IntegrityError from the underlying INSERT call. |
|
|
158
|
+
| |
|
|
159
|
+
| Targeted Fix |
|
|
160
|
+
| Use INSERT OR IGNORE, or check for existence before inserting. |
|
|
161
|
+
| Alternatively, catch the duplicate and return the existing record. |
|
|
162
|
+
| |
|
|
163
|
+
>> Code Changes |
|
|
164
|
+
| db.py:47 — wrap the insert in try/except IntegrityError and handle |
|
|
165
|
+
| the duplicate case explicitly rather than re-raising ValueError. |
|
|
166
|
+
+------------------------------------------------------------------------+
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
> **Note:** The LLM suggests logical fixes only — it does not rewrite your code. The suggestion names the exact location, explains what went wrong mechanically, and tells you what to change. What you change is up to you.
|
|
170
|
+
|
|
171
|
+
### Analyzer options
|
|
172
|
+
|
|
173
|
+
| Class | API | Use case |
|
|
174
|
+
|---|---|---|
|
|
175
|
+
| `OllamaFailureAnalyzer` | Ollama native `/api/generate` | Local Ollama |
|
|
176
|
+
| `LLMFailureAnalyzer` | OpenAI-compatible `/v1/chat/completions` | vLLM, llama.cpp, LM Studio, Ollama OpenAI mode, any hosted API |
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from runtime_narrative import LLMFailureAnalyzer
|
|
180
|
+
|
|
181
|
+
analyzer = LLMFailureAnalyzer(
|
|
182
|
+
model="llama3",
|
|
183
|
+
endpoint="http://localhost:8000/v1/chat/completions",
|
|
184
|
+
)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Both fall back silently if the endpoint is unreachable — your application's exception still propagates normally.
|
|
188
|
+
|
|
189
|
+
### Background analysis
|
|
190
|
+
|
|
191
|
+
For latency-sensitive services, use `background_analysis=True`. The `FailureOccurred` event is emitted immediately (so your error response is not delayed), and the LLM runs as a background task. When it finishes, a `LLMAnalysisReady` event is emitted:
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
async with story("Process Order", failure_analyzer=analyzer, background_analysis=True):
|
|
195
|
+
async with stage("Charge Payment"):
|
|
196
|
+
await charge(order)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Diagnostics depth
|
|
202
|
+
|
|
203
|
+
The library operates in two modes, controlled by environment variable or per-story kwargs:
|
|
204
|
+
|
|
205
|
+
| Mode | What you get |
|
|
206
|
+
|---|---|
|
|
207
|
+
| `lean` (default) | Error type, message, exact location, source line, exception chain, compressed stack summary |
|
|
208
|
+
| `rich` | Everything above + source code snippet (±2 lines around the error) + local variable values at the failing frame, with automatic redaction of secrets (`password`, `token`, `api_key`, etc.) |
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
# Enable rich diagnostics for a run
|
|
212
|
+
RUNTIME_NARRATIVE_FAILURE_DIAGNOSTICS=rich python myapp.py
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Rich mode is automatically downgraded to lean in production unless explicitly allowed:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
RUNTIME_NARRATIVE_ENV=production
|
|
219
|
+
RUNTIME_NARRATIVE_ALLOW_RICH_IN_PRODUCTION=true # override when needed
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Per-story configuration:
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
from runtime_narrative import story, FailureDiagnosticsConfig
|
|
226
|
+
|
|
227
|
+
async with story(
|
|
228
|
+
"Import Customers",
|
|
229
|
+
runtime_environment="development",
|
|
230
|
+
failure_diagnostics="rich",
|
|
231
|
+
app_roots=("/path/to/my/app",), # optional; default uses cwd
|
|
232
|
+
):
|
|
233
|
+
...
|
|
234
|
+
|
|
235
|
+
# Or pass a fully built config
|
|
236
|
+
cfg = FailureDiagnosticsConfig(failure_diagnostics="rich", app_roots=("/app",))
|
|
237
|
+
async with story("Import Customers", diagnostics_config=cfg):
|
|
238
|
+
...
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Server deployments — structured JSON logs
|
|
244
|
+
|
|
245
|
+
For production or any environment where you need machine-readable output, swap `ConsoleRenderer` for `JsonRenderer`. It emits one JSON object per lifecycle event — compatible with any structured log collector (Datadog, CloudWatch, Loki, OpenTelemetry log exporters):
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
from runtime_narrative import story, stage, JsonRenderer
|
|
249
|
+
|
|
250
|
+
async with story("Process Payment", renderers=[JsonRenderer()]):
|
|
251
|
+
async with stage("Validate Card"):
|
|
252
|
+
...
|
|
253
|
+
async with stage("Charge"):
|
|
254
|
+
...
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
On success, output is minimal — one object per event:
|
|
258
|
+
|
|
259
|
+
```json
|
|
260
|
+
{"event": "StoryStarted", "story_id": "abc-123", "story_name": "Process Payment", "timestamp": "..."}
|
|
261
|
+
{"event": "StageCompleted", "story_id": "abc-123", "stage_name": "Validate Card", "duration_seconds": 0.003, "timestamp": "..."}
|
|
262
|
+
{"event": "StoryCompleted", "story_id": "abc-123", "success": true, "progress": {"percent": 100, ...}, "timestamp": "..."}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
On failure, `FailureOccurred` carries the full diagnostics payload — exact location, stack frame classification, source snippet, local variables (rich mode), traceback — all in a structured, queryable form:
|
|
266
|
+
|
|
267
|
+
```json
|
|
268
|
+
{
|
|
269
|
+
"event": "FailureOccurred",
|
|
270
|
+
"story_id": "abc-123",
|
|
271
|
+
"stage_name": "Charge",
|
|
272
|
+
"error_type": "TimeoutError",
|
|
273
|
+
"location": {"filename": "payment.py", "lineno": 82, "function": "charge_card", "source_line": "..."},
|
|
274
|
+
"llm_analysis": "...",
|
|
275
|
+
"diagnostics_mode": "lean",
|
|
276
|
+
"stack_frames": [...],
|
|
277
|
+
"compressed_stack_summary": "2 app frame(s), 4 other/hidden in full stack (6 total)",
|
|
278
|
+
"stage_timeline": "Validate Card=completed (0.003s) | Charge=failed (0.012s)"
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Write to a file instead of stdout:
|
|
283
|
+
|
|
284
|
+
```python
|
|
285
|
+
JsonRenderer(output=open("narrative.log", "a"))
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## FastAPI / Starlette middleware
|
|
291
|
+
|
|
292
|
+
Add the middleware once and every request becomes a story automatically. Route handlers only need to declare stages:
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
from fastapi import FastAPI
|
|
296
|
+
from runtime_narrative import RuntimeNarrativeMiddleware, JsonRenderer, OllamaFailureAnalyzer
|
|
297
|
+
|
|
298
|
+
app = FastAPI()
|
|
299
|
+
app.add_middleware(
|
|
300
|
+
RuntimeNarrativeMiddleware,
|
|
301
|
+
renderers=[JsonRenderer()], # structured logs for prod
|
|
302
|
+
failure_analyzer=OllamaFailureAnalyzer(model="llama3"),
|
|
303
|
+
runtime_environment="production", # enforces lean + traceback cap
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
@app.post("/orders")
|
|
307
|
+
async def create_order(payload: OrderIn):
|
|
308
|
+
with stage("Validate Input"):
|
|
309
|
+
validate(payload)
|
|
310
|
+
|
|
311
|
+
with stage("Persist Order"):
|
|
312
|
+
order = await db.insert(payload)
|
|
313
|
+
|
|
314
|
+
return {"id": order.id}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Each request becomes a story named `"POST /orders"`. If the handler raises, the middleware captures the full failure context before returning the error response.
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## Decorators
|
|
322
|
+
|
|
323
|
+
Wrap entire functions without changing their call sites. The library detects `async def` automatically:
|
|
324
|
+
|
|
325
|
+
```python
|
|
326
|
+
from runtime_narrative import runtime_narrative_story, runtime_narrative_stage
|
|
327
|
+
|
|
328
|
+
@runtime_narrative_story(failure_analyzer=analyzer)
|
|
329
|
+
async def run_pipeline():
|
|
330
|
+
await load_data()
|
|
331
|
+
await transform()
|
|
332
|
+
await export()
|
|
333
|
+
|
|
334
|
+
@runtime_narrative_stage("Load Source Data")
|
|
335
|
+
async def load_data():
|
|
336
|
+
...
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
All `story()` kwargs — `failure_analyzer`, `failure_diagnostics`, `runtime_environment`, `background_analysis`, `renderers`, etc. — are forwarded from `@runtime_narrative_story`.
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## Custom renderer
|
|
344
|
+
|
|
345
|
+
Any object with a `handle(event)` method is a valid renderer. Async renderers (`async def handle`) are awaited automatically inside `async with story(...)`:
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
class SlackRenderer:
|
|
349
|
+
async def handle(self, event):
|
|
350
|
+
if event.__class__.__name__ == "FailureOccurred":
|
|
351
|
+
await slack.post(
|
|
352
|
+
f"*{event.story_name}* failed at *{event.stage_name}*\n"
|
|
353
|
+
f"`{event.error_type}: {event.error_message}`"
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
async with story("Nightly ETL", renderers=[SlackRenderer()]):
|
|
357
|
+
...
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Events you will receive: `StoryStarted`, `StageStarted`, `StageCompleted`, `FailureOccurred`, `StoryCompleted`, `LLMAnalysisReady` (only when `background_analysis=True`).
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## Custom failure analyzer
|
|
365
|
+
|
|
366
|
+
Any object with an `analyze_failure(...)` method works. Add `analyze_failure_async(...)` for native async — otherwise the sync version is called via `asyncio.to_thread` so it never blocks the event loop:
|
|
367
|
+
|
|
368
|
+
```python
|
|
369
|
+
class MyAnalyzer:
|
|
370
|
+
async def analyze_failure_async(
|
|
371
|
+
self, *, story_name, stage_name, failure, stage_timeline, progress_percent
|
|
372
|
+
):
|
|
373
|
+
# failure is a FailureSummary:
|
|
374
|
+
# .error_type, .error_message, .filename, .lineno,
|
|
375
|
+
# .function, .source_line, .traceback_text, .exception_chain
|
|
376
|
+
result = await my_llm_client.complete(build_prompt(failure))
|
|
377
|
+
return result.text
|
|
378
|
+
|
|
379
|
+
async with story("Import", failure_analyzer=MyAnalyzer()):
|
|
380
|
+
...
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## Environment variables
|
|
386
|
+
|
|
387
|
+
| Variable | Values | Default | Effect |
|
|
388
|
+
|---|---|---|---|
|
|
389
|
+
| `RUNTIME_NARRATIVE_ENV` | `development`, `production` | `development` | Production caps traceback length and forces lean mode |
|
|
390
|
+
| `RUNTIME_NARRATIVE_FAILURE_DIAGNOSTICS` | `lean`, `rich` | `lean` | `rich` captures local variables at the failing frames |
|
|
391
|
+
| `RUNTIME_NARRATIVE_ALLOW_RICH_IN_PRODUCTION` | `1`, `true` | off | Bypass production safeguard for rich diagnostics |
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## Philosophy
|
|
396
|
+
|
|
397
|
+
- **Zero noise on success.** One line per stage. No log spam when things work.
|
|
398
|
+
- **Full context on failure.** The library already knows what succeeded, what failed, and where. It uses that to give you an actionable report, not a raw stacktrace dropped into a log file.
|
|
399
|
+
- **LLM is optional, never required.** Every feature works without an LLM. The analyzer is purely additive. If it fails to respond, your exception still propagates normally.
|
|
400
|
+
- **Logical fixes, not code rewrites.** The LLM suggestion names the exact mechanism and location of the failure, and tells you what logic to change. It does not generate code diffs.
|
|
401
|
+
- **Async-first, sync-compatible.** Both `with story()` and `async with story()` work. The library never blocks the event loop — failure diagnostics and LLM calls both run via `asyncio.to_thread`.
|
|
402
|
+
- **No framework lock-in.** Use it in a script, a FastAPI app, a Celery worker, a CLI, or a data pipeline. The only required hook is wrapping your code in `story()` / `stage()`.
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
## License
|
|
407
|
+
|
|
408
|
+
MIT
|