veris-ai 1.12.3__py3-none-any.whl → 1.13.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,14 @@ 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
+ extract_json_schema,
24
+ get_function_parameters,
25
+ get_input_parameters,
26
+ launch_callback_task,
27
+ )
21
28
 
22
29
  logger = logging.getLogger(__name__)
23
30
 
@@ -243,8 +250,18 @@ class VerisSDK:
243
250
  mode: Literal["tool", "function"] = "tool",
244
251
  expects_response: bool | None = None,
245
252
  cache_response: bool | None = None,
253
+ input_callback: Callable[..., Any] | None = None,
254
+ output_callback: Callable[[Any], Any] | None = None,
246
255
  ) -> Callable:
247
- """Decorator for mocking tool calls."""
256
+ """Decorator for mocking tool calls.
257
+
258
+ Args:
259
+ mode: Whether to treat the function as a tool or function
260
+ expects_response: Whether the function expects a response
261
+ cache_response: Whether to cache the response
262
+ input_callback: Callable that receives input parameters as individual arguments
263
+ output_callback: Callable that receives the output value
264
+ """
248
265
  response_expectation = (
249
266
  ResponseExpectation.NONE
250
267
  if (expects_response is False or (expects_response is None and mode == "function"))
@@ -273,9 +290,11 @@ class VerisSDK:
273
290
  f"No session ID found, executing original function: {func.__name__}"
274
291
  )
275
292
  return await func(*args, **kwargs)
293
+
294
+ # Perform the mock call first
276
295
  parameters = get_function_parameters(func, args, kwargs)
277
296
  thread_id = _thread_id_context.get()
278
- return await mock_tool_call_async(
297
+ result = await mock_tool_call_async(
279
298
  func,
280
299
  session_id,
281
300
  parameters,
@@ -283,6 +302,13 @@ class VerisSDK:
283
302
  thread_id,
284
303
  )
285
304
 
305
+ # Launch callbacks as background tasks (non-blocking)
306
+ input_params = get_input_parameters(func, args, kwargs)
307
+ launch_callback_task(input_callback, input_params, unpack=True)
308
+ launch_callback_task(output_callback, result, unpack=False)
309
+
310
+ return result
311
+
286
312
  @wraps(func)
287
313
  def sync_wrapper(
288
314
  *args: tuple[object, ...],
@@ -295,9 +321,11 @@ class VerisSDK:
295
321
  f"No session ID found, executing original function: {func.__name__}"
296
322
  )
297
323
  return func(*args, **kwargs)
324
+
325
+ # Perform the mock call first
298
326
  parameters = get_function_parameters(func, args, kwargs)
299
327
  thread_id = _thread_id_context.get()
300
- return mock_tool_call(
328
+ result = mock_tool_call(
301
329
  func,
302
330
  session_id,
303
331
  parameters,
@@ -305,13 +333,31 @@ class VerisSDK:
305
333
  thread_id,
306
334
  )
307
335
 
336
+ # Execute callbacks synchronously (can't use async tasks in sync context)
337
+ input_params = get_input_parameters(func, args, kwargs)
338
+ execute_callback(input_callback, input_params, unpack=True)
339
+ execute_callback(output_callback, result, unpack=False)
340
+
341
+ return result
342
+
308
343
  # Return the appropriate wrapper based on whether the function is async
309
344
  return async_wrapper if is_async else sync_wrapper
310
345
 
311
346
  return decorator
312
347
 
313
- def stub(self, return_value: Any) -> Callable: # noqa: ANN401
314
- """Decorator for stubbing toolw calls."""
348
+ def stub(
349
+ self,
350
+ return_value: Any, # noqa: ANN401
351
+ input_callback: Callable[..., Any] | None = None,
352
+ output_callback: Callable[[Any], Any] | None = None,
353
+ ) -> Callable:
354
+ """Decorator for stubbing tool calls.
355
+
356
+ Args:
357
+ return_value: The value to return when the function is stubbed
358
+ input_callback: Callable that receives input parameters as individual arguments
359
+ output_callback: Callable that receives the output value
360
+ """
315
361
 
316
362
  def decorator(func: Callable) -> Callable:
317
363
  # Check if the original function is async
@@ -327,7 +373,14 @@ class VerisSDK:
327
373
  f"No session ID found, executing original function: {func.__name__}"
328
374
  )
329
375
  return await func(*args, **kwargs)
376
+
330
377
  logger.info(f"Stubbing function: {func.__name__}")
378
+
379
+ # Launch callbacks as background tasks (non-blocking)
380
+ input_params = get_input_parameters(func, args, kwargs)
381
+ launch_callback_task(input_callback, input_params, unpack=True)
382
+ launch_callback_task(output_callback, return_value, unpack=False)
383
+
331
384
  return return_value
332
385
 
333
386
  @wraps(func)
@@ -337,7 +390,14 @@ class VerisSDK:
337
390
  f"No session ID found, executing original function: {func.__name__}"
338
391
  )
339
392
  return func(*args, **kwargs)
393
+
340
394
  logger.info(f"Stubbing function: {func.__name__}")
395
+
396
+ # Execute callbacks synchronously (can't use async tasks in sync context)
397
+ input_params = get_input_parameters(func, args, kwargs)
398
+ execute_callback(input_callback, input_params, unpack=True)
399
+ execute_callback(output_callback, return_value, unpack=False)
400
+
341
401
  return return_value
342
402
 
343
403
  # 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,185 @@ 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")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: veris-ai
3
- Version: 1.12.3
3
+ Version: 1.13.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=olb_ywR88meK_FyoDVHpVnIi0h0B9oWM5IHV7k3hcko,24240
8
+ veris_ai/utils.py,sha256=2fzXcsKQGLm1Q1ntjtuC_Z5l6Atj56zQE723m3zXdGg,16357
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.13.0.dist-info/METADATA,sha256=5yQZuNyfZdG4QiJd30nTHkwm_e2B8WRp4DbCXequnmk,16684
14
+ veris_ai-1.13.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
+ veris_ai-1.13.0.dist-info/licenses/LICENSE,sha256=2g4i20atAgtD5einaKzhQrIB-JrPhyQgD3bC0wkHcCI,1065
16
+ veris_ai-1.13.0.dist-info/RECORD,,