morse-ai 0.4.0rc1__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 (95) hide show
  1. morse_ai-0.4.0rc1/.gitignore +93 -0
  2. morse_ai-0.4.0rc1/CHANGELOG.md +48 -0
  3. morse_ai-0.4.0rc1/LICENSE +21 -0
  4. morse_ai-0.4.0rc1/PKG-INFO +317 -0
  5. morse_ai-0.4.0rc1/README.md +269 -0
  6. morse_ai-0.4.0rc1/package.json +12 -0
  7. morse_ai-0.4.0rc1/poetry.lock +2909 -0
  8. morse_ai-0.4.0rc1/pyproject.toml +89 -0
  9. morse_ai-0.4.0rc1/src/morse_ai/__init__.py +596 -0
  10. morse_ai-0.4.0rc1/src/morse_ai/_internal/__init__.py +0 -0
  11. morse_ai-0.4.0rc1/src/morse_ai/_internal/active_framework.py +68 -0
  12. morse_ai-0.4.0rc1/src/morse_ai/_internal/auto_detect.py +236 -0
  13. morse_ai-0.4.0rc1/src/morse_ai/_internal/env.py +234 -0
  14. morse_ai-0.4.0rc1/src/morse_ai/_internal/errors.py +66 -0
  15. morse_ai-0.4.0rc1/src/morse_ai/_internal/redaction.py +334 -0
  16. morse_ai-0.4.0rc1/src/morse_ai/_internal/sentry_reporter.py +250 -0
  17. morse_ai-0.4.0rc1/src/morse_ai/_version.py +20 -0
  18. morse_ai-0.4.0rc1/src/morse_ai/adapters/__init__.py +30 -0
  19. morse_ai-0.4.0rc1/src/morse_ai/adapters/anthropic.py +1280 -0
  20. morse_ai-0.4.0rc1/src/morse_ai/adapters/base.py +142 -0
  21. morse_ai-0.4.0rc1/src/morse_ai/adapters/claude_agent_sdk.py +950 -0
  22. morse_ai-0.4.0rc1/src/morse_ai/adapters/langchain.py +804 -0
  23. morse_ai-0.4.0rc1/src/morse_ai/adapters/langgraph.py +1620 -0
  24. morse_ai-0.4.0rc1/src/morse_ai/adapters/openai.py +741 -0
  25. morse_ai-0.4.0rc1/src/morse_ai/adapters/openai_agents.py +798 -0
  26. morse_ai-0.4.0rc1/src/morse_ai/client.py +321 -0
  27. morse_ai-0.4.0rc1/src/morse_ai/context/__init__.py +1 -0
  28. morse_ai-0.4.0rc1/src/morse_ai/context/decomposer.py +345 -0
  29. morse_ai-0.4.0rc1/src/morse_ai/data/__init__.py +1 -0
  30. morse_ai-0.4.0rc1/src/morse_ai/data/compat.py +50 -0
  31. morse_ai-0.4.0rc1/src/morse_ai/data/compat_manifest.json +145 -0
  32. morse_ai-0.4.0rc1/src/morse_ai/data/pricing.json +133 -0
  33. morse_ai-0.4.0rc1/src/morse_ai/infra_capture.py +507 -0
  34. morse_ai-0.4.0rc1/src/morse_ai/instrumentation/__init__.py +0 -0
  35. morse_ai-0.4.0rc1/src/morse_ai/instrumentation/http_classifier.py +53 -0
  36. morse_ai-0.4.0rc1/src/morse_ai/instrumentation/imports.py +164 -0
  37. morse_ai-0.4.0rc1/src/morse_ai/logging.py +305 -0
  38. morse_ai-0.4.0rc1/src/morse_ai/otel_provider.py +655 -0
  39. morse_ai-0.4.0rc1/src/morse_ai/pricing.py +141 -0
  40. morse_ai-0.4.0rc1/src/morse_ai/propagation.py +132 -0
  41. morse_ai-0.4.0rc1/src/morse_ai/py.typed +0 -0
  42. morse_ai-0.4.0rc1/src/morse_ai/queue.py +61 -0
  43. morse_ai-0.4.0rc1/src/morse_ai/record.py +42 -0
  44. morse_ai-0.4.0rc1/src/morse_ai/spans.py +102 -0
  45. morse_ai-0.4.0rc1/src/morse_ai/tracing.py +802 -0
  46. morse_ai-0.4.0rc1/src/morse_ai/transport.py +528 -0
  47. morse_ai-0.4.0rc1/tests/__init__.py +0 -0
  48. morse_ai-0.4.0rc1/tests/conftest.py +75 -0
  49. morse_ai-0.4.0rc1/tests/integration/__init__.py +0 -0
  50. morse_ai-0.4.0rc1/tests/integration/test_anthropic_live.py +235 -0
  51. morse_ai-0.4.0rc1/tests/test_adapters.py +92 -0
  52. morse_ai-0.4.0rc1/tests/test_age267_roundtrip.py +105 -0
  53. morse_ai-0.4.0rc1/tests/test_anthropic_context_segments.py +363 -0
  54. morse_ai-0.4.0rc1/tests/test_anthropic_summarize.py +338 -0
  55. morse_ai-0.4.0rc1/tests/test_anthropic_wrap.py +443 -0
  56. morse_ai-0.4.0rc1/tests/test_auto_detect.py +176 -0
  57. morse_ai-0.4.0rc1/tests/test_claude_agent_sdk_adapter.py +1097 -0
  58. morse_ai-0.4.0rc1/tests/test_compat_manifest.py +63 -0
  59. morse_ai-0.4.0rc1/tests/test_context_decomposer.py +340 -0
  60. morse_ai-0.4.0rc1/tests/test_http_classifier.py +66 -0
  61. morse_ai-0.4.0rc1/tests/test_import_scanner.py +131 -0
  62. morse_ai-0.4.0rc1/tests/test_infra_capture.py +141 -0
  63. morse_ai-0.4.0rc1/tests/test_infra_capture_http.py +203 -0
  64. morse_ai-0.4.0rc1/tests/test_infra_capture_streaming.py +107 -0
  65. morse_ai-0.4.0rc1/tests/test_integration.py +69 -0
  66. morse_ai-0.4.0rc1/tests/test_langchain_adapter.py +503 -0
  67. morse_ai-0.4.0rc1/tests/test_langchain_state_capture.py +367 -0
  68. morse_ai-0.4.0rc1/tests/test_langgraph_context_segments.py +473 -0
  69. morse_ai-0.4.0rc1/tests/test_langgraph_memory_ops.py +880 -0
  70. morse_ai-0.4.0rc1/tests/test_langgraph_trace_context_bridges.py +571 -0
  71. morse_ai-0.4.0rc1/tests/test_logging_handler.py +375 -0
  72. morse_ai-0.4.0rc1/tests/test_openai_agents_adapter.py +448 -0
  73. morse_ai-0.4.0rc1/tests/test_openai_context_segments.py +478 -0
  74. morse_ai-0.4.0rc1/tests/test_openai_summarize.py +251 -0
  75. morse_ai-0.4.0rc1/tests/test_otel_provider.py +326 -0
  76. morse_ai-0.4.0rc1/tests/test_performance.py +55 -0
  77. morse_ai-0.4.0rc1/tests/test_pricing.py +92 -0
  78. morse_ai-0.4.0rc1/tests/test_propagation.py +371 -0
  79. morse_ai-0.4.0rc1/tests/test_provider_reported_cost.py +266 -0
  80. morse_ai-0.4.0rc1/tests/test_queue.py +47 -0
  81. morse_ai-0.4.0rc1/tests/test_record.py +50 -0
  82. morse_ai-0.4.0rc1/tests/test_redaction.py +550 -0
  83. morse_ai-0.4.0rc1/tests/test_resilience.py +362 -0
  84. morse_ai-0.4.0rc1/tests/test_retriever_adapter.py +127 -0
  85. morse_ai-0.4.0rc1/tests/test_sentry_isolation.py +242 -0
  86. morse_ai-0.4.0rc1/tests/test_set_context.py +85 -0
  87. morse_ai-0.4.0rc1/tests/test_tracing.py +494 -0
  88. morse_ai-0.4.0rc1/tests/test_transport.py +101 -0
  89. morse_ai-0.4.0rc1/tests/test_transport_retry.py +366 -0
  90. morse_ai-0.4.0rc1/tests/test_v2_api.py +165 -0
  91. morse_ai-0.4.0rc1/tests/test_v2_env_init.py +244 -0
  92. morse_ai-0.4.0rc1/tests/test_v2_framework_attr.py +283 -0
  93. morse_ai-0.4.0rc1/tests/test_v2_source_attr.py +201 -0
  94. morse_ai-0.4.0rc1/tests/test_v2_tracing_auto_init.py +63 -0
  95. morse_ai-0.4.0rc1/uv.lock +2122 -0
