veris-ai 1.8.0__py3-none-any.whl → 1.8.1__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.

@@ -1,16 +1,14 @@
1
1
  """OpenAI Agents wrapper for automatic tool mocking via Veris SDK."""
2
2
 
3
- import asyncio
4
- import inspect
5
- import json
6
3
  import logging
7
4
  from collections.abc import Callable
8
5
  from typing import Any
9
6
 
10
- from agents import RunResult, Runner
7
+ from agents import RunContextWrapper, RunResult, Runner
11
8
  from pydantic import BaseModel
12
9
 
13
10
  from veris_ai import veris
11
+ from veris_ai.tool_mock import mock_tool_call
14
12
 
15
13
  logger = logging.getLogger(__name__)
16
14
 
@@ -49,8 +47,8 @@ def _wrap(
49
47
 
50
48
  async def wrapped_run(starting_agent: Any, input_text: str, **kwargs: Any) -> Any: # noqa: ANN401
51
49
  """Wrapped version of Runner.run that intercepts tool calls."""
52
- # Patch all tools in the agent
53
- original_tools = []
50
+ # Store a mapping of tools to their original functions
51
+ tool_functions = {}
54
52
 
55
53
  if hasattr(starting_agent, "tools") and starting_agent.tools:
56
54
  for tool in starting_agent.tools:
@@ -61,34 +59,39 @@ def _wrap(
61
59
  if tool_name and _should_intercept_tool(
62
60
  tool_name, include_tools, exclude_tools
63
61
  ):
64
- # Save the original invoke function
65
- original_tools.append((tool, tool.on_invoke_tool)) # type: ignore[attr-defined]
66
-
67
- # Create veris-wrapped version
68
- tool.on_invoke_tool = _create_veris_wrapped_invoke( # type: ignore[attr-defined]
69
- tool,
70
- tool.on_invoke_tool, # type: ignore[attr-defined]
62
+ # Extract the original function before patching
63
+ original_func = _extract_the_func(tool.on_invoke_tool)
64
+ if original_func:
65
+ tool_functions[id(tool)] = original_func
66
+
67
+ # Store original on_invoke_tool
68
+ original_on_invoke = tool.on_invoke_tool
69
+
70
+ def make_wrapped_on_invoke_tool(
71
+ tool_id: int, orig_invoke: Callable
72
+ ) -> Callable:
73
+ """Create a wrapped on_invoke_tool with proper closure."""
74
+
75
+ async def wrapped_on_invoke_tool(
76
+ ctx: RunContextWrapper[Any], parameters: str
77
+ ) -> Any: # noqa: ANN401
78
+ """Wrapped on_invoke_tool that intercepts the tool function."""
79
+ session_id = veris.session_id
80
+ the_func = tool_functions.get(tool_id)
81
+ if the_func and session_id:
82
+ # mock_tool_call is synchronous, don't await it
83
+ return mock_tool_call(
84
+ the_func, session_id, parameters, None
85
+ )
86
+ # Fall back to original if we couldn't extract the function
87
+ return await orig_invoke(ctx, parameters)
88
+
89
+ return wrapped_on_invoke_tool
90
+
91
+ tool.on_invoke_tool = make_wrapped_on_invoke_tool(
92
+ id(tool), original_on_invoke
71
93
  )
72
-
73
- try:
74
- # Call the original Runner.run with the patched agent
75
- return await run_func(starting_agent, input_text, **kwargs)
76
- finally:
77
- # Restore all original tool functions
78
- for tool, original_invoke in original_tools:
79
- tool.on_invoke_tool = original_invoke # type: ignore[attr-defined]
80
-
81
- # Preserve function metadata
82
- wrapped_run.__name__ = getattr(run_func, "__name__", "wrapped_run")
83
- wrapped_run.__doc__ = getattr(run_func, "__doc__", "Wrapped Runner.run function")
84
-
85
- # Also provide a sync version
86
- def wrapped_run_sync(starting_agent: Any, input_text: str, **kwargs: Any) -> Any: # noqa: ANN401
87
- """Sync version of wrapped Runner.run."""
88
- return asyncio.run(wrapped_run(starting_agent, input_text, **kwargs))
89
-
90
- # Add sync version as an attribute
91
- wrapped_run.run_sync = wrapped_run_sync # type: ignore[attr-defined]
94
+ return await run_func(starting_agent, input_text, **kwargs)
92
95
 
93
96
  return wrapped_run
94
97
 
@@ -117,94 +120,108 @@ def _should_intercept_tool(
117
120
  return True
118
121
 
119
122
 
120
- def _create_veris_wrapped_invoke( # noqa: C901
121
- tool: Any, # noqa: ANN401
122
- original_invoke: Callable,
123
- ) -> Callable:
124
- """Create a wrapped invoke function that uses veris.mock().
123
+ def _extract_the_func(on_invoke_tool: Callable) -> Callable | None:
124
+ """Extract the original user function from the on_invoke_tool closure.
125
+
126
+ This function attempts multiple strategies to extract the original function:
127
+ 1. Direct attribute access (if the tool stores it)
128
+ 2. Closure inspection for known patterns
129
+ 3. Deep closure traversal as a fallback
125
130
 
126
131
  Args:
127
- tool: The FunctionTool instance
128
- original_invoke: The original on_invoke_tool function
132
+ on_invoke_tool: The on_invoke_tool function from FunctionTool
129
133
 
130
134
  Returns:
131
- A wrapped invoke function that routes through Veris SDK
135
+ The original user function if found, None otherwise
132
136
  """
133
- # Extract tool metadata
134
- tool_name = tool.name
135
- tool_schema = tool.params_json_schema if hasattr(tool, "params_json_schema") else {}
136
-
137
- # Create a function that will be decorated with veris.mock()
138
- @veris.mock(mode="tool")
139
- async def veris_tool_function(**_kwargs: Any) -> Any: # noqa: ANN401
140
- """Mock function for tool execution."""
141
- # This function's signature doesn't matter as veris.mock()
142
- # will intercept and send the metadata to the endpoint
143
- return f"Mock response for {tool_name}"
144
-
145
- async def wrapped_invoke(context: Any, arguments: str) -> Any: # noqa: ANN401
146
- """Wrapped invoke function that routes through Veris SDK."""
147
- # Only intercept if we have a session ID
148
- if not veris.session_id:
149
- # No session, run original
150
- return await original_invoke(context, arguments)
151
-
152
- # Parse arguments
153
- try:
154
- args_dict = json.loads(arguments) if arguments else {}
155
- except json.JSONDecodeError:
156
- args_dict = {"raw_arguments": arguments}
157
-
158
- # Set up the function with proper metadata for veris.mock()
159
- veris_tool_function.__name__ = tool_name
160
- veris_tool_function.__doc__ = getattr(tool, "description", "")
161
-
162
- # Add type hints based on schema if available
163
- if tool_schema and "properties" in tool_schema:
164
- # Create a simple signature based on schema
165
- params = []
166
- for param_name, param_info in tool_schema["properties"].items():
167
- # Simplified type mapping
168
- param_type: type[Any] = Any
169
- if "type" in param_info:
170
- type_map = {
171
- "string": str,
172
- "number": float,
173
- "integer": int,
174
- "boolean": bool,
175
- "array": list,
176
- "object": dict,
177
- }
178
- param_type = type_map.get(param_info["type"], Any)
179
- params.append(
180
- inspect.Parameter(
181
- param_name,
182
- inspect.Parameter.KEYWORD_ONLY,
183
- annotation=param_type,
184
- )
185
- )
186
-
187
- if params:
188
- veris_tool_function.__signature__ = inspect.Signature(params)
189
-
190
- # Call the veris-wrapped function with the arguments
137
+
138
+ # Strategy 1: Check if the tool has stored the original function as an attribute
139
+ # (This would be the cleanest approach if the agents library supported it)
140
+ if hasattr(on_invoke_tool, "__wrapped__"):
141
+ return on_invoke_tool.__wrapped__
142
+
143
+ # Strategy 2: Look for the function in the closure using known structure
144
+ # Based on the agents library implementation, we know:
145
+ # - on_invoke_tool has _on_invoke_tool_impl in its closure
146
+ # - _on_invoke_tool_impl has the_func in its closure
147
+
148
+ if not hasattr(on_invoke_tool, "__closure__") or not on_invoke_tool.__closure__:
149
+ return None
150
+
151
+ # Find _on_invoke_tool_impl by looking for a function with that name pattern
152
+ for cell in on_invoke_tool.__closure__:
191
153
  try:
192
- return await veris_tool_function(**args_dict)
193
- except Exception as e:
194
- # If mocking fails, fall back to original
195
- logger.warning(f"Veris mock failed for {tool_name}, falling back to original: {e}")
196
- return await original_invoke(context, arguments)
154
+ obj = cell.cell_contents
155
+ if not callable(obj):
156
+ continue
157
+
158
+ # Check if this looks like _on_invoke_tool_impl
159
+ if (
160
+ hasattr(obj, "__name__")
161
+ and "_on_invoke_tool_impl" in obj.__name__
162
+ and hasattr(obj, "__closure__")
163
+ and obj.__closure__
164
+ ):
165
+ # Now look for the_func in its closure
166
+ return _find_user_function_in_closure(obj.__closure__)
167
+ except (ValueError, AttributeError):
168
+ continue
169
+
170
+ # Strategy 3: Fallback - do a broader search in the closure
171
+ return _find_user_function_in_closure(on_invoke_tool.__closure__)
172
+
197
173
 
198
- # Handle sync version if original is sync
199
- if not asyncio.iscoroutinefunction(original_invoke):
174
+ def _find_user_function_in_closure(closure: tuple) -> Callable | None:
175
+ """Find the user function in a closure by filtering out known library functions.
200
176
 
201
- def sync_wrapped_invoke(context: Any, arguments: str) -> Any: # noqa: ANN401
202
- """Sync version of wrapped invoke."""
203
- return asyncio.run(wrapped_invoke(context, arguments))
177
+ Args:
178
+ closure: The closure tuple to search
204
179
 
205
- return sync_wrapped_invoke
180
+ Returns:
181
+ The user function if found, None otherwise
182
+ """
183
+ import inspect
206
184
 
207
- return wrapped_invoke
185
+ # List of module prefixes that indicate library/framework code
186
+ library_modules = ("json", "inspect", "agents", "pydantic", "openai", "typing")
187
+
188
+ for cell in closure:
189
+ try:
190
+ obj = cell.cell_contents
191
+
192
+ # Must be callable but not a class
193
+ if not callable(obj) or isinstance(obj, type):
194
+ continue
195
+
196
+ # Skip private/internal functions
197
+ if hasattr(obj, "__name__") and obj.__name__.startswith("_"):
198
+ continue
199
+
200
+ # Check the module to filter out library code
201
+ module = inspect.getmodule(obj)
202
+ if module:
203
+ # Skip if it's from a known library
204
+ if module.__name__.startswith(library_modules):
205
+ continue
206
+
207
+ # Skip if it's from site-packages (library code)
208
+ if (
209
+ hasattr(module, "__file__")
210
+ and module.__file__
211
+ and "site-packages" in module.__file__
212
+ # Unless it's user code installed as a package
213
+ # (this is a heuristic - may need adjustment)
214
+ and not any(pkg in module.__name__ for pkg in ["my_", "custom_", "app_"])
215
+ ):
216
+ continue
217
+
218
+ # If we made it here, this is likely the user function
219
+ return obj
220
+
221
+ except (ValueError, AttributeError):
222
+ continue
223
+
224
+ return None
208
225
 
209
226
 
210
227
  class VerisConfig(BaseModel):
veris_ai/api_client.py CHANGED
@@ -29,15 +29,7 @@ class SimulatorAPIClient:
29
29
 
30
30
  return headers
31
31
 
32
- async def post_async(self, endpoint: str, payload: dict[str, Any]) -> Any: # noqa: ANN401
33
- """Make an async POST request to the specified endpoint."""
34
- headers = self._build_headers()
35
- async with httpx.AsyncClient(timeout=self.timeout) as client:
36
- response = await client.post(endpoint, json=payload, headers=headers)
37
- response.raise_for_status()
38
- return response.json() if response.content else None
39
-
40
- def post_sync(self, endpoint: str, payload: dict[str, Any]) -> Any: # noqa: ANN401
32
+ def post(self, endpoint: str, payload: dict[str, Any]) -> Any: # noqa: ANN401
41
33
  """Make a synchronous POST request to the specified endpoint."""
42
34
  headers = self._build_headers()
43
35
  with httpx.Client(timeout=self.timeout) as client:
veris_ai/logging.py CHANGED
@@ -9,69 +9,28 @@ from veris_ai.api_client import get_api_client
9
9
  logger = logging.getLogger(__name__)
10
10
 
11
11
 
12
- async def log_tool_call_async(
12
+ def log_tool_call(
13
13
  session_id: str,
14
14
  function_name: str,
15
- parameters: dict[str, Any],
16
- docstring: str,
17
- ) -> None:
18
- """Log tool call asynchronously to the VERIS logging endpoint."""
19
- api_client = get_api_client()
20
- endpoint = api_client.get_log_tool_call_endpoint(session_id)
21
-
22
- payload = {
23
- "function_name": function_name,
24
- "parameters": parameters,
25
- "docstring": docstring,
26
- }
27
-
28
- try:
29
- await api_client.post_async(endpoint, payload)
30
- logger.debug(f"Tool call logged for {function_name}")
31
- except Exception as e:
32
- logger.warning(f"Failed to log tool call for {function_name}: {e}")
33
-
34
-
35
- def log_tool_call_sync(
36
- session_id: str,
37
- function_name: str,
38
- parameters: dict[str, Any],
15
+ parameters: str,
39
16
  docstring: str,
40
17
  ) -> None:
41
18
  """Log tool call synchronously to the VERIS logging endpoint."""
42
19
  api_client = get_api_client()
43
20
  endpoint = api_client.get_log_tool_call_endpoint(session_id)
44
-
45
21
  payload = {
46
22
  "function_name": function_name,
47
- "parameters": parameters,
23
+ "parameters": json.loads(parameters),
48
24
  "docstring": docstring,
49
25
  }
50
-
51
26
  try:
52
- api_client.post_sync(endpoint, payload)
27
+ api_client.post(endpoint, payload)
53
28
  logger.debug(f"Tool call logged for {function_name}")
54
29
  except Exception as e:
55
30
  logger.warning(f"Failed to log tool call for {function_name}: {e}")
56
31
 
57
32
 
58
- async def log_tool_response_async(session_id: str, response: Any) -> None: # noqa: ANN401
59
- """Log tool response asynchronously to the VERIS logging endpoint."""
60
- api_client = get_api_client()
61
- endpoint = api_client.get_log_tool_response_endpoint(session_id)
62
-
63
- payload = {
64
- "response": json.dumps(response, default=str),
65
- }
66
-
67
- try:
68
- await api_client.post_async(endpoint, payload)
69
- logger.debug("Tool response logged")
70
- except Exception as e:
71
- logger.warning(f"Failed to log tool response: {e}")
72
-
73
-
74
- def log_tool_response_sync(session_id: str, response: Any) -> None: # noqa: ANN401
33
+ def log_tool_response(session_id: str, response: Any) -> None: # noqa: ANN401
75
34
  """Log tool response synchronously to the VERIS logging endpoint."""
76
35
  api_client = get_api_client()
77
36
  endpoint = api_client.get_log_tool_response_endpoint(session_id)
@@ -81,7 +40,7 @@ def log_tool_response_sync(session_id: str, response: Any) -> None: # noqa: ANN
81
40
  }
82
41
 
83
42
  try:
84
- api_client.post_sync(endpoint, payload)
43
+ api_client.post(endpoint, payload)
85
44
  logger.debug("Tool response logged")
86
45
  except Exception as e:
87
46
  logger.warning(f"Failed to log tool response: {e}")
veris_ai/models.py CHANGED
@@ -1,6 +1,9 @@
1
1
  """Models for the VERIS SDK."""
2
2
 
3
3
  from enum import Enum
4
+ from typing import Literal
5
+
6
+ from pydantic import BaseModel
4
7
 
5
8
 
6
9
  class ResponseExpectation(str, Enum):
@@ -9,3 +12,11 @@ class ResponseExpectation(str, Enum):
9
12
  AUTO = "auto"
10
13
  REQUIRED = "required"
11
14
  NONE = "none"
15
+
16
+
17
+ class ToolCallOptions(BaseModel):
18
+ """Options for tool call."""
19
+
20
+ response_expectation: ResponseExpectation = ResponseExpectation.AUTO
21
+ cache_response: bool = False
22
+ mode: Literal["tool", "function", "spy"] = "tool"
veris_ai/tool_mock.py CHANGED
@@ -12,13 +12,12 @@ from typing import (
12
12
  get_type_hints,
13
13
  )
14
14
 
15
+
15
16
  from veris_ai.logging import (
16
- log_tool_call_async,
17
- log_tool_call_sync,
18
- log_tool_response_async,
19
- log_tool_response_sync,
17
+ log_tool_call,
18
+ log_tool_response,
20
19
  )
21
- from veris_ai.models import ResponseExpectation
20
+ from veris_ai.models import ResponseExpectation, ToolCallOptions
22
21
  from veris_ai.api_client import get_api_client
23
22
  from veris_ai.utils import convert_to_type, extract_json_schema, get_function_parameters
24
23
 
@@ -96,185 +95,109 @@ class VerisSDK:
96
95
  **params_dict,
97
96
  )
98
97
 
98
+ def spy(self) -> Callable:
99
+ """Decorator for spying on tool calls."""
100
+
101
+ def decorator(func: Callable) -> Callable:
102
+ """Decorator for spying on tool calls."""
103
+ is_async = inspect.iscoroutinefunction(func)
104
+
105
+ @wraps(func)
106
+ async def async_wrapper(*args: tuple[object, ...], **kwargs: Any) -> object: # noqa: ANN401
107
+ """Async wrapper."""
108
+ session_id = _session_id_context.get()
109
+ if not session_id:
110
+ return await func(*args, **kwargs)
111
+ parameters = get_function_parameters(func, args, kwargs)
112
+ logger.info(f"Spying on function: {func.__name__}")
113
+ log_tool_call(
114
+ session_id=session_id,
115
+ function_name=func.__name__,
116
+ parameters=parameters,
117
+ docstring=inspect.getdoc(func) or "",
118
+ )
119
+ result = await func(*args, **kwargs)
120
+ log_tool_response(session_id=session_id, response=result)
121
+ return result
122
+
123
+ @wraps(func)
124
+ def sync_wrapper(*args: tuple[object, ...], **kwargs: Any) -> object: # noqa: ANN401
125
+ """Sync wrapper."""
126
+ session_id = _session_id_context.get()
127
+ if not session_id:
128
+ return func(*args, **kwargs)
129
+ parameters = get_function_parameters(func, args, kwargs)
130
+ logger.info(f"Spying on function: {func.__name__}")
131
+ log_tool_call(
132
+ session_id=session_id,
133
+ function_name=func.__name__,
134
+ parameters=parameters,
135
+ docstring=inspect.getdoc(func) or "",
136
+ )
137
+ result = func(*args, **kwargs)
138
+ log_tool_response(session_id=session_id, response=result)
139
+ return result
140
+
141
+ return async_wrapper if is_async else sync_wrapper
142
+
143
+ return decorator
144
+
99
145
  def mock( # noqa: C901, PLR0915
100
146
  self,
101
- mode: Literal["tool", "function", "spy"] = "tool",
147
+ mode: Literal["tool", "function"] = "tool",
102
148
  expects_response: bool | None = None,
103
149
  cache_response: bool | None = None,
104
150
  ) -> Callable:
105
151
  """Decorator for mocking tool calls."""
152
+ response_expectation = (
153
+ ResponseExpectation.NONE
154
+ if (expects_response is False or (expects_response is None and mode == "function"))
155
+ else ResponseExpectation.REQUIRED
156
+ if expects_response is True
157
+ else ResponseExpectation.AUTO
158
+ )
159
+ cache_response = cache_response or False
160
+ options = ToolCallOptions(
161
+ mode=mode, response_expectation=response_expectation, cache_response=cache_response
162
+ )
106
163
 
107
164
  def decorator(func: Callable) -> Callable: # noqa: C901, PLR0915
108
165
  """Decorator for mocking tool calls."""
109
- # Check if the original function is async
110
166
  is_async = inspect.iscoroutinefunction(func)
111
167
 
112
- def create_mock_payload(
113
- *args: tuple[object, ...],
114
- **kwargs: Any, # noqa: ANN401
115
- ) -> tuple[dict[str, Any], Any]:
116
- """Create the mock payload - shared logic for both sync and async."""
117
- type_hints = get_type_hints(func)
118
-
119
- # Extract return type object (not just the name)
120
- return_type_obj = type_hints.pop("return", Any)
121
- # Create parameter info
122
- parameters = get_function_parameters(func, args, kwargs)
123
- # Get function docstring
124
- docstring = inspect.getdoc(func) or ""
125
- nonlocal expects_response
126
- if expects_response is None and mode == "function":
127
- expects_response = False
128
- # Prepare payload
129
- # Convert expects_response to response_expectation enum
130
- if expects_response is False:
131
- response_expectation = ResponseExpectation.NONE
132
- elif expects_response is True:
133
- response_expectation = ResponseExpectation.REQUIRED
134
- else:
135
- response_expectation = ResponseExpectation.AUTO
136
-
137
- payload = {
138
- "session_id": self.session_id,
139
- "response_expectation": response_expectation.value,
140
- "cache_response": bool(cache_response) if cache_response is not None else False,
141
- "tool_call": {
142
- "function_name": func.__name__,
143
- "parameters": parameters,
144
- "return_type": json.dumps(extract_json_schema(return_type_obj)),
145
- "docstring": docstring,
146
- },
147
- }
148
-
149
- return payload, return_type_obj
150
-
151
168
  @wraps(func)
152
169
  async def async_wrapper(
153
170
  *args: tuple[object, ...],
154
171
  **kwargs: Any, # noqa: ANN401
155
172
  ) -> object:
156
- # Check if we're in simulation mode
157
- if not self.session_id:
158
- # If not in simulation mode, execute the original function
173
+ """Async wrapper."""
174
+ session_id = _session_id_context.get()
175
+ if not session_id:
159
176
  return await func(*args, **kwargs)
160
-
161
- async def _execute_mock_logic(session_id: str) -> object:
162
- # Handle spy mode - execute original function and log
163
- if mode == "spy":
164
- logger.info(f"Spying on function: {func.__name__}")
165
-
166
- # Log the tool call
167
- parameters = get_function_parameters(func, args, kwargs)
168
-
169
- await log_tool_call_async(
170
- session_id=session_id,
171
- function_name=func.__name__,
172
- parameters=parameters,
173
- docstring=inspect.getdoc(func) or "",
174
- )
175
-
176
- # Execute the original function
177
- result = await func(*args, **kwargs)
178
-
179
- # Log the response
180
- await log_tool_response_async(session_id=session_id, response=result)
181
-
182
- return result
183
-
184
- # Regular mock mode
185
- api_client = get_api_client()
186
- endpoint = api_client.tool_mock_endpoint
187
-
188
- logger.info(f"Simulating function: {func.__name__}")
189
- payload, return_type_obj = create_mock_payload(*args, **kwargs)
190
-
191
- # Send request to endpoint
192
- mock_result = await api_client.post_async(endpoint, payload)
193
- logger.info(f"Mock response: {mock_result}")
194
-
195
- if isinstance(mock_result, str):
196
- with suppress(json.JSONDecodeError):
197
- mock_result = json.loads(mock_result)
198
- return convert_to_type(mock_result, return_type_obj)
199
- return convert_to_type(mock_result, return_type_obj)
200
-
201
- # Create a top-level span for the simulated mock call if OpenTelemetry is available
202
- try:
203
- from opentelemetry import trace # type: ignore[import-not-found]
204
-
205
- tracer = trace.get_tracer("veris_ai.tool_mock")
206
- span_name = f"mock.{func.__name__}"
207
- with tracer.start_as_current_span(span_name) as span: # type: ignore[attr-defined]
208
- span.set_attribute("veris_ai.session.id", self.session_id or "") # type: ignore[attr-defined]
209
- span.set_attribute("veris_ai.mock.mode", mode) # type: ignore[attr-defined]
210
- return await _execute_mock_logic(self.session_id)
211
- except Exception:
212
- # If OpenTelemetry is not available, run without span
213
- return await _execute_mock_logic(self.session_id)
177
+ parameters = get_function_parameters(func, args, kwargs)
178
+ return mock_tool_call(
179
+ func,
180
+ session_id,
181
+ parameters,
182
+ options,
183
+ )
214
184
 
215
185
  @wraps(func)
216
186
  def sync_wrapper(
217
187
  *args: tuple[object, ...],
218
188
  **kwargs: Any, # noqa: ANN401
219
189
  ) -> object:
220
- # Check if we're in simulation mode
221
- if not self.session_id:
222
- # If not in simulation mode, execute the original function
190
+ """Sync wrapper."""
191
+ session_id = _session_id_context.get()
192
+ if not session_id:
223
193
  return func(*args, **kwargs)
224
-
225
- def _execute_mock_logic(session_id: str) -> object:
226
- # Handle spy mode - execute original function and log
227
- if mode == "spy":
228
- logger.info(f"Spying on function: {func.__name__}")
229
-
230
- # Log the tool call
231
- parameters = get_function_parameters(func, args, kwargs)
232
-
233
- log_tool_call_sync(
234
- session_id=session_id,
235
- function_name=func.__name__,
236
- parameters=parameters,
237
- docstring=inspect.getdoc(func) or "",
238
- )
239
-
240
- # Execute the original function
241
- result = func(*args, **kwargs)
242
-
243
- # Log the response
244
- log_tool_response_sync(session_id=session_id, response=result)
245
-
246
- return result
247
-
248
- # Regular mock mode
249
- api_client = get_api_client()
250
- endpoint = api_client.tool_mock_endpoint
251
-
252
- logger.info(f"Simulating function: {func.__name__}")
253
- payload, return_type_obj = create_mock_payload(*args, **kwargs)
254
-
255
- # Send request to endpoint
256
- mock_result = api_client.post_sync(endpoint, payload)
257
- logger.info(f"Mock response: {mock_result}")
258
-
259
- if isinstance(mock_result, str):
260
- with suppress(json.JSONDecodeError):
261
- mock_result = json.loads(mock_result)
262
- return convert_to_type(mock_result, return_type_obj)
263
- return convert_to_type(mock_result, return_type_obj)
264
-
265
- # Create a top-level span for the simulated mock call if OpenTelemetry is available
266
- try:
267
- from opentelemetry import trace # type: ignore[import-not-found]
268
-
269
- tracer = trace.get_tracer("veris_ai.tool_mock")
270
- span_name = f"mock.{func.__name__}"
271
- with tracer.start_as_current_span(span_name) as span: # type: ignore[attr-defined]
272
- span.set_attribute("veris_ai.session.id", self.session_id or "") # type: ignore[attr-defined]
273
- span.set_attribute("veris_ai.mock.mode", mode) # type: ignore[attr-defined]
274
- return _execute_mock_logic(self.session_id)
275
- except Exception:
276
- # If OpenTelemetry is not available, run without span
277
- return _execute_mock_logic(self.session_id)
194
+ parameters = get_function_parameters(func, args, kwargs)
195
+ return mock_tool_call(
196
+ func,
197
+ session_id,
198
+ parameters,
199
+ options,
200
+ )
278
201
 
279
202
  # Return the appropriate wrapper based on whether the function is async
280
203
  return async_wrapper if is_async else sync_wrapper
@@ -313,4 +236,46 @@ class VerisSDK:
313
236
  return decorator
314
237
 
315
238
 
239
+ def mock_tool_call(
240
+ func: Callable,
241
+ session_id: str,
242
+ parameters: str,
243
+ options: ToolCallOptions | None = None,
244
+ ) -> object:
245
+ """Mock tool call."""
246
+ options = options or ToolCallOptions()
247
+ api_client = get_api_client()
248
+ endpoint = api_client.tool_mock_endpoint
249
+
250
+ logger.info(f"Simulating function: {func.__name__}")
251
+
252
+ type_hints = get_type_hints(func)
253
+
254
+ # Extract return type object (not just the name)
255
+ return_type_obj = type_hints.pop("return", Any)
256
+ # Get function docstring
257
+ docstring = inspect.getdoc(func) or ""
258
+ # Determine response expectation
259
+ payload = {
260
+ "session_id": session_id,
261
+ "response_expectation": options.response_expectation.value,
262
+ "cache_response": bool(options.cache_response),
263
+ "tool_call": {
264
+ "function_name": func.__name__,
265
+ "parameters": json.loads(parameters),
266
+ "return_type": json.dumps(extract_json_schema(return_type_obj)),
267
+ "docstring": docstring,
268
+ },
269
+ }
270
+
271
+ mock_result = api_client.post(endpoint, payload)
272
+ logger.info(f"Mock response: {mock_result}")
273
+
274
+ if isinstance(mock_result, str):
275
+ with suppress(json.JSONDecodeError):
276
+ mock_result = json.loads(mock_result)
277
+ return convert_to_type(mock_result, return_type_obj)
278
+ return convert_to_type(mock_result, return_type_obj)
279
+
280
+
316
281
  veris = VerisSDK()
veris_ai/utils.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import inspect
2
+ import json
2
3
  import sys
3
4
  import types
4
5
  import typing
@@ -278,7 +279,7 @@ def extract_json_schema(target_type: Any) -> dict: # noqa: PLR0911, PLR0912, C9
278
279
 
279
280
  def get_function_parameters(
280
281
  func: Callable, args: tuple[object, ...], kwargs: dict[str, object]
281
- ) -> dict[str, object]:
282
+ ) -> str:
282
283
  """Get the parameters for a function."""
283
284
  sig = inspect.signature(func)
284
285
  bound_args = sig.bind(*args, **kwargs)
@@ -286,4 +287,4 @@ def get_function_parameters(
286
287
  _ = bound_args.arguments.pop("ctx", None)
287
288
  _ = bound_args.arguments.pop("self", None)
288
289
  _ = bound_args.arguments.pop("cls", None)
289
- return bound_args.arguments
290
+ return json.dumps(bound_args.arguments)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: veris-ai
3
- Version: 1.8.0
3
+ Version: 1.8.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
@@ -168,7 +168,7 @@ async def your_function(param1: str, param2: int) -> dict:
168
168
  return {"result": "actual implementation"}
169
169
 
170
170
  # Spy mode: Executes function but logs calls/responses
171
- @veris.mock(mode="spy")
171
+ @veris.spy()
172
172
  async def monitored_function(data: str) -> dict:
173
173
  return process_data(data)
174
174
 
@@ -0,0 +1,17 @@
1
+ veris_ai/README.md,sha256=0EC-yWB8FtqAhtcE5Q204bS1JWJPIvfpp1bPDo6iwwc,3601
2
+ veris_ai/__init__.py,sha256=mFEF2pHXOAXoXuBh9IOQgqpKGvzLOsy8b0vUpMRJggU,2260
3
+ veris_ai/agents_wrapper.py,sha256=LmxPkOg3KLICgTrl0Bouy7enhyO4tF6_evq2cb8I_ig,10718
4
+ veris_ai/api_client.py,sha256=lpokGrDqC222eM8J65814a85DfEL_iAlHRatf9BFy2Y,2136
5
+ veris_ai/logging.py,sha256=ENduOHnc5UmzciTDZDJCZqUOXb5We-bdtwN9f9Qqwl0,1360
6
+ veris_ai/models.py,sha256=TiQJuME2pGDE6EY5J8TYKNSeajaN2Xk9ISsLoIwgtgU,502
7
+ veris_ai/observability.py,sha256=fDtWeUexfzaIQArE5YbWsja8Y-bcE_h0dXQWYbXbupY,4929
8
+ veris_ai/tool_mock.py,sha256=L2p6Yg8S98floahDTrf6cnmDqC6Dq3YKA7Q6RRvv7BQ,10253
9
+ veris_ai/utils.py,sha256=Ud4k2jKAJ6-nqSsFXIJWYrOmPGmvl5RSp36cQtgnMpg,9781
10
+ veris_ai/jaeger_interface/README.md,sha256=kd9rKcE5xf3EyNaiHu0tjn-0oES9sfaK6Ih-OhhTyCM,2821
11
+ veris_ai/jaeger_interface/__init__.py,sha256=KD7NSiMYRG_2uF6dOLKkGG5lNQe4K9ptEwucwMT4_aw,1128
12
+ veris_ai/jaeger_interface/client.py,sha256=yJrh86wRR0Dk3Gq12DId99WogcMIVbL0QQFqVSevvlE,8772
13
+ veris_ai/jaeger_interface/models.py,sha256=e64VV6IvOEFuzRUgvDAMQFyOZMRb56I-PUPZLBZ3rX0,1864
14
+ veris_ai-1.8.1.dist-info/METADATA,sha256=SCSuZSupecfwozColHl09XlE4Hy5W_R7xqHw8Wco560,9924
15
+ veris_ai-1.8.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
+ veris_ai-1.8.1.dist-info/licenses/LICENSE,sha256=2g4i20atAgtD5einaKzhQrIB-JrPhyQgD3bC0wkHcCI,1065
17
+ veris_ai-1.8.1.dist-info/RECORD,,
@@ -1,17 +0,0 @@
1
- veris_ai/README.md,sha256=0EC-yWB8FtqAhtcE5Q204bS1JWJPIvfpp1bPDo6iwwc,3601
2
- veris_ai/__init__.py,sha256=mFEF2pHXOAXoXuBh9IOQgqpKGvzLOsy8b0vUpMRJggU,2260
3
- veris_ai/agents_wrapper.py,sha256=-8R2yTkEQjHQJxkWBK5tqMHzmlPjfDpybD9_UpsSjLk,9971
4
- veris_ai/api_client.py,sha256=afvVvg9DXSHgnr0p411yXo3_8UM7EeiIoqm1q_goYgI,2602
5
- veris_ai/logging.py,sha256=Dr7dQtTK_WvUAtSpif79Srf5YpSLwDVlWs0XsQGOdu4,2595
6
- veris_ai/models.py,sha256=6HINPxNFCakCVPcyEbUswWkXwb2K4lF0A8g8EvTMal4,213
7
- veris_ai/observability.py,sha256=fDtWeUexfzaIQArE5YbWsja8Y-bcE_h0dXQWYbXbupY,4929
8
- veris_ai/tool_mock.py,sha256=77KxBy0mAxC58sXYs_jPHBT_2oEkXybsUGKzOOLH93w,13334
9
- veris_ai/utils.py,sha256=bSutoMYQAboo9nHZ4yAsOGIDEoc1zaoIXCoJr4yTvBw,9771
10
- veris_ai/jaeger_interface/README.md,sha256=kd9rKcE5xf3EyNaiHu0tjn-0oES9sfaK6Ih-OhhTyCM,2821
11
- veris_ai/jaeger_interface/__init__.py,sha256=KD7NSiMYRG_2uF6dOLKkGG5lNQe4K9ptEwucwMT4_aw,1128
12
- veris_ai/jaeger_interface/client.py,sha256=yJrh86wRR0Dk3Gq12DId99WogcMIVbL0QQFqVSevvlE,8772
13
- veris_ai/jaeger_interface/models.py,sha256=e64VV6IvOEFuzRUgvDAMQFyOZMRb56I-PUPZLBZ3rX0,1864
14
- veris_ai-1.8.0.dist-info/METADATA,sha256=r79b39D4sUnlT2sJWNxbMk2Y1ES4pRVLkaO0K7BYsDc,9935
15
- veris_ai-1.8.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
- veris_ai-1.8.0.dist-info/licenses/LICENSE,sha256=2g4i20atAgtD5einaKzhQrIB-JrPhyQgD3bC0wkHcCI,1065
17
- veris_ai-1.8.0.dist-info/RECORD,,