veris-ai 1.12.2__tar.gz → 1.13.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.2 → veris_ai-1.13.0}/PKG-INFO +1 -1
- {veris_ai-1.12.2 → veris_ai-1.13.0}/pyproject.toml +1 -1
- {veris_ai-1.12.2 → veris_ai-1.13.0}/src/veris_ai/agents_wrapper.py +13 -4
- {veris_ai-1.12.2 → veris_ai-1.13.0}/src/veris_ai/api_client.py +18 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/src/veris_ai/tool_mock.py +195 -9
- {veris_ai-1.12.2 → veris_ai-1.13.0}/src/veris_ai/utils.py +186 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/tests/test_agents_wrapper_simple.py +15 -4
- {veris_ai-1.12.2 → veris_ai-1.13.0}/tests/test_mcp_protocol_server_mocked.py +8 -2
- veris_ai-1.13.0/tests/test_side_effects.py +950 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/tests/test_tool_mock.py +24 -20
- {veris_ai-1.12.2 → veris_ai-1.13.0}/uv.lock +1 -1
- {veris_ai-1.12.2 → veris_ai-1.13.0}/.cursor/rules/documentation-management.mdc +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/.github/workflows/release.yml +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/.github/workflows/test.yml +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/.gitignore +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/.pre-commit-config.yaml +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/CHANGELOG.md +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/CLAUDE.md +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/LICENSE +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/README.md +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/examples/README.md +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/examples/__init__.py +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/examples/import_options.py +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/examples/openai_agents_example.py +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/src/veris_ai/README.md +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/src/veris_ai/__init__.py +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/src/veris_ai/jaeger_interface/README.md +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/src/veris_ai/jaeger_interface/__init__.py +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/src/veris_ai/jaeger_interface/client.py +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/src/veris_ai/jaeger_interface/models.py +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/src/veris_ai/models.py +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/src/veris_ai/observability.py +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/tests/README.md +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/tests/__init__.py +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/tests/conftest.py +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/tests/fixtures/__init__.py +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/tests/fixtures/http_server.py +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/tests/fixtures/simple_app.py +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/tests/test_agents_wrapper_extract.py +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/tests/test_helpers.py +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/tests/test_token_decoding.py +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.0}/tests/test_utils.py +0 -0
- {veris_ai-1.12.2 → veris_ai-1.13.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.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
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""OpenAI Agents wrapper for automatic tool mocking via Veris SDK."""
|
|
2
2
|
|
|
3
|
+
import inspect
|
|
3
4
|
import json
|
|
4
5
|
import logging
|
|
5
6
|
from collections.abc import Callable
|
|
@@ -9,7 +10,7 @@ from agents import RunContextWrapper, RunResult, Runner as OpenAIRunner
|
|
|
9
10
|
from pydantic import BaseModel
|
|
10
11
|
|
|
11
12
|
from veris_ai import veris
|
|
12
|
-
from veris_ai.tool_mock import mock_tool_call
|
|
13
|
+
from veris_ai.tool_mock import mock_tool_call, mock_tool_call_async
|
|
13
14
|
from veris_ai.models import ToolCallOptions
|
|
14
15
|
|
|
15
16
|
logger = logging.getLogger(__name__)
|
|
@@ -86,7 +87,17 @@ def _wrap(
|
|
|
86
87
|
thread_id = veris.thread_id
|
|
87
88
|
the_func = tool_functions.get(tool_id)
|
|
88
89
|
if the_func and session_id:
|
|
89
|
-
#
|
|
90
|
+
# Check if async or sync, call appropriate version
|
|
91
|
+
if inspect.iscoroutinefunction(the_func):
|
|
92
|
+
# Use async version (non-blocking)
|
|
93
|
+
return await mock_tool_call_async(
|
|
94
|
+
the_func,
|
|
95
|
+
session_id,
|
|
96
|
+
json.loads(parameters),
|
|
97
|
+
tool_options.get(tool_name_inner),
|
|
98
|
+
thread_id=thread_id,
|
|
99
|
+
)
|
|
100
|
+
# Use sync version for sync functions
|
|
90
101
|
return mock_tool_call(
|
|
91
102
|
the_func,
|
|
92
103
|
session_id,
|
|
@@ -191,8 +202,6 @@ def _find_user_function_in_closure(closure: tuple) -> Callable | None:
|
|
|
191
202
|
Returns:
|
|
192
203
|
The user function if found, None otherwise
|
|
193
204
|
"""
|
|
194
|
-
import inspect
|
|
195
|
-
|
|
196
205
|
# List of module prefixes that indicate library/framework code
|
|
197
206
|
library_modules = ("json", "inspect", "agents", "pydantic", "openai", "typing")
|
|
198
207
|
|
|
@@ -60,6 +60,24 @@ class SimulatorAPIClient:
|
|
|
60
60
|
response.raise_for_status()
|
|
61
61
|
return response.json() if response.content else None
|
|
62
62
|
|
|
63
|
+
async def post_async(self, endpoint: str, payload: dict[str, Any]) -> Any: # noqa: ANN401
|
|
64
|
+
"""Make an asynchronous POST request to the specified endpoint.
|
|
65
|
+
|
|
66
|
+
This method uses httpx.AsyncClient and is safe to call from async functions
|
|
67
|
+
without blocking the event loop.
|
|
68
|
+
"""
|
|
69
|
+
headers = self._build_headers()
|
|
70
|
+
# Validate endpoint URL; raise ConnectError for non-absolute URLs to
|
|
71
|
+
# mirror connection failures in tests when base URL is intentionally invalid.
|
|
72
|
+
if not endpoint.startswith(("http://", "https://")):
|
|
73
|
+
error_msg = f"Invalid endpoint URL (not absolute): {endpoint}"
|
|
74
|
+
raise httpx.ConnectError(error_msg)
|
|
75
|
+
|
|
76
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
77
|
+
response = await client.post(endpoint, json=payload, headers=headers)
|
|
78
|
+
response.raise_for_status()
|
|
79
|
+
return response.json() if response.content else None
|
|
80
|
+
|
|
63
81
|
@property
|
|
64
82
|
def tool_mock_endpoint(self) -> str:
|
|
65
83
|
"""Get the tool mock endpoint URL."""
|
|
@@ -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
|
|
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
|
|
|
@@ -206,14 +213,14 @@ class VerisSDK:
|
|
|
206
213
|
return await func(*args, **kwargs)
|
|
207
214
|
parameters = get_function_parameters(func, args, kwargs)
|
|
208
215
|
logger.info(f"Spying on function: {func.__name__}")
|
|
209
|
-
|
|
216
|
+
await log_tool_call_async(
|
|
210
217
|
session_id=session_id,
|
|
211
218
|
function_name=func.__name__,
|
|
212
219
|
parameters=parameters,
|
|
213
220
|
docstring=inspect.getdoc(func) or "",
|
|
214
221
|
)
|
|
215
222
|
result = await func(*args, **kwargs)
|
|
216
|
-
|
|
223
|
+
await log_tool_response_async(session_id=session_id, response=result)
|
|
217
224
|
return result
|
|
218
225
|
|
|
219
226
|
@wraps(func)
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
314
|
-
|
|
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
|
|
@@ -358,7 +418,7 @@ def mock_tool_call(
|
|
|
358
418
|
options: ToolCallOptions | None = None,
|
|
359
419
|
thread_id: str | None = None,
|
|
360
420
|
) -> object:
|
|
361
|
-
"""Mock tool call.
|
|
421
|
+
"""Mock tool call (synchronous).
|
|
362
422
|
|
|
363
423
|
Args:
|
|
364
424
|
func: Function being mocked
|
|
@@ -424,6 +484,84 @@ def mock_tool_call(
|
|
|
424
484
|
return convert_to_type(mock_result, return_type_obj)
|
|
425
485
|
|
|
426
486
|
|
|
487
|
+
@tenacity.retry(
|
|
488
|
+
stop=tenacity.stop_after_attempt(3),
|
|
489
|
+
wait=tenacity.wait_exponential(multiplier=1, min=4, max=10),
|
|
490
|
+
reraise=True,
|
|
491
|
+
)
|
|
492
|
+
async def mock_tool_call_async(
|
|
493
|
+
func: Callable,
|
|
494
|
+
session_id: str, # noqa: ARG001
|
|
495
|
+
parameters: dict[str, dict[str, str]],
|
|
496
|
+
options: ToolCallOptions | None = None,
|
|
497
|
+
thread_id: str | None = None,
|
|
498
|
+
) -> object:
|
|
499
|
+
"""Mock tool call (asynchronous).
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
func: Function being mocked
|
|
503
|
+
session_id: Session ID (kept for backwards compatibility, not used)
|
|
504
|
+
parameters: Function parameters
|
|
505
|
+
options: Tool call options
|
|
506
|
+
thread_id: Thread ID to use as session_id in API request (required)
|
|
507
|
+
|
|
508
|
+
Raises:
|
|
509
|
+
ValueError: If thread_id is not provided
|
|
510
|
+
"""
|
|
511
|
+
if thread_id is None:
|
|
512
|
+
raise ValueError(
|
|
513
|
+
"thread_id is required for mocking. "
|
|
514
|
+
"Use parse_token() to set both session_id and thread_id."
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
options = options or ToolCallOptions()
|
|
518
|
+
api_client = get_api_client()
|
|
519
|
+
endpoint = api_client.tool_mock_endpoint
|
|
520
|
+
|
|
521
|
+
logger.info(f"Simulating function: {func.__name__}")
|
|
522
|
+
|
|
523
|
+
type_hints = get_type_hints(func)
|
|
524
|
+
|
|
525
|
+
# Extract return type object (not just the name)
|
|
526
|
+
return_type_obj = type_hints.pop("return", Any)
|
|
527
|
+
# Get function docstring
|
|
528
|
+
docstring = inspect.getdoc(func) or ""
|
|
529
|
+
|
|
530
|
+
# Use thread_id as session_id in the payload
|
|
531
|
+
payload_session_id = thread_id
|
|
532
|
+
# Clean up parameters for V3 - just send values, not the nested dict
|
|
533
|
+
clean_params: dict[str, Any] = {}
|
|
534
|
+
for key, value in parameters.items():
|
|
535
|
+
if isinstance(value, dict) and "value" in value:
|
|
536
|
+
# Extract just the value from the nested structure
|
|
537
|
+
clean_params[key] = value["value"]
|
|
538
|
+
else:
|
|
539
|
+
# Already clean or unexpected format
|
|
540
|
+
clean_params[key] = value
|
|
541
|
+
|
|
542
|
+
# Determine response expectation
|
|
543
|
+
payload = {
|
|
544
|
+
"session_id": payload_session_id,
|
|
545
|
+
"response_expectation": options.response_expectation.value,
|
|
546
|
+
"cache_response": bool(options.cache_response),
|
|
547
|
+
"tool_call": {
|
|
548
|
+
"function_name": func.__name__,
|
|
549
|
+
"parameters": clean_params,
|
|
550
|
+
"return_type": json.dumps(extract_json_schema(return_type_obj)),
|
|
551
|
+
"docstring": docstring,
|
|
552
|
+
},
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
mock_result = await api_client.post_async(endpoint, payload)
|
|
556
|
+
logger.info(f"Mock response: {mock_result}")
|
|
557
|
+
|
|
558
|
+
if isinstance(mock_result, str):
|
|
559
|
+
with suppress(json.JSONDecodeError):
|
|
560
|
+
mock_result = json.loads(mock_result)
|
|
561
|
+
return convert_to_type(mock_result, return_type_obj)
|
|
562
|
+
return convert_to_type(mock_result, return_type_obj)
|
|
563
|
+
|
|
564
|
+
|
|
427
565
|
def log_tool_call(
|
|
428
566
|
session_id: str,
|
|
429
567
|
function_name: str,
|
|
@@ -456,6 +594,38 @@ def log_tool_call(
|
|
|
456
594
|
logger.warning(f"Failed to log tool call for {function_name}: {e}")
|
|
457
595
|
|
|
458
596
|
|
|
597
|
+
async def log_tool_call_async(
|
|
598
|
+
session_id: str,
|
|
599
|
+
function_name: str,
|
|
600
|
+
parameters: dict[str, dict[str, str]],
|
|
601
|
+
docstring: str,
|
|
602
|
+
) -> None:
|
|
603
|
+
"""Log tool call asynchronously to the VERIS logging endpoint."""
|
|
604
|
+
api_client = get_api_client()
|
|
605
|
+
endpoint = api_client.get_log_tool_call_endpoint(session_id)
|
|
606
|
+
|
|
607
|
+
# Clean up parameters for V3 - just send values, not the nested dict
|
|
608
|
+
clean_params: dict[str, Any] = {}
|
|
609
|
+
for key, value in parameters.items():
|
|
610
|
+
if isinstance(value, dict) and "value" in value:
|
|
611
|
+
# Extract just the value from the nested structure
|
|
612
|
+
clean_params[key] = value["value"]
|
|
613
|
+
else:
|
|
614
|
+
# Already clean or unexpected format
|
|
615
|
+
clean_params[key] = value
|
|
616
|
+
|
|
617
|
+
payload = {
|
|
618
|
+
"function_name": function_name,
|
|
619
|
+
"parameters": clean_params,
|
|
620
|
+
"docstring": docstring,
|
|
621
|
+
}
|
|
622
|
+
try:
|
|
623
|
+
await api_client.post_async(endpoint, payload)
|
|
624
|
+
logger.debug(f"Tool call logged for {function_name}")
|
|
625
|
+
except Exception as e:
|
|
626
|
+
logger.warning(f"Failed to log tool call for {function_name}: {e}")
|
|
627
|
+
|
|
628
|
+
|
|
459
629
|
def log_tool_response(session_id: str, response: Any) -> None: # noqa: ANN401
|
|
460
630
|
"""Log tool response synchronously to the VERIS logging endpoint."""
|
|
461
631
|
api_client = get_api_client()
|
|
@@ -472,4 +642,20 @@ def log_tool_response(session_id: str, response: Any) -> None: # noqa: ANN401
|
|
|
472
642
|
logger.warning(f"Failed to log tool response: {e}")
|
|
473
643
|
|
|
474
644
|
|
|
645
|
+
async def log_tool_response_async(session_id: str, response: Any) -> None: # noqa: ANN401
|
|
646
|
+
"""Log tool response asynchronously to the VERIS logging endpoint."""
|
|
647
|
+
api_client = get_api_client()
|
|
648
|
+
endpoint = api_client.get_log_tool_response_endpoint(session_id)
|
|
649
|
+
|
|
650
|
+
payload = {
|
|
651
|
+
"response": json.dumps(response, default=str),
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
try:
|
|
655
|
+
await api_client.post_async(endpoint, payload)
|
|
656
|
+
logger.debug("Tool response logged")
|
|
657
|
+
except Exception as e:
|
|
658
|
+
logger.warning(f"Failed to log tool response: {e}")
|
|
659
|
+
|
|
660
|
+
|
|
475
661
|
veris = VerisSDK()
|
|
@@ -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")
|
|
@@ -87,18 +87,29 @@ def mock_veris_endpoint():
|
|
|
87
87
|
"""Fixture that mocks the veris SDK's HTTP request method."""
|
|
88
88
|
calls = []
|
|
89
89
|
|
|
90
|
-
def
|
|
91
|
-
"""Mock the
|
|
90
|
+
async def mock_request_async(endpoint, payload):
|
|
91
|
+
"""Mock the post_async method."""
|
|
92
92
|
# Record the call
|
|
93
93
|
calls.append({"endpoint": endpoint, "payload": payload})
|
|
94
94
|
|
|
95
95
|
# Return a distinctive mocked value
|
|
96
96
|
return {"result": 999}
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
def mock_request_sync(endpoint, payload):
|
|
99
|
+
"""Mock the post method."""
|
|
100
|
+
# Record the call
|
|
101
|
+
calls.append({"endpoint": endpoint, "payload": payload})
|
|
102
|
+
|
|
103
|
+
# Return a distinctive mocked value
|
|
104
|
+
return {"result": 999}
|
|
105
|
+
|
|
106
|
+
# Patch both sync and async methods of the API client
|
|
99
107
|
from veris_ai.api_client import get_api_client
|
|
100
108
|
|
|
101
|
-
with
|
|
109
|
+
with (
|
|
110
|
+
patch.object(get_api_client(), "post", side_effect=mock_request_sync),
|
|
111
|
+
patch.object(get_api_client(), "post_async", side_effect=mock_request_async),
|
|
112
|
+
):
|
|
102
113
|
yield {"calls": calls}
|
|
103
114
|
|
|
104
115
|
|
|
@@ -62,10 +62,16 @@ def run_server_with_mock(server_port: int) -> None: # noqa: C901
|
|
|
62
62
|
# Return error response if something goes wrong
|
|
63
63
|
return {"error": str(e)}
|
|
64
64
|
|
|
65
|
-
# Patch the API client's post
|
|
65
|
+
# Patch the API client's post methods (both sync and async)
|
|
66
66
|
from veris_ai.api_client import get_api_client
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
async def mock_post_async(endpoint, payload):
|
|
69
|
+
return mock_post(endpoint, payload)
|
|
70
|
+
|
|
71
|
+
with (
|
|
72
|
+
patch.object(get_api_client(), "post", side_effect=mock_post),
|
|
73
|
+
patch.object(get_api_client(), "post_async", side_effect=mock_post_async),
|
|
74
|
+
):
|
|
69
75
|
# Configure the server
|
|
70
76
|
fastapi = make_simple_fastapi_app()
|
|
71
77
|
veris.set_fastapi_mcp(
|