runtime-narrative 1.5.2__tar.gz → 1.5.3__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-1.5.2/runtime_narrative.egg-info → runtime_narrative-1.5.3}/PKG-INFO +2 -1
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/README.md +1 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/pyproject.toml +1 -1
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/__init__.py +1 -1
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/analyzers/anthropic.py +23 -1
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/analyzers/ollama.py +54 -2
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3/runtime_narrative.egg-info}/PKG-INFO +2 -1
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_analyzers.py +44 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_anthropic_analyzer.py +28 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/LICENSE +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/analyzers/__init__.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/analyzers/base.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/analyzers/deduplication.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/celery.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/cli.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/context.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/decorators.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/diagnostics.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/events.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/failure.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/grpc_interceptor.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/instrumentation.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/logging_bridge.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/middleware.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/middleware_django.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/outcome.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/renderer/__init__.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/renderer/alert_renderer.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/renderer/coalescing_renderer.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/renderer/console.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/renderer/filter_renderer.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/renderer/html_renderer.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/renderer/json_renderer.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/renderer/otel_log_renderer.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/renderer/otel_metrics_renderer.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/renderer/otel_renderer.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/renderer/persistence_renderer.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/renderer/prometheus_renderer.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/renderer_defaults.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/stage.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/story.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/task_group.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/testing.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative.egg-info/SOURCES.txt +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative.egg-info/dependency_links.txt +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative.egg-info/entry_points.txt +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative.egg-info/requires.txt +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative.egg-info/top_level.txt +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/setup.cfg +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_alert_renderer.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_async_renderer.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_celery.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_coalescing_renderer.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_console_renderer.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_decorators.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_deduplication.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_diagnostics.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_dry_run.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_failure.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_filter_renderer.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_grpc_interceptor.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_html_renderer.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_instrumentation.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_instrumentation_phase2.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_issues.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_json_renderer.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_logging_bridge.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_middleware.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_middleware_django.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_middleware_propagation.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_module_capture.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_otel_log_renderer.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_otel_metrics_renderer.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_otel_renderer.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_outcome.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_persistence_renderer.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_prometheus_renderer.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_redaction_extended.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_renderer_defaults.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_stage.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_stage_metadata.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_story.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_structured_analysis.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_task_group.py +0 -0
- {runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/tests/test_testing_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: runtime-narrative
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.3
|
|
4
4
|
Summary: Model execution as human-readable stories with lean/rich failure diagnostics and optional LLM analysis
|
|
5
5
|
Author-email: Shashank Raj <shashank.raj28@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -377,6 +377,7 @@ Every script under `examples/` is runnable as-is: `uv run python examples/<name>
|
|
|
377
377
|
| `ANTHROPIC_API_KEY` | API key | — | Required by `AnthropicFailureAnalyzer`; read automatically if `api_key=` is not passed |
|
|
378
378
|
| `RUNTIME_NARRATIVE_RICH_LOG_FILE` | file path | — | Adds a file-backed `ConsoleRenderer` writing rich, human-readable output to this path, on top of whichever renderer TTY detection selects. Read by every auto-instrumentation entry point (FastAPI/Starlette, Django, Celery, gRPC) when `renderers=` is omitted |
|
|
379
379
|
| `RUNTIME_NARRATIVE_RICH_LOG_CONSOLE` | `1`, `0` | `1` | With `RUNTIME_NARRATIVE_RICH_LOG_FILE` set and stdout a TTY, `0` suppresses the terminal copy so the narrative goes to the file only |
|
|
380
|
+
| `RUNTIME_NARRATIVE_ANALYZER_TIMEOUT_SECONDS` | seconds (float) | `12` (`OllamaFailureAnalyzer`/`LLMFailureAnalyzer`), `30` (`AnthropicFailureAnalyzer`) | Request timeout for the built-in failure analyzers. Raise it for slower or cold-loading local models (e.g. Ollama loading a multi-GB model from disk); on timeout or any other request failure the analyzer logs a `logging.warning` (module `runtime_narrative.analyzers.*`) and falls back to no LLM analysis rather than failing the story |
|
|
380
381
|
|
|
381
382
|
---
|
|
382
383
|
|
|
@@ -318,6 +318,7 @@ Every script under `examples/` is runnable as-is: `uv run python examples/<name>
|
|
|
318
318
|
| `ANTHROPIC_API_KEY` | API key | — | Required by `AnthropicFailureAnalyzer`; read automatically if `api_key=` is not passed |
|
|
319
319
|
| `RUNTIME_NARRATIVE_RICH_LOG_FILE` | file path | — | Adds a file-backed `ConsoleRenderer` writing rich, human-readable output to this path, on top of whichever renderer TTY detection selects. Read by every auto-instrumentation entry point (FastAPI/Starlette, Django, Celery, gRPC) when `renderers=` is omitted |
|
|
320
320
|
| `RUNTIME_NARRATIVE_RICH_LOG_CONSOLE` | `1`, `0` | `1` | With `RUNTIME_NARRATIVE_RICH_LOG_FILE` set and stdout a TTY, `0` suppresses the terminal copy so the narrative goes to the file only |
|
|
321
|
+
| `RUNTIME_NARRATIVE_ANALYZER_TIMEOUT_SECONDS` | seconds (float) | `12` (`OllamaFailureAnalyzer`/`LLMFailureAnalyzer`), `30` (`AnthropicFailureAnalyzer`) | Request timeout for the built-in failure analyzers. Raise it for slower or cold-loading local models (e.g. Ollama loading a multi-GB model from disk); on timeout or any other request failure the analyzer logs a `logging.warning` (module `runtime_narrative.analyzers.*`) and falls back to no LLM analysis rather than failing the story |
|
|
321
322
|
|
|
322
323
|
---
|
|
323
324
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "runtime-narrative"
|
|
7
|
-
version = "1.5.
|
|
7
|
+
version = "1.5.3"
|
|
8
8
|
description = "Model execution as human-readable stories with lean/rich failure diagnostics and optional LLM analysis"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
{runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/analyzers/anthropic.py
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import logging
|
|
4
5
|
import os
|
|
5
6
|
import re
|
|
6
7
|
from dataclasses import dataclass, field
|
|
@@ -14,9 +15,12 @@ except ImportError:
|
|
|
14
15
|
_ANTHROPIC_AVAILABLE = False
|
|
15
16
|
|
|
16
17
|
from ..failure import FailureSummary
|
|
18
|
+
from .ollama import _default_analyzer_timeout_seconds
|
|
17
19
|
|
|
18
20
|
__all__ = ["AnthropicFailureAnalyzer"]
|
|
19
21
|
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
20
24
|
_DEFAULT_MODEL = "claude-haiku-4-5-20251001"
|
|
21
25
|
|
|
22
26
|
|
|
@@ -24,7 +28,7 @@ _DEFAULT_MODEL = "claude-haiku-4-5-20251001"
|
|
|
24
28
|
class AnthropicFailureAnalyzer:
|
|
25
29
|
api_key: str = field(default_factory=lambda: os.environ.get("ANTHROPIC_API_KEY", ""))
|
|
26
30
|
model: str = field(default_factory=lambda: os.environ.get("RUNTIME_NARRATIVE_MODEL", _DEFAULT_MODEL))
|
|
27
|
-
timeout_seconds: float = 30.0
|
|
31
|
+
timeout_seconds: float = field(default_factory=lambda: _default_analyzer_timeout_seconds(fallback=30.0))
|
|
28
32
|
max_tokens: int = 1024
|
|
29
33
|
system_prompt: str = "You are an expert Python debugging assistant. Be concise and specific."
|
|
30
34
|
|
|
@@ -120,6 +124,15 @@ class AnthropicFailureAnalyzer:
|
|
|
120
124
|
)
|
|
121
125
|
text = response.content[0].text.strip()
|
|
122
126
|
except Exception:
|
|
127
|
+
logger.warning(
|
|
128
|
+
"AnthropicFailureAnalyzer: request to model %s timed out or "
|
|
129
|
+
"failed (timeout_seconds=%s) -- no LLM analysis for this "
|
|
130
|
+
"failure. Raise RUNTIME_NARRATIVE_ANALYZER_TIMEOUT_SECONDS "
|
|
131
|
+
"if the API just needs more time.",
|
|
132
|
+
self.model,
|
|
133
|
+
self.timeout_seconds,
|
|
134
|
+
exc_info=True,
|
|
135
|
+
)
|
|
123
136
|
return None
|
|
124
137
|
return self._parse_response(text) or None
|
|
125
138
|
|
|
@@ -150,5 +163,14 @@ class AnthropicFailureAnalyzer:
|
|
|
150
163
|
)
|
|
151
164
|
text = response.content[0].text.strip()
|
|
152
165
|
except Exception:
|
|
166
|
+
logger.warning(
|
|
167
|
+
"AnthropicFailureAnalyzer: async request to model %s timed "
|
|
168
|
+
"out or failed (timeout_seconds=%s) -- no LLM analysis for "
|
|
169
|
+
"this failure. Raise RUNTIME_NARRATIVE_ANALYZER_TIMEOUT_SECONDS "
|
|
170
|
+
"if the API just needs more time.",
|
|
171
|
+
self.model,
|
|
172
|
+
self.timeout_seconds,
|
|
173
|
+
exc_info=True,
|
|
174
|
+
)
|
|
153
175
|
return None
|
|
154
176
|
return self._parse_response(text) or None
|
|
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import json
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
5
7
|
import re
|
|
6
8
|
from dataclasses import dataclass, field
|
|
7
9
|
from functools import partial
|
|
@@ -11,6 +13,26 @@ from urllib.request import Request, urlopen
|
|
|
11
13
|
|
|
12
14
|
from ..failure import FailureSummary
|
|
13
15
|
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _default_analyzer_timeout_seconds(fallback: float = 12.0) -> float:
|
|
20
|
+
"""Default request timeout for LLM failure analyzers.
|
|
21
|
+
|
|
22
|
+
Reads `RUNTIME_NARRATIVE_ANALYZER_TIMEOUT_SECONDS` so deployments with
|
|
23
|
+
slower/cold-loading local models (e.g. Ollama loading a multi-GB model
|
|
24
|
+
from disk) can raise it without subclassing or passing timeout_seconds=
|
|
25
|
+
at every call site. Falls back to `fallback` (each analyzer's own
|
|
26
|
+
previous hardcoded default) when the env var is unset or invalid.
|
|
27
|
+
"""
|
|
28
|
+
raw = os.getenv("RUNTIME_NARRATIVE_ANALYZER_TIMEOUT_SECONDS")
|
|
29
|
+
if not raw:
|
|
30
|
+
return fallback
|
|
31
|
+
try:
|
|
32
|
+
return float(raw)
|
|
33
|
+
except ValueError:
|
|
34
|
+
return fallback
|
|
35
|
+
|
|
14
36
|
|
|
15
37
|
_SECTION_LABELS = {
|
|
16
38
|
"exact_why": "Exact Why",
|
|
@@ -114,7 +136,7 @@ class LLMFailureAnalyzer:
|
|
|
114
136
|
|
|
115
137
|
model: str
|
|
116
138
|
endpoint: str
|
|
117
|
-
timeout_seconds: float =
|
|
139
|
+
timeout_seconds: float = field(default_factory=_default_analyzer_timeout_seconds)
|
|
118
140
|
include_traceback_lines: int = 30
|
|
119
141
|
max_context_chars: int = 8000
|
|
120
142
|
|
|
@@ -152,12 +174,27 @@ class LLMFailureAnalyzer:
|
|
|
152
174
|
with urlopen(request, timeout=self.timeout_seconds) as response:
|
|
153
175
|
body = response.read().decode("utf-8")
|
|
154
176
|
except (URLError, TimeoutError, Exception):
|
|
177
|
+
logger.warning(
|
|
178
|
+
"LLMFailureAnalyzer: request to %s timed out or failed "
|
|
179
|
+
"(timeout_seconds=%s) -- no LLM analysis for this failure. "
|
|
180
|
+
"Raise RUNTIME_NARRATIVE_ANALYZER_TIMEOUT_SECONDS if the "
|
|
181
|
+
"endpoint just needs more time (e.g. a cold model load).",
|
|
182
|
+
self.endpoint,
|
|
183
|
+
self.timeout_seconds,
|
|
184
|
+
exc_info=True,
|
|
185
|
+
)
|
|
155
186
|
return None
|
|
156
187
|
|
|
157
188
|
try:
|
|
158
189
|
parsed = json.loads(body)
|
|
159
190
|
text = parsed["choices"][0]["message"]["content"].strip()
|
|
160
191
|
except Exception:
|
|
192
|
+
logger.warning(
|
|
193
|
+
"LLMFailureAnalyzer: response from %s was not the expected "
|
|
194
|
+
"chat-completions JSON shape -- no LLM analysis for this failure.",
|
|
195
|
+
self.endpoint,
|
|
196
|
+
exc_info=True,
|
|
197
|
+
)
|
|
161
198
|
return None
|
|
162
199
|
|
|
163
200
|
return _parse_structured_response(text) or None
|
|
@@ -200,7 +237,7 @@ class OllamaFailureAnalyzer:
|
|
|
200
237
|
|
|
201
238
|
model: str
|
|
202
239
|
endpoint: str = "http://127.0.0.1:11434/api/generate"
|
|
203
|
-
timeout_seconds: float =
|
|
240
|
+
timeout_seconds: float = field(default_factory=_default_analyzer_timeout_seconds)
|
|
204
241
|
include_traceback_lines: int = 30
|
|
205
242
|
max_context_chars: int = 8000
|
|
206
243
|
|
|
@@ -238,12 +275,27 @@ class OllamaFailureAnalyzer:
|
|
|
238
275
|
with urlopen(request, timeout=self.timeout_seconds) as response:
|
|
239
276
|
body = response.read().decode("utf-8")
|
|
240
277
|
except (URLError, TimeoutError, Exception):
|
|
278
|
+
logger.warning(
|
|
279
|
+
"OllamaFailureAnalyzer: request to %s timed out or failed "
|
|
280
|
+
"(timeout_seconds=%s) -- no LLM analysis for this failure. "
|
|
281
|
+
"Raise RUNTIME_NARRATIVE_ANALYZER_TIMEOUT_SECONDS if the "
|
|
282
|
+
"endpoint just needs more time (e.g. a cold model load).",
|
|
283
|
+
self.endpoint,
|
|
284
|
+
self.timeout_seconds,
|
|
285
|
+
exc_info=True,
|
|
286
|
+
)
|
|
241
287
|
return None
|
|
242
288
|
|
|
243
289
|
try:
|
|
244
290
|
parsed = json.loads(body)
|
|
245
291
|
text = parsed.get("response", "").strip()
|
|
246
292
|
except Exception:
|
|
293
|
+
logger.warning(
|
|
294
|
+
"OllamaFailureAnalyzer: response from %s was not the expected "
|
|
295
|
+
"/api/generate JSON shape -- no LLM analysis for this failure.",
|
|
296
|
+
self.endpoint,
|
|
297
|
+
exc_info=True,
|
|
298
|
+
)
|
|
247
299
|
return None
|
|
248
300
|
|
|
249
301
|
return _parse_structured_response(text) or None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: runtime-narrative
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.3
|
|
4
4
|
Summary: Model execution as human-readable stories with lean/rich failure diagnostics and optional LLM analysis
|
|
5
5
|
Author-email: Shashank Raj <shashank.raj28@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -377,6 +377,7 @@ Every script under `examples/` is runnable as-is: `uv run python examples/<name>
|
|
|
377
377
|
| `ANTHROPIC_API_KEY` | API key | — | Required by `AnthropicFailureAnalyzer`; read automatically if `api_key=` is not passed |
|
|
378
378
|
| `RUNTIME_NARRATIVE_RICH_LOG_FILE` | file path | — | Adds a file-backed `ConsoleRenderer` writing rich, human-readable output to this path, on top of whichever renderer TTY detection selects. Read by every auto-instrumentation entry point (FastAPI/Starlette, Django, Celery, gRPC) when `renderers=` is omitted |
|
|
379
379
|
| `RUNTIME_NARRATIVE_RICH_LOG_CONSOLE` | `1`, `0` | `1` | With `RUNTIME_NARRATIVE_RICH_LOG_FILE` set and stdout a TTY, `0` suppresses the terminal copy so the narrative goes to the file only |
|
|
380
|
+
| `RUNTIME_NARRATIVE_ANALYZER_TIMEOUT_SECONDS` | seconds (float) | `12` (`OllamaFailureAnalyzer`/`LLMFailureAnalyzer`), `30` (`AnthropicFailureAnalyzer`) | Request timeout for the built-in failure analyzers. Raise it for slower or cold-loading local models (e.g. Ollama loading a multi-GB model from disk); on timeout or any other request failure the analyzer logs a `logging.warning` (module `runtime_narrative.analyzers.*`) and falls back to no LLM analysis rather than failing the story |
|
|
380
381
|
|
|
381
382
|
---
|
|
382
383
|
|
|
@@ -131,3 +131,47 @@ def test_ollama_analyzer_returns_none_on_url_error():
|
|
|
131
131
|
with patch("runtime_narrative.analyzers.ollama.urlopen", side_effect=URLError("no route")):
|
|
132
132
|
result = analyzer.analyze_failure(**_KWARGS)
|
|
133
133
|
assert result is None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ── Configurable timeout + failure visibility ─────────────────────────────────
|
|
137
|
+
|
|
138
|
+
def test_default_timeout_seconds_falls_back_when_env_unset(monkeypatch):
|
|
139
|
+
monkeypatch.delenv("RUNTIME_NARRATIVE_ANALYZER_TIMEOUT_SECONDS", raising=False)
|
|
140
|
+
assert OllamaFailureAnalyzer(model="llama3").timeout_seconds == 12.0
|
|
141
|
+
assert LLMFailureAnalyzer(model="gpt-4", endpoint="http://x/v1/chat/completions").timeout_seconds == 12.0
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_default_timeout_seconds_reads_env(monkeypatch):
|
|
145
|
+
monkeypatch.setenv("RUNTIME_NARRATIVE_ANALYZER_TIMEOUT_SECONDS", "45")
|
|
146
|
+
assert OllamaFailureAnalyzer(model="llama3").timeout_seconds == 45.0
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_default_timeout_seconds_falls_back_on_invalid_env(monkeypatch):
|
|
150
|
+
monkeypatch.setenv("RUNTIME_NARRATIVE_ANALYZER_TIMEOUT_SECONDS", "not-a-number")
|
|
151
|
+
assert OllamaFailureAnalyzer(model="llama3").timeout_seconds == 12.0
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_explicit_timeout_seconds_overrides_env(monkeypatch):
|
|
155
|
+
monkeypatch.setenv("RUNTIME_NARRATIVE_ANALYZER_TIMEOUT_SECONDS", "45")
|
|
156
|
+
assert OllamaFailureAnalyzer(model="llama3", timeout_seconds=5.0).timeout_seconds == 5.0
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_ollama_analyzer_logs_warning_on_request_failure(caplog):
|
|
160
|
+
from urllib.error import URLError
|
|
161
|
+
analyzer = OllamaFailureAnalyzer(model="llama3", timeout_seconds=12.0)
|
|
162
|
+
with caplog.at_level("WARNING", logger="runtime_narrative.analyzers.ollama"):
|
|
163
|
+
with patch("runtime_narrative.analyzers.ollama.urlopen", side_effect=URLError("no route")):
|
|
164
|
+
result = analyzer.analyze_failure(**_KWARGS)
|
|
165
|
+
assert result is None
|
|
166
|
+
assert any("no LLM analysis" in record.message for record in caplog.records)
|
|
167
|
+
assert any("12.0" in record.message for record in caplog.records)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_llm_analyzer_logs_warning_on_malformed_response(caplog):
|
|
171
|
+
body = b"not json at all"
|
|
172
|
+
analyzer = LLMFailureAnalyzer(model="gpt-4", endpoint="http://localhost/v1/chat/completions")
|
|
173
|
+
with caplog.at_level("WARNING", logger="runtime_narrative.analyzers.ollama"):
|
|
174
|
+
with patch("runtime_narrative.analyzers.ollama.urlopen", return_value=_mock_http_response(body)):
|
|
175
|
+
result = analyzer.analyze_failure(**_KWARGS)
|
|
176
|
+
assert result is None
|
|
177
|
+
assert any("no LLM analysis" in record.message for record in caplog.records)
|
|
@@ -132,3 +132,31 @@ def test_parse_response_valid_json() -> None:
|
|
|
132
132
|
def test_parse_response_fallback() -> None:
|
|
133
133
|
result = _make_analyzer()._parse_response("plain text")
|
|
134
134
|
assert result == "plain text"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_default_timeout_falls_back_to_thirty_seconds(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
138
|
+
monkeypatch.delenv("RUNTIME_NARRATIVE_ANALYZER_TIMEOUT_SECONDS", raising=False)
|
|
139
|
+
assert _make_analyzer().timeout_seconds == 30.0
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_default_timeout_reads_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
143
|
+
monkeypatch.setenv("RUNTIME_NARRATIVE_ANALYZER_TIMEOUT_SECONDS", "45")
|
|
144
|
+
assert _make_analyzer().timeout_seconds == 45.0
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_explicit_timeout_overrides_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
148
|
+
monkeypatch.setenv("RUNTIME_NARRATIVE_ANALYZER_TIMEOUT_SECONDS", "45")
|
|
149
|
+
assert _make_analyzer(timeout_seconds=5.0).timeout_seconds == 5.0
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_analyze_failure_logs_warning_on_exception(caplog: pytest.LogCaptureFixture) -> None:
|
|
153
|
+
mock_client = mock.MagicMock()
|
|
154
|
+
mock_client.messages.create.side_effect = Exception("network error")
|
|
155
|
+
|
|
156
|
+
with mock.patch.object(_anthropic_mod, "_anthropic") as patched:
|
|
157
|
+
patched.Anthropic.return_value = mock_client
|
|
158
|
+
with caplog.at_level("WARNING", logger="runtime_narrative.analyzers.anthropic"):
|
|
159
|
+
result = _make_analyzer().analyze_failure(**_CALL_KWARGS)
|
|
160
|
+
|
|
161
|
+
assert result is None
|
|
162
|
+
assert any("no LLM analysis" in record.message for record in caplog.records)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/analyzers/deduplication.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/renderer/alert_renderer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/renderer/filter_renderer.py
RENAMED
|
File without changes
|
{runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/renderer/html_renderer.py
RENAMED
|
File without changes
|
{runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/renderer/json_renderer.py
RENAMED
|
File without changes
|
{runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/renderer/otel_log_renderer.py
RENAMED
|
File without changes
|
|
File without changes
|
{runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative/renderer/otel_renderer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
{runtime_narrative-1.5.2 → runtime_narrative-1.5.3}/runtime_narrative.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|