insightfactory-databricks-langgraph-tracer 1.0.0.dev5__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 (20) hide show
  1. insightfactory_databricks_langgraph_tracer-1.0.0.dev5/.gitignore +32 -0
  2. insightfactory_databricks_langgraph_tracer-1.0.0.dev5/CHANGELOG.md +176 -0
  3. insightfactory_databricks_langgraph_tracer-1.0.0.dev5/LICENSE +5 -0
  4. insightfactory_databricks_langgraph_tracer-1.0.0.dev5/PKG-INFO +224 -0
  5. insightfactory_databricks_langgraph_tracer-1.0.0.dev5/README.md +200 -0
  6. insightfactory_databricks_langgraph_tracer-1.0.0.dev5/THIRD_PARTY_NOTICES +26 -0
  7. insightfactory_databricks_langgraph_tracer-1.0.0.dev5/pyproject.toml +82 -0
  8. insightfactory_databricks_langgraph_tracer-1.0.0.dev5/src/databricks_langgraph_tracer/__init__.py +28 -0
  9. insightfactory_databricks_langgraph_tracer-1.0.0.dev5/src/databricks_langgraph_tracer/_generated_keys.py +43 -0
  10. insightfactory_databricks_langgraph_tracer-1.0.0.dev5/src/databricks_langgraph_tracer/aggregate.py +60 -0
  11. insightfactory_databricks_langgraph_tracer-1.0.0.dev5/src/databricks_langgraph_tracer/autolog_compat.py +447 -0
  12. insightfactory_databricks_langgraph_tracer-1.0.0.dev5/src/databricks_langgraph_tracer/cost.py +173 -0
  13. insightfactory_databricks_langgraph_tracer-1.0.0.dev5/src/databricks_langgraph_tracer/databricks_utils.py +133 -0
  14. insightfactory_databricks_langgraph_tracer-1.0.0.dev5/src/databricks_langgraph_tracer/errors.py +2 -0
  15. insightfactory_databricks_langgraph_tracer-1.0.0.dev5/src/databricks_langgraph_tracer/mlflow_utils.py +316 -0
  16. insightfactory_databricks_langgraph_tracer-1.0.0.dev5/src/databricks_langgraph_tracer/pricing/model-pricing-overrides.json +28 -0
  17. insightfactory_databricks_langgraph_tracer-1.0.0.dev5/src/databricks_langgraph_tracer/pricing/model-pricing.json +1 -0
  18. insightfactory_databricks_langgraph_tracer-1.0.0.dev5/src/databricks_langgraph_tracer/py.typed +1 -0
  19. insightfactory_databricks_langgraph_tracer-1.0.0.dev5/src/databricks_langgraph_tracer/tracer.py +1083 -0
  20. insightfactory_databricks_langgraph_tracer-1.0.0.dev5/src/databricks_langgraph_tracer/uc_export.py +106 -0
