veris-ai 1.13.0__tar.gz → 1.14.1__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.13.0 → veris_ai-1.14.1}/PKG-INFO +1 -1
- {veris_ai-1.13.0 → veris_ai-1.14.1}/pyproject.toml +1 -1
- {veris_ai-1.13.0 → veris_ai-1.14.1}/src/veris_ai/api_client.py +20 -8
- {veris_ai-1.13.0 → veris_ai-1.14.1}/src/veris_ai/tool_mock.py +14 -2
- {veris_ai-1.13.0 → veris_ai-1.14.1}/src/veris_ai/utils.py +72 -0
- veris_ai-1.14.1/tests/test_api_client.py +140 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/tests/test_side_effects.py +354 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/uv.lock +1 -1
- {veris_ai-1.13.0 → veris_ai-1.14.1}/.cursor/rules/documentation-management.mdc +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/.github/workflows/release.yml +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/.github/workflows/test.yml +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/.gitignore +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/.pre-commit-config.yaml +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/CHANGELOG.md +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/CLAUDE.md +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/LICENSE +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/README.md +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/examples/README.md +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/examples/__init__.py +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/examples/import_options.py +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/examples/openai_agents_example.py +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/src/veris_ai/README.md +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/src/veris_ai/__init__.py +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/src/veris_ai/agents_wrapper.py +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/src/veris_ai/jaeger_interface/README.md +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/src/veris_ai/jaeger_interface/__init__.py +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/src/veris_ai/jaeger_interface/client.py +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/src/veris_ai/jaeger_interface/models.py +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/src/veris_ai/models.py +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/src/veris_ai/observability.py +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/tests/README.md +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/tests/__init__.py +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/tests/conftest.py +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/tests/fixtures/__init__.py +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/tests/fixtures/http_server.py +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/tests/fixtures/simple_app.py +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/tests/test_agents_wrapper_extract.py +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/tests/test_agents_wrapper_simple.py +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/tests/test_helpers.py +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/tests/test_mcp_protocol_server_mocked.py +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/tests/test_token_decoding.py +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/tests/test_tool_mock.py +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/tests/test_utils.py +0 -0
- {veris_ai-1.13.0 → veris_ai-1.14.1}/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.1
|
|
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
|
|
@@ -20,9 +20,15 @@ class SimulatorAPIClient:
|
|
|
20
20
|
changes reflected without recreating the singleton.
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
|
-
def __init__(self) -> None:
|
|
23
|
+
def __init__(self, timeout: float | None = None, base_url: str | None = None) -> None:
|
|
24
24
|
"""Initialize the API client with static timeout configuration."""
|
|
25
|
-
self.
|
|
25
|
+
self._timeout = timeout or float(os.getenv("VERIS_MOCK_TIMEOUT", "300.0"))
|
|
26
|
+
self._base_url = base_url
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def base_url(self) -> str:
|
|
30
|
+
"""Get the resolved base URL."""
|
|
31
|
+
return self._get_base_url()
|
|
26
32
|
|
|
27
33
|
def _get_base_url(self) -> str:
|
|
28
34
|
"""Resolve the base URL from environment.
|
|
@@ -33,7 +39,7 @@ class SimulatorAPIClient:
|
|
|
33
39
|
(do not fall back). This supports tests expecting connection
|
|
34
40
|
failures when an invalid endpoint is provided.
|
|
35
41
|
"""
|
|
36
|
-
return os.getenv("VERIS_API_URL") or "https://simulator.api.veris.ai"
|
|
42
|
+
return self._base_url or os.getenv("VERIS_API_URL") or "https://simulator.api.veris.ai"
|
|
37
43
|
|
|
38
44
|
def _build_headers(self) -> dict[str, str] | None:
|
|
39
45
|
"""Build headers including OpenTelemetry tracing and API key."""
|
|
@@ -55,7 +61,7 @@ class SimulatorAPIClient:
|
|
|
55
61
|
if not endpoint.startswith(("http://", "https://")):
|
|
56
62
|
raise httpx.ConnectError("Invalid endpoint URL (not absolute): {endpoint}")
|
|
57
63
|
|
|
58
|
-
with httpx.Client(timeout=self.
|
|
64
|
+
with httpx.Client(timeout=self._timeout) as client:
|
|
59
65
|
response = client.post(endpoint, json=payload, headers=headers)
|
|
60
66
|
response.raise_for_status()
|
|
61
67
|
return response.json() if response.content else None
|
|
@@ -73,7 +79,7 @@ class SimulatorAPIClient:
|
|
|
73
79
|
error_msg = f"Invalid endpoint URL (not absolute): {endpoint}"
|
|
74
80
|
raise httpx.ConnectError(error_msg)
|
|
75
81
|
|
|
76
|
-
async with httpx.AsyncClient(timeout=self.
|
|
82
|
+
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
|
77
83
|
response = await client.post(endpoint, json=payload, headers=headers)
|
|
78
84
|
response.raise_for_status()
|
|
79
85
|
return response.json() if response.content else None
|
|
@@ -81,15 +87,15 @@ class SimulatorAPIClient:
|
|
|
81
87
|
@property
|
|
82
88
|
def tool_mock_endpoint(self) -> str:
|
|
83
89
|
"""Get the tool mock endpoint URL."""
|
|
84
|
-
return urljoin(self.
|
|
90
|
+
return urljoin(self.base_url, "v3/tool_mock")
|
|
85
91
|
|
|
86
92
|
def get_log_tool_call_endpoint(self, session_id: str) -> str:
|
|
87
93
|
"""Get the log tool call endpoint URL."""
|
|
88
|
-
return urljoin(self.
|
|
94
|
+
return urljoin(self.base_url, f"v3/log_tool_call?session_id={session_id}")
|
|
89
95
|
|
|
90
96
|
def get_log_tool_response_endpoint(self, session_id: str) -> str:
|
|
91
97
|
"""Get the log tool response endpoint URL."""
|
|
92
|
-
return urljoin(self.
|
|
98
|
+
return urljoin(self.base_url, f"v3/log_tool_response?session_id={session_id}")
|
|
93
99
|
|
|
94
100
|
|
|
95
101
|
# Global singleton instance
|
|
@@ -99,3 +105,9 @@ _api_client = SimulatorAPIClient()
|
|
|
99
105
|
def get_api_client() -> SimulatorAPIClient:
|
|
100
106
|
"""Get the global API client instance."""
|
|
101
107
|
return _api_client
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def set_api_client_params(base_url: str | None = None, timeout: float | None = None) -> None:
|
|
111
|
+
"""Set the global API client instance for testing purposes."""
|
|
112
|
+
global _api_client # noqa: PLW0603
|
|
113
|
+
_api_client = SimulatorAPIClient(base_url=base_url, timeout=timeout)
|
|
@@ -16,14 +16,16 @@ from typing import (
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
from veris_ai.models import ResponseExpectation, ToolCallOptions
|
|
19
|
-
from veris_ai.api_client import get_api_client
|
|
19
|
+
from veris_ai.api_client import get_api_client, set_api_client_params
|
|
20
20
|
from veris_ai.utils import (
|
|
21
21
|
convert_to_type,
|
|
22
22
|
execute_callback,
|
|
23
|
+
execute_combined_callback,
|
|
23
24
|
extract_json_schema,
|
|
24
25
|
get_function_parameters,
|
|
25
26
|
get_input_parameters,
|
|
26
27
|
launch_callback_task,
|
|
28
|
+
launch_combined_callback_task,
|
|
27
29
|
)
|
|
28
30
|
|
|
29
31
|
logger = logging.getLogger(__name__)
|
|
@@ -90,6 +92,8 @@ class VerisSDK:
|
|
|
90
92
|
|
|
91
93
|
self._set_session_id(token_data["session_id"])
|
|
92
94
|
self._set_thread_id(token_data["thread_id"])
|
|
95
|
+
if token_data.get("api_url"):
|
|
96
|
+
set_api_client_params(base_url=token_data["api_url"])
|
|
93
97
|
logger.info(
|
|
94
98
|
f"Session ID set to {token_data['session_id']}, "
|
|
95
99
|
f"Thread ID set to {token_data['thread_id']} - mocking enabled"
|
|
@@ -245,13 +249,14 @@ class VerisSDK:
|
|
|
245
249
|
|
|
246
250
|
return decorator
|
|
247
251
|
|
|
248
|
-
def mock( # noqa: C901, PLR0915
|
|
252
|
+
def mock( # noqa: C901, PLR0915, PLR0913
|
|
249
253
|
self,
|
|
250
254
|
mode: Literal["tool", "function"] = "tool",
|
|
251
255
|
expects_response: bool | None = None,
|
|
252
256
|
cache_response: bool | None = None,
|
|
253
257
|
input_callback: Callable[..., Any] | None = None,
|
|
254
258
|
output_callback: Callable[[Any], Any] | None = None,
|
|
259
|
+
combined_callback: Callable[..., Any] | None = None,
|
|
255
260
|
) -> Callable:
|
|
256
261
|
"""Decorator for mocking tool calls.
|
|
257
262
|
|
|
@@ -261,6 +266,7 @@ class VerisSDK:
|
|
|
261
266
|
cache_response: Whether to cache the response
|
|
262
267
|
input_callback: Callable that receives input parameters as individual arguments
|
|
263
268
|
output_callback: Callable that receives the output value
|
|
269
|
+
combined_callback: Callable that receives both input parameters and mock_output
|
|
264
270
|
"""
|
|
265
271
|
response_expectation = (
|
|
266
272
|
ResponseExpectation.NONE
|
|
@@ -306,6 +312,7 @@ class VerisSDK:
|
|
|
306
312
|
input_params = get_input_parameters(func, args, kwargs)
|
|
307
313
|
launch_callback_task(input_callback, input_params, unpack=True)
|
|
308
314
|
launch_callback_task(output_callback, result, unpack=False)
|
|
315
|
+
launch_combined_callback_task(combined_callback, input_params, result)
|
|
309
316
|
|
|
310
317
|
return result
|
|
311
318
|
|
|
@@ -337,6 +344,7 @@ class VerisSDK:
|
|
|
337
344
|
input_params = get_input_parameters(func, args, kwargs)
|
|
338
345
|
execute_callback(input_callback, input_params, unpack=True)
|
|
339
346
|
execute_callback(output_callback, result, unpack=False)
|
|
347
|
+
execute_combined_callback(combined_callback, input_params, result)
|
|
340
348
|
|
|
341
349
|
return result
|
|
342
350
|
|
|
@@ -350,6 +358,7 @@ class VerisSDK:
|
|
|
350
358
|
return_value: Any, # noqa: ANN401
|
|
351
359
|
input_callback: Callable[..., Any] | None = None,
|
|
352
360
|
output_callback: Callable[[Any], Any] | None = None,
|
|
361
|
+
combined_callback: Callable[..., Any] | None = None,
|
|
353
362
|
) -> Callable:
|
|
354
363
|
"""Decorator for stubbing tool calls.
|
|
355
364
|
|
|
@@ -357,6 +366,7 @@ class VerisSDK:
|
|
|
357
366
|
return_value: The value to return when the function is stubbed
|
|
358
367
|
input_callback: Callable that receives input parameters as individual arguments
|
|
359
368
|
output_callback: Callable that receives the output value
|
|
369
|
+
combined_callback: Callable that receives both input parameters and mock_output
|
|
360
370
|
"""
|
|
361
371
|
|
|
362
372
|
def decorator(func: Callable) -> Callable:
|
|
@@ -380,6 +390,7 @@ class VerisSDK:
|
|
|
380
390
|
input_params = get_input_parameters(func, args, kwargs)
|
|
381
391
|
launch_callback_task(input_callback, input_params, unpack=True)
|
|
382
392
|
launch_callback_task(output_callback, return_value, unpack=False)
|
|
393
|
+
launch_combined_callback_task(combined_callback, input_params, return_value)
|
|
383
394
|
|
|
384
395
|
return return_value
|
|
385
396
|
|
|
@@ -397,6 +408,7 @@ class VerisSDK:
|
|
|
397
408
|
input_params = get_input_parameters(func, args, kwargs)
|
|
398
409
|
execute_callback(input_callback, input_params, unpack=True)
|
|
399
410
|
execute_callback(output_callback, return_value, unpack=False)
|
|
411
|
+
execute_combined_callback(combined_callback, input_params, return_value)
|
|
400
412
|
|
|
401
413
|
return return_value
|
|
402
414
|
|
|
@@ -489,3 +489,75 @@ def launch_callback_task(
|
|
|
489
489
|
except RuntimeError:
|
|
490
490
|
# If no event loop is running, log a warning
|
|
491
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")
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Tests for SimulatorAPIClient endpoint URL generation and configuration."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from veris_ai.api_client import SimulatorAPIClient, get_api_client, set_api_client_params
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_tool_mock_endpoint_default():
|
|
12
|
+
"""Test that tool_mock_endpoint property returns correct URL with default base_url."""
|
|
13
|
+
client = SimulatorAPIClient()
|
|
14
|
+
endpoint = client.tool_mock_endpoint
|
|
15
|
+
|
|
16
|
+
# Should use default base URL
|
|
17
|
+
assert endpoint == "https://simulator.api.veris.ai/v3/tool_mock"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_tool_mock_endpoint_custom_base_url():
|
|
21
|
+
"""Test that tool_mock_endpoint uses cached base_url from constructor."""
|
|
22
|
+
client = SimulatorAPIClient(base_url="https://custom.api.com")
|
|
23
|
+
endpoint = client.tool_mock_endpoint
|
|
24
|
+
|
|
25
|
+
assert endpoint == "https://custom.api.com/v3/tool_mock"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_tool_mock_endpoint_with_env_var():
|
|
29
|
+
"""Test that tool_mock_endpoint respects VERIS_API_URL environment variable."""
|
|
30
|
+
with patch.dict(os.environ, {"VERIS_API_URL": "https://test.api.veris.ai"}):
|
|
31
|
+
client = SimulatorAPIClient()
|
|
32
|
+
endpoint = client.tool_mock_endpoint
|
|
33
|
+
|
|
34
|
+
assert endpoint == "https://test.api.veris.ai/v3/tool_mock"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_log_tool_call_endpoint():
|
|
38
|
+
"""Test get_log_tool_call_endpoint generates correct URL."""
|
|
39
|
+
client = SimulatorAPIClient(base_url="https://test.api.com")
|
|
40
|
+
endpoint = client.get_log_tool_call_endpoint("session-123")
|
|
41
|
+
|
|
42
|
+
assert endpoint == "https://test.api.com/v3/log_tool_call?session_id=session-123"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_log_tool_response_endpoint():
|
|
46
|
+
"""Test get_log_tool_response_endpoint generates correct URL."""
|
|
47
|
+
client = SimulatorAPIClient(base_url="https://test.api.com")
|
|
48
|
+
endpoint = client.get_log_tool_response_endpoint("session-456")
|
|
49
|
+
|
|
50
|
+
assert endpoint == "https://test.api.com/v3/log_tool_response?session_id=session-456"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_base_url_without_protocol_behavior():
|
|
54
|
+
"""Test that base_url without protocol is treated as relative path by urljoin."""
|
|
55
|
+
# Note: urljoin treats URLs without protocol as relative paths
|
|
56
|
+
client = SimulatorAPIClient(base_url="api.example.com")
|
|
57
|
+
endpoint = client.tool_mock_endpoint
|
|
58
|
+
|
|
59
|
+
# urljoin treats "api.example.com" as a relative path, so result is just the path component
|
|
60
|
+
assert endpoint == "v3/tool_mock"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_base_url_with_trailing_slash():
|
|
64
|
+
"""Test that base_url with trailing slash is handled correctly."""
|
|
65
|
+
client = SimulatorAPIClient(base_url="https://test.api.com/")
|
|
66
|
+
endpoint = client.tool_mock_endpoint
|
|
67
|
+
|
|
68
|
+
# urljoin should handle trailing slash correctly
|
|
69
|
+
assert endpoint == "https://test.api.com/v3/tool_mock"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_custom_timeout():
|
|
73
|
+
"""Test that custom timeout is set correctly."""
|
|
74
|
+
client = SimulatorAPIClient(timeout=30.0)
|
|
75
|
+
assert client._timeout == 30.0
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_default_timeout():
|
|
79
|
+
"""Test that default timeout is used when not specified."""
|
|
80
|
+
with patch.dict(os.environ, {"VERIS_MOCK_TIMEOUT": "120.0"}):
|
|
81
|
+
client = SimulatorAPIClient()
|
|
82
|
+
assert client._timeout == 120.0
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_set_api_client_params():
|
|
86
|
+
"""Test that set_api_client_params reconfigures the global client."""
|
|
87
|
+
# Get original client
|
|
88
|
+
original_client = get_api_client()
|
|
89
|
+
original_endpoint = original_client.tool_mock_endpoint
|
|
90
|
+
|
|
91
|
+
# Reconfigure with custom parameters
|
|
92
|
+
set_api_client_params(base_url="https://custom.test.api", timeout=60.0)
|
|
93
|
+
|
|
94
|
+
# Get new client and verify it's been updated
|
|
95
|
+
new_client = get_api_client()
|
|
96
|
+
assert new_client.tool_mock_endpoint == "https://custom.test.api/v3/tool_mock"
|
|
97
|
+
assert new_client._timeout == 60.0
|
|
98
|
+
|
|
99
|
+
# Restore original client
|
|
100
|
+
set_api_client_params()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_set_api_client_params_partial():
|
|
104
|
+
"""Test that set_api_client_params can set only base_url or timeout."""
|
|
105
|
+
# Set only base_url
|
|
106
|
+
set_api_client_params(base_url="https://partial.test")
|
|
107
|
+
client = get_api_client()
|
|
108
|
+
assert client.tool_mock_endpoint == "https://partial.test/v3/tool_mock"
|
|
109
|
+
assert client._timeout == 300 # Should use default
|
|
110
|
+
|
|
111
|
+
# Set only timeout
|
|
112
|
+
set_api_client_params(timeout=45.0)
|
|
113
|
+
client = get_api_client()
|
|
114
|
+
assert client._timeout == 45.0
|
|
115
|
+
|
|
116
|
+
# Restore defaults
|
|
117
|
+
set_api_client_params()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_multiple_endpoint_calls_use_cached_base_url():
|
|
121
|
+
"""Test that multiple endpoint property accesses use the same cached base_url."""
|
|
122
|
+
client = SimulatorAPIClient(base_url="https://cached.api")
|
|
123
|
+
|
|
124
|
+
# Call multiple endpoint methods
|
|
125
|
+
endpoint1 = client.tool_mock_endpoint
|
|
126
|
+
endpoint2 = client.get_log_tool_call_endpoint("session-1")
|
|
127
|
+
endpoint3 = client.get_log_tool_response_endpoint("session-2")
|
|
128
|
+
|
|
129
|
+
# All should use the same base URL
|
|
130
|
+
assert endpoint1.startswith("https://cached.api/")
|
|
131
|
+
assert endpoint2.startswith("https://cached.api/")
|
|
132
|
+
assert endpoint3.startswith("https://cached.api/")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_empty_env_var_uses_default():
|
|
136
|
+
"""Test that empty VERIS_API_URL falls back to default."""
|
|
137
|
+
with patch.dict(os.environ, {"VERIS_API_URL": ""}, clear=True):
|
|
138
|
+
client = SimulatorAPIClient()
|
|
139
|
+
# Empty string should NOT be used, should fall back to default
|
|
140
|
+
assert client._get_base_url() == "https://simulator.api.veris.ai"
|
|
@@ -948,3 +948,357 @@ def test_callback_with_no_matching_parameters(simulation_env):
|
|
|
948
948
|
result = some_func(1, "test")
|
|
949
949
|
assert result == "result"
|
|
950
950
|
assert len(called) == 1
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
# Test combined_callback functionality
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
@pytest.mark.asyncio
|
|
957
|
+
async def test_mock_with_async_combined_callback(simulation_env):
|
|
958
|
+
"""Test mock decorator with async combined_callback."""
|
|
959
|
+
captured_results = []
|
|
960
|
+
|
|
961
|
+
async def combined_handler(a: int, b: int, d: int = None, mock_output: int = None):
|
|
962
|
+
"""Combined callback that uses both inputs and output."""
|
|
963
|
+
# Compute sum of inputs that are not None, then multiply by mock_output
|
|
964
|
+
total_input = a + b + (d if d is not None else 0)
|
|
965
|
+
result = total_input * (mock_output if mock_output is not None else 1)
|
|
966
|
+
captured_results.append(result)
|
|
967
|
+
|
|
968
|
+
@veris.mock(mode="function", combined_callback=combined_handler)
|
|
969
|
+
async def add(a: int, b: int, c: int = None, d: int = None) -> int:
|
|
970
|
+
"""Add numbers together."""
|
|
971
|
+
return a + b + (c if c is not None else 0) + (d if d is not None else 0)
|
|
972
|
+
|
|
973
|
+
mock_response = 6
|
|
974
|
+
|
|
975
|
+
with patch.object(get_api_client(), "post_async", return_value=mock_response):
|
|
976
|
+
result = await add(1, 2, d=4)
|
|
977
|
+
assert result == 6
|
|
978
|
+
# Wait for background tasks to complete
|
|
979
|
+
await asyncio.sleep(0.01)
|
|
980
|
+
assert len(captured_results) == 1
|
|
981
|
+
# (1 + 2 + 4) * 6 = 42
|
|
982
|
+
assert captured_results[0] == 42
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
@pytest.mark.asyncio
|
|
986
|
+
async def test_mock_with_sync_combined_callback_async_func(simulation_env):
|
|
987
|
+
"""Test mock decorator with sync combined_callback on async function."""
|
|
988
|
+
captured_results = []
|
|
989
|
+
|
|
990
|
+
def multiply_by_result(a: int, b: int, mock_output: int = None):
|
|
991
|
+
"""Multiply inputs by the mock output."""
|
|
992
|
+
result = (a + b) * (mock_output if mock_output is not None else 1)
|
|
993
|
+
captured_results.append(result)
|
|
994
|
+
|
|
995
|
+
@veris.mock(mode="function", combined_callback=multiply_by_result)
|
|
996
|
+
async def add(a: int, b: int) -> int:
|
|
997
|
+
return a + b
|
|
998
|
+
|
|
999
|
+
mock_response = 10
|
|
1000
|
+
|
|
1001
|
+
with patch.object(get_api_client(), "post_async", return_value=mock_response):
|
|
1002
|
+
result = await add(3, 7)
|
|
1003
|
+
assert result == 10
|
|
1004
|
+
# Wait for background tasks to complete
|
|
1005
|
+
await asyncio.sleep(0.01)
|
|
1006
|
+
assert len(captured_results) == 1
|
|
1007
|
+
# (3 + 7) * 10 = 100
|
|
1008
|
+
assert captured_results[0] == 100
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
def test_mock_with_sync_combined_callback_sync_func(simulation_env):
|
|
1012
|
+
"""Test mock decorator with sync combined_callback on sync function."""
|
|
1013
|
+
captured_results = []
|
|
1014
|
+
|
|
1015
|
+
def process_combined(x: int, y: str, mock_output: dict = None):
|
|
1016
|
+
"""Process both input and output."""
|
|
1017
|
+
result = {
|
|
1018
|
+
"input_x": x,
|
|
1019
|
+
"input_y": y,
|
|
1020
|
+
"output": mock_output,
|
|
1021
|
+
"combined": f"{y}:{x}:{mock_output.get('value') if mock_output else 'none'}",
|
|
1022
|
+
}
|
|
1023
|
+
captured_results.append(result)
|
|
1024
|
+
|
|
1025
|
+
@veris.mock(mode="function", combined_callback=process_combined)
|
|
1026
|
+
def test_func(x: int, y: str) -> dict:
|
|
1027
|
+
return {"value": x}
|
|
1028
|
+
|
|
1029
|
+
mock_response = {"value": 999}
|
|
1030
|
+
|
|
1031
|
+
with patch.object(get_api_client(), "post", return_value=mock_response):
|
|
1032
|
+
result = test_func(42, "test")
|
|
1033
|
+
assert result == {"value": 999}
|
|
1034
|
+
assert len(captured_results) == 1
|
|
1035
|
+
assert captured_results[0]["input_x"] == 42
|
|
1036
|
+
assert captured_results[0]["input_y"] == "test"
|
|
1037
|
+
assert captured_results[0]["output"] == {"value": 999}
|
|
1038
|
+
assert captured_results[0]["combined"] == "test:42:999"
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
@pytest.mark.asyncio
|
|
1042
|
+
async def test_stub_with_async_combined_callback(simulation_env):
|
|
1043
|
+
"""Test stub decorator with async combined_callback."""
|
|
1044
|
+
captured_results = []
|
|
1045
|
+
|
|
1046
|
+
async def combined_handler(name: str, count: int, mock_output: str = None):
|
|
1047
|
+
"""Combine input and output."""
|
|
1048
|
+
result = f"{name}:{count}:{mock_output}"
|
|
1049
|
+
captured_results.append(result)
|
|
1050
|
+
|
|
1051
|
+
@veris.stub(return_value="stubbed_value", combined_callback=combined_handler)
|
|
1052
|
+
async def process(name: str, count: int) -> str:
|
|
1053
|
+
return f"{name}_{count}"
|
|
1054
|
+
|
|
1055
|
+
result = await process("test", 5)
|
|
1056
|
+
assert result == "stubbed_value"
|
|
1057
|
+
# Wait for background tasks to complete
|
|
1058
|
+
await asyncio.sleep(0.01)
|
|
1059
|
+
assert len(captured_results) == 1
|
|
1060
|
+
assert captured_results[0] == "test:5:stubbed_value"
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
def test_stub_with_sync_combined_callback_sync_func(simulation_env):
|
|
1064
|
+
"""Test stub decorator with sync combined_callback on sync function."""
|
|
1065
|
+
captured_results = []
|
|
1066
|
+
|
|
1067
|
+
def multiply_inputs_by_output(a: int, b: int, mock_output: int = None):
|
|
1068
|
+
"""Multiply sum of inputs by output."""
|
|
1069
|
+
result = (a + b) * (mock_output if mock_output is not None else 1)
|
|
1070
|
+
captured_results.append(result)
|
|
1071
|
+
|
|
1072
|
+
@veris.stub(return_value=5, combined_callback=multiply_inputs_by_output)
|
|
1073
|
+
def add(a: int, b: int) -> int:
|
|
1074
|
+
return a + b
|
|
1075
|
+
|
|
1076
|
+
result = add(2, 3)
|
|
1077
|
+
assert result == 5
|
|
1078
|
+
assert len(captured_results) == 1
|
|
1079
|
+
# (2 + 3) * 5 = 25
|
|
1080
|
+
assert captured_results[0] == 25
|
|
1081
|
+
|
|
1082
|
+
|
|
1083
|
+
@pytest.mark.asyncio
|
|
1084
|
+
async def test_combined_callback_with_parameter_filtering(simulation_env):
|
|
1085
|
+
"""Test that combined callback only receives parameters it accepts."""
|
|
1086
|
+
captured_results = []
|
|
1087
|
+
|
|
1088
|
+
async def selective_callback(a: int, mock_output: int = None):
|
|
1089
|
+
"""Callback that only accepts 'a' and 'mock_output', not 'b' or 'c'."""
|
|
1090
|
+
result = a * (mock_output if mock_output is not None else 1)
|
|
1091
|
+
captured_results.append(result)
|
|
1092
|
+
|
|
1093
|
+
@veris.mock(mode="function", combined_callback=selective_callback)
|
|
1094
|
+
async def complex_func(a: int, b: int, c: str = "default") -> int:
|
|
1095
|
+
return a + b
|
|
1096
|
+
|
|
1097
|
+
mock_response = 7
|
|
1098
|
+
|
|
1099
|
+
with patch.object(get_api_client(), "post_async", return_value=mock_response):
|
|
1100
|
+
result = await complex_func(3, 5, c="ignored")
|
|
1101
|
+
assert result == 7
|
|
1102
|
+
# Wait for background tasks to complete
|
|
1103
|
+
await asyncio.sleep(0.01)
|
|
1104
|
+
assert len(captured_results) == 1
|
|
1105
|
+
# 3 * 7 = 21 (b and c are filtered out)
|
|
1106
|
+
assert captured_results[0] == 21
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
@pytest.mark.asyncio
|
|
1110
|
+
async def test_combined_callback_in_production_mode(production_env):
|
|
1111
|
+
"""Test that combined_callback is not called in production mode."""
|
|
1112
|
+
callback_mock = Mock()
|
|
1113
|
+
|
|
1114
|
+
@veris.mock(mode="function", combined_callback=callback_mock)
|
|
1115
|
+
async def test_func(x: int) -> int:
|
|
1116
|
+
return x * 2
|
|
1117
|
+
|
|
1118
|
+
result = await test_func(21)
|
|
1119
|
+
assert result == 42
|
|
1120
|
+
|
|
1121
|
+
# Combined callback should NOT have been called
|
|
1122
|
+
callback_mock.assert_not_called()
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
@pytest.mark.asyncio
|
|
1126
|
+
async def test_combined_callback_exception_is_logged(simulation_env):
|
|
1127
|
+
"""Test that exceptions in combined callback are caught and logged."""
|
|
1128
|
+
|
|
1129
|
+
async def failing_combined_callback(x: int, mock_output: int = None):
|
|
1130
|
+
raise ValueError("Combined callback error")
|
|
1131
|
+
|
|
1132
|
+
@veris.mock(mode="function", combined_callback=failing_combined_callback)
|
|
1133
|
+
async def test_func(x: int) -> int:
|
|
1134
|
+
return x * 2
|
|
1135
|
+
|
|
1136
|
+
mock_response = 100
|
|
1137
|
+
|
|
1138
|
+
with (
|
|
1139
|
+
patch.object(get_api_client(), "post_async", return_value=mock_response),
|
|
1140
|
+
patch("veris_ai.utils.logger.warning") as mock_logger,
|
|
1141
|
+
):
|
|
1142
|
+
# Function should still work despite combined callback failure
|
|
1143
|
+
result = await test_func(42)
|
|
1144
|
+
assert result == 100
|
|
1145
|
+
|
|
1146
|
+
# Wait for background tasks to complete
|
|
1147
|
+
await asyncio.sleep(0.01)
|
|
1148
|
+
|
|
1149
|
+
# Error should have been logged
|
|
1150
|
+
mock_logger.assert_called()
|
|
1151
|
+
assert "Combined callback execution failed" in str(mock_logger.call_args)
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
@pytest.mark.asyncio
|
|
1155
|
+
async def test_mock_with_all_three_callbacks(simulation_env):
|
|
1156
|
+
"""Test mock decorator with input, output, and combined callbacks."""
|
|
1157
|
+
captured_inputs = []
|
|
1158
|
+
captured_outputs = []
|
|
1159
|
+
captured_combined = []
|
|
1160
|
+
|
|
1161
|
+
async def capture_input(**kwargs):
|
|
1162
|
+
captured_inputs.append(kwargs)
|
|
1163
|
+
|
|
1164
|
+
async def capture_output(result: int):
|
|
1165
|
+
captured_outputs.append(result)
|
|
1166
|
+
|
|
1167
|
+
async def capture_combined(a: int, b: int, mock_output: int = None):
|
|
1168
|
+
combined = {"sum": a + b, "product": a * b, "mock": mock_output}
|
|
1169
|
+
captured_combined.append(combined)
|
|
1170
|
+
|
|
1171
|
+
@veris.mock(
|
|
1172
|
+
mode="function",
|
|
1173
|
+
input_callback=capture_input,
|
|
1174
|
+
output_callback=capture_output,
|
|
1175
|
+
combined_callback=capture_combined,
|
|
1176
|
+
)
|
|
1177
|
+
async def add(a: int, b: int) -> int:
|
|
1178
|
+
return a + b
|
|
1179
|
+
|
|
1180
|
+
mock_response = 50
|
|
1181
|
+
|
|
1182
|
+
with patch.object(get_api_client(), "post_async", return_value=mock_response):
|
|
1183
|
+
result = await add(10, 20)
|
|
1184
|
+
assert result == 50
|
|
1185
|
+
# Wait for background tasks to complete
|
|
1186
|
+
await asyncio.sleep(0.01)
|
|
1187
|
+
|
|
1188
|
+
# All callbacks should have been called
|
|
1189
|
+
assert len(captured_inputs) == 1
|
|
1190
|
+
assert captured_inputs[0] == {"a": 10, "b": 20}
|
|
1191
|
+
|
|
1192
|
+
assert len(captured_outputs) == 1
|
|
1193
|
+
assert captured_outputs[0] == 50
|
|
1194
|
+
|
|
1195
|
+
assert len(captured_combined) == 1
|
|
1196
|
+
assert captured_combined[0] == {"sum": 30, "product": 200, "mock": 50}
|
|
1197
|
+
|
|
1198
|
+
|
|
1199
|
+
def test_stub_with_all_three_callbacks(simulation_env):
|
|
1200
|
+
"""Test stub decorator with input, output, and combined callbacks."""
|
|
1201
|
+
captured_inputs = []
|
|
1202
|
+
captured_outputs = []
|
|
1203
|
+
captured_combined = []
|
|
1204
|
+
|
|
1205
|
+
def capture_input(**kwargs):
|
|
1206
|
+
captured_inputs.append(kwargs)
|
|
1207
|
+
|
|
1208
|
+
def capture_output(result: str):
|
|
1209
|
+
captured_outputs.append(result)
|
|
1210
|
+
|
|
1211
|
+
def capture_combined(name: str, value: int, mock_output: str = None):
|
|
1212
|
+
combined = f"{name}_{value}_{mock_output}"
|
|
1213
|
+
captured_combined.append(combined)
|
|
1214
|
+
|
|
1215
|
+
@veris.stub(
|
|
1216
|
+
return_value="stubbed",
|
|
1217
|
+
input_callback=capture_input,
|
|
1218
|
+
output_callback=capture_output,
|
|
1219
|
+
combined_callback=capture_combined,
|
|
1220
|
+
)
|
|
1221
|
+
def process(name: str, value: int) -> str:
|
|
1222
|
+
return f"{name}:{value}"
|
|
1223
|
+
|
|
1224
|
+
result = process("test", 42)
|
|
1225
|
+
assert result == "stubbed"
|
|
1226
|
+
|
|
1227
|
+
# All callbacks should have been called
|
|
1228
|
+
assert len(captured_inputs) == 1
|
|
1229
|
+
assert captured_inputs[0] == {"name": "test", "value": 42}
|
|
1230
|
+
|
|
1231
|
+
assert len(captured_outputs) == 1
|
|
1232
|
+
assert captured_outputs[0] == "stubbed"
|
|
1233
|
+
|
|
1234
|
+
assert len(captured_combined) == 1
|
|
1235
|
+
assert captured_combined[0] == "test_42_stubbed"
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
@pytest.mark.asyncio
|
|
1239
|
+
async def test_combined_callback_with_kwargs_accepts_all(simulation_env):
|
|
1240
|
+
"""Test combined callback with **kwargs accepts all parameters including mock_output."""
|
|
1241
|
+
captured_data = []
|
|
1242
|
+
|
|
1243
|
+
async def flexible_callback(**kwargs):
|
|
1244
|
+
"""Callback that accepts any parameters via **kwargs."""
|
|
1245
|
+
captured_data.append(kwargs)
|
|
1246
|
+
|
|
1247
|
+
@veris.mock(mode="function", combined_callback=flexible_callback)
|
|
1248
|
+
async def multi_param_func(a: int, b: str, c: float = 3.14) -> dict:
|
|
1249
|
+
return {"result": a}
|
|
1250
|
+
|
|
1251
|
+
mock_response = {"result": 999}
|
|
1252
|
+
|
|
1253
|
+
with patch.object(get_api_client(), "post_async", return_value=mock_response):
|
|
1254
|
+
result = await multi_param_func(10, "test", c=2.71)
|
|
1255
|
+
assert result == {"result": 999}
|
|
1256
|
+
# Wait for background tasks to complete
|
|
1257
|
+
await asyncio.sleep(0.01)
|
|
1258
|
+
|
|
1259
|
+
assert len(captured_data) == 1
|
|
1260
|
+
# Should have all input params plus mock_output
|
|
1261
|
+
assert captured_data[0]["a"] == 10
|
|
1262
|
+
assert captured_data[0]["b"] == "test"
|
|
1263
|
+
assert captured_data[0]["c"] == 2.71
|
|
1264
|
+
assert captured_data[0]["mock_output"] == {"result": 999}
|
|
1265
|
+
|
|
1266
|
+
|
|
1267
|
+
def test_combined_callback_user_example(simulation_env):
|
|
1268
|
+
"""Test the exact example from the user: add function with multiply_by_result."""
|
|
1269
|
+
multiplication_results = []
|
|
1270
|
+
|
|
1271
|
+
def multiply_by_result(a: int, b: int, d: int = None, mock_output: int = None):
|
|
1272
|
+
"""Multiply sum of provided inputs by the mock output."""
|
|
1273
|
+
# Sum the inputs that are not None
|
|
1274
|
+
input_sum = a + b + (d if d is not None else 0)
|
|
1275
|
+
# Multiply by mock output
|
|
1276
|
+
result = input_sum * (mock_output if mock_output is not None else 1)
|
|
1277
|
+
multiplication_results.append(result)
|
|
1278
|
+
|
|
1279
|
+
@veris.mock(mode="function", combined_callback=multiply_by_result)
|
|
1280
|
+
def add(a: int, b: int, c: int = None, d: int = None) -> int:
|
|
1281
|
+
"""Add numbers together."""
|
|
1282
|
+
total = a + b
|
|
1283
|
+
if c is not None:
|
|
1284
|
+
total += c
|
|
1285
|
+
if d is not None:
|
|
1286
|
+
total += d
|
|
1287
|
+
return total
|
|
1288
|
+
|
|
1289
|
+
# Mock the API to return 6 as the result
|
|
1290
|
+
mock_response = 6
|
|
1291
|
+
|
|
1292
|
+
with patch.object(get_api_client(), "post", return_value=mock_response):
|
|
1293
|
+
# Call add(1, 2, d=4)
|
|
1294
|
+
# Expected: inputs are 1, 2, 4 (c is None)
|
|
1295
|
+
# Mock returns 6
|
|
1296
|
+
# Combined callback should compute: (1 + 2 + 4) * 6 = 42
|
|
1297
|
+
result = add(1, 2, d=4)
|
|
1298
|
+
|
|
1299
|
+
# The function returns the mocked value
|
|
1300
|
+
assert result == 6
|
|
1301
|
+
|
|
1302
|
+
# The combined callback computed (1+2+4)*6
|
|
1303
|
+
assert len(multiplication_results) == 1
|
|
1304
|
+
assert multiplication_results[0] == 42
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|