paid-python 0.0.5a39__py3-none-any.whl → 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.
- paid/client.py +339 -233
- paid/logger.py +21 -0
- paid/tracing/__init__.py +4 -4
- paid/tracing/autoinstrumentation.py +6 -3
- paid/tracing/context_manager.py +243 -0
- paid/tracing/distributed_tracing.py +113 -0
- paid/tracing/signal.py +58 -28
- paid/tracing/tracing.py +103 -439
- paid/tracing/wrappers/anthropic/anthropicWrapper.py +11 -72
- paid/tracing/wrappers/bedrock/bedrockWrapper.py +3 -32
- paid/tracing/wrappers/gemini/geminiWrapper.py +10 -46
- paid/tracing/wrappers/langchain/paidLangChainCallback.py +3 -38
- paid/tracing/wrappers/llamaindex/llamaIndexWrapper.py +4 -38
- paid/tracing/wrappers/mistral/mistralWrapper.py +7 -118
- paid/tracing/wrappers/openai/openAiWrapper.py +56 -323
- paid/tracing/wrappers/openai_agents/openaiAgentsHook.py +8 -76
- paid/types/signal.py +18 -1
- {paid_python-0.0.5a39.dist-info → paid_python-0.1.0.dist-info}/METADATA +45 -196
- {paid_python-0.0.5a39.dist-info → paid_python-0.1.0.dist-info}/RECORD +21 -18
- {paid_python-0.0.5a39.dist-info → paid_python-0.1.0.dist-info}/LICENSE +0 -0
- {paid_python-0.0.5a39.dist-info → paid_python-0.1.0.dist-info}/WHEEL +0 -0
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
from typing import Any, Optional
|
|
2
2
|
|
|
3
|
-
from opentelemetry import
|
|
4
|
-
from opentelemetry.trace import Status, StatusCode
|
|
3
|
+
from opentelemetry.trace import Span, Status, StatusCode
|
|
5
4
|
|
|
5
|
+
from paid.logger import logger
|
|
6
6
|
from paid.tracing.tracing import (
|
|
7
7
|
get_paid_tracer,
|
|
8
|
-
logger,
|
|
9
|
-
paid_external_agent_id_var,
|
|
10
|
-
paid_external_customer_id_var,
|
|
11
|
-
paid_token_var,
|
|
12
8
|
)
|
|
13
9
|
|
|
14
10
|
try:
|
|
@@ -22,7 +18,7 @@ except ImportError:
|
|
|
22
18
|
|
|
23
19
|
# Global dictionary to store spans keyed by context object ID
|
|
24
20
|
# This avoids polluting user's context.context and works across async boundaries
|
|
25
|
-
_paid_span_store: dict[int,
|
|
21
|
+
_paid_span_store: dict[int, Span] = {}
|
|
26
22
|
|
|
27
23
|
|
|
28
24
|
class PaidOpenAIAgentsHook(RunHooks[Any]):
|
|
@@ -32,14 +28,12 @@ class PaidOpenAIAgentsHook(RunHooks[Any]):
|
|
|
32
28
|
Can optionally wrap user-provided hooks to combine Paid tracking with custom behavior.
|
|
33
29
|
"""
|
|
34
30
|
|
|
35
|
-
def __init__(self, user_hooks: Optional[RunHooks[Any]] = None
|
|
31
|
+
def __init__(self, user_hooks: Optional[RunHooks[Any]] = None):
|
|
36
32
|
"""
|
|
37
33
|
Initialize PaidAgentsHook.
|
|
38
34
|
|
|
39
35
|
Args:
|
|
40
36
|
user_hooks: Optional user-provided RunHooks to combine with Paid tracking
|
|
41
|
-
optional_tracing: If True, gracefully skip tracing when context is missing.
|
|
42
|
-
If False, raise errors when tracing context is not available.
|
|
43
37
|
|
|
44
38
|
Usage:
|
|
45
39
|
@paid_tracing("<ext_customer_id>", "<ext_agent_id>")
|
|
@@ -55,67 +49,26 @@ class PaidOpenAIAgentsHook(RunHooks[Any]):
|
|
|
55
49
|
|
|
56
50
|
my_hook = MyHook()
|
|
57
51
|
hook = PaidAgentsHook(user_hooks=my_hook)
|
|
58
|
-
|
|
59
|
-
# Optional tracing (won't raise errors if context missing)
|
|
60
|
-
hook = PaidAgentsHook(optional_tracing=True)
|
|
61
52
|
"""
|
|
62
53
|
super().__init__()
|
|
63
|
-
self.tracer = get_paid_tracer()
|
|
64
|
-
self.optional_tracing = optional_tracing
|
|
65
54
|
self.user_hooks = user_hooks
|
|
66
55
|
|
|
67
|
-
def _get_context_vars(self):
|
|
68
|
-
"""Get tracing context from context variables set by Paid.trace()."""
|
|
69
|
-
external_customer_id = paid_external_customer_id_var.get()
|
|
70
|
-
external_agent_id = paid_external_agent_id_var.get()
|
|
71
|
-
token = paid_token_var.get()
|
|
72
|
-
return external_customer_id, external_agent_id, token
|
|
73
|
-
|
|
74
|
-
def _should_skip_tracing(self, external_customer_id: Optional[str], token: Optional[str]) -> bool:
|
|
75
|
-
"""Check if tracing should be skipped."""
|
|
76
|
-
# Check if there's an active span (from Paid.trace())
|
|
77
|
-
current_span = trace.get_current_span()
|
|
78
|
-
if current_span == trace.INVALID_SPAN:
|
|
79
|
-
if self.optional_tracing:
|
|
80
|
-
logger.info(f"{self.__class__.__name__} No tracing, skipping LLM tracking.")
|
|
81
|
-
return True
|
|
82
|
-
raise RuntimeError("No OTEL span found. Make sure to call this method from Paid.trace().")
|
|
83
|
-
|
|
84
|
-
if not (external_customer_id and token):
|
|
85
|
-
if self.optional_tracing:
|
|
86
|
-
logger.info(f"{self.__class__.__name__} No external_customer_id or token, skipping LLM tracking")
|
|
87
|
-
return True
|
|
88
|
-
raise RuntimeError(
|
|
89
|
-
"Missing required tracing information: external_customer_id or token."
|
|
90
|
-
" Make sure to call this method from Paid.trace()."
|
|
91
|
-
)
|
|
92
|
-
return False
|
|
93
|
-
|
|
94
56
|
def _start_span(self, context, agent, hook_name) -> None:
|
|
95
57
|
try:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
# Skip tracing if required context is missing
|
|
99
|
-
if self._should_skip_tracing(external_customer_id, token):
|
|
100
|
-
return
|
|
58
|
+
tracer = get_paid_tracer()
|
|
101
59
|
|
|
102
60
|
# Get model name from agent
|
|
103
61
|
model_name = str(agent.model if agent.model else get_default_model())
|
|
104
62
|
|
|
105
63
|
# Start span for this LLM call
|
|
106
|
-
span =
|
|
107
|
-
logger.debug(f"{hook_name} : started span")
|
|
64
|
+
span = tracer.start_span(f"openai.agents.{hook_name}")
|
|
108
65
|
|
|
109
66
|
# Set initial attributes
|
|
110
67
|
attributes = {
|
|
111
68
|
"gen_ai.system": "openai",
|
|
112
69
|
"gen_ai.operation.name": f"{hook_name}",
|
|
113
|
-
"external_customer_id": external_customer_id,
|
|
114
|
-
"token": token,
|
|
115
70
|
"gen_ai.request.model": model_name,
|
|
116
71
|
}
|
|
117
|
-
if external_agent_id:
|
|
118
|
-
attributes["external_agent_id"] = external_agent_id
|
|
119
72
|
|
|
120
73
|
span.set_attributes(attributes)
|
|
121
74
|
|
|
@@ -123,7 +76,6 @@ class PaidOpenAIAgentsHook(RunHooks[Any]):
|
|
|
123
76
|
# This works across async boundaries without polluting user's context
|
|
124
77
|
context_id = id(context)
|
|
125
78
|
_paid_span_store[context_id] = span
|
|
126
|
-
logger.debug(f"_start_span: Stored span for context ID {context_id}")
|
|
127
79
|
|
|
128
80
|
except Exception as error:
|
|
129
81
|
logger.error(f"Error while starting span in PaidAgentsHook.{hook_name}: {error}")
|
|
@@ -133,7 +85,6 @@ class PaidOpenAIAgentsHook(RunHooks[Any]):
|
|
|
133
85
|
# Retrieve span from global dict using context object ID
|
|
134
86
|
context_id = id(context)
|
|
135
87
|
span = _paid_span_store.get(context_id)
|
|
136
|
-
logger.debug(f"_end_span: Retrieved span for context ID {context_id}: {span}")
|
|
137
88
|
|
|
138
89
|
if span:
|
|
139
90
|
# Get usage data from the response
|
|
@@ -161,17 +112,13 @@ class PaidOpenAIAgentsHook(RunHooks[Any]):
|
|
|
161
112
|
span.set_status(Status(StatusCode.ERROR, "No usage available"))
|
|
162
113
|
|
|
163
114
|
span.end()
|
|
164
|
-
logger.debug(f"{hook_name} : ended span")
|
|
165
115
|
|
|
166
116
|
# Clean up from global dict
|
|
167
117
|
del _paid_span_store[context_id]
|
|
168
|
-
logger.debug(f"_end_span: Cleaned up span for context ID {context_id}")
|
|
169
|
-
else:
|
|
170
|
-
logger.warning(f"_end_span: No span found for context ID {context_id}")
|
|
171
118
|
|
|
172
119
|
except Exception as error:
|
|
173
|
-
logger.error(f"Error while ending span in PaidAgentsHook.{hook_name}_end: {error}")
|
|
174
120
|
# Try to end span on error
|
|
121
|
+
logger.error(f"Error while ending span in PaidAgentsHook.{hook_name}: {error}")
|
|
175
122
|
try:
|
|
176
123
|
context_id = id(context)
|
|
177
124
|
span = _paid_span_store.get(context_id)
|
|
@@ -181,26 +128,18 @@ class PaidOpenAIAgentsHook(RunHooks[Any]):
|
|
|
181
128
|
span.end()
|
|
182
129
|
del _paid_span_store[context_id]
|
|
183
130
|
except:
|
|
184
|
-
|
|
131
|
+
logger.error(f"Failed to end span after error in PaidAgentsHook.{hook_name}")
|
|
185
132
|
|
|
186
133
|
async def on_llm_start(self, context, agent, system_prompt, input_items) -> None:
|
|
187
|
-
logger.debug(f"on_llm_start : context_usage : {getattr(context, 'usage', None)}")
|
|
188
|
-
|
|
189
134
|
if self.user_hooks and hasattr(self.user_hooks, "on_llm_start"):
|
|
190
135
|
await self.user_hooks.on_llm_start(context, agent, system_prompt, input_items)
|
|
191
136
|
|
|
192
137
|
async def on_llm_end(self, context, agent, response) -> None:
|
|
193
|
-
logger.debug(
|
|
194
|
-
f"on_llm_end : context_usage : {getattr(context, 'usage', None)} : response_usage : {getattr(response, 'usage', None)}"
|
|
195
|
-
)
|
|
196
|
-
|
|
197
138
|
if self.user_hooks and hasattr(self.user_hooks, "on_llm_end"):
|
|
198
139
|
await self.user_hooks.on_llm_end(context, agent, response)
|
|
199
140
|
|
|
200
141
|
async def on_agent_start(self, context, agent) -> None:
|
|
201
142
|
"""Start a span for agent operations and call user hooks."""
|
|
202
|
-
logger.debug(f"on_agent_start : context_usage : {getattr(context, 'usage', None)}")
|
|
203
|
-
|
|
204
143
|
if self.user_hooks and hasattr(self.user_hooks, "on_agent_start"):
|
|
205
144
|
await self.user_hooks.on_agent_start(context, agent)
|
|
206
145
|
|
|
@@ -208,26 +147,19 @@ class PaidOpenAIAgentsHook(RunHooks[Any]):
|
|
|
208
147
|
|
|
209
148
|
async def on_agent_end(self, context, agent, output) -> None:
|
|
210
149
|
"""End the span for agent operations and call user hooks."""
|
|
211
|
-
logger.debug(f"on_agent_end : context_usage : {getattr(context, 'usage', None)}")
|
|
212
|
-
|
|
213
150
|
self._end_span(context, "on_agent")
|
|
214
151
|
|
|
215
152
|
if self.user_hooks and hasattr(self.user_hooks, "on_agent_end"):
|
|
216
153
|
await self.user_hooks.on_agent_end(context, agent, output)
|
|
217
154
|
|
|
218
155
|
async def on_handoff(self, context, from_agent, to_agent) -> None:
|
|
219
|
-
logger.debug(f"on_handoff : context_usage : {getattr(context, 'usage', None)}")
|
|
220
156
|
if self.user_hooks and hasattr(self.user_hooks, "on_handoff"):
|
|
221
157
|
await self.user_hooks.on_handoff(context, from_agent, to_agent)
|
|
222
158
|
|
|
223
159
|
async def on_tool_start(self, context, agent, tool) -> None:
|
|
224
|
-
logger.debug(f"on_tool_start : context_usage : {getattr(context, 'usage', None)}")
|
|
225
|
-
|
|
226
160
|
if self.user_hooks and hasattr(self.user_hooks, "on_tool_start"):
|
|
227
161
|
await self.user_hooks.on_tool_start(context, agent, tool)
|
|
228
162
|
|
|
229
163
|
async def on_tool_end(self, context, agent, tool, result) -> None:
|
|
230
|
-
logger.debug(f"on_tool_end : context_usage : {getattr(context, 'usage', None)}")
|
|
231
|
-
|
|
232
164
|
if self.user_hooks and hasattr(self.user_hooks, "on_tool_end"):
|
|
233
165
|
await self.user_hooks.on_tool_end(context, agent, tool, result)
|
paid/types/signal.py
CHANGED
|
@@ -11,8 +11,25 @@ class Signal(UniversalBaseModel):
|
|
|
11
11
|
agent_id: typing.Optional[str] = None
|
|
12
12
|
external_agent_id: typing.Optional[str] = None
|
|
13
13
|
customer_id: typing.Optional[str] = None
|
|
14
|
-
|
|
14
|
+
"""
|
|
15
|
+
Deprecated. The external customer id. Use `external_customer_id` or `internal_customer_id` instead.
|
|
16
|
+
"""
|
|
17
|
+
|
|
15
18
|
data: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = None
|
|
19
|
+
idempotency_key: typing.Optional[str] = pydantic.Field(default=None)
|
|
20
|
+
"""
|
|
21
|
+
A unique key to ensure idempotent signal processing
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
internal_customer_id: typing.Optional[str] = pydantic.Field(default=None)
|
|
25
|
+
"""
|
|
26
|
+
Paid's internal customer ID
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
external_customer_id: typing.Optional[str] = pydantic.Field(default=None)
|
|
30
|
+
"""
|
|
31
|
+
Your system's customer ID
|
|
32
|
+
"""
|
|
16
33
|
|
|
17
34
|
if IS_PYDANTIC_V2:
|
|
18
35
|
model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: paid-python
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
4
4
|
Summary:
|
|
5
5
|
Requires-Python: >=3.9,<3.14
|
|
6
6
|
Classifier: Intended Audience :: Developers
|
|
@@ -135,7 +135,7 @@ from paid.tracing import paid_tracing
|
|
|
135
135
|
|
|
136
136
|
@paid_tracing("<external_customer_id>", external_agent_id="<optional_external_agent_id>")
|
|
137
137
|
def some_agent_workflow(): # your function
|
|
138
|
-
# Your logic - use any AI providers with Paid wrappers or send signals with
|
|
138
|
+
# Your logic - use any AI providers with Paid wrappers or send signals with signal().
|
|
139
139
|
# This function is typically an event processor that should lead to AI calls or events emitted as Paid signals
|
|
140
140
|
```
|
|
141
141
|
|
|
@@ -156,6 +156,7 @@ async with paid_tracing("customer_123", external_agent_id="agent_456"):
|
|
|
156
156
|
```
|
|
157
157
|
|
|
158
158
|
Both approaches:
|
|
159
|
+
|
|
159
160
|
- Initialize tracing using your API key you provided to the Paid client, falls back to `PAID_API_KEY` environment variable.
|
|
160
161
|
- Handle both sync and async functions/code blocks
|
|
161
162
|
- Gracefully fall back to normal execution if tracing fails
|
|
@@ -178,6 +179,7 @@ gemini (google-genai)
|
|
|
178
179
|
```
|
|
179
180
|
|
|
180
181
|
Example usage:
|
|
182
|
+
|
|
181
183
|
```python
|
|
182
184
|
from openai import OpenAI
|
|
183
185
|
from paid.tracing import paid_tracing
|
|
@@ -261,31 +263,24 @@ paid_autoinstrument(libraries=["anthropic", "openai"])
|
|
|
261
263
|
|
|
262
264
|
- Auto-instrumentation uses official OpenTelemetry instrumentors for each AI library
|
|
263
265
|
- It automatically wraps library calls without requiring you to use Paid wrapper classes
|
|
264
|
-
- Works seamlessly with `@paid_tracing()` decorator or
|
|
266
|
+
- Works seamlessly with `@paid_tracing()` decorator or context manager
|
|
265
267
|
- Costs are tracked in the same way as when using manual wrappers
|
|
266
268
|
- Should be called once during application startup, typically before creating AI client instances
|
|
267
269
|
|
|
268
|
-
|
|
269
270
|
## Signaling via OTEL tracing
|
|
270
271
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
The interface is `Paid.signal()`, which takes in signal name, optional data, and a flag that attaches costs from the same trace.
|
|
274
|
-
`Paid.signal()` has to be called within a trace - meaning inside of a callback to `Paid.trace()`.
|
|
275
|
-
In contrast to `Paid.usage.record_bulk()`, `Paid.signal()` is using OpenTelemetry to provide reliable delivery.
|
|
272
|
+
Signals allow you to emit events within your tracing context. They have access to all tracing information, so you need fewer arguments compared to manual API calls.
|
|
273
|
+
Use the `signal()` function which must be called within an active `@paid_tracing()` context (decorator or context manager).
|
|
276
274
|
|
|
277
275
|
Here's an example of how to use it:
|
|
278
|
-
```python
|
|
279
|
-
from paid import Paid
|
|
280
|
-
from paid.tracing import paid_tracing
|
|
281
276
|
|
|
282
|
-
|
|
283
|
-
|
|
277
|
+
```python
|
|
278
|
+
from paid.tracing import paid_tracing, signal
|
|
284
279
|
|
|
285
|
-
@paid_tracing("your_external_customer_id", "your_external_agent_id")
|
|
280
|
+
@paid_tracing("your_external_customer_id", "your_external_agent_id")
|
|
286
281
|
def do_work():
|
|
287
282
|
# ...do some work...
|
|
288
|
-
|
|
283
|
+
signal(
|
|
289
284
|
event_name="<your_signal_name>",
|
|
290
285
|
data={ } # optional data (ex. manual cost tracking data)
|
|
291
286
|
)
|
|
@@ -293,27 +288,21 @@ def do_work():
|
|
|
293
288
|
do_work()
|
|
294
289
|
```
|
|
295
290
|
|
|
296
|
-
Same
|
|
297
|
-
```python
|
|
298
|
-
from paid import Paid
|
|
291
|
+
Same approach with context manager:
|
|
299
292
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
# Initialize tracing, must be after initializing Paid SDK
|
|
304
|
-
client.initialize_tracing()
|
|
293
|
+
```python
|
|
294
|
+
from paid.tracing import paid_tracing, signal
|
|
305
295
|
|
|
306
296
|
def do_work():
|
|
307
297
|
# ...do some work...
|
|
308
|
-
|
|
298
|
+
signal(
|
|
309
299
|
event_name="<your_signal_name>",
|
|
310
300
|
data={ } # optional data (ex. manual cost tracking data)
|
|
311
301
|
)
|
|
312
302
|
|
|
313
|
-
#
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
fn = lambda: do_work())
|
|
303
|
+
# Use context manager instead
|
|
304
|
+
with paid_tracing("your_external_customer_id", "your_external_agent_id"):
|
|
305
|
+
do_work()
|
|
317
306
|
```
|
|
318
307
|
|
|
319
308
|
### Signal-costs - Attaching cost traces to a signal
|
|
@@ -325,17 +314,13 @@ as the wrappers and hooks that recorded those costs.
|
|
|
325
314
|
This will look something like this:
|
|
326
315
|
|
|
327
316
|
```python
|
|
328
|
-
from paid import
|
|
329
|
-
from paid.tracing import paid_tracing
|
|
330
|
-
|
|
331
|
-
# Initialize Paid SDK
|
|
332
|
-
client = Paid(token="PAID_API_KEY")
|
|
317
|
+
from paid.tracing import paid_tracing, signal
|
|
333
318
|
|
|
334
|
-
@paid_tracing("your_external_customer_id", "your_external_agent_id")
|
|
319
|
+
@paid_tracing("your_external_customer_id", "your_external_agent_id")
|
|
335
320
|
def do_work():
|
|
336
321
|
# ... your workflow logic
|
|
337
322
|
# ... your AI calls made through Paid wrappers or hooks
|
|
338
|
-
|
|
323
|
+
signal(
|
|
339
324
|
event_name="<your_signal_name>",
|
|
340
325
|
data={ }, # optional data (ex. manual cost tracking data)
|
|
341
326
|
enable_cost_tracing=True, # set this flag to associate it with costs
|
|
@@ -353,20 +338,17 @@ Then, all of the costs traced in @paid_tracing() context are related to that sig
|
|
|
353
338
|
Sometimes your agent workflow cannot fit into a single traceable function like above,
|
|
354
339
|
because it has to be disjoint for whatever reason. It could even be running across different machines.
|
|
355
340
|
|
|
356
|
-
For such cases, you can pass a tracing token directly to `@paid_tracing()` or
|
|
341
|
+
For such cases, you can pass a tracing token directly to `@paid_tracing()` or context manager to link distributed traces together.
|
|
357
342
|
|
|
358
343
|
#### Using `tracing_token` parameter (Recommended)
|
|
359
344
|
|
|
360
|
-
The simplest way to implement distributed tracing is to pass the token directly to the decorator or
|
|
345
|
+
The simplest way to implement distributed tracing is to pass the token directly to the decorator or context manager:
|
|
361
346
|
|
|
362
347
|
```python
|
|
363
|
-
from paid import
|
|
364
|
-
from paid.tracing import paid_tracing, generate_tracing_token
|
|
348
|
+
from paid.tracing import paid_tracing, signal, generate_tracing_token
|
|
365
349
|
from paid.tracing.wrappers.openai import PaidOpenAI
|
|
366
350
|
from openai import OpenAI
|
|
367
351
|
|
|
368
|
-
# Initialize
|
|
369
|
-
client = Paid(token="<PAID_API_KEY>")
|
|
370
352
|
openai_client = PaidOpenAI(OpenAI(api_key="<OPENAI_API_KEY>"))
|
|
371
353
|
|
|
372
354
|
# Process 1: Generate token and do initial work
|
|
@@ -384,7 +366,7 @@ def process_part_1():
|
|
|
384
366
|
messages=[{"role": "user", "content": "Analyze data"}]
|
|
385
367
|
)
|
|
386
368
|
# Signal without cost tracing
|
|
387
|
-
|
|
369
|
+
signal("part_1_complete", enable_cost_tracing=False)
|
|
388
370
|
|
|
389
371
|
process_part_1()
|
|
390
372
|
|
|
@@ -399,167 +381,44 @@ def process_part_2():
|
|
|
399
381
|
messages=[{"role": "user", "content": "Generate response"}]
|
|
400
382
|
)
|
|
401
383
|
# Signal WITH cost tracing - links all costs from both processes
|
|
402
|
-
|
|
384
|
+
signal("workflow_complete", enable_cost_tracing=True)
|
|
403
385
|
|
|
404
386
|
process_part_2()
|
|
405
387
|
# No cleanup needed - token is scoped to the decorated function
|
|
406
388
|
```
|
|
407
389
|
|
|
408
|
-
Using
|
|
390
|
+
Using context manager instead of decorator:
|
|
409
391
|
|
|
410
392
|
```python
|
|
411
|
-
from paid import
|
|
412
|
-
from paid.tracing import generate_tracing_token
|
|
393
|
+
from paid.tracing import paid_tracing, signal, generate_tracing_token
|
|
413
394
|
from paid.tracing.wrappers.openai import PaidOpenAI
|
|
414
395
|
from openai import OpenAI
|
|
415
396
|
|
|
416
397
|
# Initialize
|
|
417
|
-
client = Paid(token="<PAID_API_KEY>")
|
|
418
|
-
client.initialize_tracing()
|
|
419
398
|
openai_client = PaidOpenAI(OpenAI(api_key="<OPENAI_API_KEY>"))
|
|
420
399
|
|
|
421
|
-
# Process 1: Generate and
|
|
400
|
+
# Process 1: Generate token and do initial work
|
|
422
401
|
token = generate_tracing_token()
|
|
423
402
|
save_to_storage("workflow_123", token)
|
|
424
403
|
|
|
425
|
-
|
|
404
|
+
with paid_tracing("customer_123", external_agent_id="agent_123", tracing_token=token):
|
|
426
405
|
response = openai_client.chat.completions.create(
|
|
427
406
|
model="gpt-4",
|
|
428
407
|
messages=[{"role": "user", "content": "Analyze data"}]
|
|
429
408
|
)
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
client.trace(
|
|
433
|
-
external_customer_id="customer_123",
|
|
434
|
-
external_agent_id="agent_123",
|
|
435
|
-
tracing_token=token,
|
|
436
|
-
fn=lambda: process_part_1()
|
|
437
|
-
)
|
|
409
|
+
signal("part_1_complete", enable_cost_tracing=False)
|
|
438
410
|
|
|
439
411
|
# Process 2: Retrieve and use the same token
|
|
440
412
|
token = load_from_storage("workflow_123")
|
|
441
413
|
|
|
442
|
-
|
|
414
|
+
with paid_tracing("customer_123", external_agent_id="agent_123", tracing_token=token):
|
|
443
415
|
response = openai_client.chat.completions.create(
|
|
444
416
|
model="gpt-4",
|
|
445
417
|
messages=[{"role": "user", "content": "Generate response"}]
|
|
446
418
|
)
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
client.trace(
|
|
450
|
-
external_customer_id="customer_123",
|
|
451
|
-
external_agent_id="agent_123",
|
|
452
|
-
tracing_token=token,
|
|
453
|
-
fn=lambda: process_part_2()
|
|
454
|
-
)
|
|
455
|
-
```
|
|
456
|
-
|
|
457
|
-
#### Alternative: Using global context (Advanced)
|
|
458
|
-
|
|
459
|
-
For more complex scenarios where you need to set the tracing context globally, you can use these functions:
|
|
460
|
-
|
|
461
|
-
```python
|
|
462
|
-
from paid.tracing import (
|
|
463
|
-
generate_tracing_token,
|
|
464
|
-
generate_and_set_tracing_token,
|
|
465
|
-
set_tracing_token,
|
|
466
|
-
unset_tracing_token
|
|
467
|
-
)
|
|
468
|
-
|
|
469
|
-
def generate_tracing_token() -> int:
|
|
470
|
-
"""
|
|
471
|
-
Generates and returns a tracing token without setting it in the tracing context.
|
|
472
|
-
Useful when you only want to store or send a tracing token somewhere else
|
|
473
|
-
without immediately activating it.
|
|
474
|
-
|
|
475
|
-
Returns:
|
|
476
|
-
int: The tracing token (OpenTelemetry trace ID)
|
|
477
|
-
"""
|
|
478
|
-
|
|
479
|
-
def generate_and_set_tracing_token() -> int:
|
|
480
|
-
"""
|
|
481
|
-
This function returns tracing token and attaches it to all consequent
|
|
482
|
-
Paid.trace() or @paid_tracing tracing contexts. So all the costs and signals that share this
|
|
483
|
-
tracing context are associated with each other.
|
|
484
|
-
|
|
485
|
-
To stop associating the traces one can either call
|
|
486
|
-
generate_and_set_tracing_token() once again or call unset_tracing_token().
|
|
487
|
-
The former is suitable if you still want to trace but in a fresh
|
|
488
|
-
context, and the latter will go back to unique traces per Paid.trace().
|
|
489
|
-
|
|
490
|
-
Returns:
|
|
491
|
-
int: The tracing token (OpenTelemetry trace ID)
|
|
492
|
-
"""
|
|
493
|
-
|
|
494
|
-
def set_tracing_token(token: int):
|
|
495
|
-
"""
|
|
496
|
-
Sets tracing token. Provided token should come from generate_and_set_tracing_token()
|
|
497
|
-
or generate_tracing_token(). Once set, the consequent traces Paid.trace() or
|
|
498
|
-
@paid_tracing() will be related to each other.
|
|
499
|
-
|
|
500
|
-
Args:
|
|
501
|
-
token (int): A tracing token from generate_and_set_tracing_token() or generate_tracing_token()
|
|
502
|
-
"""
|
|
503
|
-
|
|
504
|
-
def unset_tracing_token():
|
|
505
|
-
"""
|
|
506
|
-
Unsets the token previously set by generate_and_set_tracing_token()
|
|
507
|
-
or by set_tracing_token(token). Does nothing if the token was never set.
|
|
508
|
-
"""
|
|
419
|
+
signal("workflow_complete", enable_cost_tracing=True)
|
|
509
420
|
```
|
|
510
421
|
|
|
511
|
-
Example using global context:
|
|
512
|
-
|
|
513
|
-
```python
|
|
514
|
-
from paid import Paid
|
|
515
|
-
from paid.tracing import paid_tracing, generate_and_set_tracing_token, set_tracing_token, unset_tracing_token
|
|
516
|
-
from paid.tracing.wrappers.openai import PaidOpenAI
|
|
517
|
-
from openai import OpenAI
|
|
518
|
-
|
|
519
|
-
# Initialize
|
|
520
|
-
client = Paid(token="<PAID_API_KEY>")
|
|
521
|
-
openai_client = PaidOpenAI(OpenAI(api_key="<OPENAI_API_KEY>"))
|
|
522
|
-
|
|
523
|
-
# Process 1: Generate token and do initial work
|
|
524
|
-
token = generate_and_set_tracing_token()
|
|
525
|
-
print(f"Tracing token: {token}")
|
|
526
|
-
|
|
527
|
-
# Store token for other processes (e.g., in Redis, database, message queue)
|
|
528
|
-
save_to_storage("workflow_123", token)
|
|
529
|
-
|
|
530
|
-
@paid_tracing("customer_123", external_agent_id="agent_123")
|
|
531
|
-
def process_part_1():
|
|
532
|
-
# AI calls here will be traced
|
|
533
|
-
response = openai_client.chat.completions.create(
|
|
534
|
-
model="gpt-4",
|
|
535
|
-
messages=[{"role": "user", "content": "Analyze data"}]
|
|
536
|
-
)
|
|
537
|
-
# Signal without cost tracing
|
|
538
|
-
client.signal("part_1_complete", enable_cost_tracing=False)
|
|
539
|
-
|
|
540
|
-
process_part_1()
|
|
541
|
-
|
|
542
|
-
# Process 2 (different machine/process): Retrieve and use token
|
|
543
|
-
token = load_from_storage("workflow_123")
|
|
544
|
-
set_tracing_token(token)
|
|
545
|
-
|
|
546
|
-
@paid_tracing("customer_123", external_agent_id="agent_123")
|
|
547
|
-
def process_part_2():
|
|
548
|
-
# AI calls here will be linked to the same trace
|
|
549
|
-
response = openai_client.chat.completions.create(
|
|
550
|
-
model="gpt-4",
|
|
551
|
-
messages=[{"role": "user", "content": "Generate response"}]
|
|
552
|
-
)
|
|
553
|
-
# Signal WITH cost tracing - links all costs from both processes
|
|
554
|
-
client.signal("workflow_complete", enable_cost_tracing=True)
|
|
555
|
-
|
|
556
|
-
process_part_2()
|
|
557
|
-
|
|
558
|
-
# Clean up
|
|
559
|
-
unset_tracing_token()
|
|
560
|
-
```
|
|
561
|
-
|
|
562
|
-
|
|
563
422
|
## Manual Cost Tracking
|
|
564
423
|
|
|
565
424
|
If you would prefer to not use Paid to track your costs automatically but you want to send us the costs yourself,
|
|
@@ -573,7 +432,7 @@ client = Paid(token="<PAID_API_KEY>")
|
|
|
573
432
|
signal = Signal(
|
|
574
433
|
event_name="<your_signal_name>",
|
|
575
434
|
agent_id="<your_agent_id>",
|
|
576
|
-
|
|
435
|
+
external_customer_id="<your_external_customer_id>",
|
|
577
436
|
data = {
|
|
578
437
|
"costData": {
|
|
579
438
|
"vendor": "<any_vendor_name>", # can be anything, traces are grouped by vendors in the UI
|
|
@@ -592,16 +451,12 @@ client.usage.record_bulk(signals=[signal])
|
|
|
592
451
|
Alternatively the same `costData` payload can be passed to OTLP signaling mechanism:
|
|
593
452
|
|
|
594
453
|
```python
|
|
595
|
-
from paid import
|
|
596
|
-
from paid.tracing import paid_tracing
|
|
454
|
+
from paid.tracing import paid_tracing, signal
|
|
597
455
|
|
|
598
|
-
|
|
599
|
-
client = Paid(token="PAID_API_KEY")
|
|
600
|
-
|
|
601
|
-
@paid_tracing("your_external_customer_id", "your_external_agent_id") # external_agent_id is required for sending signals
|
|
456
|
+
@paid_tracing("your_external_customer_id", "your_external_agent_id")
|
|
602
457
|
def do_work():
|
|
603
458
|
# ...do some work...
|
|
604
|
-
|
|
459
|
+
signal(
|
|
605
460
|
event_name="<your_signal_name>",
|
|
606
461
|
data={
|
|
607
462
|
"costData": {
|
|
@@ -630,7 +485,7 @@ client = Paid(token="<PAID_API_KEY>")
|
|
|
630
485
|
signal = Signal(
|
|
631
486
|
event_name="<your_signal_name>",
|
|
632
487
|
agent_id="<your_agent_id>",
|
|
633
|
-
|
|
488
|
+
external_customer_id="<your_external_customer_id>",
|
|
634
489
|
data = {
|
|
635
490
|
"costData": {
|
|
636
491
|
"vendor": "<any_vendor_name>", # can be anything, traces are grouped by vendors in the UI
|
|
@@ -650,16 +505,12 @@ client.usage.record_bulk(signals=[signal])
|
|
|
650
505
|
Same but via OTEL signaling:
|
|
651
506
|
|
|
652
507
|
```python
|
|
653
|
-
from paid import
|
|
654
|
-
from paid.tracing import paid_tracing
|
|
508
|
+
from paid.tracing import paid_tracing, signal
|
|
655
509
|
|
|
656
|
-
|
|
657
|
-
client = Paid(token="PAID_API_KEY")
|
|
658
|
-
|
|
659
|
-
@paid_tracing("your_external_customer_id", "your_external_agent_id") # external_agent_id is required for sending signals
|
|
510
|
+
@paid_tracing("your_external_customer_id", "your_external_agent_id")
|
|
660
511
|
def do_work():
|
|
661
512
|
# ...do some work...
|
|
662
|
-
|
|
513
|
+
signal(
|
|
663
514
|
event_name="<your_signal_name>",
|
|
664
515
|
data={
|
|
665
516
|
"costData": {
|
|
@@ -723,15 +574,13 @@ await generate_image()
|
|
|
723
574
|
|
|
724
575
|
### Async Signaling
|
|
725
576
|
|
|
726
|
-
The `signal()`
|
|
577
|
+
The `signal()` function works seamlessly in async contexts:
|
|
727
578
|
|
|
728
579
|
```python
|
|
729
|
-
from paid import
|
|
730
|
-
from paid.tracing import paid_tracing
|
|
580
|
+
from paid.tracing import paid_tracing, signal
|
|
731
581
|
from paid.tracing.wrappers.openai import PaidAsyncOpenAI
|
|
732
582
|
from openai import AsyncOpenAI
|
|
733
583
|
|
|
734
|
-
client = AsyncPaid(token="PAID_API_KEY")
|
|
735
584
|
openai_client = PaidAsyncOpenAI(AsyncOpenAI(api_key="<OPENAI_API_KEY>"))
|
|
736
585
|
|
|
737
586
|
@paid_tracing("your_external_customer_id", "your_external_agent_id")
|
|
@@ -742,8 +591,8 @@ async def do_work():
|
|
|
742
591
|
messages=[{"role": "user", "content": "Hello!"}]
|
|
743
592
|
)
|
|
744
593
|
|
|
745
|
-
# Send signal (
|
|
746
|
-
|
|
594
|
+
# Send signal (works in async context)
|
|
595
|
+
signal(
|
|
747
596
|
event_name="<your_signal_name>",
|
|
748
597
|
enable_cost_tracing=True # Associate with traced costs
|
|
749
598
|
)
|