@@ -0,0 +1,93 @@
1
+ # OS
2
+ .DS_Store
3
+ Thumbs.db
4
+ *.sw?
5
+
6
+ # Node / JS / TS
7
+ node_modules/
8
+ .pnpm-store/
9
+ .npm-cache/
10
+ dist/
11
+ build/
12
+ coverage/
13
+ .nyc_output/
14
+ .turbo/
15
+ .next/
16
+ out/
17
+ *.tsbuildinfo
18
+ pnpm-debug.log*
19
+ npm-debug.log*
20
+ yarn-debug.log*
21
+ yarn-error.log*
22
+
23
+ # Vite/Vitest
24
+ .vite/
25
+ vitest.config.ts.timestamp-*
26
+
27
+ # Expo / React Native
28
+ .expo/
29
+ .expo-shared/
30
+ ios/
31
+ android/
32
+
33
+ # Python
34
+ __pycache__/
35
+ *.py[cod]
36
+ *.pyo
37
+ *.pyd
38
+ *.so
39
+ *.egg-info/
40
+ .venv/
41
+ venv/
42
+ env/
43
+ .mypy_cache/
44
+ .pytest_cache/
45
+ .ruff_cache/
46
+ .coverage
47
+ htmlcov/
48
+ *.sqlite3
49
+ coverage.xml
50
+ .hypothesis/
51
+
52
+ # Django collected static files (built artifact — produced by collectstatic
53
+ # at Docker build time and at runtime; never checked in).
54
+ apps/api/staticfiles/
55
+
56
+ # Env files
57
+ .env
58
+ .env.local
59
+ .env.*.local
60
+ .env.*
61
+ !.env.example
62
+ !.env.*.example
63
+
64
+ # GSD / planning backups
65
+ .planning/STATE.md.bak-*
66
+
67
+ # Test artifacts
68
+ test-results/
69
+
70
+ # Screen recordings (marketing video build artifacts — scripts/record-video.sh)
71
+ apps/web/recordings/
72
+
73
+ # Local agent/tool folders
74
+ .agent/
75
+ .agents/
76
+ .codex/
77
+ .gemini/
78
+ .openpackage/
79
+ .aider*
80
+ .claude/worktrees/
81
+ .claude/settings.local.json
82
+ .serena/
83
+ .remember/
84
+ apps/landing/playwright-report/
85
+ # orchestration env (covered by .env.* above, but kept explicit for clarity)
86
+ .env.orchestration
87
+ .env.orchestration.bak
88
+
89
+ # QA bot run artifacts (machine-generated, high churn, binaries)
90
+ qa/state/
91
+
92
+ # Local scratch worktrees (never tracked — a stray gitlink was committed 2026-07-25)
93
+ .worktrees/
@@ -0,0 +1,48 @@
1
+ # Changelog
2
+
3
+ All notable changes to `morse-ai` are documented here.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
6
+ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ The Python and TypeScript SDKs version **independently** — they share `0.4.0` as
9
+ their first public release because they reached feature parity there, not
10
+ because they are released in lockstep. Expect the numbers to diverge.
11
+
12
+ ## [0.4.0] — 2026-07-31
13
+
14
+ First public release on PyPI.
15
+
16
+ ### Added
17
+
18
+ - **Six framework adapters**, each dogfooded end-to-end against a real
19
+ third-party application before release: Anthropic Messages API, Claude Agent
20
+ SDK, OpenAI (raw client), OpenAI Agents SDK, LangChain (LCEL + `bind_tools`),
21
+ and LangGraph.
22
+ - `input_data` / `output_data` capture on LLM spans for the Anthropic
23
+ (MHQ-772) and OpenAI (MHQ-774) adapters — prompt and completion payloads are
24
+ now visible in the trace, subject to the redaction config.
25
+ - OTLP export path alongside the REST transport, including W3C trace-context
26
+ propagation (`extract_traceparent` / `inject_traceparent`).
27
+ - Retry/backoff on the telemetry transport: 3 attempts with jittered
28
+ exponential backoff, and quota feedback read from the `X-Morse-Quota-*`
29
+ response headers.
30
+
31
+ ### Fixed
32
+
33
+ - Anthropic adapter LLM spans always reported ~0ms duration (MHQ-771).
34
+ - OpenAI adapter LLM spans had the same 0ms duration bug.
35
+ - Claude Agent SDK sub-agent spawns never finalized when the stream ended
36
+ without a terminal task message — they now finalize via a `ResultMessage`
37
+ fallback (MHQ-783), and in-flight spawn hooks are drained on shutdown
38
+ (MHQ-762).
39
+ - API keys are redacted from telemetry under both the current `mhq_` prefix and
40
+ the legacy `av_` prefix, so keys issued before the rename are still scrubbed.
41
+
42
+ ### Changed
43
+
44
+ - **The SDK version is now declared in exactly one place** (`pyproject.toml`)
45
+ and read back from package metadata at import time. It was previously
46
+ copy-pasted into four locations; the copies could drift from the released
47
+ version and would then misreport `sdk_version` on every ingested span and in
48
+ the `User-Agent` header.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Morse
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.
@@ -0,0 +1,317 @@
1
+ Metadata-Version: 2.4
2
+ Name: morse-ai
3
+ Version: 0.4.0rc1
4
+ Summary: Unified observability for AI agents and infrastructure. One SDK.
5
+ Project-URL: Homepage, https://morsehq.dev
6
+ Project-URL: Documentation, https://docs.morsehq.dev
7
+ Author-email: Morse <hello@morsehq.dev>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: <4.0,>=3.11
20
+ Requires-Dist: httpx<1.0,>=0.25.0
21
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.27.0
22
+ Requires-Dist: opentelemetry-sdk>=1.27.0
23
+ Provides-Extra: all
24
+ Requires-Dist: anthropic>=0.40.0; extra == 'all'
25
+ Requires-Dist: langchain-core>=1.0; extra == 'all'
26
+ Requires-Dist: langgraph>=1.0; extra == 'all'
27
+ Requires-Dist: openai-agents<1.0,>=0.15.0; extra == 'all'
28
+ Requires-Dist: openai>=1.50.0; extra == 'all'
29
+ Provides-Extra: anthropic
30
+ Requires-Dist: anthropic>=0.40.0; extra == 'anthropic'
31
+ Provides-Extra: dev
32
+ Requires-Dist: mypy>=1.13; extra == 'dev'
33
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
34
+ Requires-Dist: pytest-benchmark>=5.0; extra == 'dev'
35
+ Requires-Dist: pytest>=8.0; extra == 'dev'
36
+ Requires-Dist: respx>=0.22; extra == 'dev'
37
+ Requires-Dist: ruff>=0.8; extra == 'dev'
38
+ Provides-Extra: langchain
39
+ Requires-Dist: langchain-core>=1.0; extra == 'langchain'
40
+ Provides-Extra: langgraph
41
+ Requires-Dist: langchain-core>=1.0; extra == 'langgraph'
42
+ Requires-Dist: langgraph>=1.0; extra == 'langgraph'
43
+ Provides-Extra: openai
44
+ Requires-Dist: openai>=1.50.0; extra == 'openai'
45
+ Provides-Extra: openai-agents
46
+ Requires-Dist: openai-agents<1.0,>=0.15.0; extra == 'openai-agents'
47
+ Description-Content-Type: text/markdown
48
+
49
+ # Morse Python SDK
50
+
51
+ Unified observability for AI agents and infrastructure. One SDK.
52
+
53
+ ## Install
54
+
55
+ ```bash
56
+ pip install morse-ai
57
+ ```
58
+
59
+ With an adapter (installs the underlying provider SDK too):
60
+
61
+ ```bash
62
+ pip install "morse-ai[anthropic]"
63
+ pip install "morse-ai[openai]"
64
+ pip install "morse-ai[openai_agents]"
65
+ pip install "morse-ai[langgraph]" # also pulls langchain-core
66
+ pip install "morse-ai[all]" # every adapter
67
+ ```
68
+
69
+ The import name is `morse_ai` (underscore) regardless of which extra
70
+ you install — `import morse_ai`.
71
+
72
+ ## Quick Start
73
+
74
+ ```python
75
+ import morse_ai
76
+
77
+ morse_ai.init(api_key="mhq_xxxxx") # pragma: allowlist secret
78
+
79
+ morse_ai.record(
80
+ agent="lead-qualifier",
81
+ success=True,
82
+ outcome="qualified",
83
+ cost=0.043,
84
+ )
85
+ ```
86
+
87
+ Or trace a multi-step run:
88
+
89
+ ```python
90
+ with morse_ai.run("lead-qualifier") as r:
91
+ with morse_ai.span("fetch-lead", "tool"):
92
+ ...
93
+ r.set_outcome(True, "qualified")
94
+ ```
95
+
96
+ `record()`/`run()`/`span()`/`@trace_agent` all send real OpenTelemetry
97
+ spans over OTLP by default — see [Transport](#transport) below.
98
+
99
+ ## Zero-config instrumentation
100
+
101
+ ```bash
102
+ pip install "morse-ai[anthropic]"
103
+ export MORSE_API_KEY=mhq_xxxxx
104
+ # your existing anthropic / openai-agents / langgraph / claude-agent-sdk
105
+ # code is now traced — no morse_ai.wrap() calls anywhere
106
+ ```
107
+
108
+ When the SDK initializes, it scans `sys.modules` for installed
109
+ provider SDKs — and registers an import hook for ones imported later —
110
+ and calls each detected adapter's `install()`. Today that covers
111
+ `anthropic`, `openai-agents` (imports as `agents`), `langgraph`, and
112
+ `claude-agent-sdk`. Priority rule: when both LangGraph and generic
113
+ LangChain are importable, only the LangGraph adapter installs, to
114
+ avoid duplicate spans. Opt out entirely with
115
+ `MORSE_DISABLE_AUTO_DETECT=1`; you can still call any adapter's
116
+ `install()`/`wrap()` directly.
117
+
118
+ The plain LangChain adapter (`MorseCallbackHandler`, for chains
119
+ outside a LangGraph graph) is **not** auto-installed — pass it via
120
+ `callbacks=[...]` explicitly (see below).
121
+
122
+ ## Adapters
123
+
124
+ ### Anthropic
125
+
126
+ ```python
127
+ from morse_ai.adapters import anthropic as av_anthropic
128
+ import anthropic
129
+
130
+ client = av_anthropic.wrap(anthropic.Anthropic())
131
+ # every messages.create call now emits an `llm` span automatically
132
+ response = client.messages.create(
133
+ model="claude-sonnet-4-6",
134
+ max_tokens=1024,
135
+ messages=[{"role": "user", "content": "Qualify this lead..."}],
136
+ )
137
+ ```
138
+
139
+ `wrap()` is idempotent and works whether or not auto-detect already
140
+ installed the adapter globally. Pass `summarize=True` (or use the
141
+ `summarize()` context manager per-call) to also emit a sibling
142
+ `memory` span.
143
+
144
+ ### OpenAI
145
+
146
+ ```python
147
+ from morse_ai.adapters import openai as av_openai
148
+ import openai
149
+
150
+ client = av_openai.wrap(openai.OpenAI())
151
+ response = client.chat.completions.create(
152
+ model="gpt-4.1",
153
+ messages=[{"role": "user", "content": "Qualify this lead..."}],
154
+ )
155
+ ```
156
+
157
+ Context segments (messages + tools) are captured automatically on
158
+ every call — no separate extraction step.
159
+
160
+ ### OpenAI Agents SDK, LangGraph, Claude Agent SDK
161
+
162
+ These three are auto-installed globally by zero-config detection (see
163
+ above) — no wrap call needed once the package is importable and
164
+ `MORSE_API_KEY` is set. To install manually (e.g. to control ordering,
165
+ or with auto-detect disabled):
166
+
167
+ ```python
168
+ from morse_ai.adapters import openai_agents, langgraph, claude_agent_sdk
169
+
170
+ openai_agents.install() # patches agents.Runner.run / run_sync / run_streamed
171
+ langgraph.install() # registers a callback handler globally with LangChain
172
+ claude_agent_sdk.install() # patches ClaudeSDKClient.receive_response
173
+ ```
174
+
175
+ Each emits an outer **agent** span, per-call **llm** + **tool** spans,
176
+ and adapter-specific spans (LangGraph: nested chain spans; OpenAI
177
+ Agents: **subagent.spawn**-shaped `handoff` spans; Claude Agent SDK:
178
+ **subagent.spawn** spans for Task-tool fan-out and **hook** spans for
179
+ `pre_tool`/`post_tool`/`on_error`).
180
+
181
+ ### Plain LangChain (chains, LLMs, tools, retrievers)
182
+
183
+ Not auto-installed — attach `MorseCallbackHandler` via `callbacks`:
184
+
185
+ ```python
186
+ from morse_ai.adapters.langchain import MorseCallbackHandler
187
+
188
+ handler = MorseCallbackHandler("my-agent")
189
+ chain = SomeChain(..., callbacks=[handler])
190
+ ```
191
+
192
+ Called inside an existing `morse_ai.run()` context, the handler
193
+ appends child spans to that trace. Called standalone (no enclosing
194
+ `run()`), it auto-creates a trace that flushes when the outermost
195
+ chain completes — no `morse_ai.run()` wrapper required.
196
+
197
+ ## Capturing logs alongside agent traces
198
+
199
+ If you already use stdlib `logging` or `structlog`, you can ship every
200
+ log line to Morse with one line of config. Each record is
201
+ auto-correlated with the active trace (the one entered via
202
+ `morse_ai.run()`, `@morse_ai.trace_agent`, or any active
203
+ `morse_ai.span(...)`) and appears under the trace's **Logs** tab on
204
+ the dashboard.
205
+
206
+ The shim re-uses the SDK's batched transport — no blocking I/O on your
207
+ hot path.
208
+
209
+ ### Stdlib `logging`
210
+
211
+ ```python
212
+ import logging
213
+ import morse_ai
214
+ from morse_ai.logging import MorseHandler
215
+
216
+ morse_ai.init(api_key="mhq_xxxxx") # pragma: allowlist secret
217
+ logging.basicConfig(level=logging.INFO, handlers=[MorseHandler()])
218
+ ```
219
+
220
+ ### `structlog`
221
+
222
+ ```python
223
+ import structlog
224
+ import morse_ai
225
+ from morse_ai.logging import structlog_processor
226
+
227
+ morse_ai.init(api_key="mhq_xxxxx") # pragma: allowlist secret
228
+ structlog.configure(processors=[structlog_processor, structlog.processors.JSONRenderer()])
229
+ ```
230
+
231
+ Log lines emitted outside any trace context still flow to Morse — they
232
+ just won't be correlated to a specific trace. Errors inside the
233
+ handler / processor are absorbed and never propagate into your
234
+ `logger.info(...)` call.
235
+
236
+ ## Cost
237
+
238
+ `cost_usd` is computed automatically from the model + token counts on
239
+ every `llm` span the adapters above emit, using a bundled pricing
240
+ table — no separate call needed. `record()` doesn't auto-compute —
241
+ pass `cost` yourself if you have it.
242
+
243
+ ## Transport
244
+
245
+ `run()`/`span()`/`trace_agent`/`record()` emit real OpenTelemetry spans
246
+ — `TracerProvider` / `BatchSpanProcessor` / `TraceIdRatioBased` sampler
247
+ — over OTLP (protobuf+gzip). This is a **core dependency**
248
+ (`opentelemetry-sdk`, `opentelemetry-exporter-otlp-proto-http`), not an
249
+ optional extra — every install speaks OTLP. `track()`/`batch_track()`
250
+ are a lower-level, non-trace-shaped escape hatch of their own — prefer
251
+ `record()`.
252
+
253
+ ## PII Redaction
254
+
255
+ The SDK ships with PII redaction **on by default**. Span attributes and
256
+ event payloads are scrubbed inside your process before any data leaves
257
+ for the Morse ingest endpoint. You can opt out per-pattern, opt out
258
+ entirely, or extend the catalogue with your own regex.
259
+
260
+ ### What gets redacted by default
261
+
262
+ | Pattern key | Matches | Replacement |
263
+ | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
264
+ | `api_key_prefixed` | `sk-…`, `sk_live_…`, `sk_test_…`, `mhq_…`, GitHub PATs (`ghp_…`/`gho_…`/…), Slack tokens (`xoxb-…`), AWS access keys (`AKIA…`/`ASIA…`/…) | `[REDACTED:api_key]` |
265
+ | `jwt` | Three-segment base64url tokens (`eyJ…`) | `[REDACTED:jwt]` |
266
+ | `credit_card` | 13-19 digit groups passing the Luhn checksum | `[REDACTED:credit_card]` |
267
+ | `email` | RFC-5322-ish address regex | `[REDACTED:email]` |
268
+ | `phone_us` | US-format phone numbers | `[REDACTED:phone]` |
269
+ | `phone_e164` | International E.164 phone numbers | `[REDACTED:phone]` |
270
+ | `ssn_us` | US Social Security numbers (`999-99-9999`) | `[REDACTED:ssn]` |
271
+ | `ip_address` | IPv4 / IPv6 — **disabled by default**, opt-in | `[REDACTED:ip]` |
272
+
273
+ ### Configuring redaction
274
+
275
+ ```python
276
+ import morse_ai
277
+
278
+ morse_ai.init(
279
+ api_key="mhq_xxxxx", # pragma: allowlist secret
280
+ # All four kwargs below are optional; defaults are shown.
281
+ redaction_enabled=True,
282
+ redaction_disabled_patterns=None, # e.g. ["email", "phone_us"]
283
+ redaction_extra_patterns=None, # e.g. [r"INTERNAL-\w+"]
284
+ )
285
+ ```
286
+
287
+ - `redaction_enabled=False` turns OFF every built-in pattern at the SDK
288
+ layer. **The server-side mandatory floor still applies** —
289
+ `api_key_prefixed`, `jwt`, `credit_card`, and `ssn_us` are rewritten
290
+ on ingest regardless of SDK configuration. Opting out shifts _where_
291
+ redaction happens, it does not remove it.
292
+ - `redaction_disabled_patterns` lets you skip individual non-mandatory
293
+ keys (`email`, `phone_us`, `phone_e164`, `ip_address`). Trying to
294
+ disable a mandatory pattern raises `ValueError` at `init()`.
295
+ - `redaction_extra_patterns` accepts up to 10 customer regex strings
296
+ (each ≤ 256 chars). Each must compile via `re.compile()`; matches are
297
+ replaced with `[REDACTED:custom]`. Built-in patterns run before extra
298
+ patterns, so a custom regex sees the already-redacted text.
299
+
300
+ ### What is NOT touched
301
+
302
+ Non-string leaves (numbers, booleans, `None`, opaque objects) pass
303
+ through untouched. Span IDs, timestamps, durations, token counts, and
304
+ other metadata are never redacted.
305
+
306
+ ## Docs
307
+
308
+ Full reference — every adapter, all `init()` options, configuration,
309
+ troubleshooting — lives at **[docs.morsehq.dev](https://docs.morsehq.dev)**.
310
+ This README stays a quickstart.
311
+
312
+ ## Naming
313
+
314
+ The PyPI project is `morse-ai`; the import name is `morse_ai`
315
+ (Python's underscore convention). The npm package for the TypeScript
316
+ SDK is `@morsehq-dev/sdk` — see that package's README for why it
317
+ doesn't match this one's naming.