runtime-narrative 1.5.1__tar.gz → 1.5.2__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.
Files changed (85) hide show
  1. {runtime_narrative-1.5.1/runtime_narrative.egg-info → runtime_narrative-1.5.2}/PKG-INFO +1 -1
  2. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/pyproject.toml +1 -1
  3. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/__init__.py +1 -1
  4. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/diagnostics.py +122 -1
  5. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2/runtime_narrative.egg-info}/PKG-INFO +1 -1
  6. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_diagnostics.py +83 -0
  7. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/LICENSE +0 -0
  8. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/README.md +0 -0
  9. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/analyzers/__init__.py +0 -0
  10. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/analyzers/anthropic.py +0 -0
  11. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/analyzers/base.py +0 -0
  12. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/analyzers/deduplication.py +0 -0
  13. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/analyzers/ollama.py +0 -0
  14. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/celery.py +0 -0
  15. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/cli.py +0 -0
  16. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/context.py +0 -0
  17. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/decorators.py +0 -0
  18. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/events.py +0 -0
  19. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/failure.py +0 -0
  20. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/grpc_interceptor.py +0 -0
  21. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/instrumentation.py +0 -0
  22. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/logging_bridge.py +0 -0
  23. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/middleware.py +0 -0
  24. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/middleware_django.py +0 -0
  25. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/outcome.py +0 -0
  26. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/renderer/__init__.py +0 -0
  27. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/renderer/alert_renderer.py +0 -0
  28. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/renderer/coalescing_renderer.py +0 -0
  29. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/renderer/console.py +0 -0
  30. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/renderer/filter_renderer.py +0 -0
  31. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/renderer/html_renderer.py +0 -0
  32. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/renderer/json_renderer.py +0 -0
  33. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/renderer/otel_log_renderer.py +0 -0
  34. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/renderer/otel_metrics_renderer.py +0 -0
  35. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/renderer/otel_renderer.py +0 -0
  36. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/renderer/persistence_renderer.py +0 -0
  37. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/renderer/prometheus_renderer.py +0 -0
  38. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/renderer_defaults.py +0 -0
  39. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/stage.py +0 -0
  40. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/story.py +0 -0
  41. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/task_group.py +0 -0
  42. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative/testing.py +0 -0
  43. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative.egg-info/SOURCES.txt +0 -0
  44. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative.egg-info/dependency_links.txt +0 -0
  45. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative.egg-info/entry_points.txt +0 -0
  46. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative.egg-info/requires.txt +0 -0
  47. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/runtime_narrative.egg-info/top_level.txt +0 -0
  48. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/setup.cfg +0 -0
  49. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_alert_renderer.py +0 -0
  50. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_analyzers.py +0 -0
  51. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_anthropic_analyzer.py +0 -0
  52. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_async_renderer.py +0 -0
  53. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_celery.py +0 -0
  54. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_coalescing_renderer.py +0 -0
  55. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_console_renderer.py +0 -0
  56. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_decorators.py +0 -0
  57. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_deduplication.py +0 -0
  58. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_dry_run.py +0 -0
  59. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_failure.py +0 -0
  60. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_filter_renderer.py +0 -0
  61. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_grpc_interceptor.py +0 -0
  62. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_html_renderer.py +0 -0
  63. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_instrumentation.py +0 -0
  64. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_instrumentation_phase2.py +0 -0
  65. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_issues.py +0 -0
  66. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_json_renderer.py +0 -0
  67. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_logging_bridge.py +0 -0
  68. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_middleware.py +0 -0
  69. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_middleware_django.py +0 -0
  70. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_middleware_propagation.py +0 -0
  71. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_module_capture.py +0 -0
  72. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_otel_log_renderer.py +0 -0
  73. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_otel_metrics_renderer.py +0 -0
  74. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_otel_renderer.py +0 -0
  75. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_outcome.py +0 -0
  76. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_persistence_renderer.py +0 -0
  77. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_prometheus_renderer.py +0 -0
  78. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_redaction_extended.py +0 -0
  79. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_renderer_defaults.py +0 -0
  80. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_stage.py +0 -0
  81. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_stage_metadata.py +0 -0
  82. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_story.py +0 -0
  83. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_structured_analysis.py +0 -0
  84. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/tests/test_task_group.py +0 -0
  85. {runtime_narrative-1.5.1 → runtime_narrative-1.5.2}/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.1
