rudderstack-ai 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RudderStack, Inc.
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 CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,490 @@
1
+ Metadata-Version: 2.4
2
+ Name: rudderstack-ai
3
+ Version: 0.1.0
4
+ Summary: Automatic RudderStack AI events (spec v0.2) for OpenAI, Anthropic, LangChain and the Claude Agent SDK.
5
+ Keywords: rudderstack,analytics,ai,llm,openai,anthropic,langchain,claude
6
+ Author: RudderStack
7
+ Author-email: RudderStack <sdk@rudderstack.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE.md
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Typing :: Typed
20
+ Requires-Dist: rudderstack-ai[openai,anthropic,langchain,claude-agent-sdk] ; extra == 'all'
21
+ Requires-Dist: anthropic>=1.0.0,<2 ; extra == 'anthropic'
22
+ Requires-Dist: claude-agent-sdk>=0.2.82,<0.3 ; extra == 'claude-agent-sdk'
23
+ Requires-Dist: langchain-core>=1.2.0,<2 ; extra == 'langchain'
24
+ Requires-Dist: openai>=2.0.0,<4 ; extra == 'openai'
25
+ Maintainer: RudderStack
26
+ Maintainer-email: RudderStack <sdk@rudderstack.com>
27
+ Requires-Python: >=3.12
28
+ Project-URL: Homepage, https://github.com/rudderlabs/ai-sdk-python
29
+ Project-URL: Repository, https://github.com/rudderlabs/ai-sdk-python
30
+ Project-URL: Issues, https://github.com/rudderlabs/ai-sdk-python/issues
31
+ Project-URL: Changelog, https://github.com/rudderlabs/ai-sdk-python/blob/main/CHANGELOG.md
32
+ Provides-Extra: all
33
+ Provides-Extra: anthropic
34
+ Provides-Extra: claude-agent-sdk
35
+ Provides-Extra: langchain
36
+ Provides-Extra: openai
37
+ Description-Content-Type: text/markdown
38
+
39
+ # rudderstack-ai
40
+
41
+ [![CI](https://github.com/rudderlabs/ai-sdk-python/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/rudderlabs/ai-sdk-python/actions/workflows/ci.yml)
42
+
43
+ Product analytics for the AI features in your Python backend. `rudderstack-ai` wraps your OpenAI or Anthropic client, LangChain / LangGraph / Deep Agents runs, or Claude Agent SDK queries, and sends standard [AI events (spec v0.2)](https://github.com/rudderlabs/ai-sdk-python/blob/main/spec/ai-events-v0.2.schema.json) through the RudderStack client you already use.
44
+
45
+ - **One event per answer.** However many model calls, agents or tool steps an answer takes, you get one `ai_response_received` with its totals: tokens, cost, latency, models, tool calls, status.
46
+ - **Joinable.** The prompt, the answer and later feedback share an `interaction_id`, and every event carries `conversation_id`. Your app adds `intent` and `outcome`, so you can build audiences such as "users whose refund questions went unresolved".
47
+ - **Never in the way.** The original client is not modified, tracking errors are swallowed, and provider errors reach your code unchanged.
48
+ - **Private by default.** Prompt, answer and feedback text are sent only if you turn on content capture.
49
+
50
+ Server-side only: events are emitted where the model calls happen. The Node.js counterpart is [`@rudderstack/ai`](https://github.com/rudderlabs/ai-sdk-node); both emit identical events.
51
+
52
+ ## Contents
53
+
54
+ - [Install](#install)
55
+ - [Quick start](#quick-start)
56
+ - [How it works](#how-it-works)
57
+ - [Configuration](#configuration)
58
+ - [Events](#events)
59
+ - [Privacy and content capture](#privacy-and-content-capture)
60
+ - [Cost](#cost)
61
+ - [Adoption tag: `context.wrapperLibrary`](#adoption-tag-contextwrapperlibrary)
62
+ - [Compatibility](#compatibility)
63
+ - [Limitations](#limitations)
64
+ - [Examples](#examples)
65
+
66
+ ## Install
67
+
68
+ Python 3.12 or later. Install the extra for each library you use:
69
+
70
+ ```bash
71
+ pip install "rudderstack-ai[openai]" # or: uv add "rudderstack-ai[openai]"
72
+ pip install "rudderstack-ai[anthropic]"
73
+ pip install "rudderstack-ai[langchain]" # LangChain, LangGraph, Deep Agents
74
+ pip install "rudderstack-ai[claude-agent-sdk]"
75
+ pip install "rudderstack-ai[all]"
76
+ ```
77
+
78
+ The package has no dependencies of its own. It sends events through your RudderStack Python SDK client ([`rudder-sdk-python`](https://github.com/rudderlabs/rudder-sdk-python)) and never creates one. Both packages share the `rudderstack` namespace: `rudderstack.analytics` and `rudderstack.ai`.
79
+
80
+ ## Quick start
81
+
82
+ Configure once at startup with your existing client, either the module or a `Client` instance:
83
+
84
+ ```python
85
+ import rudderstack.analytics as rudder_analytics
86
+ import rudderstack.ai
87
+
88
+ rudder_analytics.write_key = "<WRITE_KEY>"
89
+ rudder_analytics.dataPlaneUrl = "<DATA_PLANE_URL>"
90
+
91
+ rudderstack.ai.configure(analytics=rudder_analytics)
92
+ ```
93
+
94
+ ### OpenAI
95
+
96
+ `chat.completions.create` / `parse` / `stream` and `responses.create` / `parse` / `stream`, sync and async, streaming or not.
97
+
98
+ ```python
99
+ from openai import OpenAI
100
+ from rudderstack.ai import ai_context, ai_interaction
101
+ from rudderstack.ai.openai import wrap_openai
102
+
103
+ client = wrap_openai(OpenAI())
104
+
105
+ with ai_context(user_id=user.id, conversation_id=chat.id, feature="support_bot"):
106
+ with ai_interaction(intent="refund_request") as interaction:
107
+ reply = client.chat.completions.create(model="gpt-4.1-mini", messages=messages)
108
+ interaction.set_outcome("resolved")
109
+
110
+ return {"answer": reply.choices[0].message.content, "interaction_id": interaction.id}
111
+ ```
112
+
113
+ `AzureOpenAI` and OpenAI-compatible endpoints work the same way; pass `provider="azure.ai.openai"` (or `openrouter`, `vllm`...) to label them.
114
+
115
+ ### Anthropic
116
+
117
+ `messages.create` (including `stream=True`), `messages.parse` and the `messages.stream()` helper, plus the same under `beta.messages`; sync and async.
118
+
119
+ ```python
120
+ from anthropic import Anthropic
121
+ from rudderstack.ai import ai_interaction
122
+ from rudderstack.ai.anthropic import wrap_anthropic
123
+
124
+ client = wrap_anthropic(Anthropic())
125
+
126
+ with ai_interaction(user_id=user.id, conversation_id=chat.id, feature="writer"):
127
+ with client.messages.stream(model="claude-haiku-4-5", max_tokens=1024, messages=messages) as stream:
128
+ for text in stream.text_stream:
129
+ print(text, end="")
130
+ ```
131
+
132
+ For `AnthropicBedrock` or `AnthropicVertex`, pass `provider="aws.bedrock"` or `provider="gcp.vertex_ai"`.
133
+
134
+ ### LangChain, LangGraph and Deep Agents
135
+
136
+ One callback handler. Subagent usage is attributed by agent name.
137
+
138
+ ```python
139
+ from rudderstack.ai.langchain import RudderStackCallbackHandler
140
+
141
+ handler = RudderStackCallbackHandler()
142
+
143
+ result = agent.invoke(
144
+ {"messages": messages},
145
+ config={
146
+ "callbacks": [handler],
147
+ "metadata": {"user_id": user.id, "conversation_id": chat.id, "intent": "refund_request"},
148
+ },
149
+ )
150
+ ```
151
+
152
+ Without an active `ai_interaction`, the handler opens one per root run and reads identity from the run `metadata` (`user_id`, `anonymous_id`, `conversation_id`, `interaction_id`, `feature`, `intent`, `trace_id`). Inside an `ai_interaction`, it reports into that interaction instead.
153
+
154
+ ### Claude Agent SDK
155
+
156
+ `tracked_query` is a drop-in for `claude_agent_sdk.query()`. Cost and per-model usage come from the SDK's final `ResultMessage`.
157
+
158
+ ```python
159
+ from rudderstack.ai import ai_context
160
+ from rudderstack.ai.claude_agent_sdk import tracked_query
161
+
162
+ with ai_context(user_id=user.id, conversation_id=chat.id):
163
+ async for message in tracked_query(prompt=question, options=options, feature="ops_agent"):
164
+ ...
165
+ ```
166
+
167
+ With `ClaudeSDKClient`, wrap its response stream: `async for message in tracked_messages(client.receive_response()): ...`, or map a result yourself with `record_claude_result(result_message)`.
168
+
169
+ ### Feedback
170
+
171
+ Return `interaction.id` with the answer, and send feedback from your server:
172
+
173
+ ```python
174
+ from rudderstack.ai import track_action
175
+
176
+ track_action(
177
+ "feedback_given",
178
+ interaction_id=body.interaction_id,
179
+ conversation_id=body.conversation_id,
180
+ user_id=user.id,
181
+ action_details={"feedback_type": "thumbs", "feedback_value": False},
182
+ )
183
+ ```
184
+
185
+ Action types: `feedback_given`, `copied_response`, `shared`, `reported`, `regenerated`. `conversation_id` is required (from the argument or the current `ai_context`); without it the event is dropped with a warning.
186
+
187
+ ## How it works
188
+
189
+ - **`ai_context`** sets identity once per request: `user_id`, `anonymous_id`, `conversation_id`, `feature`, and optionally a client and extra track `context`. It uses `contextvars`, so concurrent requests and asyncio tasks don't mix. Nested contexts inherit what they don't set.
190
+ - **`ai_interaction`** is one prompt and one answer. Every instrumented model call inside it is aggregated, and `ai_response_received` is sent when the block exits. An exception leaving the block sets the status (`error`, `timeout` or `cancelled`) and is re-raised unchanged.
191
+ - **Adapters** (`wrap_openai`, `wrap_anthropic`, the LangChain handler, `tracked_query`) only report calls into the active interaction. Calls are de-duplicated on the provider response ID, so a wrapped client used by LangChain with the handler attached is counted once.
192
+ - **Calls outside an interaction** still produce a valid event of their own: `trigger: "user"` when identity is in context, otherwise `trigger: "system"` with `anonymousId: "ai_system"` and a minted `conversation_id`. The SDK never invents an `anonymousId` when a `userId` is set: forward the browser SDK's `anonymousId` yourself to stitch profiles. No prompt event is sent for them.
193
+ - **The prompt event is held** until the first model call starts (or the interaction ends), so it goes through the same client and carries the same integration tag as the answer. It keeps its original timestamp.
194
+
195
+ ### Streaming to the browser
196
+
197
+ When a handler returns a stream, the interaction must stay open until the stream ends. Pass the generator through `interaction.bind()`:
198
+
199
+ ```python
200
+ from fastapi.responses import StreamingResponse
201
+
202
+ with ai_interaction(user_id=user.id, conversation_id=chat.id) as interaction:
203
+
204
+ async def tokens():
205
+ stream = await client.chat.completions.create(model="gpt-4.1-mini", messages=messages, stream=True)
206
+ async for chunk in stream:
207
+ if chunk.choices and chunk.choices[0].delta.content:
208
+ yield chunk.choices[0].delta.content
209
+
210
+ body = interaction.bind(tokens())
211
+ return StreamingResponse(body, headers={"x-interaction-id": interaction.id})
212
+ ```
213
+
214
+ A provider stream that is closed or abandoned before it completes counts as a cancelled call, so the answer is `cancelled` when that was the last call; a stream passed to `bind()` and closed early by the client ends the interaction as `cancelled`. For OpenAI chat streams, the wrapper asks for usage in the final chunk (`stream_options.include_usage`) and hides that extra chunk unless you requested it yourself.
215
+
216
+ ### Intent, outcome and trace ID
217
+
218
+ ```python
219
+ with ai_interaction(
220
+ user_id=u, conversation_id=c, intent="refund_request", trace_id=langfuse_trace_id
221
+ ) as interaction:
222
+ ...
223
+ interaction.set_intent("order_status") # known only later: goes on the response event
224
+ interaction.set_outcome("escalated")
225
+ interaction.set_trace_id(trace_id)
226
+ ```
227
+
228
+ `intent` given at the start goes on the prompt and response events; `outcome` and `trace_id` go on `ai_response_received`. All are free-form strings. `trace_id` links the interaction to Langfuse, LangSmith or an OpenTelemetry trace.
229
+
230
+ ### Manual instrumentation
231
+
232
+ For a client without an adapter, report calls yourself:
233
+
234
+ ```python
235
+ with ai_interaction(user_id=u, conversation_id=c) as interaction:
236
+ result = my_llm.generate(prompt)
237
+ interaction.record_call(
238
+ provider="acme",
239
+ model="acme-large",
240
+ input_tokens=result.input_tokens,
241
+ output_tokens=result.output_tokens,
242
+ response_id=result.id,
243
+ finish_reason=result.stop_reason,
244
+ )
245
+ interaction.end(response_text=result.text) # optional: end early, with the answer (content capture only)
246
+ ```
247
+
248
+ For frameworks with their own lifecycle hooks, `start_interaction()` creates an interaction without entering it; wrap work in `interaction.activate()` and call `interaction.end()`.
249
+
250
+ ## Configuration
251
+
252
+ ```python
253
+ import rudderstack.ai
254
+
255
+ rudderstack.ai.configure(
256
+ analytics=rudder_analytics, # global fallback client
257
+ prices={
258
+ "gpt-4.1-mini": {"input_per_mtok": 0.25, "output_per_mtok": 2.00, "cached_input_per_mtok": 0.025}
259
+ },
260
+ capture_content=False, # prompt_text, response_text, feedback_text
261
+ emit_llm_calls=False, # also send one ai_llm_response_received per model call
262
+ )
263
+ ```
264
+
265
+ `configure()` changes only the arguments you pass; `prices` are merged into the registered prices (call `clear_prices()` to start over). The prices above are placeholders: use your own rates.
266
+
267
+ | Option | Default | Description |
268
+ | --- | --- | --- |
269
+ | `analytics` | `None` | RudderStack client used when nothing more specific is set. |
270
+ | `prices` | none | Per-model prices in USD per 1M tokens, merged into the registry (see [Cost](#cost)). |
271
+ | `capture_content` | `False` | Send prompt, response and feedback text. |
272
+ | `emit_llm_calls` | `False` | Also send `ai_llm_response_received` for each model call. Totals belong on `ai_response_received`; don't sum both. |
273
+
274
+ **Which client is used.** `ai_interaction(analytics=...)` > `ai_context(analytics=...)` > the adapter's `analytics=` (`wrap_openai(client, analytics=...)`, `RudderStackCallbackHandler(analytics=...)`, `tracked_query(analytics=...)`) > `configure(analytics=...)`. With no client anywhere, events are dropped with a debug log.
275
+
276
+ **Logging.** The package logs to the `rudderstack.ai` logger: warnings for dropped `ai_user_action` events, debug messages for swallowed tracking errors.
277
+
278
+ **Serverless.** The RudderStack client sends events from a background queue. In AWS Lambda and similar runtimes, call `rudder_analytics.flush()` before the handler returns.
279
+
280
+ ### API reference
281
+
282
+ | Name | Signature |
283
+ | --- | --- |
284
+ | `configure` | `configure(*, analytics=..., prices=None, capture_content=..., emit_llm_calls=...)` |
285
+ | `register_price` | `register_price(model, *, input_per_mtok, output_per_mtok, cached_input_per_mtok=None, cache_write_per_mtok=None, provider=None)` |
286
+ | `clear_prices` | `clear_prices()` |
287
+ | `ai_context` | `ai_context(*, user_id=None, anonymous_id=None, conversation_id=None, feature=None, analytics=None, context=None)` |
288
+ | `ai_interaction` | `ai_interaction(*, user_id=None, anonymous_id=None, conversation_id=None, feature=None, intent=None, trace_id=None, input_method=None, prompt_text=None, interaction_id=None, analytics=None, capture_content=None, context=None)` |
289
+ | `start_interaction` | `start_interaction(*, input_method=None, prompt_text=None, emit_prompt=True, trigger="user", **interaction_fields)` |
290
+ | `Interaction` | `.id`, `.set_intent()`, `.set_outcome()`, `.set_trace_id()`, `.set_primary_model()`, `.record_call()`, `.record_tool_call()`, `.bind()`, `.activate()`, `.end()` |
291
+ | `track_action` | `track_action(action_type, *, interaction_id, conversation_id=None, action_details=None, user_id=None, anonymous_id=None, analytics=None, context=None)` |
292
+ | `current_context`, `current_interaction` | the active `AIContext` / `Interaction`, or `None` |
293
+ | `unwrap` | `unwrap(client)`: the original client of a wrapped one |
294
+ | `rudderstack.ai.openai.wrap_openai` | `wrap_openai(client, *, analytics=None, provider="openai")` |
295
+ | `rudderstack.ai.anthropic.wrap_anthropic` | `wrap_anthropic(client, *, analytics=None, provider="anthropic")` |
296
+ | `rudderstack.ai.langchain.RudderStackCallbackHandler` | `RudderStackCallbackHandler(*, analytics=None, emit_prompt=True, trigger="user", agent_name=None)` |
297
+ | `rudderstack.ai.claude_agent_sdk.tracked_query` | `tracked_query(*, prompt, options=None, transport=None, analytics=None, input_method=None, user_id=None, anonymous_id=None, conversation_id=None, feature=None, intent=None, trace_id=None)` |
298
+ | `rudderstack.ai.claude_agent_sdk.tracked_messages` | `tracked_messages(messages, *, analytics=None, prompt_text=None, ...)`: same, for any message stream |
299
+ | `rudderstack.ai.claude_agent_sdk.record_claude_result` | `record_claude_result(result, interaction=None, *, calls_by_model=None, primary_model=None, end=True, **interaction_fields)` |
300
+
301
+ Every public function and class has a docstring with the details; the package ships type hints (`py.typed`).
302
+
303
+ ## Events
304
+
305
+ All four events are `track` calls. Every event has `conversation_id` and `interaction_id`, plus `feature` when set; the spec version travels in `context.wrapperLibrary.specVersion`. IDs the SDK mints (`interaction_id`, and `conversation_id` when you don't give one) are UUID v4 strings; IDs you pass are used as given. The JSON Schema is in [`spec/ai-events-v0.2.schema.json`](https://github.com/rudderlabs/ai-sdk-python/blob/main/spec/ai-events-v0.2.schema.json); use it in a tracking plan.
306
+
307
+ ### `ai_user_prompt_created`
308
+
309
+ ```json
310
+ {
311
+ "conversation_id": "7d1f3c2a-9b4e-4c8d-a6f1-2e5b8c9d0a3f",
312
+ "interaction_id": "0b6e2f4a-3c1d-4e7f-9a8b-5c6d7e8f9a0b",
313
+ "feature": "support_chat",
314
+ "input_method": "text",
315
+ "intent": "refund_status"
316
+ }
317
+ ```
318
+
319
+ Optional: `input_method`, `intent`, `prompt_text` (content capture only).
320
+
321
+ ### `ai_response_received`
322
+
323
+ One per answer: an OpenAI tool loop with two model calls, priced with a registered price:
324
+
325
+ ```json
326
+ {
327
+ "conversation_id": "7d1f3c2a-9b4e-4c8d-a6f1-2e5b8c9d0a3f",
328
+ "interaction_id": "0b6e2f4a-3c1d-4e7f-9a8b-5c6d7e8f9a0b",
329
+ "feature": "support_chat",
330
+ "response_status": "success",
331
+ "trigger": "user",
332
+ "primary_model": "gpt-4.1-mini",
333
+ "primary_provider": "openai",
334
+ "latency_ms": 1840,
335
+ "llm_call_count": 2,
336
+ "tool_call_count": 1,
337
+ "total_input_tokens": 60,
338
+ "total_output_tokens": 24,
339
+ "total_tokens": 84,
340
+ "total_cached_input_tokens": 0,
341
+ "total_cost_usd": 0.000063,
342
+ "finish_reason": "stop",
343
+ "intent": "refund_status",
344
+ "outcome": "resolved",
345
+ "usage_breakdown": [
346
+ {
347
+ "provider": "openai",
348
+ "model": "gpt-4.1-mini",
349
+ "call_count": 2,
350
+ "input_tokens": 60,
351
+ "output_tokens": 24,
352
+ "cached_input_tokens": 0,
353
+ "total_tokens": 84,
354
+ "cost_usd": 0.000063
355
+ }
356
+ ],
357
+ "trace_id": "lf-trace-91b2"
358
+ }
359
+ ```
360
+
361
+ | Property | Notes |
362
+ | --- | --- |
363
+ | `response_status` | `success`, `error`, `timeout` or `cancelled`. The last call decides: a failed call followed by a successful retry is `success`; a last call that failed is `error`, or `timeout` / `cancelled` when it timed out or was cancelled (including a stream closed before it completed). |
364
+ | `error_type` | On non-success: `rate_limit` (every HTTP 429, including OpenAI's `insufficient_quota`), `timeout` (also HTTP 408 and 504), `cancelled`, `auth_error`, `invalid_request`, `connection_error`, `api_error`, or a framework value (e.g. `max_turns`). A failed OpenAI Responses object (`status: failed`) is `api_error`. |
365
+ | `trigger` | `user`, or `system` for calls without identity. |
366
+ | `primary_model`, `primary_provider` | The model set with `set_primary_model()` (until a later successful call), else the model of the last successful call (the main agent's model for the Claude Agent SDK); when no call succeeded, the model of the last attempted call. Providers use OTel `gen_ai.provider.name` values. |
367
+ | `latency_ms`, `time_to_first_token_ms` | From the start of the interaction. |
368
+ | `llm_call_count`, `tool_call_count` | `tool_call_count` counts every tool the model invoked, provider-hosted ones included: Anthropic `tool_use`, `server_tool_use` and `mcp_tool_use` blocks; OpenAI function calls and Responses built-in tool calls (`web_search_call`, `file_search_call`, `code_interpreter_call`, `mcp_call`...). A tool call reported by both a provider wrapper and the LangChain handler counts once (matched by its tool call ID). |
369
+ | `total_input_tokens` | Includes cached and cache-write tokens, for every provider. |
370
+ | `total_cached_input_tokens` | Cache reads. |
371
+ | `total_cost_usd` | Only when every call was priced. |
372
+ | `usage_breakdown` | One entry per agent, provider and model, with `call_count` and `total_tokens`. Entries sum to the totals. `agent_name` appears for agent frameworks. |
373
+ | `intent`, `outcome`, `trace_id` | Set by your app. |
374
+ | `response_text` | Content capture only. |
375
+ | custom | `interaction.end(extra_properties=...)` adds your own properties; SDK-owned names (every property above, plus `prompt_text` / `input_method`) are ignored. |
376
+
377
+ ### `ai_user_action`
378
+
379
+ ```json
380
+ {
381
+ "conversation_id": "7d1f3c2a-9b4e-4c8d-a6f1-2e5b8c9d0a3f",
382
+ "interaction_id": "0b6e2f4a-3c1d-4e7f-9a8b-5c6d7e8f9a0b",
383
+ "action_type": "feedback_given",
384
+ "action_details": {"feedback_type": "thumbs", "feedback_value": true}
385
+ }
386
+ ```
387
+
388
+ `action_details.feedback_type` and `feedback_text` must be strings; a value of another type is dropped with a warning.
389
+
390
+ ### `ai_llm_response_received` (optional)
391
+
392
+ Off by default (`emit_llm_calls=True`). One per model call, for debugging:
393
+
394
+ ```json
395
+ {
396
+ "conversation_id": "7d1f3c2a-9b4e-4c8d-a6f1-2e5b8c9d0a3f",
397
+ "interaction_id": "0b6e2f4a-3c1d-4e7f-9a8b-5c6d7e8f9a0b",
398
+ "feature": "support_chat",
399
+ "provider": "openai",
400
+ "model": "gpt-4.1-mini",
401
+ "input_tokens": 25,
402
+ "output_tokens": 12,
403
+ "total_tokens": 37,
404
+ "cached_input_tokens": 0,
405
+ "cost_usd": 0.00003025,
406
+ "response_id": "chatcmpl-9x2...",
407
+ "finish_reason": "tool_calls"
408
+ }
409
+ ```
410
+
411
+ The Claude Agent SDK reports usage per model, not per call, so there it sends one per model.
412
+
413
+ ## Privacy and content capture
414
+
415
+ By default no free text leaves your server: `prompt_text`, `response_text` and `action_details.feedback_text` are stripped. `configure(capture_content=True)` turns all three on; `ai_interaction(capture_content=True)` turns on prompt and response text for that interaction only (`track_action` follows the active interaction's setting when it runs inside one, else the global one). Treat captured text as personal data. (The examples turn it on so you can see the fields.)
416
+
417
+ Where the text comes from: `prompt_text` from `ai_interaction(prompt_text=...)` or the `tracked_query` prompt; `response_text` from `interaction.end(response_text=...)` or the Claude Agent SDK result (successful runs only); `feedback_text` from `track_action(action_details=...)`.
418
+
419
+ ## Cost
420
+
421
+ No price table ships with the package: list prices change and many teams pay negotiated rates. Cost is resolved in this order:
422
+
423
+ 1. **Framework-reported.** The Claude Agent SDK's `total_cost_usd` / per-model `costUSD`.
424
+ 2. **Your prices.** `register_price()` or `configure(prices=...)` (merged; `clear_prices()` empties the registry), in USD per 1M tokens. A name matches exactly or by longest prefix, so `gpt-4.1-mini` also prices `gpt-4.1-mini-2025-04-14`. A price registered with `provider=` wins over one for any provider.
425
+ 3. **None.** Events carry tokens only.
426
+
427
+ `register_price` raises `ValueError` for an empty model name or a negative, infinite or NaN rate.
428
+
429
+ ```python
430
+ from rudderstack.ai import register_price
431
+
432
+ register_price(
433
+ "claude-haiku-4-5",
434
+ input_per_mtok=1.00,
435
+ output_per_mtok=5.00, # your rates
436
+ cached_input_per_mtok=0.10,
437
+ cache_write_per_mtok=1.25,
438
+ provider="anthropic",
439
+ )
440
+ ```
441
+
442
+ Cached input defaults to the input rate when `cached_input_per_mtok` is not given; the same goes for cache writes. Failed calls count as $0. A cancelled stream's usage is at best partial, so that call is left unpriced, which leaves `total_cost_usd` out rather than understating it.
443
+
444
+ ## Adoption tag: `context.wrapperLibrary`
445
+
446
+ Every event carries `context.wrapperLibrary` beside the core SDK's `context.library`, so RudderStack can see which integration produced it:
447
+
448
+ ```json
449
+ {
450
+ "library": {"name": "analytics-python", "version": "2.1.9"},
451
+ "wrapperLibrary": {"name": "rudderstack-ai", "version": "0.1.0", "integration": "openai", "specVersion": "0.2.0"}
452
+ }
453
+ ```
454
+
455
+ `integration` is the first adapter that reported into the interaction: `openai`, `anthropic`, `langchain`, `claude-agent-sdk`, or `manual` (`track_action`, or an interaction with only hand-recorded calls). If a library built on top of this one sets its own tag with `context={"wrapperLibrary": {...}}` (on `ai_context`, `ai_interaction` or `track_action`), it is nested inside ours as `wrapperLibrary.wrapperLibrary`; other keys of that `context` are passed through. A `context` given to `ai_interaction` or `track_action` replaces the `ai_context` one for those events.
456
+
457
+ ## Compatibility
458
+
459
+ | | Supported | Tested in CI |
460
+ | --- | --- | --- |
461
+ | Python | 3.12+ | 3.12, 3.13, 3.14 |
462
+ | `openai` | `>=2.0.0,<4` | 2.0.0 (lowest), latest 3.x |
463
+ | `anthropic` | `>=1.0.0,<2` | 1.0.0 (lowest), latest 1.x |
464
+ | `langchain-core` | `>=1.2.0,<2` | 1.2.0 (lowest), latest 1.x; LangGraph 1.x and Deep Agents 0.7 with the latest |
465
+ | `claude-agent-sdk` | `>=0.2.82,<0.3` | 0.2.82 (lowest), latest 0.2.x |
466
+ | `rudder-sdk-python` | 2.x (any object with its `track()` signature) | 2.1.9 |
467
+
468
+ CI runs the test suite with the lowest and the highest versions each range allows.
469
+
470
+ ## Limitations
471
+
472
+ - Not tracked: OpenAI `responses.create(background=True)` (only queued at that point; no outcome or usage yet), OpenAI and Anthropic `with_raw_response` / `with_streaming_response`, and Anthropic's `beta.messages.tool_runner()` (it calls the API through the unwrapped client). Use `ai_interaction` + `record_call` for those.
473
+ - The `.stream()` helpers are observed by swapping the helper's private raw stream. If a future SDK renames it, the call still works but goes untracked; CI's latest-versions job catches this.
474
+ - An async stream abandoned with `break` outside any interaction is reported when it is closed or garbage-collected. Inside an interaction it is closed out as a cancelled call when the interaction ends. Close async streams (`await stream.close()` or `async with`) to release the connection.
475
+ - A stream passed to `interaction.bind()` must be consumed, or the response event is never sent.
476
+ - The LangChain handler's own interactions (opened from run `metadata`) are not visible to `wrap_openai` / `wrap_anthropic` clients called inside the run, since callbacks can't set the caller's context. When you combine a wrapped client with the handler, wrap the run in `ai_interaction` so both report into one interaction.
477
+ - A wrapped client is a proxy: attribute access, `with_options()` and type hints behave like the original, but `isinstance(wrapped, OpenAI)` is `False`. Pass `unwrap(wrapped)` to code that checks the class.
478
+ - MCP traffic is out of scope.
479
+
480
+ ## Examples
481
+
482
+ [`examples/`](https://github.com/rudderlabs/ai-sdk-python/tree/main/examples) has a runnable app per integration and a full-stack chat app (FastAPI, Anthropic and the RudderStack JavaScript SDK). Each runs against the real APIs with your keys, or offline against the bundled mock server.
483
+
484
+ ## Contributing and security
485
+
486
+ See [CONTRIBUTING.md](https://github.com/rudderlabs/ai-sdk-python/blob/main/CONTRIBUTING.md) and [SECURITY.md](https://github.com/rudderlabs/ai-sdk-python/blob/main/SECURITY.md). Releases: [RELEASING.md](https://github.com/rudderlabs/ai-sdk-python/blob/main/RELEASING.md).
487
+
488
+ ## License
489
+
490
+ [MIT](https://github.com/rudderlabs/ai-sdk-python/blob/main/LICENSE.md)