insightfactory-databricks-langgraph-tracer 1.0.0.dev5__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.
@@ -0,0 +1,176 @@
1
+ # Changelog
2
+
3
+ All notable changes to the Python package
4
+ (`insightfactory-databricks-langgraph-tracer`) are
5
+ documented here. The format is based on
6
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project follows
7
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
+
9
+ See [`../typescript/CHANGELOG.md`](../typescript/CHANGELOG.md) for changes to the
10
+ separately versioned TypeScript package.
11
+
12
+ <!--
13
+ Releasing: add changes under [Unreleased] as you go. To cut a release, move those
14
+ items into a new `## [x.y.z] - YYYY-MM-DD` section, bump `version` in
15
+ pyproject.toml, and add the version's link reference at the bottom. CI requires a
16
+ matching `## [x.y.z]` section whenever pyproject.toml is version-bumped on a PR to
17
+ main; pushing to main then tags `python-vx.y.z` and publishes a GitHub Release
18
+ from this section.
19
+ -->
20
+
21
+ ## [Unreleased]
22
+
23
+ ## [1.0.0] - 2026-09-01
24
+ ### Changed
25
+ - Renamed the PyPI distribution from `databricks-langgraph-tracer` to
26
+ `insightfactory-databricks-langgraph-tracer`. The Python import package remains
27
+ `databricks_langgraph_tracer`, so application imports do not change.
28
+ - Made the stable Python package available through public PyPI.
29
+ - Raised the minimum supported MLflow version to 3.15.0 and LangChain version to
30
+ 1.3.9 so the public release does not resolve versions with known vulnerabilities.
31
+ - Added `ty` type checking to the Python development dependencies and CI gates,
32
+ matching the Insight Factory CLI and SDK toolchain.
33
+
34
+ ## [0.6.1] - 2026-07-29
35
+ ### Changed
36
+ - Refreshed the vendored LiteLLM pricing catalog (2,719 → 2,984 entries), adding cost
37
+ coverage for the latest Foundry / Azure OpenAI models — notably the **GPT-5.6 series**
38
+ (`gpt-5.6`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`, plus their `azure/us/` and
39
+ `azure/eu/` regional variants). Deployments named after these models now resolve to a
40
+ pricing key and are costed, rather than being tagged `cost.unknown_model`. The manual
41
+ `azure_ai/deepseek-v4-flash` / `azure_ai/deepseek-v4-pro` overrides survive the refresh
42
+ unchanged.
43
+ - The refresh is not purely additive: upstream also revised rates for 21 non-Azure
44
+ entries (e.g. `sambanova/gpt-oss-120b`, `mistral/mistral-medium-latest`) and dropped
45
+ two `fireworks_ai/...whisper-v3*` keys. **No `azure/` or `azure_ai/` rate changed**, and
46
+ `resolve_pricing_key` only probes those two prefixes, so default-path costs are
47
+ unaffected; consumers passing an explicit pricing key may see reported costs move.
48
+
49
+ ### Known limitations
50
+ - Long-context tiering is still not applied: `calculate_llm_cost` reads only the flat
51
+ per-token rates, never `input_cost_per_token_above_272k_tokens` or the `_priority`
52
+ rates. Pre-existing and affecting all of gpt-5.x, but easier to reach now that the
53
+ GPT-5.6 entries carry 1,050,000-token windows — a 400k-input call under-reports by
54
+ roughly half.
55
+
56
+ ## [0.6.0] - 2026-06-25
57
+ ### Added
58
+ - `max_string_chars` option on `configure_databricks_tracing(...)` and
59
+ `DatabricksLangGraphTracer(...)` (env fallback `DATABRICKS_TRACING_MAX_STRING_CHARS`).
60
+ Opt-in (default off): when set, any plain-text string leaf longer than the
61
+ threshold in a span's inputs **or** outputs is replaced with a compact
62
+ `{"_truncated": true, "chars": N, "bytes": M, "preview": "…"}` placeholder, so
63
+ large text (not just multimodal bytes) cannot push a Unity Catalog trace past the
64
+ SQL inline read limit (issue #23). It caps string values only — never keys or
65
+ structural fields — and is a per-leaf mitigation, not a hard per-trace budget, so
66
+ pair it with consumer-side payload reduction for the heaviest spans. This is a
67
+ **distinct** knob from the removed multimodal `max_content_part_bytes`: that gated
68
+ inline binary payloads; this caps arbitrary text.
69
+
70
+ ### Changed
71
+ - The text cap now also applies to span **outputs**, not only inputs — the spans that
72
+ breach the inline read limit in the reported case (`final_output` /
73
+ `aggregate_results`) are output-side. Multimodal payload externalization stays
74
+ **input-only**: outputs are arbitrary chain / tool JSON, so running the
75
+ `data`/`base64` probe on them would mis-strip legitimate non-binary output. Runs on
76
+ a copy; the live values are untouched.
77
+
78
+ ### Fixed
79
+ - The `max_string_chars` cap now also reaches text nested inside **structured objects**
80
+ in span inputs/outputs — Pydantic models, dataclasses, tuples/namedtuples, and sets.
81
+ Previously the walk only descended through `str`/`dict`/`list`, so a long string on a
82
+ Pydantic model returned by a LangGraph node (e.g. `final_output` / `aggregate_results`)
83
+ escaped the cap while MLflow's serializer still expanded the model and recorded the
84
+ field in full. Such objects are now normalized to the same shape MLflow records (e.g.
85
+ `model_dump()`), so their leaves are capped too; normalization runs only when the cap
86
+ is set and is best-effort (a failure records the value as-is). (The TypeScript package
87
+ already JSON-normalizes inputs/outputs before the walk, so it was unaffected.)
88
+ - Autolog mode: the post-`end()` attribute force-write (`mlflow.llm.*` enrichment)
89
+ now works on `opentelemetry-sdk >= 1.43`, which made `BoundedAttributes` immutable
90
+ (`__setitem__` raises). The helper writes into the mapping's backing `_dict`
91
+ instead, restoring the enriched reduced schema on the mlflow floor.
92
+
93
+ ## [0.5.0] - 2026-06-24
94
+ ### Added
95
+ - `content_ref_resolver` option on `configure_databricks_tracing(...)` and
96
+ `DatabricksLangGraphTracer(...)`. It is called once per multimodal content part
97
+ (image / PDF / file) with the part and a `ContentPartContext` (part index/page,
98
+ inline byte length, and a shallow copy of the run metadata), and returns the
99
+ reference dict to store in the trace — e.g. a Unity Catalog volume path — or
100
+ `None` to fall back to the placeholder (issue #21).
101
+
102
+ ### Changed
103
+ - Inline base64 payloads of multimodal parts are now **never** stored in the trace:
104
+ they are always stripped before recording and replaced with a lightweight
105
+ reference (a `content_ref_resolver` result, otherwise a
106
+ `{"type": ..., "_omitted": true, "bytes": N}` placeholder), keeping Unity Catalog
107
+ trace reads under the SQL inline limit. Remote `http(s)://` image URLs are kept
108
+ verbatim. The transform runs on a copy, so the cache-stable message is untouched.
109
+
110
+ ### Removed
111
+ - The per-content-part byte cap, including the
112
+ `DATABRICKS_TRACING_MAX_CONTENT_PART_BYTES` env var and the
113
+ `max_content_part_bytes` option. Inline payloads are now always externalized, so
114
+ the cap is obsolete.
115
+
116
+ ### Security
117
+ - A resolver result that would re-introduce an inline payload — a `data:` URI, a
118
+ recognized base64 content part, or the part's raw base64 echoed back — is rejected
119
+ and the placeholder is used instead, guaranteeing no inline image bytes are
120
+ persisted. `ctx.metadata` is a shallow copy, so a resolver cannot corrupt run
121
+ state.
122
+
123
+ ## [0.4.0] - 2026-06-22
124
+ ### Added
125
+ - Capture multimodal chat-model inputs (image / PDF / file content blocks) on LLM
126
+ spans.
127
+
128
+ ### Fixed
129
+ - Keep multimodal content inline when exporting to Unity Catalog table-backed
130
+ experiments, instead of dropping it to an attachment reference.
131
+
132
+ ## [0.3.0] - 2026-06-17
133
+ ### Added
134
+ - Trace-level per-model cost and token rollup, written to the `cost.by_model` tag
135
+ on the live trace before the root span ends. Unpriceable-but-resolved models
136
+ still contribute their token split (zero cost) so the totals stay complete.
137
+
138
+ ## [0.2.2] - 2026-06-09
139
+ ### Changed
140
+ - Emit OpenInference-style `llm.*` span keys so the Databricks dashboard cost
141
+ charts read token usage and cost correctly.
142
+
143
+ ## [0.2.1] - 2026-06-04
144
+ ### Fixed
145
+ - Chunk the Unity Catalog OTLP span export so payloads stay under the 10 MiB
146
+ ingest cap.
147
+
148
+ ## [0.2.0] - 2026-06-02
149
+ ### Changed
150
+ - Harden Unity Catalog configuration guards and move to experiment-id-only
151
+ configuration.
152
+
153
+ ## [0.1.0] - 2026-06-01
154
+ ### Added
155
+ - Initial release: shared LangGraph → Databricks MLflow tracer built on a custom
156
+ `BaseTracer` (not autolog), writing all trace fields and the cost rollup on the
157
+ **live** trace before the root span ends, with fail-fast configuration and an
158
+ autolog reduced-schema fallback. Per-model pricing comes from a vendored LiteLLM
159
+ catalog with overrides and a refresh script.
160
+
161
+ <!-- Historical links (0.1.0-0.4.0) use commit SHAs: those release tags were
162
+ reserved by the repo's immutable-releases policy and cannot be created. Restore
163
+ python-vX.Y.Z tag links if the names are ever reclaimed. 0.5.0's tag is created
164
+ out-of-band (its bump predates the release workflow); 0.5.1+ tags are minted by
165
+ release.yml on push to main. -->
166
+ [Unreleased]: https://github.com/insightfactory-ai/if_s_langraph_mlflow_tracer/compare/python-v1.0.0...HEAD
167
+ [1.0.0]: https://github.com/insightfactory-ai/if_s_langraph_mlflow_tracer/compare/python-v0.6.1...python-v1.0.0
168
+ [0.6.1]: https://github.com/insightfactory-ai/if_s_langraph_mlflow_tracer/compare/python-v0.6.0...python-v0.6.1
169
+ [0.6.0]: https://github.com/insightfactory-ai/if_s_langraph_mlflow_tracer/compare/python-v0.5.0...python-v0.6.0
170
+ [0.5.0]: https://github.com/insightfactory-ai/if_s_langraph_mlflow_tracer/compare/99462dbc95c43f20085cae0bfd7a2dc0aab319b1...python-v0.5.0
171
+ [0.4.0]: https://github.com/insightfactory-ai/if_s_langraph_mlflow_tracer/compare/951a9299c4beffcdd23fc7442bd79b49e2d9b52f...99462dbc95c43f20085cae0bfd7a2dc0aab319b1
172
+ [0.3.0]: https://github.com/insightfactory-ai/if_s_langraph_mlflow_tracer/compare/1e4130acb98eb947f65302e060d6a017da0f2feb...951a9299c4beffcdd23fc7442bd79b49e2d9b52f
173
+ [0.2.2]: https://github.com/insightfactory-ai/if_s_langraph_mlflow_tracer/compare/1a868d88a6227a0528e5725c49b63848f0e03aa9...1e4130acb98eb947f65302e060d6a017da0f2feb
174
+ [0.2.1]: https://github.com/insightfactory-ai/if_s_langraph_mlflow_tracer/compare/03e34a320a8e90e046d574dab0e58cdfe58ab738...1a868d88a6227a0528e5725c49b63848f0e03aa9
175
+ [0.2.0]: https://github.com/insightfactory-ai/if_s_langraph_mlflow_tracer/compare/83c62e28dbd04ef8e987689983e1e5a53e9d204f...03e34a320a8e90e046d574dab0e58cdfe58ab738
176
+ [0.1.0]: https://github.com/insightfactory-ai/if_s_langraph_mlflow_tracer/commit/83c62e28dbd04ef8e987689983e1e5a53e9d204f
@@ -0,0 +1,28 @@
1
+ """Shared LangGraph -> Databricks MLflow tracer (Python).
2
+
3
+ Public API. No auto-bootstrap at import (design "Patterns to NOT follow") —
4
+ consumers call ``configure_databricks_tracing`` explicitly and attach the
5
+ callbacks returned by ``get_tracing_callbacks``.
6
+ """
7
+
8
+ from .errors import DatabricksTracingConfigurationError
9
+ from .mlflow_utils import (
10
+ TracingConfiguration,
11
+ configure_databricks_tracing,
12
+ get_tracing_callbacks,
13
+ )
14
+ from .tracer import (
15
+ ContentPartContext,
16
+ ContentRefResolver,
17
+ DatabricksLangGraphTracer,
18
+ )
19
+
20
+ __all__ = [
21
+ "ContentPartContext",
22
+ "ContentRefResolver",
23
+ "DatabricksLangGraphTracer",
24
+ "DatabricksTracingConfigurationError",
25
+ "TracingConfiguration",
26
+ "configure_databricks_tracing",
27
+ "get_tracing_callbacks",
28
+ ]
@@ -0,0 +1,43 @@
1
+ """Constant key names for the shared trace schema.
2
+
3
+ AUTO-GENERATED from ``schema/trace-schema.json`` — DO NOT EDIT BY HAND.
4
+ Regenerate with ``uv run python scripts/generate_keys.py``;
5
+ validate with ``uv run python scripts/generate_keys.py --check``.
6
+ """
7
+
8
+ from typing import Final
9
+
10
+
11
+ class TraceTagKey:
12
+ SOURCE: Final[str] = "source"
13
+ COST_UNKNOWN_MODEL: Final[str] = "cost.unknown_model"
14
+ COST_BY_MODEL: Final[str] = "cost.by_model"
15
+ LANGGRAPH_RUN_ID: Final[str] = "langgraph.run_id"
16
+ LANGGRAPH_GRAPH_ID: Final[str] = "langgraph.graph_id"
17
+ LANGGRAPH_ENV: Final[str] = "langgraph.env"
18
+ LANGGRAPH_API_REVISION: Final[str] = "langgraph.api_revision"
19
+ LANGGRAPH_THREAD_ID: Final[str] = "langgraph.thread_id"
20
+
21
+
22
+ class TraceMetadataKey:
23
+ TRACE_COST: Final[str] = "mlflow.trace.cost"
24
+ TRACE_SESSION: Final[str] = "mlflow.trace.session"
25
+ TRACE_USER: Final[str] = "mlflow.trace.user"
26
+
27
+
28
+ class SpanAttrKey:
29
+ LLM_COST: Final[str] = "mlflow.llm.cost"
30
+ LLM_MODEL: Final[str] = "mlflow.llm.model"
31
+ LLM_PROVIDER: Final[str] = "mlflow.llm.provider"
32
+ TOKEN_USAGE: Final[str] = "mlflow.chat.tokenUsage"
33
+ INPUT_TOKENS_CACHED: Final[str] = "gen_ai.usage.input_tokens.cached"
34
+ INPUT_TOKENS_CACHE_CREATION: Final[str] = "gen_ai.usage.input_tokens.cache_creation"
35
+ OUTPUT_TOKENS_REASONING: Final[str] = "gen_ai.usage.output_tokens.reasoning"
36
+ INPUT_TOKENS_CACHED_COST: Final[str] = "gen_ai.usage.input_tokens.cached_cost"
37
+ INPUT_TOKENS_CACHE_CREATION_COST: Final[str] = "gen_ai.usage.input_tokens.cache_creation_cost"
38
+ LLM_MODEL_NAME: Final[str] = "llm.model_name"
39
+ LLM_MODEL_PROVIDER: Final[str] = "llm.model_provider"
40
+ LLM_PROMPT_TOKENS_COST: Final[str] = "llm.usage.prompt_tokens_cost"
41
+ LLM_COMPLETION_TOKENS_COST: Final[str] = "llm.usage.completion_tokens_cost"
42
+ LANGGRAPH_NODE: Final[str] = "langgraph.node"
43
+ LANGGRAPH_STEP: Final[str] = "langgraph.step"
@@ -0,0 +1,60 @@
1
+ """Per-root cost accumulator.
2
+
3
+ MLflow aggregates ``mlflow.chat.tokenUsage`` span attributes itself, but it has
4
+ no cost aggregation — so the tracer tracks running cost totals here per root run
5
+ and flushes the reserved 3-key ``mlflow.trace.cost`` metadata on root end. Also
6
+ collects the set of unknown-model names for the ``cost.unknown_model`` trace tag.
7
+
8
+ Only the reserved 3-key totals roll up to the trace: the cache-read /
9
+ cache-creation cost split persists **per-span** as non-reserved attrs (Option B),
10
+ so it is not accumulated here.
11
+
12
+ A per-model breakdown (``by_model``) is *also* accumulated here for the
13
+ ``cost.by_model`` trace tag: model identity is a span-level attribute and a
14
+ single trace mixes several models, so per-model cost/token totals don't exist at
15
+ the trace level otherwise. Unlike the reserved 3-key rollup, the per-model bucket
16
+ also carries **tokens** — MLflow's native token aggregation is whole-trace, not
17
+ per-model, so the tracer must accumulate them itself (issue #13).
18
+ """
19
+
20
+ from dataclasses import dataclass, field
21
+
22
+
23
+ @dataclass
24
+ class ModelRollup:
25
+ """Per-model cost + token totals for one root trace (the ``cost.by_model`` map value).
26
+
27
+ Cost fields reconcile with the reserved ``mlflow.trace.cost`` rollup; token
28
+ fields reconcile with the whole-trace ``mlflow.chat.tokenUsage``. A
29
+ resolved-but-unpriceable model still contributes tokens (with zero cost) so
30
+ the token split stays complete.
31
+ """
32
+
33
+ input_cost: float = 0.0
34
+ output_cost: float = 0.0
35
+ total_cost: float = 0.0
36
+ input_tokens: int = 0
37
+ output_tokens: int = 0
38
+ cache_read_input_tokens: int = 0
39
+ cache_creation_input_tokens: int = 0
40
+
41
+
42
+ @dataclass
43
+ class TraceAggregate:
44
+ root_trace_id: str
45
+ input_cost: float = 0.0
46
+ output_cost: float = 0.0
47
+ total_cost: float = 0.0
48
+ unknown_models: set[str] = field(default_factory=set)
49
+ has_any_cost: bool = False
50
+ # Per-model cost/token breakdown, keyed by the resolved model identity
51
+ # (``mlflow.llm.model``); flushed as the ``cost.by_model`` trace tag.
52
+ by_model: dict[str, ModelRollup] = field(default_factory=dict)
53
+
54
+ def model_bucket(self, model: str) -> ModelRollup:
55
+ """Return the per-model rollup for ``model``, creating it on first use."""
56
+ bucket = self.by_model.get(model)
57
+ if bucket is None:
58
+ bucket = ModelRollup()
59
+ self.by_model[model] = bucket
60
+ return bucket