@@ -0,0 +1,32 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ .venv/
5
+ *.egg-info/
6
+ .pytest_cache/
7
+ .ruff_cache/
8
+ build/
9
+ dist/
10
+
11
+ # MLflow local stores
12
+ mlruns/
13
+
14
+ # Scratch space (databricks-traces skill scripts, ad-hoc verification)
15
+ .tmp/
16
+ mlartifacts/
17
+ mlflow.db
18
+
19
+ # Node / TypeScript (Phase 7)
20
+ node_modules/
21
+ *.tsbuildinfo
22
+ # Yarn 4 (node-modules linker; yarn provided via corepack, not committed)
23
+ .yarn/
24
+ .pnp.*
25
+
26
+ # Secrets / local env
27
+ .env
28
+ .env.*
29
+ !.env.example
30
+
31
+ # OS / editor
32
+ .DS_Store
@@ -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,5 @@
1
+ Copyright 2026 insightfactory.ai. All rights reserved.
2
+
3
+ This software is proprietary. No permission is granted to use, copy, modify,
4
+ distribute, sublicense, or sell it except under a separate written agreement
5
+ with insightfactory.ai.
@@ -0,0 +1,224 @@
1
+ Metadata-Version: 2.5
2
+ Name: insightfactory-databricks-langgraph-tracer
3
+ Version: 1.0.0.dev5
4
+ Summary: LangGraph tracing for Databricks MLflow
5
+ Project-URL: Homepage, https://insightfactory.ai
6
+ Author-email: "insightfactory.ai" <support@insightfactory.ai>
7
+ License-Expression: LicenseRef-Proprietary
8
+ License-File: LICENSE
9
+ License-File: THIRD_PARTY_NOTICES
10
+ Keywords: databricks,langchain,langgraph,mlflow,tracing
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: Other/Proprietary License
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: <3.13,>=3.12
18
+ Requires-Dist: databricks-sdk>=0.82.0
19
+ Requires-Dist: langchain-core>=1.0.0
20
+ Requires-Dist: langchain>=1.3.9
21
+ Requires-Dist: langgraph>=1.0.0
22
+ Requires-Dist: mlflow<4.0.0,>=3.15.0
23
+ Description-Content-Type: text/markdown
24
+
25
+ # insightfactory-databricks-langgraph-tracer
26
+
27
+ LangGraph tracer for Databricks MLflow. It uses a custom LangChain `BaseTracer` to write
28
+ trace tags, metadata, token usage, and cost rollups before the root span ends.
29
+
30
+ ## Install
31
+
32
+ Published on PyPI:
33
+
34
+ ```bash
35
+ uv add insightfactory-databricks-langgraph-tracer
36
+ # or: pip install insightfactory-databricks-langgraph-tracer
37
+ ```
38
+
39
+ Requires Python 3.12 and resolves `mlflow>=3.15.0,<4`.
40
+
41
+ ## Quickstart
42
+
43
+ ```python
44
+ from databricks_langgraph_tracer import (
45
+ configure_databricks_tracing,
46
+ get_tracing_callbacks,
47
+ )
48
+
49
+ # 1. Bootstrap once at startup (reads env-first; kwargs override).
50
+ configure_databricks_tracing(experiment_id="<mlflow-experiment-id>", source="my-agent")
51
+
52
+ # 2. Attach the callbacks to your LangGraph / LangChain run.
53
+ graph = build_graph().with_config({"callbacks": get_tracing_callbacks()})
54
+ graph.invoke(state)
55
+ ```
56
+
57
+ Traces appear in the configured Databricks MLflow experiment carrying the full shared schema:
58
+ the `source` tag, session/user/thread metadata, per-span model / provider / token-usage + cost,
59
+ the trace-level `mlflow.trace.cost` rollup, and the per-model `cost.by_model` cost/token rollup tag.
60
+
61
+ ## Configuration
62
+
63
+ Env-first; any kwarg to `configure_databricks_tracing(...)` overrides the matching env var.
64
+
65
+ | Setting | Env var | Notes |
66
+ |---------|---------|-------|
67
+ | Tracking URI | `MLFLOW_TRACKING_URI` | `databricks` or `databricks://<profile>` (required) |
68
+ | Experiment | `MLFLOW_EXPERIMENT_ID` | by **id** only (required) |
69
+ | Source tag | — | `source=` kwarg, default `langgraph` |
70
+ | Multimodal refs | — | inline image/PDF/file bytes are externalized to a reference; `content_ref_resolver=` chooses it — see below |
71
+ | Text cap | `DATABRICKS_TRACING_MAX_STRING_CHARS` | opt-in; `max_string_chars=` truncates over-long plain-text span content — see below |
72
+ | Disable | `TESTING` / `BUILDING` = `true`, or `enabled=False` | no-op — the only non-raising path |
73
+ | UC-backed tracing | `MLFLOW_TRACING_UC_BACKED=true` or `uc_tracing=True` | see below |
74
+ | SQL warehouse | `MLFLOW_TRACING_SQL_WAREHOUSE_ID` → `DATABRICKS_WAREHOUSE_ID` | required when UC-backed |
75
+
76
+ **Auth** (resolved by `databricks_utils`): a service principal
77
+ (`DATABRICKS_HOST` / `DATABRICKS_CLIENT_ID` / `DATABRICKS_CLIENT_SECRET`) **or** a CLI profile
78
+ (`DATABRICKS_CONFIG_PROFILE` / `profile=`).
79
+
80
+ Missing required config **raises** `DatabricksTracingConfigurationError` (fail-fast) — disable
81
+ explicitly for local/dev runs.
82
+
83
+ ### Unity-Catalog-backed tracing
84
+
85
+ For experiments whose traces are stored in **Unity Catalog** `_otel_*` tables, set
86
+ `uc_tracing=True` (or `MLFLOW_TRACING_UC_BACKED=true`) and provide a SQL warehouse. The library
87
+ validates the warehouse, resolves the experiment's UC trace location from its binding tag, and
88
+ passes it to `set_experiment` so spans actually persist to the `_otel_spans` table — without it,
89
+ MLflow silently skips span export to UC. Classic (workspace) experiments need none of this and
90
+ are the default. UC-backed tracing requires an MLflow release that provides the
91
+ `UnityCatalog` trace-location API.
92
+
93
+ ### Multimodal inputs (image / PDF / file)
94
+
95
+ Chat-model spans record their inputs as **structured messages**, preserving the
96
+ multimodal content parts a graph sends to the model — OpenAI / LangChain
97
+ `image_url`, OpenAI `file`, and Anthropic `image` / `document`. (Internally the
98
+ tracer runs the LangChain `BaseTracer` in `original+chat` mode; the default would
99
+ otherwise flatten chat messages to a text-only `prompts` string and drop every
100
+ attachment.)
101
+
102
+ The inline base64 of each such part is **never stored** in the trace: it is removed
103
+ before recording and replaced with a lightweight reference, so the Unity Catalog
104
+ trace tables stay readable (multi-MB data URIs previously pushed large invoice
105
+ traces past the SQL inline read limit — issue #21). A remote `http(s)://` image URL
106
+ is already a small reference, so this step keeps it verbatim (but see the text cap
107
+ below — if enabled, it still truncates *any* string over its threshold, URLs
108
+ included). The transform runs on a copy of the inputs, so the live message sent to
109
+ the model is untouched and prompt caching is unaffected.
110
+
111
+ By default a part becomes a `{"type": ..., "_omitted": true, "bytes": N}`
112
+ placeholder. To store a meaningful reference instead — e.g. the Unity Catalog volume
113
+ path the image was loaded from, so it can be re-fetched at runtime — pass a
114
+ `content_ref_resolver`:
115
+
116
+ ```python
117
+ from databricks_langgraph_tracer import (
118
+ ContentPartContext,
119
+ configure_databricks_tracing,
120
+ )
121
+
122
+ def image_ref(part: dict, ctx: ContentPartContext) -> dict | None:
123
+ # ctx.metadata is the run metadata — pass per-run data (e.g. a source volume
124
+ # path) via the invoke config's `metadata`, which propagates to the LLM run
125
+ # alongside langgraph_node etc.
126
+ path = ctx.metadata.get("encoded_images_path")
127
+ if path:
128
+ return {"type": part.get("type"), "ref": path, "page": ctx.index}
129
+ return None # fall back to the default placeholder
130
+
131
+ configure_databricks_tracing(experiment_id="...", content_ref_resolver=image_ref)
132
+
133
+ # ... then carry the per-run ref data on the invoke config metadata:
134
+ graph.invoke(state, config={
135
+ "callbacks": get_tracing_callbacks(),
136
+ "metadata": {"encoded_images_path": "/Volumes/cat/sch/vol/inv/pages.txt"},
137
+ })
138
+ ```
139
+
140
+ The resolver is called once per multimodal part with the part and a
141
+ `ContentPartContext` — `index` (position within the message content array, i.e. the
142
+ page number for a one-image-per-page invoice), `bytes` (the inline payload length),
143
+ and the run `metadata`. Return a dict to store as the reference, or `None` for the
144
+ default placeholder. It is also accepted by
145
+ `DatabricksLangGraphTracer(content_ref_resolver=...)` for per-graph wiring.
146
+
147
+ The library guarantees no inline image bytes are stored: if a resolver result
148
+ re-introduces an inline payload (a `data:` URI, a recognized base64 content part, or
149
+ the part's own payload echoed back under any key — anywhere in the returned object),
150
+ it is rejected and the placeholder is used. Beyond that, keep the reference
151
+ **compact** — a *fabricated* large string under a custom key is the consumer's
152
+ responsibility (the library strips inline payloads but does not otherwise bound what
153
+ a resolver returns).
154
+ `ctx.metadata` is a shallow copy of the run metadata, so a resolver cannot corrupt run
155
+ state by setting top-level keys (don't mutate its nested values, which are shared).
156
+
157
+ ### Capping large text (issue #23)
158
+
159
+ Multimodal externalization handles inline *bytes*, but large **plain text** can also
160
+ push a trace past the SQL inline read limit — e.g. a classification vocabulary or
161
+ aggregated result set threaded through every fan-out span's inputs and outputs. Set
162
+ `max_string_chars` (or `DATABRICKS_TRACING_MAX_STRING_CHARS`) to truncate it:
163
+
164
+ ```python
165
+ configure_databricks_tracing(experiment_id="...", max_string_chars=50_000)
166
+ ```
167
+
168
+ When set, any string **value** longer than the threshold — in a span's inputs *or*
169
+ outputs — is replaced with a compact placeholder:
170
+
171
+ ```json
172
+ {"_truncated": true, "chars": 812345, "bytes": 812345, "preview": "first 256 chars…"}
173
+ ```
174
+
175
+ It is **opt-in / off by default** (generic truncation costs debuggability, so you
176
+ choose the threshold). It caps string values only — never keys or structural fields —
177
+ and runs on a copy, so the live messages are untouched. The cap also reaches text
178
+ nested inside **Pydantic models / dataclasses / tuples** (e.g. a model returned by a
179
+ node such as `final_output`), normalizing them to the same shape MLflow records. Also
180
+ accepted by `DatabricksLangGraphTracer(max_string_chars=...)` for per-graph wiring.
181
+
182
+ This is a **distinct** knob from the multimodal handling above: `content_ref_resolver`
183
+ chooses references for inline image/PDF/file bytes; `max_string_chars` caps arbitrary
184
+ text (and never truncates a resolver's reference). Because it is generic, it also
185
+ truncates any other over-threshold string — including a remote `http(s)://` image URL
186
+ the multimodal step keeps verbatim — so set the threshold comfortably above your
187
+ reference / URL lengths. Note it is a **per-leaf** mitigation, not a hard per-trace
188
+ byte budget — enough sub-threshold leaves can still sum past the limit — so for the
189
+ heaviest spans also reduce what you record (pass ids/references through node state
190
+ rather than full payloads).
191
+
192
+ The threshold counts **characters** (code points here; the TypeScript package counts
193
+ UTF-16 units, so the two can differ on non-BMP text), while the inline limit is in
194
+ bytes — multibyte text can be up to ~4× larger in bytes than characters, so for
195
+ CJK/emoji-heavy content size the cap below `limit / 4`. The placeholder's `bytes`
196
+ field always reports the exact UTF-8 size of the original.
197
+
198
+ ### Autolog fallback
199
+
200
+ `mode="autolog"` wires MLflow's built-in LangChain autologging plus compatibility patches and
201
+ emits a **reduced** schema — everything except a tracer-computed
202
+ `mlflow.trace.cost` rollup (the backend may still aggregate it server-side). The default
203
+ `mode="tracer"` (custom `BaseTracer`) is the full-parity path.
204
+
205
+ ## Development
206
+
207
+ ```bash
208
+ cd python
209
+ uv sync # latest allowed mlflow (ceiling)
210
+ uv run pytest # unit + lifecycle tests
211
+ uv run ruff check src tests
212
+ uv run ty check
213
+ uv run python scripts/generate_keys.py --check # schema/keys parity
214
+
215
+ # mlflow floor matrix (CI runs both cells via UV_RESOLUTION):
216
+ UV_RESOLUTION=lowest-direct uv sync && UV_RESOLUTION=lowest-direct uv run pytest
217
+ ```
218
+
219
+ Tests use a local sqlite MLflow tracking backend; checks that need a live Databricks backend are
220
+ marked `integration`.
221
+
222
+ ## Changelog
223
+
224
+ See [`CHANGELOG.md`](CHANGELOG.md).
@@ -0,0 +1,200 @@
1
+ # insightfactory-databricks-langgraph-tracer
2
+
3
+ LangGraph tracer for Databricks MLflow. It uses a custom LangChain `BaseTracer` to write
4
+ trace tags, metadata, token usage, and cost rollups before the root span ends.
5
+
6
+ ## Install
7
+
8
+ Published on PyPI:
9
+
10
+ ```bash
11
+ uv add insightfactory-databricks-langgraph-tracer
12
+ # or: pip install insightfactory-databricks-langgraph-tracer
13
+ ```
14
+
15
+ Requires Python 3.12 and resolves `mlflow>=3.15.0,<4`.
16
+
17
+ ## Quickstart
18
+
19
+ ```python
20
+ from databricks_langgraph_tracer import (
21
+ configure_databricks_tracing,
22
+ get_tracing_callbacks,
23
+ )
24
+
25
+ # 1. Bootstrap once at startup (reads env-first; kwargs override).
26
+ configure_databricks_tracing(experiment_id="<mlflow-experiment-id>", source="my-agent")
27
+
28
+ # 2. Attach the callbacks to your LangGraph / LangChain run.
29
+ graph = build_graph().with_config({"callbacks": get_tracing_callbacks()})
30
+ graph.invoke(state)
31
+ ```
32
+
33
+ Traces appear in the configured Databricks MLflow experiment carrying the full shared schema:
34
+ the `source` tag, session/user/thread metadata, per-span model / provider / token-usage + cost,
35
+ the trace-level `mlflow.trace.cost` rollup, and the per-model `cost.by_model` cost/token rollup tag.
36
+
37
+ ## Configuration
38
+
39
+ Env-first; any kwarg to `configure_databricks_tracing(...)` overrides the matching env var.
40
+
41
+ | Setting | Env var | Notes |
42
+ |---------|---------|-------|
43
+ | Tracking URI | `MLFLOW_TRACKING_URI` | `databricks` or `databricks://<profile>` (required) |
44
+ | Experiment | `MLFLOW_EXPERIMENT_ID` | by **id** only (required) |
45
+ | Source tag | — | `source=` kwarg, default `langgraph` |
46
+ | Multimodal refs | — | inline image/PDF/file bytes are externalized to a reference; `content_ref_resolver=` chooses it — see below |
47
+ | Text cap | `DATABRICKS_TRACING_MAX_STRING_CHARS` | opt-in; `max_string_chars=` truncates over-long plain-text span content — see below |
48
+ | Disable | `TESTING` / `BUILDING` = `true`, or `enabled=False` | no-op — the only non-raising path |
49
+ | UC-backed tracing | `MLFLOW_TRACING_UC_BACKED=true` or `uc_tracing=True` | see below |
50
+ | SQL warehouse | `MLFLOW_TRACING_SQL_WAREHOUSE_ID` → `DATABRICKS_WAREHOUSE_ID` | required when UC-backed |
51
+
52
+ **Auth** (resolved by `databricks_utils`): a service principal
53
+ (`DATABRICKS_HOST` / `DATABRICKS_CLIENT_ID` / `DATABRICKS_CLIENT_SECRET`) **or** a CLI profile
54
+ (`DATABRICKS_CONFIG_PROFILE` / `profile=`).
55
+
56
+ Missing required config **raises** `DatabricksTracingConfigurationError` (fail-fast) — disable
57
+ explicitly for local/dev runs.
58
+
59
+ ### Unity-Catalog-backed tracing
60
+
61
+ For experiments whose traces are stored in **Unity Catalog** `_otel_*` tables, set
62
+ `uc_tracing=True` (or `MLFLOW_TRACING_UC_BACKED=true`) and provide a SQL warehouse. The library
63
+ validates the warehouse, resolves the experiment's UC trace location from its binding tag, and
64
+ passes it to `set_experiment` so spans actually persist to the `_otel_spans` table — without it,
65
+ MLflow silently skips span export to UC. Classic (workspace) experiments need none of this and
66
+ are the default. UC-backed tracing requires an MLflow release that provides the
67
+ `UnityCatalog` trace-location API.
68
+
69
+ ### Multimodal inputs (image / PDF / file)
70
+
71
+ Chat-model spans record their inputs as **structured messages**, preserving the
72
+ multimodal content parts a graph sends to the model — OpenAI / LangChain
73
+ `image_url`, OpenAI `file`, and Anthropic `image` / `document`. (Internally the
74
+ tracer runs the LangChain `BaseTracer` in `original+chat` mode; the default would
75
+ otherwise flatten chat messages to a text-only `prompts` string and drop every
76
+ attachment.)
77
+
78
+ The inline base64 of each such part is **never stored** in the trace: it is removed
79
+ before recording and replaced with a lightweight reference, so the Unity Catalog
80
+ trace tables stay readable (multi-MB data URIs previously pushed large invoice
81
+ traces past the SQL inline read limit — issue #21). A remote `http(s)://` image URL
82
+ is already a small reference, so this step keeps it verbatim (but see the text cap
83
+ below — if enabled, it still truncates *any* string over its threshold, URLs
84
+ included). The transform runs on a copy of the inputs, so the live message sent to
85
+ the model is untouched and prompt caching is unaffected.
86
+
87
+ By default a part becomes a `{"type": ..., "_omitted": true, "bytes": N}`
88
+ placeholder. To store a meaningful reference instead — e.g. the Unity Catalog volume
89
+ path the image was loaded from, so it can be re-fetched at runtime — pass a
90
+ `content_ref_resolver`:
91
+
92
+ ```python
93
+ from databricks_langgraph_tracer import (
94
+ ContentPartContext,
95
+ configure_databricks_tracing,
96
+ )
97
+
98
+ def image_ref(part: dict, ctx: ContentPartContext) -> dict | None:
99
+ # ctx.metadata is the run metadata — pass per-run data (e.g. a source volume
100
+ # path) via the invoke config's `metadata`, which propagates to the LLM run
101
+ # alongside langgraph_node etc.
102
+ path = ctx.metadata.get("encoded_images_path")
103
+ if path:
104
+ return {"type": part.get("type"), "ref": path, "page": ctx.index}
105
+ return None # fall back to the default placeholder
106
+
107
+ configure_databricks_tracing(experiment_id="...", content_ref_resolver=image_ref)
108
+
109
+ # ... then carry the per-run ref data on the invoke config metadata:
110
+ graph.invoke(state, config={
111
+ "callbacks": get_tracing_callbacks(),
112
+ "metadata": {"encoded_images_path": "/Volumes/cat/sch/vol/inv/pages.txt"},
113
+ })
114
+ ```
115
+
116
+ The resolver is called once per multimodal part with the part and a
117
+ `ContentPartContext` — `index` (position within the message content array, i.e. the
118
+ page number for a one-image-per-page invoice), `bytes` (the inline payload length),
119
+ and the run `metadata`. Return a dict to store as the reference, or `None` for the
120
+ default placeholder. It is also accepted by
121
+ `DatabricksLangGraphTracer(content_ref_resolver=...)` for per-graph wiring.
122
+
123
+ The library guarantees no inline image bytes are stored: if a resolver result
124
+ re-introduces an inline payload (a `data:` URI, a recognized base64 content part, or
125
+ the part's own payload echoed back under any key — anywhere in the returned object),
126
+ it is rejected and the placeholder is used. Beyond that, keep the reference
127
+ **compact** — a *fabricated* large string under a custom key is the consumer's
128
+ responsibility (the library strips inline payloads but does not otherwise bound what
129
+ a resolver returns).
130
+ `ctx.metadata` is a shallow copy of the run metadata, so a resolver cannot corrupt run
131
+ state by setting top-level keys (don't mutate its nested values, which are shared).
132
+
133
+ ### Capping large text (issue #23)
134
+
135
+ Multimodal externalization handles inline *bytes*, but large **plain text** can also
136
+ push a trace past the SQL inline read limit — e.g. a classification vocabulary or
137
+ aggregated result set threaded through every fan-out span's inputs and outputs. Set
138
+ `max_string_chars` (or `DATABRICKS_TRACING_MAX_STRING_CHARS`) to truncate it:
139
+
140
+ ```python
141
+ configure_databricks_tracing(experiment_id="...", max_string_chars=50_000)
142
+ ```
143
+
144
+ When set, any string **value** longer than the threshold — in a span's inputs *or*
145
+ outputs — is replaced with a compact placeholder:
146
+
147
+ ```json
148
+ {"_truncated": true, "chars": 812345, "bytes": 812345, "preview": "first 256 chars…"}
149
+ ```
150
+
151
+ It is **opt-in / off by default** (generic truncation costs debuggability, so you
152
+ choose the threshold). It caps string values only — never keys or structural fields —
153
+ and runs on a copy, so the live messages are untouched. The cap also reaches text
154
+ nested inside **Pydantic models / dataclasses / tuples** (e.g. a model returned by a
155
+ node such as `final_output`), normalizing them to the same shape MLflow records. Also
156
+ accepted by `DatabricksLangGraphTracer(max_string_chars=...)` for per-graph wiring.
157
+
158
+ This is a **distinct** knob from the multimodal handling above: `content_ref_resolver`
159
+ chooses references for inline image/PDF/file bytes; `max_string_chars` caps arbitrary
160
+ text (and never truncates a resolver's reference). Because it is generic, it also
161
+ truncates any other over-threshold string — including a remote `http(s)://` image URL
162
+ the multimodal step keeps verbatim — so set the threshold comfortably above your
163
+ reference / URL lengths. Note it is a **per-leaf** mitigation, not a hard per-trace
164
+ byte budget — enough sub-threshold leaves can still sum past the limit — so for the
165
+ heaviest spans also reduce what you record (pass ids/references through node state
166
+ rather than full payloads).
167
+
168
+ The threshold counts **characters** (code points here; the TypeScript package counts
169
+ UTF-16 units, so the two can differ on non-BMP text), while the inline limit is in
170
+ bytes — multibyte text can be up to ~4× larger in bytes than characters, so for
171
+ CJK/emoji-heavy content size the cap below `limit / 4`. The placeholder's `bytes`
172
+ field always reports the exact UTF-8 size of the original.
173
+
174
+ ### Autolog fallback
175
+
176
+ `mode="autolog"` wires MLflow's built-in LangChain autologging plus compatibility patches and
177
+ emits a **reduced** schema — everything except a tracer-computed
178
+ `mlflow.trace.cost` rollup (the backend may still aggregate it server-side). The default
179
+ `mode="tracer"` (custom `BaseTracer`) is the full-parity path.
180
+
181
+ ## Development
182
+
183
+ ```bash
184
+ cd python
185
+ uv sync # latest allowed mlflow (ceiling)
186
+ uv run pytest # unit + lifecycle tests
187
+ uv run ruff check src tests
188
+ uv run ty check
189
+ uv run python scripts/generate_keys.py --check # schema/keys parity
190
+
191
+ # mlflow floor matrix (CI runs both cells via UV_RESOLUTION):
192
+ UV_RESOLUTION=lowest-direct uv sync && UV_RESOLUTION=lowest-direct uv run pytest
193
+ ```
194
+
195
+ Tests use a local sqlite MLflow tracking backend; checks that need a live Databricks backend are
196
+ marked `integration`.
197
+
198
+ ## Changelog
199
+
200
+ See [`CHANGELOG.md`](CHANGELOG.md).
@@ -0,0 +1,26 @@
1
+ LiteLLM model pricing data
2
+ ==========================
3
+
4
+ This package includes model pricing data derived from LiteLLM.
5
+
6
+ MIT License
7
+
8
+ Copyright (c) 2023 Berri AI
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.