3
+ Version: 1.5.2
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "runtime-narrative"
7
- version = "1.5.1"
7
+ version = "1.5.2"
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"
@@ -1,4 +1,4 @@
1
- __version__ = "1.5.1"
1
+ __version__ = "1.5.2"
2
2
 
3
3
  from .analyzers import FailureAnalyzer, LLMFailureAnalyzer, OllamaFailureAnalyzer, DeduplicatingAnalyzer
4
4
  from .context import has_active_story
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import ast
3
4
  import os
4
5
  import re
5
6
  import sys
@@ -261,6 +262,114 @@ def _should_redact_key(
261
262
  return False
262
263
 
263
264
 
265
+ # ── Operand-type-mismatch explanation ───────────────────────────────────────
266
+ # Names the specific variable(s)/expression(s) behind a
267
+ # "TypeError: unsupported operand type(s) for OP: 'A' and 'B'" instead of just
268
+ # restating the exception message. Only activates in rich mode, reusing the
269
+ # frame locals already captured there (no extra capture cost) and the same
270
+ # redaction pipeline as the locals section. Anything it can't confidently
271
+ # resolve falls back to the generic exact_cause untouched.
272
+
273
+ _OPERAND_TYPE_ERROR_RE = re.compile(
274
+ r"^unsupported operand type\(s\) for (?P<op>\S+): '(?P<left_type>\w+)' and '(?P<right_type>\w+)'$"
275
+ )
276
+
277
+ _OP_SYMBOL_TO_AST: dict[str, type] = {
278
+ "+": ast.Add, "-": ast.Sub, "*": ast.Mult, "/": ast.Div,
279
+ "//": ast.FloorDiv, "%": ast.Mod, "**": ast.Pow, "@": ast.MatMult,
280
+ "&": ast.BitAnd, "|": ast.BitOr, "^": ast.BitXor, "<<": ast.LShift, ">>": ast.RShift,
281
+ }
282
+
283
+
284
+ def _safe_resolve_expr(node: ast.AST, frame_locals: Mapping[str, Any]) -> tuple[bool, Any]:
285
+ """Resolve a bare name or a one-level constant-key subscript (e.g. ``item["price"]``)
286
+ against already-captured frame locals. Never calls into user code (no
287
+ attribute access, no function calls) — only dict/list/tuple __getitem__ on
288
+ values we already hold a reference to."""
289
+ if isinstance(node, ast.Name):
290
+ if node.id in frame_locals:
291
+ return True, frame_locals[node.id]
292
+ return False, None
293
+ if isinstance(node, ast.Subscript):
294
+ base_ok, base_val = _safe_resolve_expr(node.value, frame_locals)
295
+ if not base_ok:
296
+ return False, None
297
+ key_node = node.slice
298
+ if isinstance(key_node, ast.Constant):
299
+ try:
300
+ return True, base_val[key_node.value]
301
+ except Exception:
302
+ return False, None
303
+ return False, None
304
+
305
+
306
+ def _describe_operand(
307
+ node: ast.AST,
308
+ type_name: str,
309
+ frame_locals: Mapping[str, Any],
310
+ *,
311
+ max_len: int,
312
+ extra_redact: tuple[str, ...],
313
+ redact_patterns: tuple[str, ...],
314
+ redact_callback: Any,
315
+ ) -> str:
316
+ try:
317
+ text = ast.unparse(node)
318
+ except Exception:
319
+ text = "<expr>"
320
+ if isinstance(node, ast.Name) and _should_redact_key(
321
+ node.id, extra=extra_redact, patterns=redact_patterns, callback=redact_callback
322
+ ):
323
+ return f"`{text}` ({type_name}, redacted)"
324
+ resolved, value = _safe_resolve_expr(node, frame_locals)
325
+ if resolved:
326
+ value_repr = _serialize_value(
327
+ value, max_len=max_len, depth=0, max_depth=1,
328
+ extra_redact=extra_redact, redact_patterns=redact_patterns, redact_callback=redact_callback,
329
+ )
330
+ return f"`{text}` = {value_repr} ({type_name})"
331
+ return f"`{text}` ({type_name})"
332
+
333
+
334
+ def _infer_operand_type_mismatch(
335
+ message: str,
336
+ source_line: str,
337
+ frame_locals: Mapping[str, Any],
338
+ *,
339
+ max_len: int,
340
+ extra_redact: tuple[str, ...],
341
+ redact_patterns: tuple[str, ...],
342
+ redact_callback: Any,
343
+ ) -> str | None:
344
+ """For 'unsupported operand type(s) for OP: A and B', name the specific
345
+ operands involved and (when safely resolvable) their actual values,
346
+ instead of just restating the exception message. Returns None on any
347
+ parse/resolution failure so callers can fall back to the generic cause."""
348
+ m = _OPERAND_TYPE_ERROR_RE.match(message.strip())
349
+ if not m:
350
+ return None
351
+ op_cls = _OP_SYMBOL_TO_AST.get(m.group("op"))
352
+ if op_cls is None:
353
+ return None
354
+ try:
355
+ tree = ast.parse(f"async def _f():\n {source_line.strip()}")
356
+ except SyntaxError:
357
+ return None
358
+ binop = next(
359
+ (n for n in ast.walk(tree) if isinstance(n, ast.BinOp) and isinstance(n.op, op_cls)),
360
+ None,
361
+ )
362
+ if binop is None:
363
+ return None
364
+ kwargs = dict(max_len=max_len, extra_redact=extra_redact, redact_patterns=redact_patterns, redact_callback=redact_callback)
365
+ left_desc = _describe_operand(binop.left, m.group("left_type"), frame_locals, **kwargs)
366
+ right_desc = _describe_operand(binop.right, m.group("right_type"), frame_locals, **kwargs)
367
+ return (
368
+ f"{left_desc} and {right_desc} can't be combined with '{m.group('op')}' - "
369
+ f"{m.group('left_type')} and {m.group('right_type')} are incompatible operand types."
370
+ )
371
+
372
+
264
373
  def _capture_locals_mapping(
265
374
  locals_map: Mapping[str, Any],
266
375
  *,
@@ -384,6 +493,18 @@ def build_enriched_failure(
384
493
  elif config.max_traceback_chars is not None:
385
494
  tb_out, truncated = _truncate_tb(full_tb, config.max_traceback_chars)
386
495
 
496
+ exact_cause = _infer_exact_cause(exc_type.__name__, str(exc), source_line)
497
+ if mode == "rich" and exc_type.__name__ == "TypeError" and frame_objs and 0 <= primary_idx < len(frame_objs):
498
+ enhanced_cause = _infer_operand_type_mismatch(
499
+ str(exc), source_line, frame_objs[primary_idx].f_locals,
500
+ max_len=config.max_local_value_len,
501
+ extra_redact=config.redact_extra,
502
+ redact_patterns=config.redact_patterns,
503
+ redact_callback=config.redact_callback,
504
+ )
505
+ if enhanced_cause is not None:
506
+ exact_cause = enhanced_cause
507
+
387
508
  summary = FailureSummary(
388
509
  error_type=exc_type.__name__,
389
510
  error_message=str(exc),
@@ -392,7 +513,7 @@ def build_enriched_failure(
392
513
  function=function,
393
514
  source_line=source_line,
394
515
  exception_chain=_build_exception_chain(exc),
395
- exact_cause=_infer_exact_cause(exc_type.__name__, str(exc), source_line),
516
+ exact_cause=exact_cause,
396
517
  traceback_text=tb_out,
397
518
  )
398
519
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runtime-narrative
3
- Version: 1.5.1
3
+ Version: 1.5.2
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
@@ -252,3 +252,86 @@ def test_compressed_stack_summary_counts_app_frames() -> None:
252
252
 
253
253
  assert "app frame" in enriched.compressed_stack_summary
254
254
  assert str(len(enriched.stack_frames)) in enriched.compressed_stack_summary
255
+
256
+
257
+ # ── Operand-type-mismatch exact_cause enhancement ─────────────────────────────
258
+
259
+ def test_exact_cause_names_operands_on_type_mismatch_in_rich_mode() -> None:
260
+ cfg = FailureDiagnosticsConfig(runtime_environment="development", failure_diagnostics="rich")
261
+
262
+ def inner() -> None:
263
+ item = {"price": 29.99}
264
+ promo = {"buy": 1, "get": 1}
265
+ item["price"] * promo # noqa: B018 - deliberately triggers TypeError
266
+
267
+ try:
268
+ inner()
269
+ except TypeError as e:
270
+ enriched = build_enriched_failure(type(e), e, e.__traceback__, config=cfg)
271
+
272
+ cause = enriched.summary.exact_cause
273
+ assert "item['price']" in cause
274
+ assert "29.99" in cause
275
+ assert "promo" in cause
276
+ assert "{'buy': 1, 'get': 1}" in cause
277
+ assert "float" in cause and "dict" in cause
278
+ # Must stay plain ASCII -- this string also flows into JSON/OTel, not just
279
+ # a Unicode-aware console.
280
+ assert all(ord(c) < 128 for c in cause)
281
+
282
+
283
+ def test_exact_cause_falls_back_to_generic_in_lean_mode() -> None:
284
+ """The enhancement only fires in rich mode -- lean mode never inspects locals."""
285
+ cfg = FailureDiagnosticsConfig(runtime_environment="development", failure_diagnostics="lean")
286
+
287
+ def inner() -> None:
288
+ item = {"price": 29.99}
289
+ promo = {"buy": 1, "get": 1}
290
+ item["price"] * promo # noqa: B018
291
+
292
+ try:
293
+ inner()
294
+ except TypeError as e:
295
+ enriched = build_enriched_failure(type(e), e, e.__traceback__, config=cfg)
296
+
297
+ assert enriched.summary.exact_cause.startswith("The statement `")
298
+
299
+
300
+ def test_exact_cause_redacts_operand_named_like_a_secret() -> None:
301
+ cfg = FailureDiagnosticsConfig(runtime_environment="development", failure_diagnostics="rich")
302
+
303
+ def inner() -> None:
304
+ api_token = {"scope": "read"}
305
+ 1.5 * api_token # noqa: B018
306
+
307
+ try:
308
+ inner()
309
+ except TypeError as e:
310
+ enriched = build_enriched_failure(type(e), e, e.__traceback__, config=cfg)
311
+
312
+ cause = enriched.summary.exact_cause
313
+ assert "redacted" in cause
314
+ assert "scope" not in cause
315
+
316
+
317
+ def test_exact_cause_falls_back_when_operand_not_a_simple_expression() -> None:
318
+ """Function calls / complex expressions aren't safely resolvable -- the
319
+ enhancement should still fire (naming the expression text) without
320
+ evaluating anything, and never raise."""
321
+ cfg = FailureDiagnosticsConfig(runtime_environment="development", failure_diagnostics="rich")
322
+
323
+ def get_promo() -> dict:
324
+ return {"buy": 1}
325
+
326
+ def inner() -> None:
327
+ price = 10.0
328
+ price * get_promo() # noqa: B018
329
+
330
+ try:
331
+ inner()
332
+ except TypeError as e:
333
+ enriched = build_enriched_failure(type(e), e, e.__traceback__, config=cfg)
334
+
335
+ cause = enriched.summary.exact_cause
336
+ assert "get_promo()" in cause
337
+ assert "price" in cause