payi 0.1.0a41__py3-none-any.whl → 0.1.0a43__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 +9 -2
- payi/_version.py +1 -1
- payi/lib/AnthropicInstrumentor.py +44 -18
- payi/lib/BedrockInstrumentor.py +39 -45
- payi/lib/OpenAIInstrumentor.py +42 -15
- payi/lib/Stopwatch.py +1 -1
- payi/lib/instrument.py +399 -140
- payi/resources/ingest.py +78 -0
- payi/types/__init__.py +3 -0
- payi/types/bulk_ingest_response.py +51 -0
- payi/types/ingest_bulk_params.py +14 -0
- payi/types/ingest_event_param.py +60 -0
- {payi-0.1.0a41.dist-info → payi-0.1.0a43.dist-info}/METADATA +1 -1
- {payi-0.1.0a41.dist-info → payi-0.1.0a43.dist-info}/RECORD +16 -13
- {payi-0.1.0a41.dist-info → payi-0.1.0a43.dist-info}/WHEEL +0 -0
- {payi-0.1.0a41.dist-info → payi-0.1.0a43.dist-info}/licenses/LICENSE +0 -0
payi/lib/instrument.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import uuid
|
|
3
|
-
import asyncio
|
|
4
3
|
import inspect
|
|
5
4
|
import logging
|
|
6
5
|
import traceback
|
|
@@ -11,6 +10,7 @@ from wrapt import ObjectProxy # type: ignore
|
|
|
11
10
|
|
|
12
11
|
from payi import Payi, AsyncPayi
|
|
13
12
|
from payi.types import IngestUnitsParams
|
|
13
|
+
from payi.types.ingest_response import IngestResponse
|
|
14
14
|
from payi.types.ingest_units_params import Units
|
|
15
15
|
from payi.types.pay_i_common_models_api_router_header_info_param import PayICommonModelsAPIRouterHeaderInfoParam
|
|
16
16
|
|
|
@@ -88,79 +88,92 @@ class PayiInstrumentor:
|
|
|
88
88
|
except Exception as e:
|
|
89
89
|
logging.error(f"Error instrumenting AWS bedrock: {e}")
|
|
90
90
|
|
|
91
|
-
def
|
|
92
|
-
# return early if there are no units to ingest and on a successul ingest request
|
|
91
|
+
def _process_ingest_units(self, ingest_units: IngestUnitsParams, log_data: 'dict[str, str]') -> bool:
|
|
93
92
|
if int(ingest_units.get("http_status_code") or 0) < 400:
|
|
94
93
|
units = ingest_units.get("units", {})
|
|
95
94
|
if not units or all(unit.get("input", 0) == 0 and unit.get("output", 0) == 0 for unit in units.values()):
|
|
96
95
|
logging.error(
|
|
97
96
|
'No units to ingest. For OpenAI streaming calls, make sure you pass stream_options={"include_usage": True}'
|
|
98
97
|
)
|
|
99
|
-
return
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
if self._log_prompt_and_response and self._prompt_and_response_logger:
|
|
101
|
+
response_json = ingest_units.pop("provider_response_json", None)
|
|
102
|
+
request_json = ingest_units.pop("provider_request_json", None)
|
|
103
|
+
stack_trace = ingest_units.get("properties", {}).pop("system.stack_trace", None) # type: ignore
|
|
104
|
+
|
|
105
|
+
if response_json is not None:
|
|
106
|
+
# response_json is a list of strings, convert a single json string
|
|
107
|
+
log_data["provider_response_json"] = json.dumps(response_json)
|
|
108
|
+
if request_json is not None:
|
|
109
|
+
log_data["provider_request_json"] = request_json
|
|
110
|
+
if stack_trace is not None:
|
|
111
|
+
log_data["stack_trace"] = stack_trace
|
|
112
|
+
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
def _process_ingest_units_response(self, ingest_response: IngestResponse) -> None:
|
|
116
|
+
if ingest_response.xproxy_result.limits:
|
|
117
|
+
for limit_id, state in ingest_response.xproxy_result.limits.items():
|
|
118
|
+
removeBlockedId: bool = False
|
|
119
|
+
|
|
120
|
+
if state.state == "blocked":
|
|
121
|
+
self._blocked_limits.add(limit_id)
|
|
122
|
+
elif state.state == "exceeded":
|
|
123
|
+
self._exceeded_limits.add(limit_id)
|
|
124
|
+
removeBlockedId = True
|
|
125
|
+
elif state.state == "ok":
|
|
126
|
+
removeBlockedId = True
|
|
127
|
+
|
|
128
|
+
# opportunistically remove blocked limits
|
|
129
|
+
if removeBlockedId:
|
|
130
|
+
self._blocked_limits.discard(limit_id)
|
|
131
|
+
|
|
132
|
+
async def _aingest_units(self, ingest_units: IngestUnitsParams) -> None:
|
|
133
|
+
# return early if there are no units to ingest and on a successul ingest request
|
|
134
|
+
log_data: 'dict[str,str]' = {}
|
|
135
|
+
if not self._process_ingest_units(ingest_units, log_data):
|
|
136
|
+
return
|
|
100
137
|
|
|
101
138
|
try:
|
|
102
139
|
if isinstance(self._payi, AsyncPayi):
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
ingest_result = self._payi.ingest.units(**ingest_units)
|
|
140
|
+
ingest_response= await self._payi.ingest.units(**ingest_units)
|
|
141
|
+
|
|
142
|
+
self._process_ingest_units_response(ingest_response)
|
|
143
|
+
|
|
144
|
+
if self._log_prompt_and_response and self._prompt_and_response_logger:
|
|
145
|
+
request_id = ingest_response.xproxy_result.request_id
|
|
146
|
+
self._prompt_and_response_logger(request_id, log_data) # type: ignore
|
|
111
147
|
else:
|
|
112
148
|
logging.error("No payi instance to ingest units")
|
|
113
149
|
return
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logging.error(f"Error Pay-i ingesting result: {e}")
|
|
114
152
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
self._blocked_limits.add(limit_id)
|
|
121
|
-
elif state.state == "exceeded":
|
|
122
|
-
self._exceeded_limits.add(limit_id)
|
|
123
|
-
removeBlockedId = True
|
|
124
|
-
elif state.state == "ok":
|
|
125
|
-
removeBlockedId = True
|
|
126
|
-
|
|
127
|
-
# opportunistically remove blocked limits
|
|
128
|
-
if removeBlockedId:
|
|
129
|
-
self._blocked_limits.discard(limit_id)
|
|
130
|
-
|
|
131
|
-
if self._log_prompt_and_response and self._prompt_and_response_logger:
|
|
132
|
-
request_id = ingest_result.xproxy_result.request_id
|
|
133
|
-
|
|
134
|
-
log_data = {}
|
|
135
|
-
response_json = ingest_units.pop("provider_response_json", None)
|
|
136
|
-
request_json = ingest_units.pop("provider_request_json", None)
|
|
137
|
-
stack_trace = ingest_units.get("properties", {}).pop("system.stack_trace", None) # type: ignore
|
|
153
|
+
def _ingest_units(self, ingest_units: IngestUnitsParams) -> None:
|
|
154
|
+
# return early if there are no units to ingest and on a successul ingest request
|
|
155
|
+
log_data: 'dict[str,str]' = {}
|
|
156
|
+
if not self._process_ingest_units(ingest_units, log_data):
|
|
157
|
+
return
|
|
138
158
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if request_json is not None:
|
|
143
|
-
log_data["provider_request_json"] = request_json
|
|
144
|
-
if stack_trace is not None:
|
|
145
|
-
log_data["stack_trace"] = stack_trace
|
|
159
|
+
try:
|
|
160
|
+
if isinstance(self._payi, Payi):
|
|
161
|
+
ingest_response = self._payi.ingest.units(**ingest_units)
|
|
146
162
|
|
|
147
|
-
self.
|
|
163
|
+
self._process_ingest_units_response(ingest_response)
|
|
148
164
|
|
|
165
|
+
if self._log_prompt_and_response and self._prompt_and_response_logger:
|
|
166
|
+
request_id = ingest_response.xproxy_result.request_id
|
|
167
|
+
self._prompt_and_response_logger(request_id, log_data) # type: ignore
|
|
168
|
+
else:
|
|
169
|
+
logging.error("No payi instance to ingest units")
|
|
170
|
+
return
|
|
149
171
|
except Exception as e:
|
|
150
172
|
logging.error(f"Error Pay-i ingesting result: {e}")
|
|
151
173
|
|
|
152
|
-
def
|
|
153
|
-
self
|
|
154
|
-
|
|
155
|
-
proxy: bool,
|
|
156
|
-
limit_ids: Optional["list[str]"],
|
|
157
|
-
request_tags: Optional["list[str]"],
|
|
158
|
-
experience_name: Optional[str],
|
|
159
|
-
experience_id: Optional[str],
|
|
160
|
-
user_id: Optional[str],
|
|
161
|
-
*args: Any,
|
|
162
|
-
**kwargs: Any,
|
|
163
|
-
) -> Any:
|
|
174
|
+
def _setup_call_func(
|
|
175
|
+
self
|
|
176
|
+
) -> 'tuple[dict[str, Any], Optional[str], Optional[str]]':
|
|
164
177
|
if len(self._context_stack) > 0:
|
|
165
178
|
# copy current context into the upcoming context
|
|
166
179
|
context = self._context_stack[-1].copy()
|
|
@@ -171,36 +184,100 @@ class PayiInstrumentor:
|
|
|
171
184
|
context = {}
|
|
172
185
|
previous_experience_name = None
|
|
173
186
|
previous_experience_id = None
|
|
187
|
+
return (context, previous_experience_name, previous_experience_id)
|
|
174
188
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
189
|
+
def _init_context(
|
|
190
|
+
self,
|
|
191
|
+
context: "dict[str, Any]",
|
|
192
|
+
previous_experience_name: Optional[str],
|
|
193
|
+
previous_experience_id: Optional[str],
|
|
194
|
+
proxy: bool,
|
|
195
|
+
limit_ids: Optional["list[str]"],
|
|
196
|
+
request_tags: Optional["list[str]"],
|
|
197
|
+
experience_name: Optional[str],
|
|
198
|
+
experience_id: Optional[str],
|
|
199
|
+
user_id: Optional[str],
|
|
200
|
+
) -> None:
|
|
201
|
+
context["proxy"] = proxy
|
|
202
|
+
|
|
203
|
+
# Handle experience name and ID logic
|
|
204
|
+
if not experience_name:
|
|
205
|
+
# If no experience_name specified, use previous values
|
|
206
|
+
context["experience_name"] = previous_experience_name
|
|
207
|
+
context["experience_id"] = previous_experience_id
|
|
208
|
+
else:
|
|
209
|
+
# If experience_name is specified
|
|
210
|
+
if experience_name == previous_experience_name:
|
|
211
|
+
# Same experience name, use previous ID unless new one specified
|
|
212
|
+
context["experience_name"] = experience_name
|
|
213
|
+
context["experience_id"] = experience_id if experience_id else previous_experience_id
|
|
183
214
|
else:
|
|
184
|
-
#
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
215
|
+
# Different experience name, use specified ID or generate one
|
|
216
|
+
context["experience_name"] = experience_name
|
|
217
|
+
context["experience_id"] = experience_id if experience_id else str(uuid.uuid4())
|
|
218
|
+
|
|
219
|
+
# set any values explicitly passed by the caller, otherwise use what is already in the context
|
|
220
|
+
if limit_ids:
|
|
221
|
+
context["limit_ids"] = limit_ids
|
|
222
|
+
if request_tags:
|
|
223
|
+
context["request_tags"] = request_tags
|
|
224
|
+
if user_id:
|
|
225
|
+
context["user_id"] = user_id
|
|
226
|
+
|
|
227
|
+
self.set_context(context)
|
|
228
|
+
|
|
229
|
+
async def _acall_func(
|
|
230
|
+
self,
|
|
231
|
+
func: Any,
|
|
232
|
+
proxy: bool,
|
|
233
|
+
limit_ids: Optional["list[str]"],
|
|
234
|
+
request_tags: Optional["list[str]"],
|
|
235
|
+
experience_name: Optional[str],
|
|
236
|
+
experience_id: Optional[str],
|
|
237
|
+
user_id: Optional[str],
|
|
238
|
+
*args: Any,
|
|
239
|
+
**kwargs: Any,
|
|
240
|
+
) -> Any:
|
|
241
|
+
context, previous_experience_name, previous_experience_id = self._setup_call_func()
|
|
193
242
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
context
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
243
|
+
with self:
|
|
244
|
+
self._init_context(
|
|
245
|
+
context,
|
|
246
|
+
previous_experience_name,
|
|
247
|
+
previous_experience_id,
|
|
248
|
+
proxy,
|
|
249
|
+
limit_ids,
|
|
250
|
+
request_tags,
|
|
251
|
+
experience_name,
|
|
252
|
+
experience_id,
|
|
253
|
+
user_id)
|
|
254
|
+
return await func(*args, **kwargs)
|
|
201
255
|
|
|
202
|
-
|
|
256
|
+
def _call_func(
|
|
257
|
+
self,
|
|
258
|
+
func: Any,
|
|
259
|
+
proxy: bool,
|
|
260
|
+
limit_ids: Optional["list[str]"],
|
|
261
|
+
request_tags: Optional["list[str]"],
|
|
262
|
+
experience_name: Optional[str],
|
|
263
|
+
experience_id: Optional[str],
|
|
264
|
+
user_id: Optional[str],
|
|
265
|
+
*args: Any,
|
|
266
|
+
**kwargs: Any,
|
|
267
|
+
) -> Any:
|
|
268
|
+
context, previous_experience_name, previous_experience_id = self._setup_call_func()
|
|
203
269
|
|
|
270
|
+
with self:
|
|
271
|
+
self._init_context(
|
|
272
|
+
context,
|
|
273
|
+
previous_experience_name,
|
|
274
|
+
previous_experience_id,
|
|
275
|
+
proxy,
|
|
276
|
+
limit_ids,
|
|
277
|
+
request_tags,
|
|
278
|
+
experience_name,
|
|
279
|
+
experience_id,
|
|
280
|
+
user_id)
|
|
204
281
|
return func(*args, **kwargs)
|
|
205
282
|
|
|
206
283
|
def __enter__(self) -> Any:
|
|
@@ -222,11 +299,50 @@ class PayiInstrumentor:
|
|
|
222
299
|
# Return the current top of the stack
|
|
223
300
|
return self._context_stack[-1] if self._context_stack else None
|
|
224
301
|
|
|
225
|
-
|
|
302
|
+
|
|
303
|
+
def _prepare_ingest(
|
|
304
|
+
self,
|
|
305
|
+
ingest: IngestUnitsParams,
|
|
306
|
+
ingest_extra_headers: "dict[str, str]", # do not coflict potential kwargs["extra_headers"]
|
|
307
|
+
**kwargs: Any,
|
|
308
|
+
) -> None:
|
|
309
|
+
limit_ids = ingest_extra_headers.pop("xProxy-Limit-IDs", None)
|
|
310
|
+
request_tags = ingest_extra_headers.pop("xProxy-Request-Tags", None)
|
|
311
|
+
experience_name = ingest_extra_headers.pop("xProxy-Experience-Name", None)
|
|
312
|
+
experience_id = ingest_extra_headers.pop("xProxy-Experience-ID", None)
|
|
313
|
+
user_id = ingest_extra_headers.pop("xProxy-User-ID", None)
|
|
314
|
+
|
|
315
|
+
if limit_ids:
|
|
316
|
+
ingest["limit_ids"] = limit_ids.split(",")
|
|
317
|
+
if request_tags:
|
|
318
|
+
ingest["request_tags"] = request_tags.split(",")
|
|
319
|
+
if experience_name:
|
|
320
|
+
ingest["experience_name"] = experience_name
|
|
321
|
+
if experience_id:
|
|
322
|
+
ingest["experience_id"] = experience_id
|
|
323
|
+
if user_id:
|
|
324
|
+
ingest["user_id"] = user_id
|
|
325
|
+
|
|
326
|
+
if len(ingest_extra_headers) > 0:
|
|
327
|
+
ingest["provider_request_headers"] = [PayICommonModelsAPIRouterHeaderInfoParam(name=k, value=v) for k, v in ingest_extra_headers.items()]
|
|
328
|
+
|
|
329
|
+
provider_prompt = {}
|
|
330
|
+
for k, v in kwargs.items():
|
|
331
|
+
if k == "messages":
|
|
332
|
+
provider_prompt[k] = [m.model_dump() if hasattr(m, "model_dump") else m for m in v]
|
|
333
|
+
elif k in ["extra_headers", "extra_query"]:
|
|
334
|
+
pass
|
|
335
|
+
else:
|
|
336
|
+
provider_prompt[k] = v
|
|
337
|
+
|
|
338
|
+
if self._log_prompt_and_response:
|
|
339
|
+
ingest["provider_request_json"] = json.dumps(provider_prompt)
|
|
340
|
+
|
|
341
|
+
async def achat_wrapper(
|
|
226
342
|
self,
|
|
227
343
|
category: str,
|
|
228
344
|
process_chunk: Optional[Callable[[Any, IngestUnitsParams], None]],
|
|
229
|
-
process_request: Optional[Callable[[IngestUnitsParams, Any], None]],
|
|
345
|
+
process_request: Optional[Callable[[IngestUnitsParams, Any, Any], None]],
|
|
230
346
|
process_synchronous_response: Any,
|
|
231
347
|
is_streaming: IsStreaming,
|
|
232
348
|
wrapped: Any,
|
|
@@ -244,7 +360,7 @@ class PayiInstrumentor:
|
|
|
244
360
|
kwargs.pop("extra_headers", None)
|
|
245
361
|
|
|
246
362
|
# wrapped function invoked outside of decorator scope
|
|
247
|
-
return wrapped(*args, **kwargs)
|
|
363
|
+
return await wrapped(*args, **kwargs)
|
|
248
364
|
|
|
249
365
|
# after _udpate_headers, all metadata to add to ingest is in extra_headers, keyed by the xproxy-xxx header name
|
|
250
366
|
extra_headers = kwargs.get("extra_headers", {})
|
|
@@ -254,7 +370,7 @@ class PayiInstrumentor:
|
|
|
254
370
|
if "extra_headers" not in kwargs:
|
|
255
371
|
kwargs["extra_headers"] = extra_headers
|
|
256
372
|
|
|
257
|
-
return wrapped(*args, **kwargs)
|
|
373
|
+
return await wrapped(*args, **kwargs)
|
|
258
374
|
|
|
259
375
|
ingest: IngestUnitsParams = {"category": category, "units": {}} # type: ignore
|
|
260
376
|
if is_bedrock:
|
|
@@ -271,7 +387,7 @@ class PayiInstrumentor:
|
|
|
271
387
|
ingest['properties'] = { 'system.stack_trace': json.dumps(stack) }
|
|
272
388
|
|
|
273
389
|
if process_request:
|
|
274
|
-
process_request(ingest,
|
|
390
|
+
process_request(ingest, (), instance)
|
|
275
391
|
|
|
276
392
|
sw = Stopwatch()
|
|
277
393
|
stream: bool = False
|
|
@@ -284,40 +400,122 @@ class PayiInstrumentor:
|
|
|
284
400
|
stream = False
|
|
285
401
|
|
|
286
402
|
try:
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
ingest
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
403
|
+
self._prepare_ingest(ingest, extra_headers, **kwargs)
|
|
404
|
+
sw.start()
|
|
405
|
+
response = await wrapped(*args, **kwargs)
|
|
406
|
+
|
|
407
|
+
except Exception as e: # pylint: disable=broad-except
|
|
408
|
+
sw.stop()
|
|
409
|
+
duration = sw.elapsed_ms_int()
|
|
410
|
+
|
|
411
|
+
# TODO ingest error
|
|
412
|
+
|
|
413
|
+
raise e
|
|
414
|
+
|
|
415
|
+
if stream:
|
|
416
|
+
stream_result = ChatStreamWrapper(
|
|
417
|
+
response=response,
|
|
418
|
+
instance=instance,
|
|
419
|
+
instrumentor=self,
|
|
420
|
+
log_prompt_and_response=self._log_prompt_and_response,
|
|
421
|
+
ingest=ingest,
|
|
422
|
+
stopwatch=sw,
|
|
423
|
+
process_chunk=process_chunk,
|
|
424
|
+
is_bedrock=is_bedrock,
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
if is_bedrock:
|
|
428
|
+
if "body" in response:
|
|
429
|
+
response["body"] = stream_result
|
|
313
430
|
else:
|
|
314
|
-
|
|
431
|
+
response["stream"] = stream_result
|
|
432
|
+
return response
|
|
433
|
+
|
|
434
|
+
return stream_result
|
|
435
|
+
|
|
436
|
+
sw.stop()
|
|
437
|
+
duration = sw.elapsed_ms_int()
|
|
438
|
+
ingest["end_to_end_latency_ms"] = duration
|
|
439
|
+
ingest["http_status_code"] = 200
|
|
440
|
+
|
|
441
|
+
if process_synchronous_response:
|
|
442
|
+
return_result: Any = process_synchronous_response(
|
|
443
|
+
response=response,
|
|
444
|
+
ingest=ingest,
|
|
445
|
+
log_prompt_and_response=self._log_prompt_and_response,
|
|
446
|
+
instrumentor=self)
|
|
447
|
+
if return_result:
|
|
448
|
+
return return_result
|
|
449
|
+
|
|
450
|
+
await self._aingest_units(ingest)
|
|
451
|
+
|
|
452
|
+
return response
|
|
453
|
+
|
|
454
|
+
def chat_wrapper(
|
|
455
|
+
self,
|
|
456
|
+
category: str,
|
|
457
|
+
process_chunk: Optional[Callable[[Any, IngestUnitsParams], None]],
|
|
458
|
+
process_request: Optional[Callable[[IngestUnitsParams, Any, Any], None]],
|
|
459
|
+
process_synchronous_response: Any,
|
|
460
|
+
is_streaming: IsStreaming,
|
|
461
|
+
wrapped: Any,
|
|
462
|
+
instance: Any,
|
|
463
|
+
args: Any,
|
|
464
|
+
kwargs: Any,
|
|
465
|
+
) -> Any:
|
|
466
|
+
context = self.get_context()
|
|
467
|
+
|
|
468
|
+
is_bedrock:bool = category == "system.aws.bedrock"
|
|
469
|
+
|
|
470
|
+
if not context:
|
|
471
|
+
if is_bedrock:
|
|
472
|
+
# boto3 doesn't allow extra_headers
|
|
473
|
+
kwargs.pop("extra_headers", None)
|
|
474
|
+
|
|
475
|
+
# wrapped function invoked outside of decorator scope
|
|
476
|
+
return wrapped(*args, **kwargs)
|
|
477
|
+
|
|
478
|
+
# after _udpate_headers, all metadata to add to ingest is in extra_headers, keyed by the xproxy-xxx header name
|
|
479
|
+
extra_headers = kwargs.get("extra_headers", {})
|
|
480
|
+
self._update_headers(context, extra_headers)
|
|
481
|
+
|
|
482
|
+
if context.get("proxy", True):
|
|
483
|
+
if "extra_headers" not in kwargs:
|
|
484
|
+
kwargs["extra_headers"] = extra_headers
|
|
485
|
+
|
|
486
|
+
return wrapped(*args, **kwargs)
|
|
487
|
+
|
|
488
|
+
ingest: IngestUnitsParams = {"category": category, "units": {}} # type: ignore
|
|
489
|
+
if is_bedrock:
|
|
490
|
+
# boto3 doesn't allow extra_headers
|
|
491
|
+
kwargs.pop("extra_headers", None)
|
|
492
|
+
ingest["resource"] = kwargs.get("modelId", "")
|
|
493
|
+
else:
|
|
494
|
+
ingest["resource"] = kwargs.get("model", "")
|
|
495
|
+
|
|
496
|
+
current_frame = inspect.currentframe()
|
|
497
|
+
# f_back excludes the current frame, strip() cleans up whitespace and newlines
|
|
498
|
+
stack = [frame.strip() for frame in traceback.format_stack(current_frame.f_back)] # type: ignore
|
|
499
|
+
|
|
500
|
+
ingest['properties'] = { 'system.stack_trace': json.dumps(stack) }
|
|
315
501
|
|
|
316
|
-
|
|
317
|
-
|
|
502
|
+
if process_request:
|
|
503
|
+
process_request(ingest, (), kwargs)
|
|
504
|
+
|
|
505
|
+
sw = Stopwatch()
|
|
506
|
+
stream: bool = False
|
|
507
|
+
|
|
508
|
+
if is_streaming == IsStreaming.kwargs:
|
|
509
|
+
stream = kwargs.get("stream", False)
|
|
510
|
+
elif is_streaming == IsStreaming.true:
|
|
511
|
+
stream = True
|
|
512
|
+
else:
|
|
513
|
+
stream = False
|
|
318
514
|
|
|
515
|
+
try:
|
|
516
|
+
self._prepare_ingest(ingest, extra_headers, **kwargs)
|
|
319
517
|
sw.start()
|
|
320
|
-
response = wrapped(*args, **kwargs
|
|
518
|
+
response = wrapped(*args, **kwargs)
|
|
321
519
|
|
|
322
520
|
except Exception as e: # pylint: disable=broad-except
|
|
323
521
|
sw.stop()
|
|
@@ -431,14 +629,30 @@ class PayiInstrumentor:
|
|
|
431
629
|
o,
|
|
432
630
|
wrapped,
|
|
433
631
|
instance,
|
|
434
|
-
args,
|
|
435
|
-
kwargs,
|
|
632
|
+
*args,
|
|
633
|
+
**kwargs,
|
|
436
634
|
)
|
|
437
635
|
|
|
438
636
|
return wrapper
|
|
439
637
|
|
|
440
638
|
return _payi_wrapper
|
|
441
639
|
|
|
640
|
+
@staticmethod
|
|
641
|
+
def payi_awrapper(func: Any) -> Any:
|
|
642
|
+
def _payi_awrapper(o: Any) -> Any:
|
|
643
|
+
async def wrapper(wrapped: Any, instance: Any, args: Any, kwargs: Any) -> Any:
|
|
644
|
+
return await func(
|
|
645
|
+
o,
|
|
646
|
+
wrapped,
|
|
647
|
+
instance,
|
|
648
|
+
*args,
|
|
649
|
+
**kwargs,
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
return wrapper
|
|
653
|
+
|
|
654
|
+
return _payi_awrapper
|
|
655
|
+
|
|
442
656
|
class ChatStreamWrapper(ObjectProxy): # type: ignore
|
|
443
657
|
def __init__(
|
|
444
658
|
self,
|
|
@@ -530,7 +744,7 @@ class ChatStreamWrapper(ObjectProxy): # type: ignore
|
|
|
530
744
|
chunk: Any = await self.__wrapped__.__anext__() # type: ignore
|
|
531
745
|
except Exception as e:
|
|
532
746
|
if isinstance(e, StopAsyncIteration):
|
|
533
|
-
self.
|
|
747
|
+
await self._astop_iteration()
|
|
534
748
|
raise e
|
|
535
749
|
else:
|
|
536
750
|
self._evaluate_chunk(chunk)
|
|
@@ -547,7 +761,7 @@ class ChatStreamWrapper(ObjectProxy): # type: ignore
|
|
|
547
761
|
if self._process_chunk:
|
|
548
762
|
self._process_chunk(chunk, self._ingest)
|
|
549
763
|
|
|
550
|
-
def
|
|
764
|
+
def _process_stop_iteration(self) -> None:
|
|
551
765
|
self._stopwatch.stop()
|
|
552
766
|
self._ingest["end_to_end_latency_ms"] = self._stopwatch.elapsed_ms_int()
|
|
553
767
|
self._ingest["http_status_code"] = 200
|
|
@@ -555,6 +769,12 @@ class ChatStreamWrapper(ObjectProxy): # type: ignore
|
|
|
555
769
|
if self._log_prompt_and_response:
|
|
556
770
|
self._ingest["provider_response_json"] = self._responses
|
|
557
771
|
|
|
772
|
+
async def _astop_iteration(self) -> None:
|
|
773
|
+
self._process_stop_iteration()
|
|
774
|
+
await self._instrumentor._aingest_units(self._ingest)
|
|
775
|
+
|
|
776
|
+
def _stop_iteration(self) -> None:
|
|
777
|
+
self._process_stop_iteration()
|
|
558
778
|
self._instrumentor._ingest_units(self._ingest)
|
|
559
779
|
|
|
560
780
|
@staticmethod
|
|
@@ -586,7 +806,6 @@ def payi_instrument(
|
|
|
586
806
|
prompt_and_response_logger=prompt_and_response_logger,
|
|
587
807
|
)
|
|
588
808
|
|
|
589
|
-
|
|
590
809
|
def ingest(
|
|
591
810
|
limit_ids: Optional["list[str]"] = None,
|
|
592
811
|
request_tags: Optional["list[str]"] = None,
|
|
@@ -595,21 +814,36 @@ def ingest(
|
|
|
595
814
|
user_id: Optional[str] = None,
|
|
596
815
|
) -> Any:
|
|
597
816
|
def _ingest(func: Any) -> Any:
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
817
|
+
import asyncio
|
|
818
|
+
if asyncio.iscoroutinefunction(func):
|
|
819
|
+
async def awrapper(*args: Any, **kwargs: Any) -> Any:
|
|
820
|
+
# Call the instrumentor's _call_func for async functions
|
|
821
|
+
return await _instrumentor._acall_func(
|
|
822
|
+
func,
|
|
823
|
+
False,
|
|
824
|
+
limit_ids,
|
|
825
|
+
request_tags,
|
|
826
|
+
experience_name,
|
|
827
|
+
experience_id,
|
|
828
|
+
user_id,
|
|
829
|
+
*args,
|
|
830
|
+
*kwargs,
|
|
831
|
+
)
|
|
832
|
+
return awrapper
|
|
833
|
+
else:
|
|
834
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
835
|
+
return _instrumentor._call_func(
|
|
836
|
+
func,
|
|
837
|
+
False,
|
|
838
|
+
limit_ids,
|
|
839
|
+
request_tags,
|
|
840
|
+
experience_name,
|
|
841
|
+
experience_id,
|
|
842
|
+
user_id,
|
|
843
|
+
*args,
|
|
844
|
+
**kwargs,
|
|
845
|
+
)
|
|
846
|
+
return wrapper
|
|
613
847
|
return _ingest
|
|
614
848
|
|
|
615
849
|
def proxy(
|
|
@@ -620,11 +854,36 @@ def proxy(
|
|
|
620
854
|
user_id: Optional[str] = None,
|
|
621
855
|
) -> Any:
|
|
622
856
|
def _proxy(func: Any) -> Any:
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
857
|
+
import asyncio
|
|
858
|
+
if asyncio.iscoroutinefunction(func):
|
|
859
|
+
async def _proxy_awrapper(*args: Any, **kwargs: Any) -> Any:
|
|
860
|
+
return await _instrumentor._call_func(
|
|
861
|
+
func,
|
|
862
|
+
True,
|
|
863
|
+
limit_ids,
|
|
864
|
+
request_tags,
|
|
865
|
+
experience_name,
|
|
866
|
+
experience_id,
|
|
867
|
+
user_id,
|
|
868
|
+
*args,
|
|
869
|
+
**kwargs
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
return _proxy_awrapper
|
|
873
|
+
else:
|
|
874
|
+
def _proxy_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
875
|
+
return _instrumentor._call_func(
|
|
876
|
+
func,
|
|
877
|
+
True,
|
|
878
|
+
limit_ids,
|
|
879
|
+
request_tags,
|
|
880
|
+
experience_name,
|
|
881
|
+
experience_id,
|
|
882
|
+
user_id,
|
|
883
|
+
*args,
|
|
884
|
+
**kwargs
|
|
885
|
+
)
|
|
627
886
|
|
|
628
|
-
|
|
887
|
+
return _proxy_wrapper
|
|
629
888
|
|
|
630
889
|
return _proxy
|