ebaml 0.1.0__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 (161) hide show
  1. ebaml-0.1.0/LICENSE +21 -0
  2. ebaml-0.1.0/PKG-INFO +450 -0
  3. ebaml-0.1.0/README.md +422 -0
  4. ebaml-0.1.0/pyproject.toml +280 -0
  5. ebaml-0.1.0/pyproject.toml.orig +280 -0
  6. ebaml-0.1.0/src/baml_sdk/.baml-generator-output-root +1 -0
  7. ebaml-0.1.0/src/baml_sdk/.baml-generator-output.json +551 -0
  8. ebaml-0.1.0/src/baml_sdk/.gitignore +3 -0
  9. ebaml-0.1.0/src/baml_sdk/__init__.py +822 -0
  10. ebaml-0.1.0/src/baml_sdk/__init__.pyi +746 -0
  11. ebaml-0.1.0/src/baml_sdk/_baml_sources.py +31 -0
  12. ebaml-0.1.0/src/baml_sdk/_function_registry.py +125 -0
  13. ebaml-0.1.0/src/baml_sdk/_inlinedbaml.py +25 -0
  14. ebaml-0.1.0/src/baml_sdk/_invocation_types.py +34 -0
  15. ebaml-0.1.0/src/baml_sdk/_invocation_types.pyi +73 -0
  16. ebaml-0.1.0/src/baml_sdk/_typemap.py +584 -0
  17. ebaml-0.1.0/src/baml_sdk/ai/__init__.py +473 -0
  18. ebaml-0.1.0/src/baml_sdk/ai/__init__.pyi +391 -0
  19. ebaml-0.1.0/src/baml_sdk/ai/clients/__init__.py +276 -0
  20. ebaml-0.1.0/src/baml_sdk/ai/clients/__init__.pyi +309 -0
  21. ebaml-0.1.0/src/baml_sdk/ai/content/__init__.py +139 -0
  22. ebaml-0.1.0/src/baml_sdk/ai/content/__init__.pyi +121 -0
  23. ebaml-0.1.0/src/baml_sdk/ai/errors/__init__.py +297 -0
  24. ebaml-0.1.0/src/baml_sdk/ai/errors/__init__.pyi +218 -0
  25. ebaml-0.1.0/src/baml_sdk/ai/events/__init__.py +297 -0
  26. ebaml-0.1.0/src/baml_sdk/ai/events/__init__.pyi +237 -0
  27. ebaml-0.1.0/src/baml_sdk/ai/internal/__init__.py +467 -0
  28. ebaml-0.1.0/src/baml_sdk/ai/internal/__init__.pyi +645 -0
  29. ebaml-0.1.0/src/baml_sdk/ai/mcp/__init__.py +77 -0
  30. ebaml-0.1.0/src/baml_sdk/ai/mcp/__init__.pyi +120 -0
  31. ebaml-0.1.0/src/baml_sdk/ai/stream/__init__.py +336 -0
  32. ebaml-0.1.0/src/baml_sdk/ai/stream/__init__.pyi +453 -0
  33. ebaml-0.1.0/src/baml_sdk/ai/tools/__init__.py +146 -0
  34. ebaml-0.1.0/src/baml_sdk/ai/tools/__init__.pyi +165 -0
  35. ebaml-0.1.0/src/baml_sdk/ai/wire/__init__.py +280 -0
  36. ebaml-0.1.0/src/baml_sdk/ai/wire/__init__.pyi +459 -0
  37. ebaml-0.1.0/src/baml_sdk/baml/__init__.py +837 -0
  38. ebaml-0.1.0/src/baml_sdk/baml/__init__.pyi +4768 -0
  39. ebaml-0.1.0/src/baml_sdk/baml/crypto/__init__.py +277 -0
  40. ebaml-0.1.0/src/baml_sdk/baml/crypto/__init__.pyi +153 -0
  41. ebaml-0.1.0/src/baml_sdk/baml/csv/__init__.py +1076 -0
  42. ebaml-0.1.0/src/baml_sdk/baml/csv/__init__.pyi +1362 -0
  43. ebaml-0.1.0/src/baml_sdk/baml/env/__init__.py +138 -0
  44. ebaml-0.1.0/src/baml_sdk/baml/env/__init__.pyi +145 -0
  45. ebaml-0.1.0/src/baml_sdk/baml/errors/__init__.py +275 -0
  46. ebaml-0.1.0/src/baml_sdk/baml/errors/__init__.pyi +161 -0
  47. ebaml-0.1.0/src/baml_sdk/baml/fs/__init__.py +358 -0
  48. ebaml-0.1.0/src/baml_sdk/baml/fs/__init__.pyi +414 -0
  49. ebaml-0.1.0/src/baml_sdk/baml/future/__init__.py +182 -0
  50. ebaml-0.1.0/src/baml_sdk/baml/future/__init__.pyi +244 -0
  51. ebaml-0.1.0/src/baml_sdk/baml/glob/__init__.py +98 -0
  52. ebaml-0.1.0/src/baml_sdk/baml/glob/__init__.pyi +93 -0
  53. ebaml-0.1.0/src/baml_sdk/baml/hash/__init__.py +63 -0
  54. ebaml-0.1.0/src/baml_sdk/baml/hash/__init__.pyi +43 -0
  55. ebaml-0.1.0/src/baml_sdk/baml/host/__init__.py +55 -0
  56. ebaml-0.1.0/src/baml_sdk/baml/host/__init__.pyi +85 -0
  57. ebaml-0.1.0/src/baml_sdk/baml/http/__init__.py +398 -0
  58. ebaml-0.1.0/src/baml_sdk/baml/http/__init__.pyi +602 -0
  59. ebaml-0.1.0/src/baml_sdk/baml/io/__init__.py +88 -0
  60. ebaml-0.1.0/src/baml_sdk/baml/io/__init__.pyi +133 -0
  61. ebaml-0.1.0/src/baml_sdk/baml/iter/__init__.py +485 -0
  62. ebaml-0.1.0/src/baml_sdk/baml/iter/__init__.pyi +281 -0
  63. ebaml-0.1.0/src/baml_sdk/baml/json/__init__.py +898 -0
  64. ebaml-0.1.0/src/baml_sdk/baml/json/__init__.pyi +850 -0
  65. ebaml-0.1.0/src/baml_sdk/baml/media/__init__.py +46 -0
  66. ebaml-0.1.0/src/baml_sdk/baml/media/__init__.pyi +45 -0
  67. ebaml-0.1.0/src/baml_sdk/baml/net/__init__.py +140 -0
  68. ebaml-0.1.0/src/baml_sdk/baml/net/__init__.pyi +211 -0
  69. ebaml-0.1.0/src/baml_sdk/baml/ops/__init__.py +68 -0
  70. ebaml-0.1.0/src/baml_sdk/baml/ops/__init__.pyi +203 -0
  71. ebaml-0.1.0/src/baml_sdk/baml/panics/__init__.py +356 -0
  72. ebaml-0.1.0/src/baml_sdk/baml/panics/__init__.pyi +121 -0
  73. ebaml-0.1.0/src/baml_sdk/baml/random/__init__.py +121 -0
  74. ebaml-0.1.0/src/baml_sdk/baml/random/__init__.pyi +124 -0
  75. ebaml-0.1.0/src/baml_sdk/baml/regex/__init__.py +295 -0
  76. ebaml-0.1.0/src/baml_sdk/baml/regex/__init__.pyi +378 -0
  77. ebaml-0.1.0/src/baml_sdk/baml/sap/__init__.py +193 -0
  78. ebaml-0.1.0/src/baml_sdk/baml/sap/__init__.pyi +194 -0
  79. ebaml-0.1.0/src/baml_sdk/baml/spawn/__init__.py +191 -0
  80. ebaml-0.1.0/src/baml_sdk/baml/spawn/__init__.pyi +194 -0
  81. ebaml-0.1.0/src/baml_sdk/baml/sys/__init__.py +283 -0
  82. ebaml-0.1.0/src/baml_sdk/baml/sys/__init__.pyi +293 -0
  83. ebaml-0.1.0/src/baml_sdk/baml/time/__init__.py +661 -0
  84. ebaml-0.1.0/src/baml_sdk/baml/time/__init__.pyi +2048 -0
  85. ebaml-0.1.0/src/baml_sdk/baml/toml/__init__.py +187 -0
  86. ebaml-0.1.0/src/baml_sdk/baml/toml/__init__.pyi +187 -0
  87. ebaml-0.1.0/src/baml_sdk/baml/ws/__init__.py +108 -0
  88. ebaml-0.1.0/src/baml_sdk/baml/ws/__init__.pyi +163 -0
  89. ebaml-0.1.0/src/baml_sdk/baml/yaml/__init__.py +140 -0
  90. ebaml-0.1.0/src/baml_sdk/baml/yaml/__init__.pyi +129 -0
  91. ebaml-0.1.0/src/baml_sdk/experimental/__init__.py +26 -0
  92. ebaml-0.1.0/src/baml_sdk/experimental/__init__.pyi +28 -0
  93. ebaml-0.1.0/src/baml_sdk/py.typed +0 -0
  94. ebaml-0.1.0/src/baml_sdk/reflect/__init__.py +681 -0
  95. ebaml-0.1.0/src/baml_sdk/reflect/__init__.pyi +962 -0
  96. ebaml-0.1.0/src/baml_sdk/reflect/class_/__init__.py +131 -0
  97. ebaml-0.1.0/src/baml_sdk/reflect/class_/__init__.pyi +180 -0
  98. ebaml-0.1.0/src/baml_sdk/reflect/enum/__init__.py +44 -0
  99. ebaml-0.1.0/src/baml_sdk/reflect/enum/__init__.pyi +41 -0
  100. ebaml-0.1.0/src/baml_sdk/reflect/errors/__init__.py +122 -0
  101. ebaml-0.1.0/src/baml_sdk/reflect/errors/__init__.pyi +67 -0
  102. ebaml-0.1.0/src/baml_sdk/reflect/function/__init__.py +46 -0
  103. ebaml-0.1.0/src/baml_sdk/reflect/function/__init__.pyi +39 -0
  104. ebaml-0.1.0/src/baml_sdk/reflect/interface/__init__.py +55 -0
  105. ebaml-0.1.0/src/baml_sdk/reflect/interface/__init__.pyi +64 -0
  106. ebaml-0.1.0/src/baml_sdk/vendor/__init__.py +47 -0
  107. ebaml-0.1.0/src/baml_sdk/vendor/__init__.pyi +33 -0
  108. ebaml-0.1.0/src/baml_sdk/vendor/anthropic/__init__.py +143 -0
  109. ebaml-0.1.0/src/baml_sdk/vendor/anthropic/__init__.pyi +159 -0
  110. ebaml-0.1.0/src/baml_sdk/vendor/anthropic/internal/__init__.py +774 -0
  111. ebaml-0.1.0/src/baml_sdk/vendor/anthropic/internal/__init__.pyi +787 -0
  112. ebaml-0.1.0/src/baml_sdk/vendor/assert_/__init__.py +69 -0
  113. ebaml-0.1.0/src/baml_sdk/vendor/assert_/__init__.pyi +210 -0
  114. ebaml-0.1.0/src/baml_sdk/vendor/aws/__init__.py +159 -0
  115. ebaml-0.1.0/src/baml_sdk/vendor/aws/__init__.pyi +87 -0
  116. ebaml-0.1.0/src/baml_sdk/vendor/aws/internal/__init__.py +1430 -0
  117. ebaml-0.1.0/src/baml_sdk/vendor/aws/internal/__init__.pyi +1197 -0
  118. ebaml-0.1.0/src/baml_sdk/vendor/claude_code/__init__.py +74 -0
  119. ebaml-0.1.0/src/baml_sdk/vendor/claude_code/__init__.pyi +60 -0
  120. ebaml-0.1.0/src/baml_sdk/vendor/claude_code/internal/__init__.py +247 -0
  121. ebaml-0.1.0/src/baml_sdk/vendor/claude_code/internal/__init__.pyi +262 -0
  122. ebaml-0.1.0/src/baml_sdk/vendor/google/__init__.py +177 -0
  123. ebaml-0.1.0/src/baml_sdk/vendor/google/__init__.pyi +165 -0
  124. ebaml-0.1.0/src/baml_sdk/vendor/google/internal/__init__.py +1111 -0
  125. ebaml-0.1.0/src/baml_sdk/vendor/google/internal/__init__.pyi +1141 -0
  126. ebaml-0.1.0/src/baml_sdk/vendor/openai/__init__.py +485 -0
  127. ebaml-0.1.0/src/baml_sdk/vendor/openai/__init__.pyi +669 -0
  128. ebaml-0.1.0/src/baml_sdk/vendor/openai/internal/__init__.py +2248 -0
  129. ebaml-0.1.0/src/baml_sdk/vendor/openai/internal/__init__.pyi +2155 -0
  130. ebaml-0.1.0/src/baml_sdk/vendor/openai/internal/decisions/__init__.py +207 -0
  131. ebaml-0.1.0/src/baml_sdk/vendor/openai/internal/decisions/__init__.pyi +182 -0
  132. ebaml-0.1.0/src/baml_sdk/vendor/testing/__init__.py +580 -0
  133. ebaml-0.1.0/src/baml_sdk/vendor/testing/__init__.pyi +667 -0
  134. ebaml-0.1.0/src/baml_sdk/vendor/trace/__init__.py +304 -0
  135. ebaml-0.1.0/src/baml_sdk/vendor/trace/__init__.pyi +321 -0
  136. ebaml-0.1.0/src/baml_sdk/vendor/typesafeai/__init__.py +74 -0
  137. ebaml-0.1.0/src/baml_sdk/vendor/typesafeai/__init__.pyi +56 -0
  138. ebaml-0.1.0/src/baml_sdk/vendor/typesafeai/internal/__init__.py +205 -0
  139. ebaml-0.1.0/src/baml_sdk/vendor/typesafeai/internal/__init__.pyi +175 -0
  140. ebaml-0.1.0/src/baml_sdk/vendor/vercel/__init__.py +104 -0
  141. ebaml-0.1.0/src/baml_sdk/vendor/vercel/__init__.pyi +93 -0
  142. ebaml-0.1.0/src/baml_sdk/vendor/vercel/internal/__init__.py +336 -0
  143. ebaml-0.1.0/src/baml_sdk/vendor/vercel/internal/__init__.pyi +294 -0
  144. ebaml-0.1.0/src/ebaml/__init__.py +58 -0
  145. ebaml-0.1.0/src/ebaml/benchmark.py +378 -0
  146. ebaml-0.1.0/src/ebaml/core/__init__.py +1 -0
  147. ebaml-0.1.0/src/ebaml/core/custom_types.py +128 -0
  148. ebaml-0.1.0/src/ebaml/core/exceptions.py +115 -0
  149. ebaml-0.1.0/src/ebaml/graphs/__init__.py +1 -0
  150. ebaml-0.1.0/src/ebaml/graphs/ebm_graph.py +560 -0
  151. ebaml-0.1.0/src/ebaml/graphs/graph_text.py +203 -0
  152. ebaml-0.1.0/src/ebaml/llm/__init__.py +1 -0
  153. ebaml-0.1.0/src/ebaml/llm/descriptors.py +456 -0
  154. ebaml-0.1.0/src/ebaml/llm/options.py +164 -0
  155. ebaml-0.1.0/src/ebaml/llm/provider_errors.py +301 -0
  156. ebaml-0.1.0/src/ebaml/llm/retry.py +73 -0
  157. ebaml-0.1.0/src/ebaml/llm/sdk_setup.py +326 -0
  158. ebaml-0.1.0/src/ebaml/local/__init__.py +1 -0
  159. ebaml-0.1.0/src/ebaml/local/fact_text.py +276 -0
  160. ebaml-0.1.0/src/ebaml/local/facts.py +550 -0
  161. ebaml-0.1.0/src/ebaml/py.typed +0 -0
