aeonic-agentguard-sdk-python 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,770 @@
1
+ Metadata-Version: 2.4
2
+ Name: aeonic-agentguard-sdk-python
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for Aeonic — runtime monitoring and governance for AI agents
5
+ Author-email: Aeonic <support@aeonic.ai>
6
+ License: MIT
7
+ Project-URL: Homepage, http://45.79.111.106:3290/doc/sdk/python
8
+ Project-URL: Documentation, http://45.79.111.106:3290/doc/sdk/python
9
+ Project-URL: Repository, https://github.com/aeonic/aeonic-sdk-python
10
+ Project-URL: Issues, https://github.com/aeonic/aeonic-sdk-python/issues
11
+ Keywords: ai,agent,agentic-ai,sdk,monitoring,governance,llm,fastapi,django,middleware
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Operating System :: OS Independent
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: requests>=2.25.0
23
+ Provides-Extra: fastapi
24
+ Requires-Dist: fastapi>=0.68.0; extra == "fastapi"
25
+ Provides-Extra: django
26
+ Requires-Dist: django>=3.2.0; extra == "django"
27
+ Provides-Extra: dev
28
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
29
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
30
+ Requires-Dist: types-requests>=2.31.0.0; extra == "dev"
31
+ Requires-Dist: python-dotenv>=1.0.0; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # Aeonic Python SDK
35
+
36
+ Aeonic Python SDK enables **runtime monitoring, drift detection, and governance**
37
+ for AI agents running inside Python applications.
38
+
39
+ It works by **observing agent-powered routes at runtime**, capturing
40
+ request/response samples, and securely sending them to the Aeonic platform
41
+ for analysis.
42
+
43
+ The SDK is:
44
+ - Framework-aware
45
+ - Declarative
46
+ - Non-intrusive
47
+ - Production-safe
48
+
49
+ ---
50
+
51
+ ## ✨ Supported Frameworks
52
+
53
+ - **FastAPI**
54
+ - **Django**
55
+
56
+ > Only routes explicitly marked as **agent routes** are tracked.
57
+ > All other routes are completely ignored by the SDK.
58
+
59
+ ---
60
+
61
+ ## 📦 Installation
62
+
63
+ Install the base SDK (core only — `requests` is included):
64
+
65
+ ```bash
66
+ pip install aeonic-sdk-python
67
+ ```
68
+
69
+ Install with your framework (recommended):
70
+
71
+ ```bash
72
+ # FastAPI
73
+ pip install aeonic-sdk-python[fastapi]
74
+
75
+ # Django
76
+ pip install aeonic-sdk-python[django]
77
+ ```
78
+
79
+ > **Note:** The PyPI package name is `aeonic-sdk-python`, but you import it as `aeonic`.
80
+
81
+ Check the installed version:
82
+
83
+ ```python
84
+ import aeonic
85
+ print(aeonic.__version__)
86
+ ```
87
+
88
+ ---
89
+
90
+ ## 📋 Requirements
91
+
92
+ | Requirement | Details |
93
+ |---|---|
94
+ | Python | `>=3.9` |
95
+ | Core dependency | `requests>=2.25.0` (installed automatically) |
96
+ | FastAPI (optional) | `fastapi>=0.68.0` — install via `[fastapi]` extra |
97
+ | Django (optional) | `django>=3.2.0` — install via `[django]` extra |
98
+
99
+ ### Environment variables (optional)
100
+
101
+ | Variable | Purpose |
102
+ |---|---|
103
+ | `AEONIC_ENDPOINT` | Aeonic platform endpoint URL (provided in your Aeonic account or onboarding) |
104
+ | `AEONIC_DEBUG` | Set to any value to enable debug logging |
105
+ | `ORIGIN` / `BASE_URL` | Your application's public URL, sent with sample payloads |
106
+ | `PORT` / `HOST` | Used to auto-detect your app origin when `ORIGIN` is not set |
107
+
108
+ ---
109
+
110
+ ## 🚀 Quick Start (FastAPI)
111
+
112
+ ```python
113
+ from fastapi import Depends, FastAPI
114
+ from aeonic import init
115
+ from aeonic.adapters.fastapi import agent_middleware, with_agent
116
+
117
+ app = FastAPI()
118
+ app.add_middleware(agent_middleware)
119
+
120
+ init({
121
+ "api_key": "your_api_key",
122
+ "app": app,
123
+ "service_name": "my-service",
124
+ })
125
+
126
+ @app.post(
127
+ "/agent/chat",
128
+ dependencies=[Depends(with_agent({"name": "ChatAgent", "type": "support", "model": ["gpt-4o"]}))],
129
+ )
130
+ async def chat_agent(payload: dict):
131
+ return {"reply": "Hello!"}
132
+ ```
133
+
134
+ Run with:
135
+
136
+ ```bash
137
+ uvicorn main:app --reload
138
+ ```
139
+
140
+ ---
141
+
142
+ ## 🔑 SDK Initialization (Required)
143
+
144
+ Initialize the SDK **once at application startup**.
145
+
146
+ ### Configuration Options
147
+
148
+ ```python
149
+ init({
150
+ "api_key": str, # REQUIRED
151
+ "app": Any | None, # Optional: FastAPI/Django app for route introspection
152
+ "service_name": str | None, # Optional (recommended)
153
+ "introspection_delay_ms": int, # Optional: Delay before route scan (default: 2000ms)
154
+ "capture_payloads": {
155
+ "max_samples": int # Default: 10
156
+ }
157
+ })
158
+ ```
159
+
160
+ ### Example (Basic)
161
+
162
+ ```python
163
+ from aeonic import init
164
+
165
+ init({
166
+ "api_key": "ag_test_123",
167
+ "service_name": "payments-api",
168
+ "capture_payloads": {
169
+ "max_samples": 10
170
+ }
171
+ })
172
+ ```
173
+
174
+ ### Example (With Route Introspection) ⭐ NEW
175
+
176
+ ```python
177
+ from aeonic import init
178
+ from fastapi import FastAPI
179
+
180
+ app = FastAPI()
181
+
182
+ # Pass app instance for automatic route discovery
183
+ init({
184
+ "api_key": "ag_test_123",
185
+ "app": app, # ← Enables automatic agent discovery at startup
186
+ "service_name": "payments-api",
187
+ "introspection_delay_ms": 2000, # Wait 2s for routes to be defined
188
+ "capture_payloads": {
189
+ "max_samples": 10
190
+ }
191
+ })
192
+ ```
193
+
194
+ ### What this does
195
+
196
+ * Authenticates your service with Aeonic
197
+ * Automatically resolves tenant identity via `api_key`
198
+ * Sets payload sampling limits per agent route
199
+ * **NEW:** Discovers and registers all agent routes at startup (if `app` is provided)
200
+ * **NEW:** Emits complete agent inventory to the Aeonic platform before serving traffic
201
+
202
+ > **📖 Route introspection:** Pass your `app` instance to `init()` so the SDK can discover agent routes automatically at startup.
203
+
204
+ ---
205
+
206
+ # 🚀 FastAPI Integration
207
+
208
+ Aeonic uses **two components** in FastAPI:
209
+
210
+ 1. A **global middleware** (registered once)
211
+ 2. A **route-level dependency** to mark agent routes
212
+
213
+ ---
214
+
215
+ ## 1️⃣ Register Global Middleware
216
+
217
+ ```python
218
+ from fastapi import FastAPI
219
+ from aeonic.adapters.fastapi import agent_middleware
220
+
221
+ app = FastAPI()
222
+
223
+ # 🔴 MUST be registered once
224
+ app.add_middleware(agent_middleware)
225
+ ```
226
+
227
+ This middleware:
228
+
229
+ * Hooks into the response lifecycle
230
+ * Does **nothing** unless the route is marked as an agent route
231
+
232
+ ---
233
+
234
+ ## 2️⃣ Mark Agent Routes (Required)
235
+
236
+ Only routes using `with_agent()` are tracked.
237
+
238
+ ```python
239
+ from fastapi import Depends
240
+ from aeonic.adapters.fastapi import with_agent
241
+
242
+ @app.post(
243
+ "/agent/finance",
244
+ dependencies=[
245
+ Depends(
246
+ with_agent({
247
+ "name": "RefundRiskAgent",
248
+ "type": "finance",
249
+ "model": ["gpt-4o"]
250
+ })
251
+ )
252
+ ]
253
+ )
254
+ async def finance_agent(payload: dict):
255
+ return {
256
+ "approved": True,
257
+ "amount": payload["amount"]
258
+ }
259
+ ```
260
+
261
+ ### What happens internally
262
+
263
+ * Request + response payloads are captured
264
+ * Samples are buffered per agent route
265
+ * Once `max_samples` is reached, data is sent to the Aeonic platform
266
+ * Agent health status is evaluated (healthy / warning / drift)
267
+
268
+ ---
269
+
270
+ ## ✅ FastAPI Summary
271
+
272
+ | Feature | Behavior |
273
+ | -------------------------- | -------- |
274
+ | Only marked routes tracked | ✅ |
275
+ | Async safe | ✅ |
276
+ | Middleware-based | ✅ |
277
+ | No contextvars hacks | ✅ |
278
+ | Production ready | ✅ |
279
+
280
+ ---
281
+
282
+ # 🏛️ Django Integration
283
+
284
+ Django uses:
285
+
286
+ * A **global middleware**
287
+ * A **route decorator** to mark agent routes
288
+
289
+ ---
290
+
291
+ ## 1️⃣ Register Middleware
292
+
293
+ ### `settings.py`
294
+
295
+ ```python
296
+ MIDDLEWARE = [
297
+ ...
298
+ "aeonic.adapters.django.agent_middleware",
299
+ ]
300
+ ```
301
+
302
+ > Ensure `init()` is called during startup (e.g., in `settings.py` or `wsgi.py`).
303
+
304
+ ---
305
+
306
+ ## 2️⃣ Mark Agent Routes with Decorator
307
+
308
+ ```python
309
+ from django.http import JsonResponse
310
+ from aeonic.adapters.django import with_agent
311
+
312
+ @with_agent({
313
+ "name": "RefundRiskAgent",
314
+ "type": "finance",
315
+ "model": ["gpt-4o"]
316
+ })
317
+ def finance_agent(request):
318
+ return JsonResponse({
319
+ "approved": True,
320
+ "amount": 500
321
+ })
322
+ ```
323
+
324
+ ---
325
+
326
+ ## ✅ Django Summary
327
+
328
+ | Feature | Behavior |
329
+ | ------------------------- | -------- |
330
+ | Declarative agent marking | ✅ |
331
+ | Middleware-based capture | ✅ |
332
+ | Sync-safe | ✅ |
333
+ | No framework coupling | ✅ |
334
+ | Production ready | ✅ |
335
+
336
+ ---
337
+
338
+ # 🧠 Agent Concepts
339
+
340
+ ### Agent Identity
341
+
342
+ Each agent route is identified by:
343
+
344
+ * `name` – logical agent name
345
+ * `type` – business domain (finance, healthcare, etc.)
346
+ * `model` – AI models used
347
+
348
+ ### Route → Agent Mapping
349
+
350
+ Aeonic correlates:
351
+
352
+ ```
353
+ HTTP Route + Method + Agent Metadata
354
+ ```
355
+
356
+ This enables:
357
+
358
+ * Drift detection
359
+ * Agent behavior consistency checks
360
+ * Health classification per route
361
+
362
+ ---
363
+
364
+ # 🔐 Security & Privacy
365
+
366
+ * Payloads are **sampled**, not streamed
367
+ * Only up to `max_samples` are collected per flush
368
+ * SDK never blocks or alters application behavior
369
+ * Non-agent routes are never inspected
370
+
371
+ ---
372
+
373
+ # 📊 Agent Status Lifecycle
374
+
375
+ The Aeonic platform classifies agents as:
376
+
377
+ * **Healthy**
378
+ * **Warning**
379
+ * **Drifting**
380
+ * **Failed**
381
+
382
+ Status updates are visible in the Aeonic dashboard.
383
+
384
+ ---
385
+
386
+ # ⚠️ Important Rules
387
+
388
+ * ❌ Do NOT wrap business logic in SDK functions
389
+ * ❌ Do NOT rely on thread-local or contextvar hacks
390
+ * ✅ Always mark agent routes explicitly
391
+ * ✅ Initialize SDK before app starts
392
+
393
+ ---
394
+
395
+ # 🧩 Advanced Use Cases
396
+
397
+ * Background workers
398
+ * Batch inference jobs
399
+ * Non-HTTP agents
400
+
401
+ (Contact the Aeonic team for advanced SDK APIs.)
402
+
403
+ ---
404
+
405
+ # 🔧 Troubleshooting
406
+
407
+ ### `RuntimeError: Aeonic SDK not initialized`
408
+
409
+ Call `init({...})` once at application startup before any SDK features are used.
410
+
411
+ ### Samples are not being captured
412
+
413
+ 1. Ensure the route is marked with `with_agent()` (FastAPI dependency or Django decorator).
414
+ 2. Ensure global middleware is registered (`agent_middleware`).
415
+ 3. Confirm the agent is not `blocked` or `quarantined` (check Aeonic dashboard).
416
+ 4. Set `AEONIC_DEBUG=1` to see SDK debug output.
417
+
418
+ ### `ModuleNotFoundError: No module named 'fastapi'` or `'django'`
419
+
420
+ Install the matching framework extra:
421
+
422
+ ```bash
423
+ pip install aeonic-sdk-python[fastapi]
424
+ # or
425
+ pip install aeonic-sdk-python[django]
426
+ ```
427
+
428
+ ### Agent inventory not emitted at startup
429
+
430
+ - For **FastAPI**, pass `"app": app` to `init()`.
431
+ - For **Django**, ensure `init()` runs during startup and routes are registered before introspection completes.
432
+ - Increase `introspection_delay_ms` if routes are registered late (e.g. `3000` or `5000`).
433
+
434
+ ### Platform connection issues
435
+
436
+ Set your Aeonic endpoint URL (provided during onboarding):
437
+
438
+ ```bash
439
+ export AEONIC_ENDPOINT=https://your-aeonic-endpoint
440
+ ```
441
+
442
+ On Windows PowerShell:
443
+
444
+ ```powershell
445
+ $env:AEONIC_ENDPOINT="https://your-aeonic-endpoint"
446
+ ```
447
+
448
+ ---
449
+
450
+ # 📄 License
451
+
452
+ MIT © Aeonic
453
+
454
+ ---
455
+
456
+ ## 📚 SDK Function Reference
457
+
458
+ This section documents the public SDK functions and middleware for integrating Aeonic into your application.
459
+
460
+ ---
461
+
462
+ ### `aeonic.init(config: dict)`
463
+
464
+ **Purpose**
465
+
466
+ Initializes the Aeonic SDK once at application startup, validates and normalizes configuration, kicks off background route introspection, and (optionally) registers the process as an AgentGuard instance.
467
+
468
+ **Arguments**
469
+
470
+ - **`config`** (`dict`) – configuration object with the following keys:
471
+ - **`api_key`** (`str`, required): Your Aeonic API key. If missing, `init` raises `ValueError`.
472
+ - **`app`** (`FastAPI | Django | None`, optional): Application instance used for automatic route introspection and agent discovery at startup. Required for FastAPI introspection; optional for Django (Django can be introspected without `app`).
473
+ - **`service_name`** (`str | None`, optional): Logical name for the service (e.g. `"payments-api"`). Propagated to all emitted events and inventories.
474
+ - **`capture_payloads`** (`dict`, optional): Controls how request/response payload samples are collected.
475
+ - **`max_samples`** (`int`, optional): Number of samples to buffer per agent route before flushing. Defaults to `10` when omitted.
476
+ - **`introspection_delay_ms`** (`int`, optional): Delay (in milliseconds) before route introspection runs. Defaults to `2000` ms, giving the application time to register all routes.
477
+ - **`agentguard_enabled`** (`bool`, optional): When `True` (default), the SDK registers itself as an AgentGuard instance in a background thread. When `False`, this registration step is skipped, but sample collection still works.
478
+
479
+ **Behavior**
480
+
481
+ - Stores a normalized configuration internally containing:
482
+ - `api_key`, `service_name`, `max_samples`, `app`, `introspection_delay_ms`, `agentguard_enabled`.
483
+ - Logs a startup message (`"[Aeonic] SDK initialized."`) using the `aeonic` logger.
484
+ - If `agentguard_enabled` is `True`, registers your service with Aeonic in a **background daemon thread**:
485
+ - Failures are logged as warnings but never raised; the SDK continues collecting samples even if registration fails.
486
+ - Schedules **deferred route introspection** in another background daemon thread:
487
+ - Waits `introspection_delay_ms` before running.
488
+ - For FastAPI: requires `config["app"]` and calls `introspect_routes(app)`.
489
+ - For Django: if `app` is `None` but Django is installed, calls `introspect_routes(None)`, which uses Django’s URL resolver.
490
+ - On success, emits an **agent inventory** event with all registered agents (name, type, model, route, method, framework, timestamps).
491
+ - On any error, logs the traceback and falls back to emitting inventory with whatever agents were registered manually.
492
+ - If introspection is skipped (no `app` and Django not installed), calls a fallback that attempts to emit inventory once agents exist.
493
+ - Never blocks the main thread; all network calls and introspection work are done in background daemon threads.
494
+
495
+ **Typical usage**
496
+
497
+ ```python
498
+ from aeonic import init
499
+ from fastapi import FastAPI
500
+
501
+ app = FastAPI()
502
+
503
+ init({
504
+ "api_key": "ag_test_123",
505
+ "app": app,
506
+ "service_name": "payments-api",
507
+ "introspection_delay_ms": 2000,
508
+ "capture_payloads": {
509
+ "max_samples": 10,
510
+ },
511
+ # Optional – disable AgentGuard registration if needed:
512
+ # "agentguard_enabled": False,
513
+ })
514
+ ```
515
+
516
+ **Accessing effective config (advanced)**
517
+
518
+ For debugging or advanced inspection, you can read the effective configuration:
519
+
520
+ ```python
521
+ from aeonic.core import get_config
522
+
523
+ cfg = get_config() # raises RuntimeError if init() was not called
524
+ ```
525
+
526
+ ---
527
+
528
+ ### `aeonic.adapters.fastapi.agent_middleware`
529
+
530
+ **Purpose**
531
+
532
+ FastAPI-compatible middleware (`BaseHTTPMiddleware`) that observes responses and captures samples **only for requests that have been marked as agent routes** via `with_agent`.
533
+
534
+ **How to register**
535
+
536
+ ```python
537
+ from fastapi import FastAPI
538
+ from aeonic.adapters.fastapi import agent_middleware
539
+
540
+ app = FastAPI()
541
+
542
+ app.add_middleware(agent_middleware)
543
+ ```
544
+
545
+ **Request/response handling**
546
+
547
+ - For each request:
548
+ - Calls the next handler (`call_next(request)`) first to let your business logic run.
549
+ - Reads `request.state.aeonic`:
550
+ - If missing or `is_agent` is falsy, returns the response immediately (non-agent route).
551
+ - Skips **assessment traffic**:
552
+ - If header `x-aeonic-assessment: true` is present, no samples are collected.
553
+ - When the `AEONIC_DEBUG` environment variable is set, the middleware logs a debug message and includes optional `x-aeonic-job-id` for traceability.
554
+ - Skips **blocked/quarantined agents**:
555
+ - Uses `should_collect_samples(agent_key)` from `aeonic.core.agent_status_cache`.
556
+ - `agent_key` is `agent["name"]` when available, otherwise `"<METHOD>:<PATH>"`.
557
+ - When `AEONIC_DEBUG` is set and an agent is blocked, a message is printed for easier debugging.
558
+
559
+ **Payload capture**
560
+
561
+ - **Request payload**:
562
+ - Read from `request.state.aeonic["req_payload"]`, which is set by the `with_agent` dependency (to avoid re-consuming the body).
563
+ - Serialized via `safe_serialize` to guarantee JSON-safe structures.
564
+ - **Response payload**:
565
+ - Attempts to read `response.body` directly when available (typical for JSON responses).
566
+ - If only a `body_iterator` exists (streaming responses), it:
567
+ - Iterates over the body iterator, collects all chunks, and rebuilds a new `Response` with the same status, headers, and media type.
568
+ - Checks `Content-Type` header:
569
+ - If it contains `"json"` and the body is non-empty, attempts `json.loads()` and then `safe_serialize` the result.
570
+ - Any parsing failures are silently ignored (SDK never breaks your response).
571
+
572
+ **Sample creation and buffering**
573
+
574
+ - Builds a sample dictionary:
575
+ - `{"req": req_payload, "res": res_payload, "timestamp": int(time()), "status_code": status_code}`.
576
+ - Reads SDK config via `get_config()`:
577
+ - Uses `config["max_samples"]`, `config["api_key"]`, and `config["service_name"]`.
578
+ - If the status code is **2xx**:
579
+ - Resolves or creates a per-agent `SampleBuffer` in an in-memory `_BUFFERS` map, keyed by `agent_key`.
580
+ - Caps the total number of in-memory buffers at `100` agents; when exceeded, the **oldest key** is evicted.
581
+ - Calls `buffer.add(sample)`:
582
+ - When the buffer is full (`max_samples`), `buffer.flush()` is called and the payload is enqueued via `enqueue(...)` for asynchronous delivery to Aeonic.
583
+ - If the status code is **non-2xx**:
584
+ - Immediately calls `emit_error_sample(...)` with a single-sample payload.
585
+
586
+ **Key properties**
587
+
588
+ - Only applies to routes where `with_agent` has attached metadata.
589
+ - Never throws or mutates your response in a way that changes semantics; at worst it falls back to no-op.
590
+ - Designed to be safe in high-throughput, async production environments.
591
+
592
+ ---
593
+
594
+ ### `aeonic.adapters.fastapi.with_agent(agent: AgentContext)`
595
+
596
+ **Purpose**
597
+
598
+ FastAPI dependency factory that **marks an endpoint as an agent route** and attaches agent metadata and request payloads to the `request.state.aeonic` object.
599
+
600
+ **Agent context (`AgentContext`)**
601
+
602
+ `AgentContext` is a `dict`-like structure with keys such as:
603
+
604
+ - **`name`** (`str`, recommended): Logical agent name, used as the primary key for statistics and status.
605
+ - **`type`** (`str | None`): Business/domain category (e.g. `"finance"`, `"support"`, `"claims"`).
606
+ - **`model`** (`str | list[str] | None`): Model identifier(s) powering the agent (e.g. `"gpt-4o"`).
607
+ - Additional custom keys are supported and propagated as metadata.
608
+
609
+ **Declaration-time behavior**
610
+
611
+ - When you call `with_agent(agent)` at import time:
612
+ - Immediately calls `_register_agent_declaration(agent, framework="fastapi")`, which in turn:
613
+ - Resolves the current Aeonic config (if initialized) to obtain `service_name`.
614
+ - Registers the agent in the global agent registry with:
615
+ - `name`, `type`, `model`, `framework="fastapi"`, and `service_name`.
616
+ - `route_path=None` and `http_method=None` initially (filled in later by route introspection).
617
+ - Stores a copy of the agent metadata (with `"source": "manual"`) in an internal map keyed by the dependency function’s ID.
618
+ - Also attaches metadata to the dependency function as `__aeonic_metadata__` for robust detection, even when FastAPI wraps or decorates dependencies.
619
+
620
+ **Request-time behavior**
621
+
622
+ - `with_agent(agent)` returns an async dependency callable compatible with `Depends`:
623
+
624
+ ```python
625
+ from fastapi import Depends
626
+ from aeonic.adapters.fastapi import with_agent
627
+
628
+ @app.post(
629
+ "/agent/finance",
630
+ dependencies=[Depends(with_agent({"name": "RefundRiskAgent", "type": "finance", "model": ["gpt-4o"]}))],
631
+ )
632
+ async def finance_agent(payload: dict):
633
+ ...
634
+ ```
635
+
636
+ - On each request, the dependency:
637
+ - Attempts to parse `await request.json()`:
638
+ - If parsing succeeds, `body` is serialized with `safe_serialize`.
639
+ - On any error (non-JSON, invalid body, etc.), `body` is set to `None`.
640
+ - Writes to `request.state.aeonic`:
641
+ - `{"is_agent": True, "agent": {**agent, "source": "manual"}, "req_payload": body}`.
642
+ - This `request.state.aeonic` object is later consumed by `agent_middleware` to decide whether and how to capture samples.
643
+
644
+ **Introspection helper: `get_agent_metadata(func)` (advanced)**
645
+
646
+ - `aeonic.adapters.fastapi.get_agent_metadata(func)`:
647
+ - Attempts to recover an `AgentContext` from:
648
+ - Direct ID lookup in the metadata map.
649
+ - `__aeonic_metadata__` attribute on the function or wrapped function.
650
+ - Closure variables containing the agent context.
651
+ - Underlying `.call` attribute (for FastAPI dependency objects).
652
+ - Used internally by the Aeonic introspection system to match FastAPI routes and dependencies to their agents.
653
+ - Typical SDK consumers do **not** need to call this directly; it is useful only for advanced tooling or framework integrations built on top of Aeonic.
654
+
655
+ ---
656
+
657
+ ### `aeonic.adapters.django.agent_middleware`
658
+
659
+ **Purpose**
660
+
661
+ Global Django middleware class that observes responses and captures samples **only for views that have been marked as agent routes** via the `with_agent` decorator.
662
+
663
+ **How to register**
664
+
665
+ ```python
666
+ # settings.py
667
+
668
+ MIDDLEWARE = [
669
+ ...
670
+ "aeonic.adapters.django.agent_middleware",
671
+ ]
672
+ ```
673
+
674
+ **Request/response handling**
675
+
676
+ - Constructed once per process with `get_response`.
677
+ - For each request:
678
+ - Calls `response = get_response(request)` to run your Django view and middleware stack.
679
+ - Reads `request.aeonic`:
680
+ - If missing or `is_agent` is falsy, returns the response immediately (non-agent route).
681
+ - Skips **assessment traffic**:
682
+ - Uses `request.META["HTTP_X_AEONIC_ASSESSMENT"]` to detect the header `X-Aeonic-Assessment: true`.
683
+ - When `AEONIC_DEBUG` is set, logs a message including optional `HTTP_X_AEONIC_JOB_ID`.
684
+ - Skips **blocked/quarantined agents**:
685
+ - Uses `should_collect_samples(agent_key)` with `agent_key = agent["name"]` or `"<METHOD>:<PATH>"`.
686
+ - When `AEONIC_DEBUG` is set and an agent is blocked, logs a message.
687
+
688
+ **Payload capture**
689
+
690
+ - **Request payload**:
691
+ - Read from `request.aeonic["req_payload"]`, which is set by the `with_agent` decorator.
692
+ - Serialized via `safe_serialize`.
693
+ - **Response payload**:
694
+ - Checks `response["Content-Type"]` header.
695
+ - If it contains `"json"`, attempts:
696
+ - Decode `response.content` as UTF‑8.
697
+ - Parse JSON and then `safe_serialize` the result.
698
+ - Any parsing or decoding error is swallowed; the SDK never breaks your response pipeline.
699
+
700
+ **Sample creation and buffering**
701
+
702
+ - Builds a sample dictionary identical to the FastAPI middleware:
703
+ - `{"req": req_payload, "res": res_payload, "timestamp": int(time()), "status_code": status_code}`.
704
+ - Reads SDK config via `get_config()` and uses `max_samples`, `api_key`, and `service_name`.
705
+ - For **2xx** responses:
706
+ - Uses a per-agent in-memory `SampleBuffer` in `_BUFFERS`, capped at `100` distinct agent keys with eviction of the oldest entry when full.
707
+ - When a buffer fills up (`max_samples`), calls `buffer.flush()` and enqueues the batch via `enqueue(...)`.
708
+ - For **non-2xx** responses:
709
+ - Calls `emit_error_sample(...)` immediately with a single sample.
710
+
711
+ ---
712
+
713
+ ### `aeonic.adapters.django.with_agent(agent: AgentContext)`
714
+
715
+ **Purpose**
716
+
717
+ Decorator that **marks a Django view as an agent route** and attaches agent metadata and payloads to `request.aeonic`.
718
+
719
+ **Declaration-time behavior**
720
+
721
+ - When the decorator is applied:
722
+ - Calls `_register_agent_declaration(agent, framework="django")`:
723
+ - Registers the agent in the global registry with `name`, `type`, `model`, `framework="django"`, `service_name`, and `route_path/http_method` initially set to `None`.
724
+ - Wraps the original view function with `functools.wraps`.
725
+ - Stores a copy of the agent context (with `"source": "manual"`) in an internal map keyed by the wrapped view’s ID.
726
+ - Attaches metadata to the wrapped view as `__aeonic_metadata__` for easier introspection later.
727
+
728
+ **Request-time behavior**
729
+
730
+ - The wrapped view:
731
+ - Tries to build `req_payload`:
732
+ - For `POST`, `PUT`, `PATCH`:
733
+ - Reads `CONTENT_TYPE` from `request.META`.
734
+ - If it contains `"json"` and `request.body` is non-empty:
735
+ - Decodes body as UTF‑8, parses JSON, and `safe_serialize`s the result.
736
+ - If body exists but is not JSON:
737
+ - Decodes as UTF‑8 string and serializes that.
738
+ - For `GET`:
739
+ - If `request.GET` has query parameters, serializes `dict(request.GET)`.
740
+ - If no query params, sets `req_payload` to an empty dict `{}`.
741
+ - On any exception, `req_payload` falls back to `None`.
742
+ - Sets:
743
+ - `request.aeonic = {"is_agent": True, "agent": {**agent, "source": "manual"}, "req_payload": req_payload}`.
744
+ - Calls and returns the original view function.
745
+
746
+ **Introspection helper: `get_agent_metadata(func)` (advanced)**
747
+
748
+ - `aeonic.adapters.django.get_agent_metadata(func)`:
749
+ - First checks the internal metadata map by ID.
750
+ - Then falls back to the `__aeonic_metadata__` attribute if present.
751
+ - Used by internal route introspection to match Django URL patterns to their agents.
752
+ - Typical SDK users do **not** need to call this directly, but it is useful for custom tooling.
753
+
754
+ ---
755
+
756
+ ### Summary of Public Entry Points
757
+
758
+ - **Initialization**
759
+ - `aeonic.init(config: dict)` – one-time SDK setup, background introspection, and optional AgentGuard registration.
760
+ - **FastAPI**
761
+ - `aeonic.adapters.fastapi.agent_middleware` – global middleware for observing and sampling agent routes.
762
+ - `aeonic.adapters.fastapi.with_agent(agent: AgentContext)` – dependency factory to mark FastAPI endpoints as agent routes and capture request bodies.
763
+ - **Django**
764
+ - `aeonic.adapters.django.agent_middleware` – middleware class for global observation and sampling of agent views.
765
+ - `aeonic.adapters.django.with_agent(agent: AgentContext)` – decorator to mark Django views as agent routes and capture request data.
766
+ - **Advanced (optional)**
767
+ - `aeonic.core.get_config()` – read the effective runtime SDK configuration.
768
+ - `aeonic.adapters.fastapi.get_agent_metadata(func)` / `aeonic.adapters.django.get_agent_metadata(func)` – used by introspection to map framework objects back to agent metadata.
769
+
770
+ You can map each function/middleware above to your integration: use the descriptions as guidance and the code snippets as copy-paste examples.