veris-ai 1.8.0__tar.gz → 1.8.2__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.

Files changed (44) hide show
  1. {veris_ai-1.8.0 → veris_ai-1.8.2}/PKG-INFO +2 -2
  2. {veris_ai-1.8.0 → veris_ai-1.8.2}/README.md +1 -1
  3. {veris_ai-1.8.0 → veris_ai-1.8.2}/examples/README.md +1 -1
  4. {veris_ai-1.8.0 → veris_ai-1.8.2}/pyproject.toml +1 -1
  5. veris_ai-1.8.2/src/veris_ai/agents_wrapper.py +283 -0
  6. {veris_ai-1.8.0 → veris_ai-1.8.2}/src/veris_ai/api_client.py +5 -12
  7. veris_ai-1.8.2/src/veris_ai/logging.py +46 -0
  8. veris_ai-1.8.2/src/veris_ai/models.py +22 -0
  9. veris_ai-1.8.2/src/veris_ai/tool_mock.py +281 -0
  10. {veris_ai-1.8.0 → veris_ai-1.8.2}/src/veris_ai/utils.py +3 -2
  11. veris_ai-1.8.2/tests/test_agents_wrapper_extract.py +163 -0
  12. {veris_ai-1.8.0 → veris_ai-1.8.2}/tests/test_mcp_protocol_server_mocked.py +3 -3
  13. {veris_ai-1.8.0 → veris_ai-1.8.2}/tests/test_tool_mock.py +177 -17
  14. {veris_ai-1.8.0 → veris_ai-1.8.2}/uv.lock +715 -715
  15. veris_ai-1.8.0/src/veris_ai/agents_wrapper.py +0 -266
  16. veris_ai-1.8.0/src/veris_ai/logging.py +0 -87
  17. veris_ai-1.8.0/src/veris_ai/models.py +0 -11
  18. veris_ai-1.8.0/src/veris_ai/tool_mock.py +0 -316
  19. {veris_ai-1.8.0 → veris_ai-1.8.2}/.cursor/rules/documentation-management.mdc +0 -0
  20. {veris_ai-1.8.0 → veris_ai-1.8.2}/.github/workflows/release.yml +0 -0
  21. {veris_ai-1.8.0 → veris_ai-1.8.2}/.github/workflows/test.yml +0 -0
  22. {veris_ai-1.8.0 → veris_ai-1.8.2}/.gitignore +0 -0
  23. {veris_ai-1.8.0 → veris_ai-1.8.2}/.pre-commit-config.yaml +0 -0
  24. {veris_ai-1.8.0 → veris_ai-1.8.2}/CHANGELOG.md +0 -0
  25. {veris_ai-1.8.0 → veris_ai-1.8.2}/CLAUDE.md +0 -0
  26. {veris_ai-1.8.0 → veris_ai-1.8.2}/LICENSE +0 -0
  27. {veris_ai-1.8.0 → veris_ai-1.8.2}/examples/__init__.py +0 -0
  28. {veris_ai-1.8.0 → veris_ai-1.8.2}/examples/import_options.py +0 -0
  29. {veris_ai-1.8.0 → veris_ai-1.8.2}/examples/openai_agents_example.py +0 -0
  30. {veris_ai-1.8.0 → veris_ai-1.8.2}/src/veris_ai/README.md +0 -0
  31. {veris_ai-1.8.0 → veris_ai-1.8.2}/src/veris_ai/__init__.py +0 -0
  32. {veris_ai-1.8.0 → veris_ai-1.8.2}/src/veris_ai/jaeger_interface/README.md +0 -0
  33. {veris_ai-1.8.0 → veris_ai-1.8.2}/src/veris_ai/jaeger_interface/__init__.py +0 -0
  34. {veris_ai-1.8.0 → veris_ai-1.8.2}/src/veris_ai/jaeger_interface/client.py +0 -0
  35. {veris_ai-1.8.0 → veris_ai-1.8.2}/src/veris_ai/jaeger_interface/models.py +0 -0
  36. {veris_ai-1.8.0 → veris_ai-1.8.2}/src/veris_ai/observability.py +0 -0
  37. {veris_ai-1.8.0 → veris_ai-1.8.2}/tests/README.md +0 -0
  38. {veris_ai-1.8.0 → veris_ai-1.8.2}/tests/__init__.py +0 -0
  39. {veris_ai-1.8.0 → veris_ai-1.8.2}/tests/conftest.py +0 -0
  40. {veris_ai-1.8.0 → veris_ai-1.8.2}/tests/fixtures/__init__.py +0 -0
  41. {veris_ai-1.8.0 → veris_ai-1.8.2}/tests/fixtures/http_server.py +0 -0
  42. {veris_ai-1.8.0 → veris_ai-1.8.2}/tests/fixtures/simple_app.py +0 -0
  43. {veris_ai-1.8.0 → veris_ai-1.8.2}/tests/test_agents_wrapper_simple.py +0 -0
  44. {veris_ai-1.8.0 → veris_ai-1.8.2}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: veris-ai