ebaml-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026, Johnny Godoy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
ebaml-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,450 @@
1
+ Metadata-Version: 2.4
2
+ Name: ebaml
3
+ Version: 0.1.0
4
+ Summary: BAML for Explainable Boosting Machines: talk to your EBM.
5
+ Keywords: baml,llm,explainable-ai,explainable-boosting-machine,interpretml
6
+ Author: Johnny Godoy
7
+ Author-email: Johnny Godoy <johnny.godoy@ing.uchile.cl>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
15
+ Classifier: Typing :: Typed
16
+ Requires-Dist: attrs>=26.1.0
17
+ Requires-Dist: baml-bridge>=0.23.0
18
+ Requires-Dist: beartype>=0.23.1
19
+ Requires-Dist: interpret>=0.7.8
20
+ Requires-Dist: orjson>=3.13.0
21
+ Requires-Dist: structlog>=26.1.0
22
+ Requires-Python: >=3.13
23
+ Project-URL: Documentation, https://johnny-godoy.github.io/EBaMl/
24
+ Project-URL: Source, https://github.com/johnny-godoy/EBaMl
25
+ Project-URL: Issues, https://github.com/johnny-godoy/EBaMl/issues
26
+ Project-URL: Changelog, https://github.com/johnny-godoy/EBaMl/blob/main/CHANGELOG.md
27
+ Description-Content-Type: text/markdown
28
+
29
+ # EBaMl
30
+
31
+ <!-- --8<-- [start:intro] -->
32
+ BAML for Explainable Boosting Machines. Python extracts the graphs from an EBM, and BAML holds the prompts, the LLM
33
+ clients and the chain-of-thought pipelines.
34
+
35
+ EBaMl is based on [TalkToEBM](https://github.com/interpretml/TalkToEBM) and adds local
36
+ explanations (why did the model predict this for this input?) computed exactly and put in words for a layperson or a
37
+ data scientist, a benchmark that scores answers automatically, a configurable LLM provider, token budgets and typed,
38
+ structured logging.
39
+
40
+ LLM calls go through [OpenRouter](https://openrouter.ai)'s free router (`openrouter/free`), so no paid API key is needed.
41
+
42
+ <!-- --8<-- [end:intro] -->
43
+ ## Supported models
44
+
45
+ <!-- --8<-- [start:supported-models] -->
46
+ | Feature | Binary classifier | Regressor | Multiclass classifier | Interactions |
47
+ | --- | :---: | :---: | :---: | :---: |
48
+ | Graph descriptions (`describe_graph`) | ✅ | ✅ | ❌ | ⚠️ |
49
+ | Model summary (`describe_ebm`) | ✅ | ✅ | ❌ | ⚠️ |
50
+ | Graph text (`EBMGraph`) | ✅ | ✅ | ❌ | ⚠️ |
51
+ | Local explanations (`explain_instance`) | ✅ | ❌ | ❌ | ❌ |
52
+
53
+ ✅ supported. ⚠️ partly supported: the main effects are described, and the interaction terms are skipped without a
54
+ warning. ❌ not supported yet. Full support is on the
55
+ [roadmap](https://github.com/johnny-godoy/EBaMl/blob/main/ROADMAP.md).
56
+ <!-- --8<-- [end:supported-models] -->
57
+ ## Setup
58
+
59
+ <!-- --8<-- [start:setup] -->
60
+ ### Requirements
61
+
62
+ - Python 3.13 or later and [uv](https://docs.astral.sh/uv/getting-started/installation/).
63
+ - The BAML toolchain 0.23 or later (step 1).
64
+ - An API key for an LLM provider. The default is [OpenRouter](https://openrouter.ai)'s free router, which needs no
65
+ paid plan. You can use another provider instead, see "Choosing the model".
66
+
67
+ ### 1. Install the BAML toolchain
68
+
69
+ ```bash
70
+ curl -fsSL https://pkg.boundaryml.com/install.sh | sh -s
71
+ ```
72
+
73
+ The installer prints a line to add to your shell profile. Add it, then open a new shell:
74
+
75
+ ```bash
76
+ . "$HOME/.baml/env"
77
+ ```
78
+
79
+ Check it:
80
+
81
+ ```bash
82
+ baml --version # toolchain 0.23 or later
83
+ ```
84
+
85
+ If `baml` resolves to an older install (Homebrew `baml`, or `baml-py` in a virtualenv), remove it or put `~/.baml/bin`
86
+ first on `PATH`. The old CLIs have no `describe`, `generate add` or `agent` commands.
87
+
88
+ If a command stops with "the BAML agent skill is required", run `baml agent install` and retry.
89
+
90
+ ### 2. Get the code and install the Python dependencies
91
+
92
+ ```bash
93
+ git clone <repository-url> EBaMl
94
+ cd EBaMl
95
+ uv sync
96
+ ```
97
+
98
+ ### 3. Set the API key of your LLM provider
99
+
100
+ For the default provider, OpenRouter:
101
+
102
+ 1. Create an account at <https://openrouter.ai> and open <https://openrouter.ai/settings/keys>.
103
+ 2. Create a key. Keys start with `sk-or-`.
104
+ 3. Export it in the shell you run the code from:
105
+
106
+ ```bash
107
+ export OPENROUTER_API_KEY=sk-or-...
108
+ ```
109
+
110
+ The key is read from the environment when a request is sent. It is never written to a `.baml` file.
111
+
112
+ Using another provider? Set its variable instead (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, ...) and pass `model=` as
113
+ described in "Choosing the model". Local models (Ollama) need no key.
114
+
115
+ ### 4. Generate the Python SDK
116
+
117
+ ```bash
118
+ baml generate
119
+ ```
120
+
121
+ This writes the `src/baml_sdk/` package from `baml_src/`, next to `ebaml`, so it imports wherever the project is
122
+ installed. Rerun it every time a `.baml` file changes, including when you change the provider.
123
+
124
+ If `src/baml_sdk/` does not exist, `ebaml.llm.descriptors` runs `baml generate` when you import it. If the package was
125
+ generated by another BAML toolchain than the one that `baml-bridge` expects (a version skew), the import runs
126
+ `baml toolchain use <version>` and `baml generate`. If the agent skill check stops `baml generate`, the import runs it
127
+ again with `--agent-skill-check off`. If none of this works, `SDKUnavailableError` lists the steps that fix the cause.
128
+
129
+ ### 5. Check the setup
130
+
131
+ ```bash
132
+ OPENROUTER_API_KEY=unused baml test -x root::Live # offline tests on a scripted client, no network
133
+ baml run main # prints "hello from baml"
134
+ ```
135
+
136
+ These two do not call an LLM, so they pass without a working key or provider. They check the toolchain and the SDK.
137
+
138
+ ### 6. Live test (optional)
139
+
140
+ Makes real requests to your provider (9 on the OpenRouter free router, so it can be slow or rate limited). It does
141
+ nothing without `OPENROUTER_API_KEY`, so with another provider run the example under "Usage" instead.
142
+
143
+ ```bash
144
+ OPENROUTER_API_KEY=sk-or-... uv run python scripts/live_openrouter.py
145
+ ```
146
+
147
+ It fits a small EBM, runs the three-turn chain directly and checks that the provider reports token usage, then runs
148
+ `describe_graph` and `describe_ebm`. It exits non-zero on the first failed assertion.
149
+
150
+ ### Troubleshooting
151
+
152
+ | Symptom | Cause and fix |
153
+ | --- | --- |
154
+ | `baml: command not found` | The profile line from step 1 is not loaded. Open a new shell or run `. "$HOME/.baml/env"`. |
155
+ | `unknown command: describe` (or `generate add`, `agent`) | An older `baml` is first on `PATH`. See step 1. |
156
+ | `SDKUnavailableError`, or `ModuleNotFoundError: baml_sdk` | The SDK is not generated or has the wrong version, and `ebaml` could not fix it. The message of the error lists the steps. Run `baml generate` (step 4). |
157
+ | `MissingAPIKeyError` | The key of the provider is not exported in this shell. The message names the variable. See "Choosing the model" for the variable of each provider. |
158
+ | `APIKeyRejectedError` | The provider rejected the key. Check the key, its account and its credit. |
159
+ | `RateLimitedError` | The provider limited the rate. Lower the concurrency, `describe_ebm(ebm, max_concurrency=2)`, or pin a paid model. |
160
+ | `ProviderUnreachableError` | A request did not reach the provider. Check the network, `HTTPS_PROXY` and `base_url`. |
161
+
162
+ <!-- --8<-- [end:setup] -->
163
+ ## Usage
164
+
165
+ <!-- --8<-- [start:usage] -->
166
+ ```python
167
+ from interpret.glassbox import ExplainableBoostingClassifier
168
+
169
+ from ebaml.llm.descriptors import describe_ebm, describe_graph
170
+ from ebaml.llm.options import PromptOptions
171
+
172
+ ebm = ExplainableBoostingClassifier().fit(X, y)
173
+
174
+ print(describe_graph(ebm, feature_index=0))
175
+ print(describe_ebm(ebm, num_sentences=30, prompt=PromptOptions(dataset_description="...")))
176
+ ```
177
+
178
+ A graph can be turned into text with `EBMGraph.to_text` and back with `EBMGraph.from_text`, which also reads a graph
179
+ embedded in a longer prompt. It needs the confidence bounds, and it only reads continuous features.
180
+
181
+ Three keyword-only arguments set the options. `graph=GraphOptions(...)` sets the options of `EBMGraph.from_ebm` and
182
+ `EBMGraph.to_text`, for example `max_tokens`. `prompt=PromptOptions(...)` sets the descriptions that the prompts use
183
+ (`expert_description`, `graph_description`, `dataset_description` and `task_description`). `model=` selects the LLM, see
184
+ "Choosing the model". The classes are in `ebaml.llm.options`. Token usage reported by the provider is logged at `INFO` on the `ebaml.llm.descriptors` logger.
185
+
186
+ ### Explaining one prediction
187
+
188
+ ```python
189
+ from ebaml.llm.descriptors import describe_ebm, explain_instance
190
+
191
+ ebm = ExplainableBoostingClassifier(interactions=0).fit(X, y) # binary classifier, no interactions
192
+ global_explanation = describe_ebm(ebm) # computed once, prepended to every local prompt
193
+
194
+ explanation = explain_instance(ebm, X.iloc[0].to_dict(), global_explanation, audience="layperson")
195
+ print(explanation.counterfactual) # what would have to change to flip the prediction
196
+ print(explanation.sufficient_reason) # which inputs settle the prediction on their own
197
+ print(explanation.completions) # how certain the outcome is as inputs become known
198
+ ```
199
+
200
+ The facts are computed exactly from the scores and confidence intervals of the EBM (`ebaml.local`); the LLM only puts
201
+ them in words. They are the three queries of [arXiv:2010.12265](https://arxiv.org/abs/2010.12265): minimum change
202
+ required, minimum sufficient reason and count completions. `audience="layperson"` (default) gives no model numbers;
203
+ `audience="data_scientist"` quotes scores, probabilities and confidence intervals. It supports binary classifiers
204
+ without interactions, and `explanation.facts` holds the numbers behind the text. The "Local explanations" page of the documentation explains what each explanation means, the options and the limits,
205
+ and `notebooks/local_explanations.py` walks through them on the Titanic data.
206
+
207
+ ### Errors
208
+
209
+ Every error that `ebaml` raises on purpose derives from `ebaml.EBaMlError`. Use `except EBaMlError` to catch the errors
210
+ of `ebaml` apart from the errors of other code. Each error for a bad value also derives from `ValueError`, so an
211
+ `except ValueError` catches it too.
212
+
213
+ | Error | `ValueError` | Raised when |
214
+ | --- | --- | --- |
215
+ | `InvalidOptionError` | yes | An option is not one of its valid values, for example an unknown `Normalization`. |
216
+ | `OptionRangeError` | yes | A number option is outside its range, for example `confidence_level=1`. |
217
+ | `InvalidInstanceError` | yes | The instance to explain lacks a value, or has a category that the EBM did not see. |
218
+ | `UnsupportedModelError` | yes | A local explanation gets an EBM with interactions, or one that is not a binary classifier. |
219
+ | `GraphTextError` | yes | `EBMGraph.from_text` cannot read the text of a graph. |
220
+ | `EBMGraphError` | no | The base class of the errors of a graph. |
221
+ | `EBMGraphShapeError` | yes | The bins of a graph do not fit its scores, or a boolean format gets more than two bins. |
222
+ | `EBMGraphTokenLimitError` | no | The text of a graph is over the token budget, and its format cannot be simplified. |
223
+ | `EBMGraphSimplificationError` | no | The text of a graph is still over the token budget at the largest simplification. |
224
+ | `SDKUnavailableError` | no | `baml_sdk` is missing, and `ebaml` cannot run `baml generate`. The error also derives from `ImportError`. |
225
+ | `LLMRequestError` | no | The base class of the errors of a provider. Its message lists the steps that fix the cause. |
226
+ | `MissingAPIKeyError` | no | A provider needs an API key and the environment has none. |
227
+ | `APIKeyRejectedError` | no | A provider answers a request with "unauthorized" or "forbidden". |
228
+ | `RateLimitedError` | no | A provider limits the rate of the requests. |
229
+ | `ProviderUnreachableError` | no | A request does not reach the provider. |
230
+
231
+ `EBMGraphShapeError`, `EBMGraphTokenLimitError` and `EBMGraphSimplificationError` derive from `EBMGraphError`. The four provider errors derive from `LLMRequestError`. All classes are in `ebaml.core.exceptions`, and `ebaml` exports
232
+ them too.
233
+
234
+ <!-- --8<-- [end:usage] -->
235
+ ## How it works
236
+
237
+ | Where | What |
238
+ | --- | --- |
239
+ | `src/ebaml/core/` | Shared kernel: types and errors. It imports no other part of `ebaml`. |
240
+ | `src/ebaml/graphs/` | Graph extraction and simplification (numpy, `interpret`), and the text the LLM reads. |
241
+ | `src/ebaml/llm/` | The edge: the calls into BAML (`descriptors.py`), their options and the retry after a cut-off reply. |
242
+ | `baml_src/talk_to_ebm.baml` | Prompts, the default LLM clients, the three-turn description chain, the summary, `describe_ebm`, token accounting. |
243
+ | `baml_src/model_spec.baml` | The model of a call: `ModelSpec` and one client builder for each provider. |
244
+ | `src/ebaml/local/`, `baml_src/local_explanations.baml` | Local explanations: the facts of one prediction computed deterministically, their rendering as JSON texts, and the prompts that put them in words. |
245
+ | `baml_src/talk_to_ebm_test.baml`, `baml_src/model_spec_test.baml` | Offline tests on a scripted client. |
246
+ | `baml.toml` | Generator config for the Python SDK. |
247
+ | `src/ebaml/benchmark.py`, `benchmark/` | The benchmark: loading, answer parsing and scoring, and the data and runner (see [Benchmark](#benchmark)). |
248
+ | `notebooks/` | Example notebooks, written with marimo. |
249
+ | `docs/`, `zensical.toml` | The documentation, built with Zensical (see [Development](#development)). |
250
+ | `tests/unit/` | Python unit tests (100% coverage required). `scripts/live_openrouter.py` is the optional live test. |
251
+ | `scripts/` | Maintenance scripts, see [scripts/README.md](scripts/README.md). |
252
+ | `.github/` | CI workflow and the scripts that turn failures into reports. |
253
+ | `src/baml_sdk/` | Generated. Do not edit. |
254
+
255
+ <!-- --8<-- [start:behaviour] -->
256
+ - **Model:** `openrouter/free` picks a free model for each request, so the model can change between calls and between
257
+ the turns of one conversation. Quality and speed vary with it.
258
+ - **Retries:** transient failures (rate limits, network errors, 5xx) are retried up to 3 times. Requests the provider
259
+ rejects are not retried.
260
+ - **Concurrency:** `describe_ebm` describes features concurrently, at most `max_concurrency` at a time (default 8).
261
+ Free models are rate limited, so lower it (`describe_ebm(ebm, max_concurrency=2)`) if you see rate-limit errors.
262
+ - **Token budget:** there is no tokenizer before a call, so the check uses a rough estimate (about 3 characters per
263
+ token). Going a little over `max_tokens` only warns. Going more than `overbudget_percent` (default 50) over makes
264
+ `to_text` simplify the graph, or raise if it cannot. The provider's own error is the real limit. The functions in
265
+ `ebaml.llm.descriptors` pass the BAML check. A direct call of `EBMGraph.to_text` checks nothing, unless you pass
266
+ `check_budget`, for example `baml_sdk.check_token_budget`.
267
+
268
+ <!-- --8<-- [end:behaviour] -->
269
+ ## Notebook
270
+
271
+ [`notebooks/titanic.py`](notebooks/titanic.py) is a [marimo](https://marimo.io) notebook that ports TalkToEBM's example.
272
+ It trains an EBM on the Titanic dataset (downloaded from OpenML), asks the LLM to describe a graph and the whole model,
273
+ and shows the graph and text API (`from_ebm`, `simplify`, `to_text`, `from_text`). The LLM cells run only when you press
274
+ their button. They use the default model, which needs `OPENROUTER_API_KEY`. See "Choosing the model" for other models.
275
+
276
+ ```bash
277
+ uv run marimo edit notebooks/titanic.py # interactive
278
+ uv run marimo run notebooks/titanic.py # as an app
279
+ ```
280
+
281
+ [`notebooks/local_explanations.py`](notebooks/local_explanations.py) is the place to learn the local explanations: pick a
282
+ passenger, see the facts with no API key, move the confidence slider, check the guarantees against random completions,
283
+ compare what the LLM reads for a layperson and a data scientist, and (with a key) get the explanations in words.
284
+
285
+ ```bash
286
+ uv run marimo edit notebooks/local_explanations.py
287
+ ```
288
+
289
+ ## Benchmark
290
+
291
+ The benchmark follows [TalkToEBM's benchmark](https://github.com/interpretml/TalkToEBM/tree/main/benchmark) that checks how well a
292
+ model reads EBM graphs: function recognition (easy and hard), the largest jump, monotonicity, reading a value and the
293
+ widest confidence interval. It makes real requests, and it asks two LLM turns per question (6 tasks, 480 questions in
294
+ all). The default model needs `OPENROUTER_API_KEY`. The flag `--model provider/model` selects another model.
295
+
296
+ ```bash
297
+ uv run python benchmark/run.py --task monotonicity --limit 10 --output results.json
298
+ ```
299
+
300
+ See [benchmark/README.md](benchmark/README.md) for the tasks, the scoring and what it leaves out.
301
+
302
+ ## Choosing the model
303
+
304
+ <!-- --8<-- [start:changing-model] -->
305
+ The default model is OpenRouter's free router, `openrouter/free`. It needs `OPENROUTER_API_KEY`. The `model` argument
306
+ of `describe_graph`, `describe_ebm` and `explain_instance` selects another model for one call. EBaMl sends every LLM
307
+ request of that call to that model. You do not change the package.
308
+
309
+ ### Quick start
310
+
311
+ Pass the shorthand `"provider/model"`:
312
+
313
+ ```python
314
+ from ebaml.llm.descriptors import describe_ebm
315
+
316
+ print(describe_ebm(ebm, model="anthropic/claude-haiku-4-5"))
317
+ ```
318
+
319
+ Pass `ModelOptions` to set more:
320
+
321
+ ```python
322
+ from ebaml.llm.options import ModelOptions
323
+
324
+ model = ModelOptions(provider="anthropic", model="claude-haiku-4-5", temperature=0.2, max_output_tokens=8000)
325
+ print(describe_ebm(ebm, model=model))
326
+ ```
327
+
328
+ EBaMl splits the shorthand at the first `/`. The text `"openrouter/openai/gpt-4o-mini"` selects the provider
329
+ `openrouter` and the model `openai/gpt-4o-mini`. EBaMl checks the provider before the first paid request. It raises
330
+ `InvalidOptionError` if the provider is not in the table below.
331
+
332
+ ### Providers
333
+
334
+ The provider is a BAML provider prefix. Set the environment variable of the key in the shell that runs Python.
335
+
336
+ | Provider | Default key variable | Notes |
337
+ | --- | --- | --- |
338
+ | `openrouter` | `OPENROUTER_API_KEY` | The model name is the OpenRouter name, for example `openai/gpt-4o-mini`. |
339
+ | `openai` | `OPENAI_API_KEY` | |
340
+ | `anthropic` | `ANTHROPIC_API_KEY` | |
341
+ | `google` | `GOOGLE_API_KEY` | The Gemini API. |
342
+ | `ollama` | None | A local server. `base_url` defaults to `http://localhost:11434/v1`. |
343
+ | `openai-generic` | None | Any OpenAI-compatible server. `base_url` is required. Set `api_key_env` if the server needs a key. |
344
+ | `azure` | `AZURE_OPENAI_API_KEY` | The model name is the name of the deployment. Set `base_url` to the deployment, or set `AZURE_OPENAI_ENDPOINT`. |
345
+ | `vertex` | None | Google Cloud credentials (`GOOGLE_APPLICATION_CREDENTIALS` or `gcloud auth`). The project comes from `GOOGLE_CLOUD_PROJECT`. |
346
+ | `bedrock` | None | The AWS credentials of the machine. The region comes from `AWS_REGION`. `api_key_env` has no effect. |
347
+
348
+ Run `baml describe <client>.new`, for example `baml describe aws.BedrockClient.new`, to see all settings of a client.
349
+
350
+ ### Options
351
+
352
+ | Field | Meaning | Default |
353
+ | --- | --- | --- |
354
+ | `provider` | The BAML provider prefix from the table. | Required. |
355
+ | `model` | The name of the model at the provider. | Required. |
356
+ | `api_key_env` | The name of the environment variable that holds the key. | The variable of the provider. |
357
+ | `base_url` | The address of the server. | The address of the provider. |
358
+ | `temperature` | The sampling temperature. | 0.7 (0.0 in the benchmark). |
359
+ | `max_output_tokens` | The most tokens of one reply. | See below. |
360
+ | `headers` | Headers to add to each request. | None. |
361
+
362
+ A field that you set wins over the default of the call. The output limit has two defaults:
363
+
364
+ | Calls | `max_output_tokens` |
365
+ | --- | --- |
366
+ | Turn 1 of a graph description, the draft of the summary, and the two turns of the benchmark | 8000 |
367
+ | All other calls, including the local explanations | 6000 |
368
+
369
+ ### Examples
370
+
371
+ ```python
372
+ # OpenRouter, a pinned model (the free router picks a new model for each request)
373
+ describe_ebm(ebm, model="openrouter/openai/gpt-4o-mini")
374
+
375
+ # OpenAI
376
+ describe_ebm(ebm, model="openai/gpt-4o-mini")
377
+
378
+ # Google Gemini
379
+ describe_ebm(ebm, model="google/gemini-2.5-flash")
380
+
381
+ # A local model with Ollama. It needs no key.
382
+ describe_ebm(ebm, model="ollama/llama3.1")
383
+
384
+ # A company gateway with an OpenAI-compatible API
385
+ gateway = ModelOptions(
386
+ provider="openai-generic",
387
+ model="my-model",
388
+ base_url="https://llm.example.com/v1",
389
+ api_key_env="MY_GATEWAY_KEY",
390
+ headers={"x-team": "ebm"},
391
+ )
392
+ describe_ebm(ebm, model=gateway)
393
+ ```
394
+
395
+ The benchmark has the flag `--model`:
396
+
397
+ ```bash
398
+ uv run python benchmark/run.py --model anthropic/claude-haiku-4-5 --limit 10
399
+ ```
400
+
401
+ ### Keys
402
+
403
+ Put the key in an environment variable, in the shell or with `os.environ`. `ModelOptions` holds the name of the
404
+ variable, never the key, so the key does not appear in a `repr`, a log line or a traceback. EBaMl reads the variable
405
+ only when it sends a request.
406
+
407
+ ### Limits
408
+
409
+ - One call uses one model. The turns of a call cannot use different models.
410
+ - Python cannot set up fallback chains or round robin. A BAML client object cannot cross from Python to BAML, so
411
+ Python sends plain data and BAML builds the client. A BAML client declaration can use `ai.clients.Fallback`: see
412
+ "Change the default model" in `CONTRIBUTING.md`.
413
+ - A reasoning model counts its hidden tokens in `max_output_tokens`. Keep the limit large, or the reply can end before
414
+ the answer.
415
+ - Quality, speed and cost depend on the model. Small models can ignore the number of sentences that you ask for.
416
+
417
+ Every client has the retry policy of the default clients: EBaMl retries a transient failure up to 3 times.
418
+ <!-- --8<-- [end:changing-model] -->
419
+
420
+ ## Development
421
+
422
+ <!-- --8<-- [start:development] -->
423
+
424
+ ```bash
425
+ baml fmt # format .baml files
426
+ baml check # compile check
427
+ baml test -x root::Live # run the offline BAML tests
428
+ baml generate # regenerate src/baml_sdk/
429
+ uv run pytest # Python unit tests, fails under 100% coverage
430
+ uv run pytest README.md docs src --no-cov # the code examples of the docs and the docstrings
431
+ prek run --all-files # every hook: formatters, ruff, vulture, pyrefly, baml fmt, ...
432
+ uv run python scripts/mutation.py # mutation testing of all modules, fails if any mutant survives
433
+ uv run python scripts/mutation.py --base origin/main # the same, but only for modules changed since main
434
+ uv run python scripts/mutation.py --module src/ebaml/local/facts.py # the same, but only for one module
435
+ uv run python -m scripts.check_ignores # every lint/type/test workaround needs a reason written next to it
436
+ uv run python -m scripts.compile_ignores # all workarounds and their reasons as one Markdown report to audit
437
+ uv run --group docs python -m scripts.build_docs # build the docs; fails on a broken link or a docstring warning
438
+ uv run --group docs python -m scripts.build_docs serve # preview the docs (exports the notebooks, so the first run is slow)
439
+ ```
440
+
441
+ CI (`.github/workflows/ci.yml`) runs prek, secret and security scans (trufflehog, semgrep, trivy), `baml check` and
442
+ `baml test`, pytest with 100% coverage, the code examples of the documentation, and mutation testing of the changed modules with a 100% score. A weekly run tests all modules, one job for each module. Lint and type workarounds
443
+ (`# noqa`, `# type: ignore`, skipped tests, ...) must carry a written reason next to them.
444
+
445
+ The `python` code blocks of `README.md` and `docs/*.md` run in order, and the blocks of one file share their variables.
446
+ The `Examples` sections of the docstrings run as doctests. `tests/docs/conftest.py` gives each Markdown file a small
447
+ data set as `X` and `y`, and it replaces the LLM calls with fakes, so the examples send no request. Put setup that the
448
+ reader does not need in an `<!-- invisible-code-block: python -->` comment. To skip a block, put
449
+ `<!-- skip: next -->` above it, and write the reason in the text.
450
+ <!-- --8<-- [end:development] -->