veris-ai 1.12.3__py3-none-any.whl → 1.14.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.

Potentially problematic release.


This version of veris-ai might be problematic. Click here for more details.

veris_ai/tool_mock.py CHANGED
@@ -17,7 +17,16 @@ from typing import (
17
17
 
18
18
  from veris_ai.models import ResponseExpectation, ToolCallOptions
19
19
  from veris_ai.api_client import get_api_client
20
- from veris_ai.utils import convert_to_type, extract_json_schema, get_function_parameters
20
+ from veris_ai.utils import (
21
+ convert_to_type,
22
+ execute_callback,
23
+ execute_combined_callback,
24
+ extract_json_schema,
25
+ get_function_parameters,
26
+ get_input_parameters,
27
+ launch_callback_task,
28
+ launch_combined_callback_task,
29
+ )
21
30
 
22
31
  logger = logging.getLogger(__name__)
23
32
 
@@ -238,13 +247,25 @@ class VerisSDK:
238
247
 
239
248
  return decorator
240
249
 
241
- def mock( # noqa: C901, PLR0915
250
+ def mock( # noqa: C901, PLR0915, PLR0913
242
251
  self,
243
252
  mode: Literal["tool", "function"] = "tool",
244
253
  expects_response: bool | None = None,
245
254
  cache_response: bool | None = None,
255
+ input_callback: Callable[..., Any] | None = None,
256
+ output_callback: Callable[[Any], Any] | None = None,
257
+ combined_callback: Callable[..., Any] | None = None,
246
258
  ) -> Callable:
247
- """Decorator for mocking tool calls."""
259
+ """Decorator for mocking tool calls.
260
+
261
+ Args:
262
+ mode: Whether to treat the function as a tool or function
263
+ expects_response: Whether the function expects a response
264
+ cache_response: Whether to cache the response
265
+ input_callback: Callable that receives input parameters as individual arguments
266
+ output_callback: Callable that receives the output value
267
+ combined_callback: Callable that receives both input parameters and mock_output
268
+ """
248
269
  response_expectation = (
249
270
  ResponseExpectation.NONE
250
271
  if (expects_response is False or (expects_response is None and mode == "function"))
@@ -273,9 +294,11 @@ class VerisSDK:
273
294
  f"No session ID found, executing original function: {func.__name__}"
274
295
  )
275
296
  return await func(*args, **kwargs)
297
+
298
+ # Perform the mock call first
276
299
  parameters = get_function_parameters(func, args, kwargs)
277
300
  thread_id = _thread_id_context.get()
278
- return await mock_tool_call_async(
301
+ result = await mock_tool_call_async(
279
302
  func,
280
303
  session_id,
281
304
  parameters,
@@ -283,6 +306,14 @@ class VerisSDK:
283
306
  thread_id,
284
307
  )
285
308
 
309
+ # Launch callbacks as background tasks (non-blocking)
310
+ input_params = get_input_parameters(func, args, kwargs)
311
+ launch_callback_task(input_callback, input_params, unpack=True)
312
+ launch_callback_task(output_callback, result, unpack=False)
313
+ launch_combined_callback_task(combined_callback, input_params, result)
314
+
315
+ return result
316
+
286
317
  @wraps(func)
287
318
  def sync_wrapper(
288
319
  *args: tuple[object, ...],
@@ -295,9 +326,11 @@ class VerisSDK:
295
326
  f"No session ID found, executing original function: {func.__name__}"
296
327
  )
297
328
  return func(*args, **kwargs)
329
+
330
+ # Perform the mock call first
298
331
  parameters = get_function_parameters(func, args, kwargs)
299
332
  thread_id = _thread_id_context.get()
300
- return mock_tool_call(
333
+ result = mock_tool_call(
301
334
  func,
302
335
  session_id,
303
336
  parameters,
@@ -305,13 +338,34 @@ class VerisSDK:
305
338
  thread_id,
306
339
  )
307
340
 
341
+ # Execute callbacks synchronously (can't use async tasks in sync context)
342
+ input_params = get_input_parameters(func, args, kwargs)
343
+ execute_callback(input_callback, input_params, unpack=True)
344
+ execute_callback(output_callback, result, unpack=False)
345
+ execute_combined_callback(combined_callback, input_params, result)
346
+
347
+ return result
348
+
308
349
  # Return the appropriate wrapper based on whether the function is async
309
350
  return async_wrapper if is_async else sync_wrapper
310
351
 
311
352
  return decorator
312
353
 
313
- def stub(self, return_value: Any) -> Callable: # noqa: ANN401
314
- """Decorator for stubbing toolw calls."""
354
+ def stub(
355
+ self,
356
+ return_value: Any, # noqa: ANN401
357
+ input_callback: Callable[..., Any] | None = None,
358
+ output_callback: Callable[[Any], Any] | None = None,
359
+ combined_callback: Callable[..., Any] | None = None,
360
+ ) -> Callable:
361
+ """Decorator for stubbing tool calls.
362
+
363
+ Args:
364
+ return_value: The value to return when the function is stubbed
365
+ input_callback: Callable that receives input parameters as individual arguments
366
+ output_callback: Callable that receives the output value
367
+ combined_callback: Callable that receives both input parameters and mock_output
368
+ """
315
369
 
316
370
  def decorator(func: Callable) -> Callable:
317
371
  # Check if the original function is async
@@ -327,7 +381,15 @@ class VerisSDK:
327
381
  f"No session ID found, executing original function: {func.__name__}"
328
382
  )
329
383
  return await func(*args, **kwargs)
384
+
330
385
  logger.info(f"Stubbing function: {func.__name__}")
386
+
387
+ # Launch callbacks as background tasks (non-blocking)
388
+ input_params = get_input_parameters(func, args, kwargs)
389
+ launch_callback_task(input_callback, input_params, unpack=True)
390
+ launch_callback_task(output_callback, return_value, unpack=False)
391
+ launch_combined_callback_task(combined_callback, input_params, return_value)
392
+
331
393
  return return_value
332
394
 
333
395
  @wraps(func)
@@ -337,7 +399,15 @@ class VerisSDK:
337
399
  f"No session ID found, executing original function: {func.__name__}"
338
400
  )
339
401
  return func(*args, **kwargs)
402
+
340
403
  logger.info(f"Stubbing function: {func.__name__}")
404
+
405
+ # Execute callbacks synchronously (can't use async tasks in sync context)
406
+ input_params = get_input_parameters(func, args, kwargs)
407
+ execute_callback(input_callback, input_params, unpack=True)
408
+ execute_callback(output_callback, return_value, unpack=False)
409
+ execute_combined_callback(combined_callback, input_params, return_value)
410
+
341
411
  return return_value
342
412
 
343
413
  # Return the appropriate wrapper based on whether the function is async
veris_ai/utils.py CHANGED
@@ -1,4 +1,6 @@
1
+ import asyncio
1
2
  import inspect
3
+ import logging
2
4
  import sys
3
5
  import types
4
6
  import typing
@@ -18,6 +20,8 @@ from collections.abc import Callable
18
20
 
19
21
  from pydantic import BaseModel
20
22
 
23
+ logger = logging.getLogger(__name__)
24
+
21
25
 
22
26
  def convert_to_type(value: object, target_type: type) -> object:
23
27
  """Convert a value to the specified type."""
@@ -303,3 +307,257 @@ def get_function_parameters(
303
307
  "type": str(get_type_hints(func).get(param_name, Any)),
304
308
  }
305
309
  return params_info
310
+
311
+
312
+ # Callback utility functions
313
+
314
+
315
+ def get_input_parameters(func: Callable, args: tuple, kwargs: dict) -> dict[str, Any]:
316
+ """Get the actual input parameters for callbacks.
317
+
318
+ Args:
319
+ func: The function being called
320
+ args: Positional arguments
321
+ kwargs: Keyword arguments
322
+
323
+ Returns:
324
+ Dictionary of parameter names to their actual values (not stringified),
325
+ excluding self and cls parameters. Preserves ctx if present.
326
+ """
327
+ sig = inspect.signature(func)
328
+ bound_args = sig.bind(*args, **kwargs)
329
+ bound_args.apply_defaults()
330
+
331
+ # Remove only self and cls - preserve ctx and all other parameters
332
+ params = dict(bound_args.arguments)
333
+ params.pop("self", None)
334
+ params.pop("cls", None)
335
+
336
+ return params
337
+
338
+
339
+ def filter_callback_parameters(callback: Callable, params: dict[str, Any]) -> dict[str, Any]:
340
+ """Filter parameters to match what the callback can accept.
341
+
342
+ Args:
343
+ callback: The callback function to inspect
344
+ params: Dictionary of all available parameters
345
+
346
+ Returns:
347
+ Filtered dictionary containing only parameters the callback accepts
348
+ """
349
+ try:
350
+ sig = inspect.signature(callback)
351
+
352
+ # Check if callback accepts **kwargs (VAR_KEYWORD parameter)
353
+ has_var_keyword = any(
354
+ p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()
355
+ )
356
+
357
+ # If callback accepts **kwargs, pass all parameters
358
+ if has_var_keyword:
359
+ return params
360
+
361
+ # Otherwise, filter to only include parameters the callback accepts
362
+ accepted_params = {}
363
+ for param_name in sig.parameters:
364
+ if param_name in params:
365
+ accepted_params[param_name] = params[param_name]
366
+
367
+ return accepted_params
368
+ except (ValueError, TypeError):
369
+ # If we can't inspect the signature, pass all parameters
370
+ # and let the callback handle it (will fail if incompatible)
371
+ return params
372
+
373
+
374
+ def execute_callback(
375
+ callback: Callable | None,
376
+ data: Any, # noqa: ANN401
377
+ unpack: bool = False,
378
+ ) -> None:
379
+ """Execute a callback synchronously if provided.
380
+
381
+ Args:
382
+ callback: The callback callable to execute
383
+ data: The data to pass to the callback
384
+ unpack: If True and data is a dict, unpack it as keyword arguments
385
+
386
+ Note:
387
+ Exceptions in callbacks are caught and logged to prevent breaking the main flow.
388
+ """
389
+ if callback is None:
390
+ return
391
+
392
+ try:
393
+ if unpack and isinstance(data, dict):
394
+ # Filter parameters to match callback signature
395
+ filtered_data = filter_callback_parameters(callback, data)
396
+ callback(**filtered_data)
397
+ else:
398
+ callback(data)
399
+ except Exception as e:
400
+ logger.warning(f"Callback execution failed: {e}", exc_info=True)
401
+
402
+
403
+ async def execute_callback_async(
404
+ callback: Callable | None,
405
+ data: Any, # noqa: ANN401
406
+ unpack: bool = False,
407
+ ) -> None:
408
+ """Execute a callback asynchronously if provided.
409
+
410
+ Handles both sync and async callback callables.
411
+
412
+ Args:
413
+ callback: The callback callable to execute (can be sync or async)
414
+ data: The data to pass to the callback
415
+ unpack: If True and data is a dict, unpack it as keyword arguments
416
+
417
+ Note:
418
+ Exceptions in callbacks are caught and logged to prevent breaking the main flow.
419
+ """
420
+ if callback is None:
421
+ return
422
+
423
+ try:
424
+ if inspect.iscoroutinefunction(callback):
425
+ if unpack and isinstance(data, dict):
426
+ # Filter parameters to match callback signature
427
+ filtered_data = filter_callback_parameters(callback, data)
428
+ await callback(**filtered_data)
429
+ else:
430
+ await callback(data)
431
+ else:
432
+ if unpack and isinstance(data, dict):
433
+ # Filter parameters to match callback signature
434
+ filtered_data = filter_callback_parameters(callback, data)
435
+ result = callback(**filtered_data)
436
+ else:
437
+ result = callback(data)
438
+ # If the result is a coroutine (can happen with functools.partial), await it
439
+ if inspect.iscoroutine(result):
440
+ await result
441
+ except Exception as e:
442
+ logger.warning(f"Callback execution failed: {e}", exc_info=True)
443
+
444
+
445
+ def launch_callback_task(
446
+ callback: Callable | None,
447
+ data: Any, # noqa: ANN401
448
+ unpack: bool = False,
449
+ ) -> None:
450
+ """Launch a callback as a background task (fire-and-forget).
451
+
452
+ Args:
453
+ callback: The callback callable to execute (can be sync or async)
454
+ data: The data to pass to the callback
455
+ unpack: If True and data is a dict, unpack it as keyword arguments
456
+
457
+ Note:
458
+ This launches the callback without blocking. Errors are logged but won't
459
+ affect the main execution flow.
460
+ """
461
+ if callback is None:
462
+ return
463
+
464
+ async def _run_callback() -> None:
465
+ """Wrapper to run callback with error handling."""
466
+ try:
467
+ if inspect.iscoroutinefunction(callback):
468
+ if unpack and isinstance(data, dict):
469
+ # Filter parameters to match callback signature
470
+ filtered_data = filter_callback_parameters(callback, data)
471
+ await callback(**filtered_data)
472
+ else:
473
+ await callback(data)
474
+ else:
475
+ if unpack and isinstance(data, dict):
476
+ # Filter parameters to match callback signature
477
+ filtered_data = filter_callback_parameters(callback, data)
478
+ result = callback(**filtered_data)
479
+ else:
480
+ result = callback(data)
481
+ if inspect.iscoroutine(result):
482
+ await result
483
+ except Exception as e:
484
+ logger.warning(f"Callback execution failed: {e}", exc_info=True)
485
+
486
+ # Create task without awaiting (fire-and-forget)
487
+ try:
488
+ asyncio.create_task(_run_callback())
489
+ except RuntimeError:
490
+ # If no event loop is running, log a warning
491
+ logger.warning("Cannot launch callback task: no event loop running")
492
+
493
+
494
+ def execute_combined_callback(
495
+ callback: Callable | None,
496
+ input_params: dict[str, Any],
497
+ mock_output: Any, # noqa: ANN401
498
+ ) -> None:
499
+ """Execute a combined callback synchronously with input parameters and mock output.
500
+
501
+ Args:
502
+ callback: The callback callable to execute
503
+ input_params: Dictionary of input parameters
504
+ mock_output: The output from the mock/stub call
505
+
506
+ Note:
507
+ Exceptions in callbacks are caught and logged to prevent breaking the main flow.
508
+ """
509
+ if callback is None:
510
+ return
511
+
512
+ try:
513
+ # Combine input params with mock_output
514
+ combined_data = {**input_params, "mock_output": mock_output}
515
+ # Filter parameters to match callback signature
516
+ filtered_data = filter_callback_parameters(callback, combined_data)
517
+ callback(**filtered_data)
518
+ except Exception as e:
519
+ logger.warning(f"Combined callback execution failed: {e}", exc_info=True)
520
+
521
+
522
+ def launch_combined_callback_task(
523
+ callback: Callable | None,
524
+ input_params: dict[str, Any],
525
+ mock_output: Any, # noqa: ANN401
526
+ ) -> None:
527
+ """Launch a combined callback as a background task (fire-and-forget).
528
+
529
+ Args:
530
+ callback: The callback callable to execute (can be sync or async)
531
+ input_params: Dictionary of input parameters
532
+ mock_output: The output from the mock/stub call
533
+
534
+ Note:
535
+ This launches the callback without blocking. Errors are logged but won't
536
+ affect the main execution flow.
537
+ """
538
+ if callback is None:
539
+ return
540
+
541
+ async def _run_callback() -> None:
542
+ """Wrapper to run combined callback with error handling."""
543
+ try:
544
+ # Combine input params with mock_output
545
+ combined_data = {**input_params, "mock_output": mock_output}
546
+ # Filter parameters to match callback signature
547
+ filtered_data = filter_callback_parameters(callback, combined_data)
548
+
549
+ if inspect.iscoroutinefunction(callback):
550
+ await callback(**filtered_data)
551
+ else:
552
+ result = callback(**filtered_data)
553
+ if inspect.iscoroutine(result):
554
+ await result
555
+ except Exception as e:
556
+ logger.warning(f"Combined callback execution failed: {e}", exc_info=True)
557
+
558
+ # Create task without awaiting (fire-and-forget)
559
+ try:
560
+ asyncio.create_task(_run_callback())
561
+ except RuntimeError:
562
+ # If no event loop is running, log a warning
563
+ logger.warning("Cannot launch combined callback task: no event loop running")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: veris-ai
3
- Version: 1.12.3
3
+ Version: 1.14.0
4
4
  Summary: A Python package for Veris AI tools
5
5
  Project-URL: Homepage, https://github.com/veris-ai/veris-python-sdk
6
6
  Project-URL: Bug Tracker, https://github.com/veris-ai/veris-python-sdk/issues
@@ -4,13 +4,13 @@ veris_ai/agents_wrapper.py,sha256=gLUd_0TyCVsqqilQLvsSJIpsU5uu2CdjjWOQ4QJjoJk,12
4
4
  veris_ai/api_client.py,sha256=I1XyQ7J0ZU_JK9sZjF3XqFv5gGsrdKF38euOZmW8BG0,4150
5
5
  veris_ai/models.py,sha256=xKeheSJQle2tBeJG1DsGJzMDwv24p5jECjX6RAa39n4,495
6
6
  veris_ai/observability.py,sha256=eSIXmk6fpOAoWM-sDbsvzyUASh1ZwU6tRIPduy09RxY,4206
7
- veris_ai/tool_mock.py,sha256=wqklgub07C9zon25P9XMAXTXUsRVKAnZo6nv4lJROCo,21850
8
- veris_ai/utils.py,sha256=hJetCiN8Bubhy0nqSoS1C2awN9cdkKuHM1v7YhtwtTs,10066
7
+ veris_ai/tool_mock.py,sha256=uFVSMqalSrXbhG1S2BuwK5mttRui4kV48Exvsy2BHOE,24973
8
+ veris_ai/utils.py,sha256=qwPPxS0CrsS36OoN_924hricz9jGRXx9FSDgLok0wgY,18940
9
9
  veris_ai/jaeger_interface/README.md,sha256=kd9rKcE5xf3EyNaiHu0tjn-0oES9sfaK6Ih-OhhTyCM,2821
10
10
  veris_ai/jaeger_interface/__init__.py,sha256=KD7NSiMYRG_2uF6dOLKkGG5lNQe4K9ptEwucwMT4_aw,1128
11
11
  veris_ai/jaeger_interface/client.py,sha256=yJrh86wRR0Dk3Gq12DId99WogcMIVbL0QQFqVSevvlE,8772
12
12
  veris_ai/jaeger_interface/models.py,sha256=e64VV6IvOEFuzRUgvDAMQFyOZMRb56I-PUPZLBZ3rX0,1864
13
- veris_ai-1.12.3.dist-info/METADATA,sha256=P6ZtsocGvpf7cifFzmvQLUZnFaYb5bVGiaYggn9KkC8,16684
14
- veris_ai-1.12.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
- veris_ai-1.12.3.dist-info/licenses/LICENSE,sha256=2g4i20atAgtD5einaKzhQrIB-JrPhyQgD3bC0wkHcCI,1065
16
- veris_ai-1.12.3.dist-info/RECORD,,
13
+ veris_ai-1.14.0.dist-info/METADATA,sha256=2heFuh9Ox9ok24zrdvse_dB1SF6UUOkOGbBcZXpkkP0,16684
14
+ veris_ai-1.14.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
+ veris_ai-1.14.0.dist-info/licenses/LICENSE,sha256=2g4i20atAgtD5einaKzhQrIB-JrPhyQgD3bC0wkHcCI,1065
16
+ veris_ai-1.14.0.dist-info/RECORD,,