flightdeck-sensor 0.5.3__tar.gz → 0.7.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.
- flightdeck_sensor-0.7.0/PKG-INFO +239 -0
- flightdeck_sensor-0.7.0/README.md +179 -0
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/flightdeck_sensor/__init__.py +127 -78
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/flightdeck_sensor/config.py +7 -3
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/flightdeck_sensor/core/agent_id.py +8 -10
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/flightdeck_sensor/core/context.py +7 -7
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/flightdeck_sensor/core/errors.py +311 -34
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/flightdeck_sensor/core/exceptions.py +7 -7
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/flightdeck_sensor/core/mcp_policy.py +55 -31
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/flightdeck_sensor/core/policy.py +8 -8
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/flightdeck_sensor/core/schemas.py +1 -1
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/flightdeck_sensor/core/session.py +152 -57
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/flightdeck_sensor/core/types.py +86 -25
- flightdeck_sensor-0.7.0/flightdeck_sensor/interceptor/anthropic.py +1120 -0
- flightdeck_sensor-0.7.0/flightdeck_sensor/interceptor/base.py +1913 -0
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/flightdeck_sensor/interceptor/crewai.py +9 -5
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/flightdeck_sensor/interceptor/langgraph.py +30 -16
- flightdeck_sensor-0.7.0/flightdeck_sensor/interceptor/litellm.py +544 -0
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/flightdeck_sensor/interceptor/mcp.py +369 -182
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/flightdeck_sensor/interceptor/mcp_identity.py +8 -9
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/flightdeck_sensor/interceptor/openai.py +424 -70
- flightdeck_sensor-0.7.0/flightdeck_sensor/interceptor/platform.py +126 -0
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/flightdeck_sensor/provider.py +13 -13
- flightdeck_sensor-0.7.0/flightdeck_sensor/providers/anthropic.py +554 -0
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/flightdeck_sensor/providers/litellm.py +249 -10
- flightdeck_sensor-0.7.0/flightdeck_sensor/providers/openai.py +878 -0
- flightdeck_sensor-0.7.0/flightdeck_sensor/providers/protocol.py +271 -0
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/flightdeck_sensor/transport/client.py +13 -11
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/flightdeck_sensor/transport/retry.py +13 -10
- flightdeck_sensor-0.7.0/pyproject.toml +222 -0
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/conftest.py +85 -2
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/unit/test_agent_id.py +9 -9
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/unit/test_agent_type_validation.py +1 -1
- flightdeck_sensor-0.7.0/tests/unit/test_anthropic_resource_patch.py +1516 -0
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/unit/test_compat_crewai_mcp.py +2 -2
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/unit/test_cross_agent_messages.py +36 -11
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/unit/test_custom_directives.py +2 -2
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/unit/test_egress_url_validation.py +1 -1
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/unit/test_errors.py +354 -6
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/unit/test_framework_attribution.py +50 -44
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/unit/test_interceptor.py +44 -24
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/unit/test_interceptor_crewai.py +26 -3
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/unit/test_interceptor_langgraph.py +116 -2
- flightdeck_sensor-0.7.0/tests/unit/test_interceptor_litellm.py +1038 -0
- flightdeck_sensor-0.7.0/tests/unit/test_mcp_2x_paths.py +141 -0
- flightdeck_sensor-0.7.0/tests/unit/test_mcp_availability.py +45 -0
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/unit/test_mcp_identity.py +1 -1
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/unit/test_mcp_interceptor.py +117 -61
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/unit/test_mcp_policy_emission_enriched.py +6 -6
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/unit/test_mcp_policy_enforcement.py +54 -5
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/unit/test_mcp_policy_enforcement_all_paths.py +4 -4
- flightdeck_sensor-0.7.0/tests/unit/test_openai_parse_responses.py +1404 -0
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/unit/test_originating_event_id.py +1 -1
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/unit/test_patch.py +148 -143
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/unit/test_policy.py +3 -3
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/unit/test_policy_decision_summary.py +2 -2
- flightdeck_sensor-0.7.0/tests/unit/test_policy_directive_events.py +113 -0
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/unit/test_policy_emission_enriched.py +3 -3
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/unit/test_policy_events.py +5 -5
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/unit/test_prompt_capture.py +2 -2
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/unit/test_provider_enum.py +6 -6
- flightdeck_sensor-0.7.0/tests/unit/test_providers.py +499 -0
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/unit/test_session.py +4 -5
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/unit/test_session_lifecycle_enrichment.py +2 -2
- flightdeck_sensor-0.7.0/tests/unit/test_stop_reason_errors.py +528 -0
- flightdeck_sensor-0.7.0/tests/unit/test_streaming_usage.py +1842 -0
- flightdeck_sensor-0.7.0/tests/unit/test_token_cost_fields.py +557 -0
- flightdeck_sensor-0.5.3/tests/unit/test_d150_tool_capture_destination.py → flightdeck_sensor-0.7.0/tests/unit/test_tool_capture_destination.py +11 -12
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/unit/test_transport.py +20 -8
- flightdeck_sensor-0.7.0/uv.lock +6504 -0
- flightdeck_sensor-0.5.3/PKG-INFO +0 -192
- flightdeck_sensor-0.5.3/README.md +0 -141
- flightdeck_sensor-0.5.3/flightdeck_sensor/interceptor/anthropic.py +0 -517
- flightdeck_sensor-0.5.3/flightdeck_sensor/interceptor/base.py +0 -1194
- flightdeck_sensor-0.5.3/flightdeck_sensor/interceptor/litellm.py +0 -392
- flightdeck_sensor-0.5.3/flightdeck_sensor/providers/anthropic.py +0 -294
- flightdeck_sensor-0.5.3/flightdeck_sensor/providers/openai.py +0 -396
- flightdeck_sensor-0.5.3/flightdeck_sensor/providers/protocol.py +0 -133
- flightdeck_sensor-0.5.3/pyproject.toml +0 -155
- flightdeck_sensor-0.5.3/tests/unit/test_providers.py +0 -202
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/.gitignore +0 -0
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/CLAUDE.md +0 -0
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/Makefile +0 -0
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/flightdeck_sensor/compat/__init__.py +0 -0
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/flightdeck_sensor/compat/crewai_mcp.py +0 -0
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/flightdeck_sensor/core/__init__.py +0 -0
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/flightdeck_sensor/interceptor/__init__.py +0 -0
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/flightdeck_sensor/providers/__init__.py +0 -0
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/flightdeck_sensor/py.typed +0 -0
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/flightdeck_sensor/transport/__init__.py +0 -0
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/__init__.py +0 -0
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/unit/__init__.py +0 -0
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/unit/test_context.py +0 -0
- {flightdeck_sensor-0.5.3 → flightdeck_sensor-0.7.0}/tests/unit/test_llm_event_enrichment.py +0 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: flightdeck-sensor
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: In-process agent observability sensor for Flightdeck
|
|
5
|
+
Project-URL: Repository, https://github.com/flightdeckhq/flightdeck
|
|
6
|
+
Author-email: Omri Arnon <omri.arnon@gmail.com>
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: <3.15,>=3.11
|
|
18
|
+
Requires-Dist: pydantic<3,>=2.10
|
|
19
|
+
Provides-Extra: anthropic
|
|
20
|
+
Requires-Dist: anthropic>=0.86; extra == 'anthropic'
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: anthropic>=0.86; extra == 'dev'
|
|
23
|
+
Requires-Dist: crewai>=1.14; (python_version < '3.14') and extra == 'dev'
|
|
24
|
+
Requires-Dist: httpx>=0.25; extra == 'dev'
|
|
25
|
+
Requires-Dist: langchain-anthropic<2,>=1; extra == 'dev'
|
|
26
|
+
Requires-Dist: langchain-mcp-adapters<0.4,>=0.3; extra == 'dev'
|
|
27
|
+
Requires-Dist: langchain-openai<2,>=1; extra == 'dev'
|
|
28
|
+
Requires-Dist: langchain<2,>=1; extra == 'dev'
|
|
29
|
+
Requires-Dist: langgraph<2,>=1; extra == 'dev'
|
|
30
|
+
Requires-Dist: litellm>=1.100; extra == 'dev'
|
|
31
|
+
Requires-Dist: llama-index-llms-anthropic>=0.1; extra == 'dev'
|
|
32
|
+
Requires-Dist: llama-index-llms-openai>=0.1; extra == 'dev'
|
|
33
|
+
Requires-Dist: llama-index-tools-mcp>=0.1; extra == 'dev'
|
|
34
|
+
Requires-Dist: mcp<2,>=1.28; extra == 'dev'
|
|
35
|
+
Requires-Dist: mcpadapt>=0.0.6; extra == 'dev'
|
|
36
|
+
Requires-Dist: mypy>=2.4; extra == 'dev'
|
|
37
|
+
Requires-Dist: openai>=1.0; extra == 'dev'
|
|
38
|
+
Requires-Dist: pytest-asyncio>=1.0; extra == 'dev'
|
|
39
|
+
Requires-Dist: pytest-cov>=7.0; extra == 'dev'
|
|
40
|
+
Requires-Dist: pytest-xdist<4,>=3.5; extra == 'dev'
|
|
41
|
+
Requires-Dist: pytest>=9; extra == 'dev'
|
|
42
|
+
Requires-Dist: respx>=0.20; extra == 'dev'
|
|
43
|
+
Requires-Dist: ruff>=0.3; extra == 'dev'
|
|
44
|
+
Provides-Extra: litellm
|
|
45
|
+
Requires-Dist: litellm>=1.83.0; extra == 'litellm'
|
|
46
|
+
Provides-Extra: mcp
|
|
47
|
+
Requires-Dist: mcp<3,>=1.28; extra == 'mcp'
|
|
48
|
+
Provides-Extra: openai
|
|
49
|
+
Requires-Dist: openai>=1.0; extra == 'openai'
|
|
50
|
+
Requires-Dist: tiktoken>=0.5; extra == 'openai'
|
|
51
|
+
Provides-Extra: test
|
|
52
|
+
Requires-Dist: mypy>=2.4; extra == 'test'
|
|
53
|
+
Requires-Dist: pytest-asyncio>=1.0; extra == 'test'
|
|
54
|
+
Requires-Dist: pytest-xdist<4,>=3.5; extra == 'test'
|
|
55
|
+
Requires-Dist: pytest>=9; extra == 'test'
|
|
56
|
+
Requires-Dist: respx>=0.20; extra == 'test'
|
|
57
|
+
Requires-Dist: ruff>=0.3; extra == 'test'
|
|
58
|
+
Requires-Dist: tiktoken>=0.5; extra == 'test'
|
|
59
|
+
Description-Content-Type: text/markdown
|
|
60
|
+
|
|
61
|
+
# flightdeck-sensor
|
|
62
|
+
|
|
63
|
+
In-process agent observability sensor for [Flightdeck](https://github.com/flightdeckhq/flightdeck).
|
|
64
|
+
|
|
65
|
+
## Local development venv
|
|
66
|
+
|
|
67
|
+
Flightdeck pins **Python 3.12** for local development. The sensor
|
|
68
|
+
supports `3.11 ≤ x < 3.15` (pyproject `requires-python`); `sensor/.venv`
|
|
69
|
+
is the canonical interpreter for every Make target that runs Python.
|
|
70
|
+
|
|
71
|
+
Recreate from a fresh clone or after a `rm -rf sensor/.venv`:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
python3.12 -m venv sensor/.venv
|
|
75
|
+
./sensor/.venv/bin/python -m pip install -e ".[dev,anthropic,openai]"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`make -C sensor install` does the same once the venv exists. To get
|
|
79
|
+
the exact versions CI tests with, install from the lockfile instead:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
cd sensor && uv sync --locked --python 3.12 --extra dev --extra anthropic --extra openai
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
CI installs from `sensor/uv.lock` into the `actions/setup-python`
|
|
86
|
+
interpreter and overrides via env (`PYTHON=python make -C sensor
|
|
87
|
+
test`). The lock covers Python 3.11 to 3.14 and both CI lanes: the
|
|
88
|
+
framework lane (`dev` extra) and the newest-SDK lane (`sdk-majors`
|
|
89
|
+
dependency group). `uv lock` resolves only releases at least 14 days
|
|
90
|
+
old, anthropic a day old (`[tool.uv]` in `pyproject.toml`).
|
|
91
|
+
|
|
92
|
+
## Optional `session_id` hint
|
|
93
|
+
|
|
94
|
+
By default `init()` auto-generates a fresh UUID every time the process
|
|
95
|
+
starts. Orchestrators that re-run the same logical workflow (Temporal,
|
|
96
|
+
Airflow, cron) can instead pass a stable identifier; if the backend
|
|
97
|
+
already has a row for that session, the new execution is attached to it
|
|
98
|
+
and appears as a continuation of the prior run in the fleet view.
|
|
99
|
+
|
|
100
|
+
Supply the hint via either the `session_id=` kwarg or the
|
|
101
|
+
`FLIGHTDECK_SESSION_ID` environment variable. The env var takes
|
|
102
|
+
precedence.
|
|
103
|
+
|
|
104
|
+
The value MUST parse as a canonical UUID (any version) -- the
|
|
105
|
+
sessions table column is UUID-typed. If you pass a non-UUID the
|
|
106
|
+
sensor logs a warning and falls back to auto-generating one.
|
|
107
|
+
Orchestrators that use string identifiers (Temporal workflow_id,
|
|
108
|
+
Airflow dag_run_id) should hash the identifier into a deterministic
|
|
109
|
+
UUID with `uuid.uuid5`.
|
|
110
|
+
|
|
111
|
+
### Temporal workflow example
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
import uuid
|
|
115
|
+
import flightdeck_sensor as fd
|
|
116
|
+
from temporalio import workflow
|
|
117
|
+
|
|
118
|
+
# Pick any fixed namespace UUID for your deployment. The same
|
|
119
|
+
# workflow_id + namespace always produces the same session UUID,
|
|
120
|
+
# so re-runs of the same workflow all map to the same sessions row.
|
|
121
|
+
FLIGHTDECK_NS = uuid.UUID("00000000-0000-0000-0000-000000000001")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@workflow.defn
|
|
125
|
+
class MyWorkflow:
|
|
126
|
+
@workflow.run
|
|
127
|
+
async def run(self, input):
|
|
128
|
+
ctx = workflow.info()
|
|
129
|
+
fd.init(
|
|
130
|
+
server="http://flightdeck.internal/ingest",
|
|
131
|
+
token="ftd_...",
|
|
132
|
+
session_id=str(uuid.uuid5(FLIGHTDECK_NS, ctx.workflow_id)),
|
|
133
|
+
)
|
|
134
|
+
# If this workflow_id has run before, the backend attaches
|
|
135
|
+
# this execution to the existing session automatically; the
|
|
136
|
+
# sensor logs INFO on the first response that confirms it.
|
|
137
|
+
...
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The sensor logs a single WARNING at `init()` time whenever a custom
|
|
141
|
+
`session_id` is in play so the behaviour is visible in operational
|
|
142
|
+
logs, and an INFO line on the first response where the backend
|
|
143
|
+
confirms attachment. See ARCHITECTURE.md ("Session attachment flow")
|
|
144
|
+
for the full protocol.
|
|
145
|
+
|
|
146
|
+
## Framework support
|
|
147
|
+
|
|
148
|
+
`flightdeck_sensor.patch()` installs six interceptors process-wide:
|
|
149
|
+
three LLM-call interceptors (class-level or module-level), one MCP
|
|
150
|
+
interceptor, and two sub-agent interceptors. Every LLM call that
|
|
151
|
+
flows through a patched entry point emits a `pre_call` / `post_call`
|
|
152
|
+
event pair without any framework-specific wiring; the MCP and
|
|
153
|
+
sub-agent interceptors emit their own event types, described in the
|
|
154
|
+
table.
|
|
155
|
+
|
|
156
|
+
| Framework | Interceptor | Entry points |
|
|
157
|
+
| --- | --- | --- |
|
|
158
|
+
| Anthropic SDK — every client class (`Anthropic`, `AnthropicBedrock`, `AnthropicVertex`, `AnthropicFoundry`, `AnthropicAWS`, `AnthropicBedrockMantle`, `AnthropicGoogleCloud` and their async forms), direct or via a framework that constructs one internally (LangChain, LlamaIndex, CrewAI native) | `patch_anthropic_classes` | `messages.create` (including `stream=True`), `.stream`, `.parse`, async + sync, beta resources, the raw view (`with_raw_response`) and the streaming view (`with_streaming_response`, metered at `parse()`); streamed calls are metered from the in-stream usage events; every `post_call` carries `provider_metadata.platform` (`anthropic`, `bedrock`, `bedrock-mantle`, `aws`, `vertex`, `google-cloud`, `foundry`, or a gateway's host) |
|
|
159
|
+
| OpenAI SDK (direct or via a framework that constructs `OpenAI()` internally — LangChain, LlamaIndex, CrewAI native) | `patch_openai_classes` | `OpenAI.chat.completions.create` / `.parse`, `.responses.create` / `.parse`, and their `with_raw_response` counterparts (`create` / `parse`), `.embeddings.create`, async + sync; `create(stream=True)` on chat and Responses is metered from the chat usage chunk and `response.completed`; the `.stream()` helpers and `with_streaming_response` are not intercepted |
|
|
160
|
+
| litellm (router that aggregates many providers behind one function surface) | `patch_litellm_functions` | `litellm.completion`, `litellm.acompletion`, `litellm.embedding`, `litellm.aembedding` |
|
|
161
|
+
| MCP (official Python `mcp` SDK, reached transitively by `langchain-mcp-adapters`, `llama-index-tools-mcp` and `mcpadapt`) | `patch_mcp_classes` | `mcp.client.session.ClientSession.initialize` on mcp 1.x / `.adopt` on mcp 2.x (server fingerprint capture, every 2.x handshake path), `.list_tools`, `.call_tool`, `.list_resources`, `.read_resource`, `.list_prompts`, `.get_prompt` (one `mcp_*` event type each), plus the stdio / SSE / streamable-HTTP / websocket transport client factories and mcp 2.x's `Client._build_session` for transport attribution. `call_tool` also enforces the MCP Protection Policy. Supported SDK range: `mcp>=1.20,<3`. |
|
|
162
|
+
| CrewAI (sub-agent sessions) | `patch_crewai_classes` | `crewai.Agent.execute_task` / `.aexecute_task` — each Crew member's turn lands as a child session (`session_start` / `session_end`) with `parent_session_id`, `agent_role` taken from `Agent.role`, and incoming / outgoing message capture gated on `capture_prompts` |
|
|
163
|
+
| LangGraph (sub-agent sessions) | `patch_langgraph_classes` | `langgraph.graph.StateGraph.add_node` — the registered node callable is wrapped so each node run lands as a child session with `agent_role` taken from the node name; every node is treated as agent-bearing unless narrowed with `init(langgraph_agent_node_pattern=...)` |
|
|
164
|
+
|
|
165
|
+
### litellm example
|
|
166
|
+
|
|
167
|
+
litellm's Anthropic route uses raw httpx instead of the Anthropic
|
|
168
|
+
SDK, so it bypasses the SDK-class patches. The litellm interceptor
|
|
169
|
+
patches `litellm.completion` / `litellm.acompletion` directly to
|
|
170
|
+
close this gap. An Anthropic model string via litellm exercises the
|
|
171
|
+
bypass case:
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
import flightdeck_sensor
|
|
175
|
+
import litellm
|
|
176
|
+
|
|
177
|
+
flightdeck_sensor.init(
|
|
178
|
+
server="http://flightdeck.internal/ingest",
|
|
179
|
+
token="ftd_...",
|
|
180
|
+
)
|
|
181
|
+
flightdeck_sensor.patch()
|
|
182
|
+
|
|
183
|
+
# After patch(), this call routes through the sensor's pre/post-call
|
|
184
|
+
# plumbing regardless of which underlying provider litellm picks for
|
|
185
|
+
# the model string.
|
|
186
|
+
response = litellm.completion(
|
|
187
|
+
model="claude-haiku-4-5-20251001",
|
|
188
|
+
messages=[{"role": "user", "content": "hi"}],
|
|
189
|
+
max_tokens=5,
|
|
190
|
+
)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Install the optional dependency: `pip install flightdeck-sensor[litellm]`
|
|
194
|
+
(the extra requires `litellm>=1.83.0`; 1.82.7 and 1.82.8 were compromised
|
|
195
|
+
builds).
|
|
196
|
+
|
|
197
|
+
### What the litellm interceptor catches, and what it doesn't
|
|
198
|
+
|
|
199
|
+
**Catches.** Direct callers of `litellm.completion(**kwargs)` and
|
|
200
|
+
`litellm.acompletion(**kwargs)` — the vast majority of litellm
|
|
201
|
+
integrations, including the default chat-completion surface exposed
|
|
202
|
+
by user code, Router, and frameworks that route through the public
|
|
203
|
+
API (CrewAI non-native flavors, langchain-community's litellm
|
|
204
|
+
adapter, etc.) — streaming included: `stream=True` on either returns
|
|
205
|
+
the sensor's stream guard around litellm's `CustomStreamWrapper`,
|
|
206
|
+
iterated the same way (`for` / `async for`), metered from the token
|
|
207
|
+
counts litellm reports in its trailing usage chunk; that chunk is
|
|
208
|
+
kept from a caller who did not ask for `stream_options.include_usage`.
|
|
209
|
+
A call litellm serves through the OpenAI SDK (which the sensor also
|
|
210
|
+
patches) is counted once, by the litellm wrapper. Embedding calls
|
|
211
|
+
through `litellm.embedding(**kwargs)` and `litellm.aembedding(**kwargs)`
|
|
212
|
+
are wrapped the same way, so embedding-model token usage lands too
|
|
213
|
+
(`12_litellm.py` in the playground exercises sync and async streaming,
|
|
214
|
+
the single-count assertion and the capture round-trip).
|
|
215
|
+
|
|
216
|
+
**Does NOT catch.**
|
|
217
|
+
- **Lower-level litellm entry points.** Some integrations reach
|
|
218
|
+
past `completion` into `litellm.llms.custom_httpx.http_handler`
|
|
219
|
+
or other internal helpers directly. Those calls bypass the
|
|
220
|
+
module-level wrapper. A broader httpx-level interceptor was
|
|
221
|
+
considered when the litellm interceptor was scoped and deferred —
|
|
222
|
+
if framework reports surface calls that slip past the current
|
|
223
|
+
coverage, file a new issue.
|
|
224
|
+
- **`litellm.text_completion` / `atext_completion` and
|
|
225
|
+
`litellm.responses` / `aresponses`.** Neither is wrapped (root
|
|
226
|
+
`README.md` Roadmap), but some routes are still reported. The legacy
|
|
227
|
+
completion API calls `completion` through the name bound in
|
|
228
|
+
`litellm.main`, which the module-attribute swap does not reach; on
|
|
229
|
+
an OpenAI chat model the OpenAI interceptor reports the SDK call it
|
|
230
|
+
makes, and litellm's own httpx routes are unobserved. The Responses
|
|
231
|
+
API on a provider without a native Responses API (for example
|
|
232
|
+
Anthropic) is bridged through the patched `litellm.completion` and reported
|
|
233
|
+
once as a chat call; native routes (OpenAI) go through litellm's own
|
|
234
|
+
httpx handler and are unobserved.
|
|
235
|
+
- **SDK calls made from inside a litellm call.** The litellm wrapper
|
|
236
|
+
marks the call it serves so the SDK interceptors pass it through;
|
|
237
|
+
the mark covers the whole call, so an SDK call made while it is in
|
|
238
|
+
flight by a litellm callback, guardrail or custom provider, or by a
|
|
239
|
+
task created inside the call, is neither policed nor metered.
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# flightdeck-sensor
|
|
2
|
+
|
|
3
|
+
In-process agent observability sensor for [Flightdeck](https://github.com/flightdeckhq/flightdeck).
|
|
4
|
+
|
|
5
|
+
## Local development venv
|
|
6
|
+
|
|
7
|
+
Flightdeck pins **Python 3.12** for local development. The sensor
|
|
8
|
+
supports `3.11 ≤ x < 3.15` (pyproject `requires-python`); `sensor/.venv`
|
|
9
|
+
is the canonical interpreter for every Make target that runs Python.
|
|
10
|
+
|
|
11
|
+
Recreate from a fresh clone or after a `rm -rf sensor/.venv`:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
python3.12 -m venv sensor/.venv
|
|
15
|
+
./sensor/.venv/bin/python -m pip install -e ".[dev,anthropic,openai]"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`make -C sensor install` does the same once the venv exists. To get
|
|
19
|
+
the exact versions CI tests with, install from the lockfile instead:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cd sensor && uv sync --locked --python 3.12 --extra dev --extra anthropic --extra openai
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
CI installs from `sensor/uv.lock` into the `actions/setup-python`
|
|
26
|
+
interpreter and overrides via env (`PYTHON=python make -C sensor
|
|
27
|
+
test`). The lock covers Python 3.11 to 3.14 and both CI lanes: the
|
|
28
|
+
framework lane (`dev` extra) and the newest-SDK lane (`sdk-majors`
|
|
29
|
+
dependency group). `uv lock` resolves only releases at least 14 days
|
|
30
|
+
old, anthropic a day old (`[tool.uv]` in `pyproject.toml`).
|
|
31
|
+
|
|
32
|
+
## Optional `session_id` hint
|
|
33
|
+
|
|
34
|
+
By default `init()` auto-generates a fresh UUID every time the process
|
|
35
|
+
starts. Orchestrators that re-run the same logical workflow (Temporal,
|
|
36
|
+
Airflow, cron) can instead pass a stable identifier; if the backend
|
|
37
|
+
already has a row for that session, the new execution is attached to it
|
|
38
|
+
and appears as a continuation of the prior run in the fleet view.
|
|
39
|
+
|
|
40
|
+
Supply the hint via either the `session_id=` kwarg or the
|
|
41
|
+
`FLIGHTDECK_SESSION_ID` environment variable. The env var takes
|
|
42
|
+
precedence.
|
|
43
|
+
|
|
44
|
+
The value MUST parse as a canonical UUID (any version) -- the
|
|
45
|
+
sessions table column is UUID-typed. If you pass a non-UUID the
|
|
46
|
+
sensor logs a warning and falls back to auto-generating one.
|
|
47
|
+
Orchestrators that use string identifiers (Temporal workflow_id,
|
|
48
|
+
Airflow dag_run_id) should hash the identifier into a deterministic
|
|
49
|
+
UUID with `uuid.uuid5`.
|
|
50
|
+
|
|
51
|
+
### Temporal workflow example
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import uuid
|
|
55
|
+
import flightdeck_sensor as fd
|
|
56
|
+
from temporalio import workflow
|
|
57
|
+
|
|
58
|
+
# Pick any fixed namespace UUID for your deployment. The same
|
|
59
|
+
# workflow_id + namespace always produces the same session UUID,
|
|
60
|
+
# so re-runs of the same workflow all map to the same sessions row.
|
|
61
|
+
FLIGHTDECK_NS = uuid.UUID("00000000-0000-0000-0000-000000000001")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@workflow.defn
|
|
65
|
+
class MyWorkflow:
|
|
66
|
+
@workflow.run
|
|
67
|
+
async def run(self, input):
|
|
68
|
+
ctx = workflow.info()
|
|
69
|
+
fd.init(
|
|
70
|
+
server="http://flightdeck.internal/ingest",
|
|
71
|
+
token="ftd_...",
|
|
72
|
+
session_id=str(uuid.uuid5(FLIGHTDECK_NS, ctx.workflow_id)),
|
|
73
|
+
)
|
|
74
|
+
# If this workflow_id has run before, the backend attaches
|
|
75
|
+
# this execution to the existing session automatically; the
|
|
76
|
+
# sensor logs INFO on the first response that confirms it.
|
|
77
|
+
...
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The sensor logs a single WARNING at `init()` time whenever a custom
|
|
81
|
+
`session_id` is in play so the behaviour is visible in operational
|
|
82
|
+
logs, and an INFO line on the first response where the backend
|
|
83
|
+
confirms attachment. See ARCHITECTURE.md ("Session attachment flow")
|
|
84
|
+
for the full protocol.
|
|
85
|
+
|
|
86
|
+
## Framework support
|
|
87
|
+
|
|
88
|
+
`flightdeck_sensor.patch()` installs six interceptors process-wide:
|
|
89
|
+
three LLM-call interceptors (class-level or module-level), one MCP
|
|
90
|
+
interceptor, and two sub-agent interceptors. Every LLM call that
|
|
91
|
+
flows through a patched entry point emits a `pre_call` / `post_call`
|
|
92
|
+
event pair without any framework-specific wiring; the MCP and
|
|
93
|
+
sub-agent interceptors emit their own event types, described in the
|
|
94
|
+
table.
|
|
95
|
+
|
|
96
|
+
| Framework | Interceptor | Entry points |
|
|
97
|
+
| --- | --- | --- |
|
|
98
|
+
| Anthropic SDK — every client class (`Anthropic`, `AnthropicBedrock`, `AnthropicVertex`, `AnthropicFoundry`, `AnthropicAWS`, `AnthropicBedrockMantle`, `AnthropicGoogleCloud` and their async forms), direct or via a framework that constructs one internally (LangChain, LlamaIndex, CrewAI native) | `patch_anthropic_classes` | `messages.create` (including `stream=True`), `.stream`, `.parse`, async + sync, beta resources, the raw view (`with_raw_response`) and the streaming view (`with_streaming_response`, metered at `parse()`); streamed calls are metered from the in-stream usage events; every `post_call` carries `provider_metadata.platform` (`anthropic`, `bedrock`, `bedrock-mantle`, `aws`, `vertex`, `google-cloud`, `foundry`, or a gateway's host) |
|
|
99
|
+
| OpenAI SDK (direct or via a framework that constructs `OpenAI()` internally — LangChain, LlamaIndex, CrewAI native) | `patch_openai_classes` | `OpenAI.chat.completions.create` / `.parse`, `.responses.create` / `.parse`, and their `with_raw_response` counterparts (`create` / `parse`), `.embeddings.create`, async + sync; `create(stream=True)` on chat and Responses is metered from the chat usage chunk and `response.completed`; the `.stream()` helpers and `with_streaming_response` are not intercepted |
|
|
100
|
+
| litellm (router that aggregates many providers behind one function surface) | `patch_litellm_functions` | `litellm.completion`, `litellm.acompletion`, `litellm.embedding`, `litellm.aembedding` |
|
|
101
|
+
| MCP (official Python `mcp` SDK, reached transitively by `langchain-mcp-adapters`, `llama-index-tools-mcp` and `mcpadapt`) | `patch_mcp_classes` | `mcp.client.session.ClientSession.initialize` on mcp 1.x / `.adopt` on mcp 2.x (server fingerprint capture, every 2.x handshake path), `.list_tools`, `.call_tool`, `.list_resources`, `.read_resource`, `.list_prompts`, `.get_prompt` (one `mcp_*` event type each), plus the stdio / SSE / streamable-HTTP / websocket transport client factories and mcp 2.x's `Client._build_session` for transport attribution. `call_tool` also enforces the MCP Protection Policy. Supported SDK range: `mcp>=1.20,<3`. |
|
|
102
|
+
| CrewAI (sub-agent sessions) | `patch_crewai_classes` | `crewai.Agent.execute_task` / `.aexecute_task` — each Crew member's turn lands as a child session (`session_start` / `session_end`) with `parent_session_id`, `agent_role` taken from `Agent.role`, and incoming / outgoing message capture gated on `capture_prompts` |
|
|
103
|
+
| LangGraph (sub-agent sessions) | `patch_langgraph_classes` | `langgraph.graph.StateGraph.add_node` — the registered node callable is wrapped so each node run lands as a child session with `agent_role` taken from the node name; every node is treated as agent-bearing unless narrowed with `init(langgraph_agent_node_pattern=...)` |
|
|
104
|
+
|
|
105
|
+
### litellm example
|
|
106
|
+
|
|
107
|
+
litellm's Anthropic route uses raw httpx instead of the Anthropic
|
|
108
|
+
SDK, so it bypasses the SDK-class patches. The litellm interceptor
|
|
109
|
+
patches `litellm.completion` / `litellm.acompletion` directly to
|
|
110
|
+
close this gap. An Anthropic model string via litellm exercises the
|
|
111
|
+
bypass case:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
import flightdeck_sensor
|
|
115
|
+
import litellm
|
|
116
|
+
|
|
117
|
+
flightdeck_sensor.init(
|
|
118
|
+
server="http://flightdeck.internal/ingest",
|
|
119
|
+
token="ftd_...",
|
|
120
|
+
)
|
|
121
|
+
flightdeck_sensor.patch()
|
|
122
|
+
|
|
123
|
+
# After patch(), this call routes through the sensor's pre/post-call
|
|
124
|
+
# plumbing regardless of which underlying provider litellm picks for
|
|
125
|
+
# the model string.
|
|
126
|
+
response = litellm.completion(
|
|
127
|
+
model="claude-haiku-4-5-20251001",
|
|
128
|
+
messages=[{"role": "user", "content": "hi"}],
|
|
129
|
+
max_tokens=5,
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Install the optional dependency: `pip install flightdeck-sensor[litellm]`
|
|
134
|
+
(the extra requires `litellm>=1.83.0`; 1.82.7 and 1.82.8 were compromised
|
|
135
|
+
builds).
|
|
136
|
+
|
|
137
|
+
### What the litellm interceptor catches, and what it doesn't
|
|
138
|
+
|
|
139
|
+
**Catches.** Direct callers of `litellm.completion(**kwargs)` and
|
|
140
|
+
`litellm.acompletion(**kwargs)` — the vast majority of litellm
|
|
141
|
+
integrations, including the default chat-completion surface exposed
|
|
142
|
+
by user code, Router, and frameworks that route through the public
|
|
143
|
+
API (CrewAI non-native flavors, langchain-community's litellm
|
|
144
|
+
adapter, etc.) — streaming included: `stream=True` on either returns
|
|
145
|
+
the sensor's stream guard around litellm's `CustomStreamWrapper`,
|
|
146
|
+
iterated the same way (`for` / `async for`), metered from the token
|
|
147
|
+
counts litellm reports in its trailing usage chunk; that chunk is
|
|
148
|
+
kept from a caller who did not ask for `stream_options.include_usage`.
|
|
149
|
+
A call litellm serves through the OpenAI SDK (which the sensor also
|
|
150
|
+
patches) is counted once, by the litellm wrapper. Embedding calls
|
|
151
|
+
through `litellm.embedding(**kwargs)` and `litellm.aembedding(**kwargs)`
|
|
152
|
+
are wrapped the same way, so embedding-model token usage lands too
|
|
153
|
+
(`12_litellm.py` in the playground exercises sync and async streaming,
|
|
154
|
+
the single-count assertion and the capture round-trip).
|
|
155
|
+
|
|
156
|
+
**Does NOT catch.**
|
|
157
|
+
- **Lower-level litellm entry points.** Some integrations reach
|
|
158
|
+
past `completion` into `litellm.llms.custom_httpx.http_handler`
|
|
159
|
+
or other internal helpers directly. Those calls bypass the
|
|
160
|
+
module-level wrapper. A broader httpx-level interceptor was
|
|
161
|
+
considered when the litellm interceptor was scoped and deferred —
|
|
162
|
+
if framework reports surface calls that slip past the current
|
|
163
|
+
coverage, file a new issue.
|
|
164
|
+
- **`litellm.text_completion` / `atext_completion` and
|
|
165
|
+
`litellm.responses` / `aresponses`.** Neither is wrapped (root
|
|
166
|
+
`README.md` Roadmap), but some routes are still reported. The legacy
|
|
167
|
+
completion API calls `completion` through the name bound in
|
|
168
|
+
`litellm.main`, which the module-attribute swap does not reach; on
|
|
169
|
+
an OpenAI chat model the OpenAI interceptor reports the SDK call it
|
|
170
|
+
makes, and litellm's own httpx routes are unobserved. The Responses
|
|
171
|
+
API on a provider without a native Responses API (for example
|
|
172
|
+
Anthropic) is bridged through the patched `litellm.completion` and reported
|
|
173
|
+
once as a chat call; native routes (OpenAI) go through litellm's own
|
|
174
|
+
httpx handler and are unobserved.
|
|
175
|
+
- **SDK calls made from inside a litellm call.** The litellm wrapper
|
|
176
|
+
marks the call it serves so the SDK interceptors pass it through;
|
|
177
|
+
the mark covers the whole call, so an SDK call made while it is in
|
|
178
|
+
flight by a litellm callback, guardrail or custom provider, or by a
|
|
179
|
+
task created inside the call, is neither policed nor metered.
|