Commit cac2739
authored
refactor(core): deprecate raising_dispatch (#17941)
## Description
Deprecates and removes `core.raising_dispatch` in favor of an opt-in `allow_raise=True` flag on the existing `core.dispatch` API. Tightens the in-tree blocking-exception hierarchy and aligns LangChain non-stream wrappers to tag the LLM span on AI Guard block.
### What changed
**1. `core.dispatch` gains `allow_raise=True`**
`ddtrace/internal/core/event_hub.py` — `dispatch()` and `dispatch_event()` accept a keyword-only `allow_raise: bool = False`. When set, listener `Exception`s propagate to the caller (first raiser wins; subsequent listeners are skipped). `BaseException`-derived exceptions (including `DDBlockException`) always propagate regardless of the flag — matching the existing default-deny semantics for blocks.
This collapses two near-duplicate dispatch entry points into one and lets call sites be explicit about whether listener exceptions should surface.
**2. `core.raising_dispatch` removed**
The symbol is removed from `ddtrace.internal.core` and `event_hub.py`. All in-tree call sites are migrated:
- `ddtrace/contrib/internal/langchain/patch.py` — six `before` events (chat/llm × generate/agenerate/stream).
- `ddtrace/contrib/internal/openai/patch.py` — four sites (sync `before`/`after`, async `before`/`after`).
The `.sg` rule and snapshot tests for `core-raising-dispatch` are removed since the symbol no longer exists.
**3. New `DDBlockException` base class**
`ddtrace/internal/_exceptions.py` introduces `DDBlockException(BaseException)` as the umbrella for any in-tree decision to abort the current operation (web-request blocking, AI Guard policy abort, future product blocks). `BlockingException` (ASM) and `AIGuardAbortError` (AI Guard) now derive from it.
This gives integrations a single, semantically-correct catch target for "the platform decided to block this call" — distinct from arbitrary user/library `Exception`s. It still inherits from `BaseException` so a generic `except Exception:` cannot silently swallow a block decision.
**4. LangChain non-stream wrappers: tag the LLM span on block**
The four non-stream wrappers (`traced_llm_generate`, `traced_llm_agenerate`, `traced_chat_model_generate`, `traced_chat_model_agenerate`) now have an explicit `except DDBlockException:` arm that calls `span.set_exc_info(*sys.exc_info())` before re-raising, in addition to the existing `except Exception:` arm.
Required because `AIGuardAbortError` derives from `BaseException` (via `DDBlockException`) and is therefore *not* caught by `except Exception:`. Without the explicit arm, blocked LangChain calls would emit an LLMObs span with no error info — leaving a hole between the AI Guard span (block decision) and the LLM span (no link back to the abort). Aligns LangChain's non-stream behavior with the contract from #17913 for openai (`_patched_endpoint` uses `except BaseException as e: err = e; raise`, then `_traced_endpoint`'s `finally` calls `set_exc_info`).
**5. `OpenAIAIGuardAbortError`: documented catchability asymmetry**
The compound class inherits from both `openai.UnprocessableEntityError` (`Exception`-derived) and `AIGuardAbortError` (now `BaseException`-derived). Via MRO it remains `Exception`-derived — so `except openai.APIError:` blocks still work for users migrating from non-AI-Guard error handling. The asymmetry vs plain `AIGuardAbortError` is now spelled out in an AIDEV-NOTE on the class. Code that wants uniform block detection across providers should branch on `isinstance(e, AIGuardAbortError)`.
## Testing
- `tests/internal/test_context_events_api.py` — new coverage for `dispatch(..., allow_raise=True)` semantics: `Exception`s propagate when set, are swallowed when not, listeners short-circuit on first raise, `BaseException`-derived listener exceptions (`DDBlockException` subclasses) always propagate regardless of the flag, the `dispatch_event` variant behaves identically, and the new exception inheritance is asserted.
- `tests/appsec/ai_guard/langchain/test_langchain.py` — two new tests pin the `set_exc_info`-on-block contract on the LLM span (sync `chat.invoke` + async `chat.ainvoke`). A regression that swaps the explicit `except DDBlockException` arm back to `except Exception` would surface immediately.
- Local runs:
- `appsec::ai_guard_langchain` venv `5484ca0` (Python 3.13) — passes.
- `appsec::ai_guard_openai` venv `1224d93` (Python 3.12) — passes (this was the suite that initially exposed the missing openai-contrib migration).
- `hatch run lint:fmt` clean on all modified files.
## Risks
- **`AIGuardAbortError` inheritance change is user-visible.** It now derives from `DDBlockException(BaseException)` instead of `Exception`. User code today doing `try: model.invoke(...) except Exception: ...` to handle aborts **will silently stop catching them**. This is intentional (the `BaseException` derivation is what prevents accidental swallowing of blocks) but it does break code that relied on the prior behavior. Spelled out in the release note's `upgrade` section. The OpenAI-compatible variant (`OpenAIAIGuardAbortError`) remains catchable by `except Exception:` via MRO for SDK compatibility.
- **`core.raising_dispatch` removal.** Internal helper under `ddtrace.internal.core` — not part of the public API. All in-tree call sites are migrated in this PR. The release note's `deprecations` section documents the migration path for any out-of-tree consumer that imported it.
- **Trace shape change on blocked LangChain non-stream calls.** The LLM span is now finished with `error == 1` and `error.type` containing `AIGuardAbortError`. Downstream consumers that filter LLM spans by `error == 0` will see blocked LangChain calls as errored — desired observability change per #17913 but worth flagging.
- **No public API breakage on `core.dispatch`.** `allow_raise` is keyword-only, defaults to `False`, preserves existing non-raising behavior for every existing caller.
## Additional Notes
Release note: `releasenotes/notes/aiguard-abort-baseexception-deprecate-raising-dispatch-3a5c7f61ee4d2e1b.yaml` covers the `AIGuardAbortError` inheritance change (`upgrade` section) and the `raising_dispatch` removal (`deprecations` section).
Co-authored-by: alberto.vara <alberto.vara@datadoghq.com>1 parent bb65de3 commit cac2739
17 files changed
Lines changed: 264 additions & 261 deletions
File tree
- .github/workflows
- .sg
- rules
- tests
- __snapshots__
- ddtrace
- appsec
- _ai_guard
- ai_guard
- contrib/internal
- langchain
- openai
- internal
- core
- releasenotes/notes
- tests
- appsec/ai_guard
- langchain
- openai
- internal
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
95 | 95 | | |
96 | 96 | | |
97 | 97 | | |
98 | | - | |
| 98 | + | |
99 | 99 | | |
100 | 100 | | |
101 | 101 | | |
| |||
148 | 148 | | |
149 | 149 | | |
150 | 150 | | |
151 | | - | |
| 151 | + | |
152 | 152 | | |
153 | 153 | | |
154 | 154 | | |
| |||
458 | 458 | | |
459 | 459 | | |
460 | 460 | | |
461 | | - | |
| 461 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
19 | | - | |
| 19 | + | |
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
| |||
This file was deleted.
Lines changed: 0 additions & 108 deletions
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
224 | 224 | | |
225 | 225 | | |
226 | 226 | | |
227 | | - | |
228 | | - | |
| 227 | + | |
| 228 | + | |
229 | 229 | | |
230 | 230 | | |
231 | 231 | | |
| |||
247 | 247 | | |
248 | 248 | | |
249 | 249 | | |
250 | | - | |
| 250 | + | |
251 | 251 | | |
252 | | - | |
253 | | - | |
254 | | - | |
255 | | - | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
256 | 256 | | |
257 | 257 | | |
258 | 258 | | |
259 | 259 | | |
260 | 260 | | |
261 | 261 | | |
262 | 262 | | |
263 | | - | |
264 | | - | |
| 263 | + | |
| 264 | + | |
265 | 265 | | |
266 | 266 | | |
267 | 267 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
66 | 66 | | |
67 | 67 | | |
68 | 68 | | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
69 | 82 | | |
70 | 83 | | |
71 | 84 | | |
| |||
285 | 298 | | |
286 | 299 | | |
287 | 300 | | |
288 | | - | |
| 301 | + | |
289 | 302 | | |
290 | 303 | | |
291 | 304 | | |
| |||
298 | 311 | | |
299 | 312 | | |
300 | 313 | | |
301 | | - | |
| 314 | + | |
302 | 315 | | |
303 | 316 | | |
304 | 317 | | |
| |||
316 | 329 | | |
317 | 330 | | |
318 | 331 | | |
319 | | - | |
| 332 | + | |
320 | 333 | | |
321 | 334 | | |
322 | 335 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
| 17 | + | |
17 | 18 | | |
18 | 19 | | |
19 | 20 | | |
| |||
94 | 95 | | |
95 | 96 | | |
96 | 97 | | |
97 | | - | |
98 | | - | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
99 | 105 | | |
100 | 106 | | |
101 | 107 | | |
| |||
0 commit comments