3
- Version: 1.8.0
3
+ Version: 1.8.2
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
 
@@ -126,7 +126,7 @@ async def your_function(param1: str, param2: int) -> dict:
126
126
  return {"result": "actual implementation"}
127
127
 
128
128
  # Spy mode: Executes function but logs calls/responses
129
- @veris.mock(mode="spy")
129
+ @veris.spy()
130
130
  async def monitored_function(data: str) -> dict:
131
131
  return process_data(data)
132
132
 
@@ -64,7 +64,7 @@ if os.getenv("USE_FASTAPI") == "true":
64
64
 
65
65
  ### Decorator Usage
66
66
  - **Function Mocking**: `@veris.mock()` for simulation mode
67
- - **Spy Mode**: `@veris.mock(mode="spy")` for logging
67
+ - **Spy Mode**: `@veris.spy()` for logging
68
68
  - **Stub Mode**: `@veris.stub(return_value={})` for fixed responses
69
69
 
70
70
  ### Error Handling
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "veris-ai"
7
- version = "1.8.0"
7
+ version = "1.8.2"
8
8
  description = "A Python package for Veris AI tools"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -0,0 +1,283 @@
1
+ """OpenAI Agents wrapper for automatic tool mocking via Veris SDK."""
2
+
3
+ import logging
4
+ from collections.abc import Callable
5
+ from typing import Any
6
+
7
+ from agents import RunContextWrapper, RunResult, Runner
8
+ from pydantic import BaseModel
9
+
10
+ from veris_ai import veris
11
+ from veris_ai.tool_mock import mock_tool_call
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def _wrap(
17
+ include_tools: list[str] | None = None,
18
+ exclude_tools: list[str] | None = None,
19
+ ) -> Callable:
20
+ """Private wrapper for OpenAI agents Runner to intercept tool calls through Veris SDK.
21
+
22
+ This function transparently intercepts tool calls from OpenAI agents and
23
+ routes them through the Veris SDK's mocking infrastructure.
24
+
25
+ Args:
26
+ include_tools: Optional list of tool names to intercept (only these if provided)
27
+ exclude_tools: Optional list of tool names to NOT intercept (these run normally)
28
+
29
+ Returns:
30
+ A wrapped Runner.run function
31
+
32
+ Raises:
33
+ ValueError: If both include_tools and exclude_tools are specified
34
+ ImportError: If agents package is not installed
35
+ """
36
+ if include_tools and exclude_tools:
37
+ msg = "Cannot specify both include_tools and exclude_tools"
38
+ raise ValueError(msg)
39
+
40
+ def wrapped_run_func(run_func: Callable) -> Callable:
41
+ """Inner wrapper that takes the actual Runner.run function."""
42
+ try:
43
+ from agents import FunctionTool # type: ignore[import-untyped] # noqa: PLC0415
44
+ except ImportError as e:
45
+ msg = "openai-agents package not installed. Install with: pip install veris-ai[agents]"
46
+ raise ImportError(msg) from e
47
+
48
+ async def wrapped_run(starting_agent: Any, input_text: str, **kwargs: Any) -> Any: # noqa: ANN401
49
+ """Wrapped version of Runner.run that intercepts tool calls."""
50
+ # Store a mapping of tools to their original functions
51
+ tool_functions = {}
52
+
53
+ if hasattr(starting_agent, "tools") and starting_agent.tools:
54
+ for tool in starting_agent.tools:
55
+ if isinstance(tool, FunctionTool):
56
+ tool_name = getattr(tool, "name", None)
57
+
58
+ # Check if we should patch this tool
59
+ if tool_name and _should_intercept_tool(
60
+ tool_name, include_tools, exclude_tools
61
+ ):
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
93
+ )
94
+ return await run_func(starting_agent, input_text, **kwargs)
95
+
96
+ return wrapped_run
97
+
98
+ return wrapped_run_func
99
+
100
+
101
+ def _should_intercept_tool(
102
+ tool_name: str,
103
+ include_tools: list[str] | None,
104
+ exclude_tools: list[str] | None,
105
+ ) -> bool:
106
+ """Determine if a tool should be intercepted based on include/exclude lists.
107
+
108
+ Args:
109
+ tool_name: Name of the tool
110
+ include_tools: If provided, only these tools are intercepted
111
+ exclude_tools: If provided, these tools are NOT intercepted
112
+
113
+ Returns:
114
+ True if the tool should be intercepted, False otherwise
115
+ """
116
+ if include_tools:
117
+ return tool_name in include_tools
118
+ if exclude_tools:
119
+ return tool_name not in exclude_tools
120
+ return True
121
+
122
+
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
130
+
131
+ Args:
132
+ on_invoke_tool: The on_invoke_tool function from FunctionTool
133
+
134
+ Returns:
135
+ The original user function if found, None otherwise
136
+ """
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__:
153
+ try:
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
+
173
+
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.
176
+
177
+ Args:
178
+ closure: The closure tuple to search
179
+
180
+ Returns:
181
+ The user function if found, None otherwise
182
+ """
183
+ import inspect
184
+
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
225
+
226
+
227
+ class VerisConfig(BaseModel):
228
+ """Configuration for the Veris SDK."""
229
+
230
+ include_tools: list[str] | None = None
231
+ exclude_tools: list[str] | None = None
232
+
233
+
234
+ def veris_runner(
235
+ starting_agent: Any, # noqa: ANN401
236
+ input_text: str,
237
+ veris_config: VerisConfig | None = None,
238
+ **kwargs: Any, # noqa: ANN401
239
+ ) -> RunResult: # noqa: ANN401
240
+ """Veris-wrapped version of OpenAI agents Runner.run.
241
+
242
+ This function wraps the OpenAI agents Runner.run to intercept tool calls
243
+ and route them through the Veris SDK's mocking infrastructure. It can be
244
+ used as a drop-in replacement for Runner.run with an additional veris_config parameter.
245
+
246
+ Args:
247
+ starting_agent: The OpenAI agent to run
248
+ input_text: The input text to process
249
+ veris_config: Optional configuration for Veris SDK tool interception
250
+ **kwargs: Additional arguments to pass to Runner.run
251
+
252
+ Returns:
253
+ The result from Runner.run
254
+
255
+ Example:
256
+ ```python
257
+ from veris_ai import veris_runner, VerisConfig
258
+ from agents import Agent, FunctionTool
259
+
260
+ # Define your agent with tools
261
+ agent = Agent(...)
262
+
263
+ # Use veris_runner instead of Runner.run
264
+ result = await veris_runner(agent, "Process this input")
265
+
266
+ # Or with specific tool configuration
267
+ config = VerisConfig(include_tools=["calculator", "search"])
268
+ result = await veris_runner(agent, "Calculate 2+2", veris_config=config)
269
+ ```
270
+ """
271
+
272
+ # Extract config values
273
+ include_tools = None
274
+ exclude_tools = None
275
+ if veris_config:
276
+ include_tools = veris_config.include_tools
277
+ exclude_tools = veris_config.exclude_tools
278
+
279
+ # Create the wrapped version of Runner.run with the config
280
+ wrapped_run = _wrap(include_tools=include_tools, exclude_tools=exclude_tools)(Runner.run)
281
+
282
+ # Execute the wrapped run function
283
+ return wrapped_run(starting_agent, input_text, **kwargs)
@@ -5,6 +5,7 @@ import os
5
5
  from typing import Any
