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.
- insightfactory_databricks_langgraph_tracer-1.0.0.dev5/.gitignore +32 -0
- insightfactory_databricks_langgraph_tracer-1.0.0.dev5/CHANGELOG.md +176 -0
- insightfactory_databricks_langgraph_tracer-1.0.0.dev5/LICENSE +5 -0
- insightfactory_databricks_langgraph_tracer-1.0.0.dev5/PKG-INFO +224 -0
- insightfactory_databricks_langgraph_tracer-1.0.0.dev5/README.md +200 -0
- insightfactory_databricks_langgraph_tracer-1.0.0.dev5/THIRD_PARTY_NOTICES +26 -0
- insightfactory_databricks_langgraph_tracer-1.0.0.dev5/pyproject.toml +82 -0
- insightfactory_databricks_langgraph_tracer-1.0.0.dev5/src/databricks_langgraph_tracer/__init__.py +28 -0
- insightfactory_databricks_langgraph_tracer-1.0.0.dev5/src/databricks_langgraph_tracer/_generated_keys.py +43 -0
- insightfactory_databricks_langgraph_tracer-1.0.0.dev5/src/databricks_langgraph_tracer/aggregate.py +60 -0
- insightfactory_databricks_langgraph_tracer-1.0.0.dev5/src/databricks_langgraph_tracer/autolog_compat.py +447 -0
- insightfactory_databricks_langgraph_tracer-1.0.0.dev5/src/databricks_langgraph_tracer/cost.py +173 -0
- insightfactory_databricks_langgraph_tracer-1.0.0.dev5/src/databricks_langgraph_tracer/databricks_utils.py +133 -0
- insightfactory_databricks_langgraph_tracer-1.0.0.dev5/src/databricks_langgraph_tracer/errors.py +2 -0
- insightfactory_databricks_langgraph_tracer-1.0.0.dev5/src/databricks_langgraph_tracer/mlflow_utils.py +316 -0
- insightfactory_databricks_langgraph_tracer-1.0.0.dev5/src/databricks_langgraph_tracer/pricing/model-pricing-overrides.json +28 -0
- insightfactory_databricks_langgraph_tracer-1.0.0.dev5/src/databricks_langgraph_tracer/pricing/model-pricing.json +1 -0
- insightfactory_databricks_langgraph_tracer-1.0.0.dev5/src/databricks_langgraph_tracer/py.typed +1 -0
- insightfactory_databricks_langgraph_tracer-1.0.0.dev5/src/databricks_langgraph_tracer/tracer.py +1083 -0
- 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,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.
|