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.
- rudderstack_ai-0.1.0/LICENSE.md +20 -0
- rudderstack_ai-0.1.0/PKG-INFO +490 -0
- rudderstack_ai-0.1.0/README.md +452 -0
- rudderstack_ai-0.1.0/pyproject.toml +198 -0
- rudderstack_ai-0.1.0/pyproject.toml.orig +141 -0
- rudderstack_ai-0.1.0/src/rudderstack/ai/__init__.py +58 -0
- rudderstack_ai-0.1.0/src/rudderstack/ai/_core.py +1192 -0
- rudderstack_ai-0.1.0/src/rudderstack/ai/_intercept.py +297 -0
- rudderstack_ai-0.1.0/src/rudderstack/ai/_pricing.py +139 -0
- rudderstack_ai-0.1.0/src/rudderstack/ai/_proxy.py +227 -0
- rudderstack_ai-0.1.0/src/rudderstack/ai/_version.py +3 -0
- rudderstack_ai-0.1.0/src/rudderstack/ai/anthropic.py +173 -0
- rudderstack_ai-0.1.0/src/rudderstack/ai/claude_agent_sdk.py +374 -0
- rudderstack_ai-0.1.0/src/rudderstack/ai/langchain.py +383 -0
- rudderstack_ai-0.1.0/src/rudderstack/ai/openai.py +259 -0
- rudderstack_ai-0.1.0/src/rudderstack/ai/py.typed +0 -0
|
@@ -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
|
+
[](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)
|