6
6
 
7
7
  import httpx
8
+ from urllib.parse import urljoin
8
9
 
9
10
  logger = logging.getLogger(__name__)
10
11
 
@@ -29,15 +30,7 @@ class SimulatorAPIClient:
29
30
 
30
31
  return headers
31
32
 
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
33
+ def post(self, endpoint: str, payload: dict[str, Any]) -> Any: # noqa: ANN401
41
34
  """Make a synchronous POST request to the specified endpoint."""
42
35
  headers = self._build_headers()
43
36
  with httpx.Client(timeout=self.timeout) as client:
@@ -48,15 +41,15 @@ class SimulatorAPIClient:
48
41
  @property
49
42
  def tool_mock_endpoint(self) -> str:
50
43
  """Get the tool mock endpoint URL."""
51
- return f"{self.base_url}/v2/tool_mock"
44
+ return urljoin(self.base_url, "v2/tool_mock")
52
45
 
53
46
  def get_log_tool_call_endpoint(self, session_id: str) -> str:
54
47
  """Get the log tool call endpoint URL."""
55
- return f"{self.base_url}/v2/simulations/{session_id}/log_tool_call"
48
+ return urljoin(self.base_url, f"v2/simulations/{session_id}/log_tool_call")
56
49
 
57
50
  def get_log_tool_response_endpoint(self, session_id: str) -> str:
