veris-ai 1.12.3__tar.gz → 1.14.0__tar.gz
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-1.12.3 → veris_ai-1.14.0}/PKG-INFO +1 -1
- {veris_ai-1.12.3 → veris_ai-1.14.0}/pyproject.toml +1 -1
- {veris_ai-1.12.3 → veris_ai-1.14.0}/src/veris_ai/tool_mock.py +77 -7
- {veris_ai-1.12.3 → veris_ai-1.14.0}/src/veris_ai/utils.py +258 -0
- veris_ai-1.14.0/tests/test_side_effects.py +1304 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/uv.lock +1 -1
- {veris_ai-1.12.3 → veris_ai-1.14.0}/.cursor/rules/documentation-management.mdc +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/.github/workflows/release.yml +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/.github/workflows/test.yml +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/.gitignore +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/.pre-commit-config.yaml +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/CHANGELOG.md +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/CLAUDE.md +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/LICENSE +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/README.md +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/examples/README.md +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/examples/__init__.py +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/examples/import_options.py +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/examples/openai_agents_example.py +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/src/veris_ai/README.md +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/src/veris_ai/__init__.py +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/src/veris_ai/agents_wrapper.py +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/src/veris_ai/api_client.py +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/src/veris_ai/jaeger_interface/README.md +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/src/veris_ai/jaeger_interface/__init__.py +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/src/veris_ai/jaeger_interface/client.py +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/src/veris_ai/jaeger_interface/models.py +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/src/veris_ai/models.py +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/src/veris_ai/observability.py +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/tests/README.md +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/tests/__init__.py +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/tests/conftest.py +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/tests/fixtures/__init__.py +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/tests/fixtures/http_server.py +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/tests/fixtures/simple_app.py +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/tests/test_agents_wrapper_extract.py +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/tests/test_agents_wrapper_simple.py +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/tests/test_helpers.py +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/tests/test_mcp_protocol_server_mocked.py +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/tests/test_token_decoding.py +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/tests/test_tool_mock.py +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/tests/test_utils.py +0 -0
- {veris_ai-1.12.3 → veris_ai-1.14.0}/tests/test_veris_runner_tool_options.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: veris-ai
|
|
3
|
-
Version: 1.
|
|
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
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
314
|
-
|
|
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
|
|
@@ -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")
|