payi 0.1.0a67__py3-none-any.whl → 0.1.0a69__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.
Potentially problematic release.
This version of payi might be problematic. Click here for more details.
- payi/_base_client.py +207 -233
- payi/_client.py +1 -4
- payi/_models.py +2 -3
- payi/_utils/_transform.py +24 -1
- payi/_utils/_typing.py +3 -1
- payi/_utils/_utils.py +9 -1
- payi/_version.py +1 -1
- payi/lib/AnthropicInstrumentor.py +52 -20
- payi/lib/BedrockInstrumentor.py +103 -25
- payi/lib/OpenAIInstrumentor.py +108 -36
- payi/lib/helpers.py +2 -1
- payi/lib/instrument.py +308 -167
- payi/resources/categories/resources.py +1 -4
- payi/resources/experiences/properties.py +1 -4
- payi/resources/experiences/types/limit_config.py +1 -4
- payi/resources/experiences/types/types.py +1 -4
- payi/resources/ingest.py +1 -5
- payi/resources/limits/limits.py +1 -4
- payi/resources/limits/tags.py +1 -4
- payi/resources/requests/properties.py +1 -4
- payi/resources/use_cases/definitions/definitions.py +1 -4
- payi/resources/use_cases/definitions/kpis.py +1 -4
- payi/resources/use_cases/definitions/limit_config.py +1 -4
- payi/resources/use_cases/kpis.py +1 -4
- payi/resources/use_cases/properties.py +1 -4
- {payi-0.1.0a67.dist-info → payi-0.1.0a69.dist-info}/METADATA +1 -1
- {payi-0.1.0a67.dist-info → payi-0.1.0a69.dist-info}/RECORD +29 -29
- {payi-0.1.0a67.dist-info → payi-0.1.0a69.dist-info}/WHEEL +0 -0
- {payi-0.1.0a67.dist-info → payi-0.1.0a69.dist-info}/licenses/LICENSE +0 -0
payi/lib/instrument.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import json
|
|
2
3
|
import uuid
|
|
3
4
|
import asyncio
|
|
4
5
|
import inspect
|
|
5
6
|
import logging
|
|
6
7
|
import traceback
|
|
8
|
+
from abc import abstractmethod
|
|
7
9
|
from enum import Enum
|
|
8
10
|
from typing import Any, Set, Union, Callable, Optional, TypedDict
|
|
9
11
|
from datetime import datetime, timezone
|
|
@@ -23,19 +25,52 @@ from .Stopwatch import Stopwatch
|
|
|
23
25
|
|
|
24
26
|
|
|
25
27
|
class _ProviderRequest:
|
|
26
|
-
def __init__(self, instrumentor: '_PayiInstrumentor'):
|
|
28
|
+
def __init__(self, instrumentor: '_PayiInstrumentor', category: str):
|
|
27
29
|
self._instrumentor: '_PayiInstrumentor' = instrumentor
|
|
28
30
|
self._estimated_prompt_tokens: Optional[int] = None
|
|
29
|
-
self.
|
|
30
|
-
|
|
31
|
-
def process_request(self, _kwargs: Any) -> None:
|
|
32
|
-
return
|
|
31
|
+
self._category: str = category
|
|
32
|
+
self._ingest: IngestUnitsParams = { "category": category, "units": {} } # type: ignore
|
|
33
33
|
|
|
34
34
|
def process_chunk(self, _chunk: Any) -> bool:
|
|
35
35
|
return True
|
|
36
36
|
|
|
37
37
|
def process_synchronous_response(self, response: Any, log_prompt_and_response: bool, kwargs: Any) -> Optional[object]: # noqa: ARG002
|
|
38
38
|
return None
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def process_request(self, instance: Any, extra_headers: 'dict[str, str]', kwargs: Any) -> bool:
|
|
42
|
+
...
|
|
43
|
+
|
|
44
|
+
def is_bedrock(self) -> bool:
|
|
45
|
+
return self._category == PayiCategories.aws_bedrock
|
|
46
|
+
|
|
47
|
+
def process_exception(self, exception: Exception, kwargs: Any, ) -> bool: # noqa: ARG002
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
def exception_to_semantic_failure(self, e: Exception) -> None:
|
|
51
|
+
exception_str = f"{type(e).__name__}"
|
|
52
|
+
|
|
53
|
+
fields: list[str] = []
|
|
54
|
+
# fields += f"args: {e.args}"
|
|
55
|
+
|
|
56
|
+
for attr in dir(e):
|
|
57
|
+
if not attr.startswith("__"):
|
|
58
|
+
try:
|
|
59
|
+
value = getattr(e, attr)
|
|
60
|
+
if value and not inspect.ismethod(value) and not inspect.isfunction(value) and not callable(value):
|
|
61
|
+
fields.append(f"{attr}={value}")
|
|
62
|
+
except Exception as _ex:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
properties: 'dict[str,str]' = { "system.failure": exception_str }
|
|
66
|
+
if fields:
|
|
67
|
+
failure_description = ",".join(fields)
|
|
68
|
+
properties["system.failure.description"] = failure_description[:128]
|
|
69
|
+
self._ingest["properties"] = properties
|
|
70
|
+
|
|
71
|
+
if "http_status_code" not in self._ingest:
|
|
72
|
+
# use a non existent http status code so when presented to the user, the origin is clear
|
|
73
|
+
self._ingest["http_status_code"] = 299
|
|
39
74
|
|
|
40
75
|
class PayiInstrumentConfig(TypedDict, total=False):
|
|
41
76
|
proxy: bool
|
|
@@ -49,7 +84,7 @@ class PayiInstrumentConfig(TypedDict, total=False):
|
|
|
49
84
|
user_id: Optional[str]
|
|
50
85
|
|
|
51
86
|
class _Context(TypedDict, total=False):
|
|
52
|
-
proxy: bool
|
|
87
|
+
proxy: Optional[bool]
|
|
53
88
|
experience_name: Optional[str]
|
|
54
89
|
experience_id: Optional[str]
|
|
55
90
|
use_case_name: Optional[str]
|
|
@@ -57,12 +92,34 @@ class _Context(TypedDict, total=False):
|
|
|
57
92
|
use_case_version: Optional[int]
|
|
58
93
|
limit_ids: Optional['list[str]']
|
|
59
94
|
user_id: Optional[str]
|
|
95
|
+
request_tags: Optional["list[str]"]
|
|
96
|
+
route_as_resource: Optional[str]
|
|
97
|
+
resource_scope: Optional[str]
|
|
60
98
|
|
|
61
99
|
class _IsStreaming(Enum):
|
|
62
100
|
false = 0
|
|
63
101
|
true = 1
|
|
64
102
|
kwargs = 2
|
|
65
103
|
|
|
104
|
+
class _TrackContext:
|
|
105
|
+
def __init__(
|
|
106
|
+
self,
|
|
107
|
+
context: _Context,
|
|
108
|
+
) -> None:
|
|
109
|
+
self._context = context
|
|
110
|
+
|
|
111
|
+
def __enter__(self) -> Any:
|
|
112
|
+
if not _instrumentor:
|
|
113
|
+
return self
|
|
114
|
+
|
|
115
|
+
_instrumentor.__enter__()
|
|
116
|
+
_instrumentor._init_current_context(**self._context)
|
|
117
|
+
return self
|
|
118
|
+
|
|
119
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
120
|
+
if _instrumentor:
|
|
121
|
+
_instrumentor.__exit__(exc_type, exc_val, exc_tb)
|
|
122
|
+
|
|
66
123
|
class _PayiInstrumentor:
|
|
67
124
|
def __init__(
|
|
68
125
|
self,
|
|
@@ -73,7 +130,8 @@ class _PayiInstrumentor:
|
|
|
73
130
|
prompt_and_response_logger: Optional[
|
|
74
131
|
Callable[[str, "dict[str, str]"], None]
|
|
75
132
|
] = None, # (request id, dict of data to store) -> None
|
|
76
|
-
global_config:
|
|
133
|
+
global_config: PayiInstrumentConfig = {},
|
|
134
|
+
caller_filename: str = ""
|
|
77
135
|
):
|
|
78
136
|
self._payi: Optional[Payi] = payi
|
|
79
137
|
self._apayi: Optional[AsyncPayi] = apayi
|
|
@@ -85,28 +143,38 @@ class _PayiInstrumentor:
|
|
|
85
143
|
self._blocked_limits: set[str] = set()
|
|
86
144
|
self._exceeded_limits: set[str] = set()
|
|
87
145
|
|
|
146
|
+
# default is instrument and ingest metrics
|
|
147
|
+
self._proxy_default: bool = global_config.get("proxy", False)
|
|
148
|
+
|
|
149
|
+
global_instrumentation = global_config.pop("global_instrumentation", True)
|
|
150
|
+
|
|
88
151
|
if instruments is None or "*" in instruments:
|
|
89
152
|
self._instrument_all()
|
|
90
153
|
else:
|
|
91
154
|
self._instrument_specific(instruments)
|
|
92
155
|
|
|
93
|
-
global_instrumentation = global_config.pop("global_instrumentation", True) if global_config else True
|
|
94
|
-
|
|
95
156
|
if global_instrumentation:
|
|
96
|
-
if global_config is None:
|
|
97
|
-
global_config = {}
|
|
98
157
|
if "proxy" not in global_config:
|
|
99
|
-
global_config["proxy"] =
|
|
158
|
+
global_config["proxy"] = self._proxy_default
|
|
100
159
|
|
|
101
160
|
# Use default clients if not provided for global ingest instrumentation
|
|
102
|
-
if not self._payi and not self._apayi and global_config.get("proxy")
|
|
161
|
+
if not self._payi and not self._apayi and global_config.get("proxy") is False:
|
|
103
162
|
self._payi = Payi()
|
|
104
163
|
self._apayi = AsyncPayi()
|
|
105
164
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
165
|
+
if "use_case_name" not in global_config and caller_filename:
|
|
166
|
+
try:
|
|
167
|
+
if self._payi:
|
|
168
|
+
self._payi.use_cases.definitions.create(name=caller_filename, description='')
|
|
169
|
+
elif self._apayi:
|
|
170
|
+
self._call_async_use_case_definition_create(use_case_name=caller_filename)
|
|
171
|
+
global_config["use_case_name"] = caller_filename
|
|
172
|
+
except Exception as e:
|
|
173
|
+
logging.error(f"Error creating default use case definition based on file name {caller_filename}: {e}")
|
|
174
|
+
|
|
175
|
+
self.__enter__()
|
|
176
|
+
# _init_current_context will update the currrent context stack location
|
|
177
|
+
self._init_current_context(**global_config) # type: ignore
|
|
110
178
|
|
|
111
179
|
def _instrument_all(self) -> None:
|
|
112
180
|
self._instrument_openai()
|
|
@@ -152,9 +220,7 @@ class _PayiInstrumentor:
|
|
|
152
220
|
if int(ingest_units.get("http_status_code") or 0) < 400:
|
|
153
221
|
units = ingest_units.get("units", {})
|
|
154
222
|
if not units or all(unit.get("input", 0) == 0 and unit.get("output", 0) == 0 for unit in units.values()):
|
|
155
|
-
logging.error(
|
|
156
|
-
'No units to ingest. For OpenAI streaming calls, make sure you pass stream_options={"include_usage": True}'
|
|
157
|
-
)
|
|
223
|
+
logging.error('No units to ingest')
|
|
158
224
|
return False
|
|
159
225
|
|
|
160
226
|
if self._log_prompt_and_response and self._prompt_and_response_logger:
|
|
@@ -219,6 +285,25 @@ class _PayiInstrumentor:
|
|
|
219
285
|
|
|
220
286
|
return None
|
|
221
287
|
|
|
288
|
+
def _call_async_use_case_definition_create(self, use_case_name: str) -> None:
|
|
289
|
+
if not self._apayi:
|
|
290
|
+
return
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
loop = asyncio.get_running_loop()
|
|
294
|
+
except RuntimeError:
|
|
295
|
+
loop = None
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
if loop and loop.is_running():
|
|
299
|
+
nest_asyncio.apply(loop) # type: ignore
|
|
300
|
+
asyncio.run(self._apayi.use_cases.definitions.create(name=use_case_name, description=""))
|
|
301
|
+
else:
|
|
302
|
+
# When there's no running loop, create a new one
|
|
303
|
+
asyncio.run(self._apayi.use_cases.definitions.create(name=use_case_name, description=""))
|
|
304
|
+
except Exception as e:
|
|
305
|
+
logging.error(f"Error calling async use_cases.definitions.create synchronously: {e}")
|
|
306
|
+
|
|
222
307
|
def _call_aingest_sync(self, ingest_units: IngestUnitsParams) -> Optional[IngestResponse]:
|
|
223
308
|
try:
|
|
224
309
|
loop = asyncio.get_running_loop()
|
|
@@ -268,23 +353,17 @@ class _PayiInstrumentor:
|
|
|
268
353
|
|
|
269
354
|
def _setup_call_func(
|
|
270
355
|
self
|
|
271
|
-
) ->
|
|
272
|
-
context: _Context = {}
|
|
273
|
-
parentContext: _Context = {}
|
|
356
|
+
) -> _Context:
|
|
274
357
|
|
|
275
358
|
if len(self._context_stack) > 0:
|
|
276
359
|
# copy current context into the upcoming context
|
|
277
|
-
|
|
278
|
-
context.pop("proxy")
|
|
279
|
-
parentContext = {**context}
|
|
360
|
+
return self._context_stack[-1].copy()
|
|
280
361
|
|
|
281
|
-
return
|
|
362
|
+
return {}
|
|
282
363
|
|
|
283
|
-
def
|
|
364
|
+
def _init_current_context(
|
|
284
365
|
self,
|
|
285
|
-
|
|
286
|
-
parentContext: _Context,
|
|
287
|
-
proxy: bool,
|
|
366
|
+
proxy: Optional[bool] = None,
|
|
288
367
|
limit_ids: Optional["list[str]"] = None,
|
|
289
368
|
experience_name: Optional[str] = None,
|
|
290
369
|
experience_id: Optional[str] = None,
|
|
@@ -292,11 +371,20 @@ class _PayiInstrumentor:
|
|
|
292
371
|
use_case_id: Optional[str]= None,
|
|
293
372
|
use_case_version: Optional[int]= None,
|
|
294
373
|
user_id: Optional[str]= None,
|
|
374
|
+
request_tags: Optional["list[str]"] = None,
|
|
375
|
+
route_as_resource: Optional[str] = None,
|
|
376
|
+
resource_scope: Optional[str] = None,
|
|
295
377
|
) -> None:
|
|
296
|
-
context["proxy"] = proxy
|
|
297
378
|
|
|
298
|
-
|
|
299
|
-
|
|
379
|
+
# there will always be a current context
|
|
380
|
+
context: _Context = self.get_context() # type: ignore
|
|
381
|
+
parent_context: _Context = self._context_stack[-2] if len(self._context_stack) > 1 else {}
|
|
382
|
+
|
|
383
|
+
parent_proxy = parent_context.get("proxy", self._proxy_default)
|
|
384
|
+
context["proxy"] = proxy if proxy else parent_proxy
|
|
385
|
+
|
|
386
|
+
parent_experience_name = parent_context.get("experience_name", None)
|
|
387
|
+
parent_experience_id = parent_context.get("experience_id", None)
|
|
300
388
|
|
|
301
389
|
if experience_name is None:
|
|
302
390
|
# If no experience_name specified, use previous values
|
|
@@ -317,9 +405,9 @@ class _PayiInstrumentor:
|
|
|
317
405
|
context["experience_name"] = experience_name
|
|
318
406
|
context["experience_id"] = experience_id if experience_id else str(uuid.uuid4())
|
|
319
407
|
|
|
320
|
-
parent_use_case_name =
|
|
321
|
-
parent_use_case_id =
|
|
322
|
-
parent_use_case_version =
|
|
408
|
+
parent_use_case_name = parent_context.get("use_case_name", None)
|
|
409
|
+
parent_use_case_id = parent_context.get("use_case_id", None)
|
|
410
|
+
parent_use_case_version = parent_context.get("use_case_version", None)
|
|
323
411
|
|
|
324
412
|
if use_case_name is None:
|
|
325
413
|
# If no use_case_name specified, use previous values
|
|
@@ -343,7 +431,7 @@ class _PayiInstrumentor:
|
|
|
343
431
|
context["use_case_id"] = use_case_id if use_case_id else str(uuid.uuid4())
|
|
344
432
|
context["use_case_version"] = use_case_version if use_case_version else None
|
|
345
433
|
|
|
346
|
-
parent_limit_ids =
|
|
434
|
+
parent_limit_ids = parent_context.get("limit_ids", None)
|
|
347
435
|
if limit_ids is None:
|
|
348
436
|
# use the parent limit_ids if it exists
|
|
349
437
|
context["limit_ids"] = parent_limit_ids
|
|
@@ -354,21 +442,27 @@ class _PayiInstrumentor:
|
|
|
354
442
|
# union of new and parent lists if the parent context contains limit ids
|
|
355
443
|
context["limit_ids"] = list(set(limit_ids) | set(parent_limit_ids)) if parent_limit_ids else limit_ids
|
|
356
444
|
|
|
445
|
+
parent_user_id = parent_context.get("user_id", None)
|
|
357
446
|
if user_id is None:
|
|
358
447
|
# use the parent user_id if it exists
|
|
359
|
-
context["user_id"] =
|
|
448
|
+
context["user_id"] = parent_user_id
|
|
360
449
|
elif len(user_id) == 0:
|
|
361
450
|
# caller passing an empty string explicitly blocks inheriting from the parent state
|
|
362
451
|
context["user_id"] = None
|
|
363
452
|
else:
|
|
364
453
|
context["user_id"] = user_id
|
|
365
454
|
|
|
366
|
-
|
|
455
|
+
if request_tags:
|
|
456
|
+
context["request_tags"] = request_tags
|
|
457
|
+
if route_as_resource:
|
|
458
|
+
context["route_as_resource"] = route_as_resource
|
|
459
|
+
if resource_scope:
|
|
460
|
+
context["resource_scope"] = resource_scope
|
|
367
461
|
|
|
368
462
|
async def _acall_func(
|
|
369
463
|
self,
|
|
370
464
|
func: Any,
|
|
371
|
-
proxy: bool,
|
|
465
|
+
proxy: Optional[bool],
|
|
372
466
|
limit_ids: Optional["list[str]"],
|
|
373
467
|
experience_name: Optional[str],
|
|
374
468
|
experience_id: Optional[str],
|
|
@@ -379,12 +473,8 @@ class _PayiInstrumentor:
|
|
|
379
473
|
*args: Any,
|
|
380
474
|
**kwargs: Any,
|
|
381
475
|
) -> Any:
|
|
382
|
-
context, parentContext = self._setup_call_func()
|
|
383
|
-
|
|
384
476
|
with self:
|
|
385
|
-
self.
|
|
386
|
-
context,
|
|
387
|
-
parentContext,
|
|
477
|
+
self._init_current_context(
|
|
388
478
|
proxy,
|
|
389
479
|
limit_ids,
|
|
390
480
|
experience_name,
|
|
@@ -398,7 +488,7 @@ class _PayiInstrumentor:
|
|
|
398
488
|
def _call_func(
|
|
399
489
|
self,
|
|
400
490
|
func: Any,
|
|
401
|
-
proxy: bool,
|
|
491
|
+
proxy: Optional[bool],
|
|
402
492
|
limit_ids: Optional["list[str]"],
|
|
403
493
|
experience_name: Optional[str],
|
|
404
494
|
experience_id: Optional[str],
|
|
@@ -409,12 +499,8 @@ class _PayiInstrumentor:
|
|
|
409
499
|
*args: Any,
|
|
410
500
|
**kwargs: Any,
|
|
411
501
|
) -> Any:
|
|
412
|
-
context, parentContext = self._setup_call_func()
|
|
413
|
-
|
|
414
502
|
with self:
|
|
415
|
-
self.
|
|
416
|
-
context,
|
|
417
|
-
parentContext,
|
|
503
|
+
self._init_current_context(
|
|
418
504
|
proxy,
|
|
419
505
|
limit_ids,
|
|
420
506
|
experience_name,
|
|
@@ -435,15 +521,14 @@ class _PayiInstrumentor:
|
|
|
435
521
|
if self._context_stack:
|
|
436
522
|
self._context_stack.pop()
|
|
437
523
|
|
|
438
|
-
def set_context(self, context: _Context) -> None:
|
|
439
|
-
# Update the current top of the stack with the provided context
|
|
440
|
-
if self._context_stack:
|
|
441
|
-
self._context_stack[-1].update(context)
|
|
442
|
-
|
|
443
524
|
def get_context(self) -> Optional[_Context]:
|
|
444
525
|
# Return the current top of the stack
|
|
445
526
|
return self._context_stack[-1] if self._context_stack else None
|
|
446
527
|
|
|
528
|
+
def get_context_safe(self) -> _Context:
|
|
529
|
+
# Return the current top of the stack
|
|
530
|
+
return self.get_context() or {}
|
|
531
|
+
|
|
447
532
|
def _prepare_ingest(
|
|
448
533
|
self,
|
|
449
534
|
ingest: IngestUnitsParams,
|
|
@@ -499,8 +584,7 @@ class _PayiInstrumentor:
|
|
|
499
584
|
|
|
500
585
|
async def achat_wrapper(
|
|
501
586
|
self,
|
|
502
|
-
|
|
503
|
-
provider: _ProviderRequest,
|
|
587
|
+
request: _ProviderRequest,
|
|
504
588
|
is_streaming: _IsStreaming,
|
|
505
589
|
wrapped: Any,
|
|
506
590
|
instance: Any,
|
|
@@ -519,45 +603,20 @@ class _PayiInstrumentor:
|
|
|
519
603
|
extra_headers = kwargs.get("extra_headers", {})
|
|
520
604
|
self._update_extra_headers(context, extra_headers)
|
|
521
605
|
|
|
522
|
-
if context.get("proxy",
|
|
523
|
-
if "extra_headers" not in kwargs:
|
|
606
|
+
if context.get("proxy", self._proxy_default):
|
|
607
|
+
if "extra_headers" not in kwargs and extra_headers:
|
|
524
608
|
kwargs["extra_headers"] = extra_headers
|
|
525
609
|
|
|
526
610
|
return await wrapped(*args, **kwargs)
|
|
527
611
|
|
|
528
|
-
provider._ingest = {"category": category, "units": {}} # type: ignore
|
|
529
|
-
provider._ingest["resource"] = kwargs.get("model", "")
|
|
530
|
-
|
|
531
|
-
if category == PayiCategories.openai and instance and hasattr(instance, "_client"):
|
|
532
|
-
from .OpenAIInstrumentor import OpenAiInstrumentor # noqa: I001
|
|
533
|
-
|
|
534
|
-
if OpenAiInstrumentor.is_azure(instance):
|
|
535
|
-
route_as_resource = extra_headers.pop(PayiHeaderNames.route_as_resource, None)
|
|
536
|
-
resource_scope = extra_headers.pop(PayiHeaderNames.resource_scope, None)
|
|
537
|
-
|
|
538
|
-
if not route_as_resource:
|
|
539
|
-
logging.error("Azure OpenAI route as resource not found, not ingesting")
|
|
540
|
-
return await wrapped(*args, **kwargs)
|
|
541
|
-
|
|
542
|
-
if resource_scope:
|
|
543
|
-
if not(resource_scope in ["global", "datazone"] or resource_scope.startswith("region")):
|
|
544
|
-
logging.error("Azure OpenAI invalid resource scope, not ingesting")
|
|
545
|
-
return wrapped(*args, **kwargs)
|
|
546
|
-
|
|
547
|
-
provider._ingest["resource_scope"] = resource_scope
|
|
548
|
-
|
|
549
|
-
category = PayiCategories.azure_openai
|
|
550
|
-
|
|
551
|
-
provider._ingest["category"] = category
|
|
552
|
-
provider._ingest["resource"] = route_as_resource
|
|
553
|
-
|
|
554
612
|
current_frame = inspect.currentframe()
|
|
555
613
|
# f_back excludes the current frame, strip() cleans up whitespace and newlines
|
|
556
614
|
stack = [frame.strip() for frame in traceback.format_stack(current_frame.f_back)] # type: ignore
|
|
557
615
|
|
|
558
|
-
|
|
616
|
+
request._ingest['properties'] = { 'system.stack_trace': json.dumps(stack) }
|
|
559
617
|
|
|
560
|
-
|
|
618
|
+
if request.process_request(instance, extra_headers, kwargs) is False:
|
|
619
|
+
return await wrapped(*args, **kwargs)
|
|
561
620
|
|
|
562
621
|
sw = Stopwatch()
|
|
563
622
|
stream: bool = False
|
|
@@ -570,7 +629,7 @@ class _PayiInstrumentor:
|
|
|
570
629
|
stream = False
|
|
571
630
|
|
|
572
631
|
try:
|
|
573
|
-
self._prepare_ingest(
|
|
632
|
+
self._prepare_ingest(request._ingest, extra_headers, kwargs)
|
|
574
633
|
sw.start()
|
|
575
634
|
response = await wrapped(*args, **kwargs)
|
|
576
635
|
|
|
@@ -578,7 +637,9 @@ class _PayiInstrumentor:
|
|
|
578
637
|
sw.stop()
|
|
579
638
|
duration = sw.elapsed_ms_int()
|
|
580
639
|
|
|
581
|
-
|
|
640
|
+
if request.process_exception(e, kwargs):
|
|
641
|
+
request._ingest["end_to_end_latency_ms"] = duration
|
|
642
|
+
await self._aingest_units(request._ingest)
|
|
582
643
|
|
|
583
644
|
raise e
|
|
584
645
|
|
|
@@ -589,18 +650,17 @@ class _PayiInstrumentor:
|
|
|
589
650
|
instrumentor=self,
|
|
590
651
|
log_prompt_and_response=self._log_prompt_and_response,
|
|
591
652
|
stopwatch=sw,
|
|
592
|
-
|
|
593
|
-
is_bedrock=False,
|
|
653
|
+
request=request,
|
|
594
654
|
)
|
|
595
655
|
|
|
596
656
|
return stream_result
|
|
597
657
|
|
|
598
658
|
sw.stop()
|
|
599
659
|
duration = sw.elapsed_ms_int()
|
|
600
|
-
|
|
601
|
-
|
|
660
|
+
request._ingest["end_to_end_latency_ms"] = duration
|
|
661
|
+
request._ingest["http_status_code"] = 200
|
|
602
662
|
|
|
603
|
-
return_result: Any =
|
|
663
|
+
return_result: Any = request.process_synchronous_response(
|
|
604
664
|
response=response,
|
|
605
665
|
log_prompt_and_response=self._log_prompt_and_response,
|
|
606
666
|
kwargs=kwargs)
|
|
@@ -608,14 +668,13 @@ class _PayiInstrumentor:
|
|
|
608
668
|
if return_result:
|
|
609
669
|
return return_result
|
|
610
670
|
|
|
611
|
-
await self._aingest_units(
|
|
671
|
+
await self._aingest_units(request._ingest)
|
|
612
672
|
|
|
613
673
|
return response
|
|
614
674
|
|
|
615
675
|
def chat_wrapper(
|
|
616
676
|
self,
|
|
617
|
-
|
|
618
|
-
provider: _ProviderRequest,
|
|
677
|
+
request: _ProviderRequest,
|
|
619
678
|
is_streaming: _IsStreaming,
|
|
620
679
|
wrapped: Any,
|
|
621
680
|
instance: Any,
|
|
@@ -624,10 +683,8 @@ class _PayiInstrumentor:
|
|
|
624
683
|
) -> Any:
|
|
625
684
|
context = self.get_context()
|
|
626
685
|
|
|
627
|
-
is_bedrock:bool = category == PayiCategories.aws_bedrock
|
|
628
|
-
|
|
629
686
|
if not context:
|
|
630
|
-
if is_bedrock:
|
|
687
|
+
if request.is_bedrock():
|
|
631
688
|
# boto3 doesn't allow extra_headers
|
|
632
689
|
kwargs.pop("extra_headers", None)
|
|
633
690
|
|
|
@@ -638,50 +695,24 @@ class _PayiInstrumentor:
|
|
|
638
695
|
extra_headers = kwargs.get("extra_headers", {})
|
|
639
696
|
self._update_extra_headers(context, extra_headers)
|
|
640
697
|
|
|
641
|
-
if context.get("proxy",
|
|
642
|
-
if
|
|
698
|
+
if context.get("proxy", self._proxy_default):
|
|
699
|
+
if request.is_bedrock():
|
|
700
|
+
# boto3 doesn't allow extra_headers
|
|
701
|
+
kwargs.pop("extra_headers", None)
|
|
702
|
+
elif "extra_headers" not in kwargs and extra_headers:
|
|
703
|
+
# assumes anthropic and openai clients
|
|
643
704
|
kwargs["extra_headers"] = extra_headers
|
|
644
705
|
|
|
645
706
|
return wrapped(*args, **kwargs)
|
|
646
707
|
|
|
647
|
-
provider._ingest = {"category": category, "units": {}} # type: ignore
|
|
648
|
-
if is_bedrock:
|
|
649
|
-
# boto3 doesn't allow extra_headers
|
|
650
|
-
kwargs.pop("extra_headers", None)
|
|
651
|
-
provider._ingest["resource"] = kwargs.get("modelId", "")
|
|
652
|
-
else:
|
|
653
|
-
provider._ingest["resource"] = kwargs.get("model", "")
|
|
654
|
-
|
|
655
|
-
if category == PayiCategories.openai and instance and hasattr(instance, "_client"):
|
|
656
|
-
from .OpenAIInstrumentor import OpenAiInstrumentor # noqa: I001
|
|
657
|
-
|
|
658
|
-
if OpenAiInstrumentor.is_azure(instance):
|
|
659
|
-
route_as_resource:str = extra_headers.pop(PayiHeaderNames.route_as_resource, None)
|
|
660
|
-
resource_scope:str = extra_headers.pop(PayiHeaderNames.resource_scope, None)
|
|
661
|
-
|
|
662
|
-
if not route_as_resource:
|
|
663
|
-
logging.error("Azure OpenAI route as resource not found, not ingesting")
|
|
664
|
-
return wrapped(*args, **kwargs)
|
|
665
|
-
|
|
666
|
-
if resource_scope:
|
|
667
|
-
if not(resource_scope in ["global", "datazone"] or resource_scope.startswith("region")):
|
|
668
|
-
logging.error("Azure OpenAI invalid resource scope, not ingesting")
|
|
669
|
-
return wrapped(*args, **kwargs)
|
|
670
|
-
|
|
671
|
-
provider._ingest["resource_scope"] = resource_scope
|
|
672
|
-
|
|
673
|
-
category = PayiCategories.azure_openai
|
|
674
|
-
|
|
675
|
-
provider._ingest["category"] = category
|
|
676
|
-
provider._ingest["resource"] = route_as_resource
|
|
677
|
-
|
|
678
708
|
current_frame = inspect.currentframe()
|
|
679
709
|
# f_back excludes the current frame, strip() cleans up whitespace and newlines
|
|
680
710
|
stack = [frame.strip() for frame in traceback.format_stack(current_frame.f_back)] # type: ignore
|
|
681
711
|
|
|
682
|
-
|
|
712
|
+
request._ingest['properties'] = { 'system.stack_trace': json.dumps(stack) }
|
|
683
713
|
|
|
684
|
-
|
|
714
|
+
if request.process_request(instance, extra_headers, kwargs) is False:
|
|
715
|
+
return wrapped(*args, **kwargs)
|
|
685
716
|
|
|
686
717
|
sw = Stopwatch()
|
|
687
718
|
stream: bool = False
|
|
@@ -694,7 +725,7 @@ class _PayiInstrumentor:
|
|
|
694
725
|
stream = False
|
|
695
726
|
|
|
696
727
|
try:
|
|
697
|
-
self._prepare_ingest(
|
|
728
|
+
self._prepare_ingest(request._ingest, extra_headers, kwargs)
|
|
698
729
|
sw.start()
|
|
699
730
|
response = wrapped(*args, **kwargs)
|
|
700
731
|
|
|
@@ -702,7 +733,9 @@ class _PayiInstrumentor:
|
|
|
702
733
|
sw.stop()
|
|
703
734
|
duration = sw.elapsed_ms_int()
|
|
704
735
|
|
|
705
|
-
|
|
736
|
+
if request.process_exception(e, kwargs):
|
|
737
|
+
request._ingest["end_to_end_latency_ms"] = duration
|
|
738
|
+
self._ingest_units(request._ingest)
|
|
706
739
|
|
|
707
740
|
raise e
|
|
708
741
|
|
|
@@ -713,11 +746,10 @@ class _PayiInstrumentor:
|
|
|
713
746
|
instrumentor=self,
|
|
714
747
|
log_prompt_and_response=self._log_prompt_and_response,
|
|
715
748
|
stopwatch=sw,
|
|
716
|
-
|
|
717
|
-
is_bedrock=is_bedrock,
|
|
749
|
+
request=request,
|
|
718
750
|
)
|
|
719
751
|
|
|
720
|
-
if is_bedrock:
|
|
752
|
+
if request.is_bedrock():
|
|
721
753
|
if "body" in response:
|
|
722
754
|
response["body"] = stream_result
|
|
723
755
|
else:
|
|
@@ -728,20 +760,30 @@ class _PayiInstrumentor:
|
|
|
728
760
|
|
|
729
761
|
sw.stop()
|
|
730
762
|
duration = sw.elapsed_ms_int()
|
|
731
|
-
|
|
732
|
-
|
|
763
|
+
request._ingest["end_to_end_latency_ms"] = duration
|
|
764
|
+
request._ingest["http_status_code"] = 200
|
|
733
765
|
|
|
734
|
-
return_result: Any =
|
|
766
|
+
return_result: Any = request.process_synchronous_response(
|
|
735
767
|
response=response,
|
|
736
768
|
log_prompt_and_response=self._log_prompt_and_response,
|
|
737
769
|
kwargs=kwargs)
|
|
738
770
|
if return_result:
|
|
739
771
|
return return_result
|
|
740
772
|
|
|
741
|
-
self._ingest_units(
|
|
773
|
+
self._ingest_units(request._ingest)
|
|
742
774
|
|
|
743
775
|
return response
|
|
744
776
|
|
|
777
|
+
def _create_extra_headers(
|
|
778
|
+
self
|
|
779
|
+
) -> 'dict[str, str]':
|
|
780
|
+
extra_headers: dict[str, str] = {}
|
|
781
|
+
context = self.get_context()
|
|
782
|
+
if context:
|
|
783
|
+
self._update_extra_headers(context, extra_headers)
|
|
784
|
+
|
|
785
|
+
return extra_headers
|
|
786
|
+
|
|
745
787
|
@staticmethod
|
|
746
788
|
def _update_extra_headers(
|
|
747
789
|
context: _Context,
|
|
@@ -754,6 +796,9 @@ class _PayiInstrumentor:
|
|
|
754
796
|
context_use_case_id: Optional[str] = context.get("use_case_id")
|
|
755
797
|
context_use_case_version: Optional[int] = context.get("use_case_version")
|
|
756
798
|
context_user_id: Optional[str] = context.get("user_id")
|
|
799
|
+
context_request_tags: Optional[list[str]] = context.get("request_tags")
|
|
800
|
+
context_route_as_resource: Optional[str] = context.get("route_as_resource")
|
|
801
|
+
context_resource_scope: Optional[str] = context.get("resource_scope")
|
|
757
802
|
|
|
758
803
|
# headers_limit_ids = extra_headers.get(PayiHeaderNames.limit_ids, None)
|
|
759
804
|
|
|
@@ -812,6 +857,15 @@ class _PayiInstrumentor:
|
|
|
812
857
|
if context_experience_id is not None:
|
|
813
858
|
extra_headers[PayiHeaderNames.experience_id] = context_experience_id
|
|
814
859
|
|
|
860
|
+
if PayiHeaderNames.request_tags not in extra_headers and context_request_tags:
|
|
861
|
+
extra_headers[PayiHeaderNames.request_tags] = ",".join(context_request_tags)
|
|
862
|
+
|
|
863
|
+
if PayiHeaderNames.route_as_resource not in extra_headers and context_route_as_resource:
|
|
864
|
+
extra_headers[PayiHeaderNames.route_as_resource] = context_route_as_resource
|
|
865
|
+
|
|
866
|
+
if PayiHeaderNames.resource_scope not in extra_headers and context_resource_scope:
|
|
867
|
+
extra_headers[PayiHeaderNames.resource_scope] = context_resource_scope
|
|
868
|
+
|
|
815
869
|
@staticmethod
|
|
816
870
|
def update_for_vision(input: int, units: 'dict[str, Units]', estimated_prompt_tokens: Optional[int]) -> int:
|
|
817
871
|
if estimated_prompt_tokens:
|
|
@@ -861,14 +915,13 @@ class ChatStreamWrapper(ObjectProxy): # type: ignore
|
|
|
861
915
|
instance: Any,
|
|
862
916
|
instrumentor: _PayiInstrumentor,
|
|
863
917
|
stopwatch: Stopwatch,
|
|
864
|
-
|
|
918
|
+
request: _ProviderRequest,
|
|
865
919
|
log_prompt_and_response: bool = True,
|
|
866
|
-
is_bedrock: bool = False,
|
|
867
920
|
) -> None:
|
|
868
921
|
|
|
869
922
|
bedrock_from_stream: bool = False
|
|
870
|
-
if is_bedrock:
|
|
871
|
-
|
|
923
|
+
if request.is_bedrock():
|
|
924
|
+
request._ingest["provider_response_id"] = response["ResponseMetadata"]["RequestId"]
|
|
872
925
|
stream = response.get("stream", None)
|
|
873
926
|
|
|
874
927
|
if stream:
|
|
@@ -888,10 +941,10 @@ class ChatStreamWrapper(ObjectProxy): # type: ignore
|
|
|
888
941
|
self._log_prompt_and_response: bool = log_prompt_and_response
|
|
889
942
|
self._responses: list[str] = []
|
|
890
943
|
|
|
891
|
-
self.
|
|
944
|
+
self._request: _ProviderRequest = request
|
|
892
945
|
|
|
893
946
|
self._first_token: bool = True
|
|
894
|
-
self._is_bedrock: bool = is_bedrock
|
|
947
|
+
self._is_bedrock: bool = request.is_bedrock()
|
|
895
948
|
self._bedrock_from_stream: bool = bedrock_from_stream
|
|
896
949
|
|
|
897
950
|
def __enter__(self) -> Any:
|
|
@@ -929,9 +982,9 @@ class ChatStreamWrapper(ObjectProxy): # type: ignore
|
|
|
929
982
|
def __aiter__(self) -> Any:
|
|
930
983
|
return self
|
|
931
984
|
|
|
932
|
-
def __next__(self) ->
|
|
985
|
+
def __next__(self) -> object:
|
|
933
986
|
try:
|
|
934
|
-
chunk:
|
|
987
|
+
chunk: object = self.__wrapped__.__next__() # type: ignore
|
|
935
988
|
except Exception as e:
|
|
936
989
|
if isinstance(e, StopIteration):
|
|
937
990
|
self._stop_iteration()
|
|
@@ -940,11 +993,11 @@ class ChatStreamWrapper(ObjectProxy): # type: ignore
|
|
|
940
993
|
if self._evaluate_chunk(chunk) == False:
|
|
941
994
|
return self.__next__()
|
|
942
995
|
|
|
943
|
-
return chunk
|
|
996
|
+
return chunk # type: ignore
|
|
944
997
|
|
|
945
|
-
async def __anext__(self) ->
|
|
998
|
+
async def __anext__(self) -> object:
|
|
946
999
|
try:
|
|
947
|
-
chunk:
|
|
1000
|
+
chunk: object = await self.__wrapped__.__anext__() # type: ignore
|
|
948
1001
|
except Exception as e:
|
|
949
1002
|
if isinstance(e, StopAsyncIteration):
|
|
950
1003
|
await self._astop_iteration()
|
|
@@ -952,33 +1005,34 @@ class ChatStreamWrapper(ObjectProxy): # type: ignore
|
|
|
952
1005
|
else:
|
|
953
1006
|
if self._evaluate_chunk(chunk) == False:
|
|
954
1007
|
return await self.__anext__()
|
|
955
|
-
|
|
1008
|
+
|
|
1009
|
+
return chunk # type: ignore
|
|
956
1010
|
|
|
957
1011
|
def _evaluate_chunk(self, chunk: Any) -> bool:
|
|
958
1012
|
if self._first_token:
|
|
959
|
-
self.
|
|
1013
|
+
self._request._ingest["time_to_first_token_ms"] = self._stopwatch.elapsed_ms_int()
|
|
960
1014
|
self._first_token = False
|
|
961
1015
|
|
|
962
1016
|
if self._log_prompt_and_response:
|
|
963
1017
|
self._responses.append(self.chunk_to_json(chunk))
|
|
964
1018
|
|
|
965
|
-
return self.
|
|
1019
|
+
return self._request.process_chunk(chunk)
|
|
966
1020
|
|
|
967
1021
|
def _process_stop_iteration(self) -> None:
|
|
968
1022
|
self._stopwatch.stop()
|
|
969
|
-
self.
|
|
970
|
-
self.
|
|
1023
|
+
self._request._ingest["end_to_end_latency_ms"] = self._stopwatch.elapsed_ms_int()
|
|
1024
|
+
self._request._ingest["http_status_code"] = 200
|
|
971
1025
|
|
|
972
1026
|
if self._log_prompt_and_response:
|
|
973
|
-
self.
|
|
1027
|
+
self._request._ingest["provider_response_json"] = self._responses
|
|
974
1028
|
|
|
975
1029
|
async def _astop_iteration(self) -> None:
|
|
976
1030
|
self._process_stop_iteration()
|
|
977
|
-
await self._instrumentor._aingest_units(self.
|
|
1031
|
+
await self._instrumentor._aingest_units(self._request._ingest)
|
|
978
1032
|
|
|
979
1033
|
def _stop_iteration(self) -> None:
|
|
980
1034
|
self._process_stop_iteration()
|
|
981
|
-
self._instrumentor._ingest_units(self.
|
|
1035
|
+
self._instrumentor._ingest_units(self._request._ingest)
|
|
982
1036
|
|
|
983
1037
|
@staticmethod
|
|
984
1038
|
def chunk_to_json(chunk: Any) -> str:
|
|
@@ -1019,7 +1073,11 @@ def payi_instrument(
|
|
|
1019
1073
|
payi_param = p
|
|
1020
1074
|
elif isinstance(p, AsyncPayi): # type: ignore
|
|
1021
1075
|
apayi_param = p
|
|
1022
|
-
|
|
1076
|
+
frameinfo = inspect.stack()[1]
|
|
1077
|
+
caller_filename = os.path.basename(frameinfo.filename).replace(' ', '_').lower()
|
|
1078
|
+
if caller_filename.endswith('.py'):
|
|
1079
|
+
caller_filename = caller_filename[:-3]
|
|
1080
|
+
|
|
1023
1081
|
# allow for both payi and apayi to be None for the @proxy case
|
|
1024
1082
|
_instrumentor = _PayiInstrumentor(
|
|
1025
1083
|
payi=payi_param,
|
|
@@ -1027,9 +1085,92 @@ def payi_instrument(
|
|
|
1027
1085
|
instruments=instruments,
|
|
1028
1086
|
log_prompt_and_response=log_prompt_and_response,
|
|
1029
1087
|
prompt_and_response_logger=prompt_and_response_logger,
|
|
1030
|
-
global_config=config,
|
|
1088
|
+
global_config=config if config else PayiInstrumentConfig(),
|
|
1089
|
+
caller_filename=caller_filename
|
|
1031
1090
|
)
|
|
1032
1091
|
|
|
1092
|
+
def track(
|
|
1093
|
+
limit_ids: Optional["list[str]"] = None,
|
|
1094
|
+
use_case_name: Optional[str] = None,
|
|
1095
|
+
use_case_id: Optional[str] = None,
|
|
1096
|
+
use_case_version: Optional[int] = None,
|
|
1097
|
+
user_id: Optional[str] = None,
|
|
1098
|
+
proxy: Optional[bool] = None,
|
|
1099
|
+
) -> Any:
|
|
1100
|
+
|
|
1101
|
+
def _track(func: Any) -> Any:
|
|
1102
|
+
import asyncio
|
|
1103
|
+
if asyncio.iscoroutinefunction(func):
|
|
1104
|
+
async def awrapper(*args: Any, **kwargs: Any) -> Any:
|
|
1105
|
+
if not _instrumentor:
|
|
1106
|
+
return await func(*args, **kwargs)
|
|
1107
|
+
# Call the instrumentor's _call_func for async functions
|
|
1108
|
+
return await _instrumentor._acall_func(
|
|
1109
|
+
func,
|
|
1110
|
+
proxy,
|
|
1111
|
+
limit_ids,
|
|
1112
|
+
None, # experience_name,
|
|
1113
|
+
None, #experience_id,
|
|
1114
|
+
use_case_name,
|
|
1115
|
+
use_case_id,
|
|
1116
|
+
use_case_version,
|
|
1117
|
+
user_id,
|
|
1118
|
+
*args,
|
|
1119
|
+
**kwargs,
|
|
1120
|
+
)
|
|
1121
|
+
return awrapper
|
|
1122
|
+
else:
|
|
1123
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
1124
|
+
if not _instrumentor:
|
|
1125
|
+
return func(*args, **kwargs)
|
|
1126
|
+
return _instrumentor._call_func(
|
|
1127
|
+
func,
|
|
1128
|
+
proxy,
|
|
1129
|
+
limit_ids,
|
|
1130
|
+
None, # experience_name,
|
|
1131
|
+
None, # experience_id,
|
|
1132
|
+
use_case_name,
|
|
1133
|
+
use_case_id,
|
|
1134
|
+
use_case_version,
|
|
1135
|
+
user_id,
|
|
1136
|
+
*args,
|
|
1137
|
+
**kwargs,
|
|
1138
|
+
)
|
|
1139
|
+
return wrapper
|
|
1140
|
+
return _track
|
|
1141
|
+
|
|
1142
|
+
def track_context(
|
|
1143
|
+
limit_ids: Optional["list[str]"] = None,
|
|
1144
|
+
experience_name: Optional[str] = None,
|
|
1145
|
+
experience_id: Optional[str] = None,
|
|
1146
|
+
use_case_name: Optional[str] = None,
|
|
1147
|
+
use_case_id: Optional[str] = None,
|
|
1148
|
+
use_case_version: Optional[int] = None,
|
|
1149
|
+
user_id: Optional[str] = None,
|
|
1150
|
+
request_tags: Optional["list[str]"] = None,
|
|
1151
|
+
route_as_resource: Optional[str] = None,
|
|
1152
|
+
resource_scope: Optional[str] = None,
|
|
1153
|
+
proxy: Optional[bool] = None,
|
|
1154
|
+
) -> _TrackContext:
|
|
1155
|
+
if not _instrumentor:
|
|
1156
|
+
raise RuntimeError("Pay-i instrumentor not initialized. Use payi_instrument() to initialize.")
|
|
1157
|
+
|
|
1158
|
+
# Create a new context for tracking
|
|
1159
|
+
context: _Context = {}
|
|
1160
|
+
context["limit_ids"] = limit_ids
|
|
1161
|
+
context["experience_name"] = experience_name
|
|
1162
|
+
context["experience_id"] = experience_id
|
|
1163
|
+
context["use_case_name"] = use_case_name
|
|
1164
|
+
context["use_case_id"] = use_case_id
|
|
1165
|
+
context["use_case_version"] = use_case_version
|
|
1166
|
+
context["user_id"] = user_id
|
|
1167
|
+
context["request_tags"] = request_tags
|
|
1168
|
+
context["route_as_resource"] = route_as_resource
|
|
1169
|
+
context["resource_scope"] = resource_scope
|
|
1170
|
+
context["proxy"] = proxy
|
|
1171
|
+
|
|
1172
|
+
return _TrackContext(context)
|
|
1173
|
+
|
|
1033
1174
|
def ingest(
|
|
1034
1175
|
limit_ids: Optional["list[str]"] = None,
|
|
1035
1176
|
experience_name: Optional[str] = None,
|