58
51
  """Get the log tool response endpoint URL."""
59
- return f"{self.base_url}/v2/simulations/{session_id}/log_tool_response"
52
+ return urljoin(self.base_url, f"v2/simulations/{session_id}/log_tool_response")
60
53
 
61
54
 
62
55
  # Global singleton instance
@@ -0,0 +1,46 @@
1
+ """Logging utilities for VERIS tool calls and responses."""
2
+
3
+ import json
4
+ import logging
5
+ from typing import Any
6
+
7
+ from veris_ai.api_client import get_api_client
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ def log_tool_call(
13
+ session_id: str,
14
+ function_name: str,
15
+ parameters: str,
16
+ docstring: str,
17
+ ) -> None:
18
+ """Log tool call synchronously to the VERIS logging endpoint."""
19
+ api_client = get_api_client()
20
+ endpoint = api_client.get_log_tool_call_endpoint(session_id)
21
+ payload = {
22
+ "function_name": function_name,
23
+ "parameters": json.loads(parameters),
24
+ "docstring": docstring,
25
+ }
26
+ try:
27
+ api_client.post(endpoint, payload)
28
+ logger.debug(f"Tool call logged for {function_name}")
29
+ except Exception as e:
30
+ logger.warning(f"Failed to log tool call for {function_name}: {e}")
31
+
32
+
33
+ def log_tool_response(session_id: str, response: Any) -> None: # noqa: ANN401
34
+ """Log tool response synchronously to the VERIS logging endpoint."""
35
+ api_client = get_api_client()
36
+ endpoint = api_client.get_log_tool_response_endpoint(session_id)
37
+
38
+ payload = {
39
+ "response": json.dumps(response, default=str),
40
+ }
41
+
42
+ try:
43
+ api_client.post(endpoint, payload)
44
+ logger.debug("Tool response logged")
45
+ except Exception as e:
46
+ logger.warning(f"Failed to log tool response: {e}")
@@ -0,0 +1,22 @@
1
+ """Models for the VERIS SDK."""
2
+
3
+ from enum import Enum
4
+ from typing import Literal
5
+
6
+ from pydantic import BaseModel
7
+
8
+
9
+ class ResponseExpectation(str, Enum):
10
+ """Expected response behavior for tool mocking."""
11
+
12
+ AUTO = "auto"
13
+ REQUIRED = "required"
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"