veris-ai 1.7.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.

veris_ai/README.md CHANGED
@@ -49,8 +49,33 @@ Environment variables are processed in [`tool_mock.py`](tool_mock.py):
49
49
  - `VERIS_ENDPOINT_URL`: Mock server endpoint
50
50
  - `VERIS_MOCK_TIMEOUT`: Request timeout (default: 90s)
51
51
  - `ENV`: Set to `"simulation"` for mock mode
52
- - `VERIS_SERVICE_NAME`: Tracing service identifier
53
- - `VERIS_OTLP_ENDPOINT`: OpenTelemetry collector endpoint
52
+
53
+ ### Observability (OTLP / Logfire)
54
+
55
+ When using the observability helpers (`init_observability`, `instrument_fastapi_app`), configure the following environment variables so traces export correctly and are attributed to the right service name:
56
+
57
+ - `OTEL_SERVICE_NAME` — e.g. `simulation-server` (keep consistent with any `VERIS_SERVICE_NAME` you use)
58
+ - `OTEL_EXPORTER_OTLP_ENDPOINT` — e.g. `https://logfire-api.pydantic.dev`
59
+ - `LOGFIRE_TOKEN` — API token for Logfire
60
+ - `OTEL_EXPORTER_OTLP_HEADERS` — e.g. `Authorization=Bearer <LOGFIRE_TOKEN>` (quote the value)
61
+
62
+ Minimal shell setup:
63
+
64
+ ```bash
65
+ export OTEL_SERVICE_NAME="simulation-server"
66
+ export OTEL_EXPORTER_OTLP_ENDPOINT="https://logfire-api.pydantic.dev"
67
+ export LOGFIRE_TOKEN="<your-token>"
68
+ export OTEL_EXPORTER_OTLP_HEADERS="Authorization=${LOGFIRE_TOKEN}"
69
+ ```
70
+
71
+ Then in code:
72
+
73
+ ```python
74
+ from veris_ai import init_observability, instrument_fastapi_app
75
+ init_observability()
76
+ app = FastAPI()
77
+ instrument_fastapi_app(app)
78
+ ```
54
79
 
55
80
  ## Development Notes
56
81
 
veris_ai/__init__.py CHANGED
@@ -1,12 +1,63 @@
1
1
  """Veris AI Python SDK."""
2
2
 
3
+ from typing import Any
4
+
3
5
  __version__ = "0.1.0"
4
6
 
5
7
  # Import lightweight modules that only use base dependencies
6
8
  from .jaeger_interface import JaegerClient
7
9
  from .models import ResponseExpectation
8
- from .tool_mock import veris
9
10
  from .observability import init_observability, instrument_fastapi_app
11
+ from .tool_mock import veris
12
+
13
+ # Lazy import for modules with heavy dependencies
14
+ _veris_runner = None
15
+ _VerisConfig = None
16
+
17
+
18
+ def veris_runner(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401
19
+ """Lazy loader for the veris_runner function from agents_wrapper.
20
+
21
+ This function wraps OpenAI agents Runner.run to intercept tool calls
22
+ through the Veris SDK's mocking infrastructure.
23
+
24
+ This function requires the 'agents' extra dependencies:
25
+ pip install veris-ai[agents]
26
+ """
27
+ global _veris_runner # noqa: PLW0603
28
+ if _veris_runner is None:
29
+ try:
30
+ from .agents_wrapper import veris_runner as _veris_runner_impl # noqa: PLC0415
31
+
32
+ _veris_runner = _veris_runner_impl
33
+ except ImportError as e:
34
+ error_msg = (
35
+ "The 'veris_runner' function requires additional dependencies. "
36
+ "Please install them with: pip install veris-ai[agents]"
37
+ )
38
+ raise ImportError(error_msg) from e
39
+ return _veris_runner(*args, **kwargs)
40
+
41
+
42
+ def __getattr__(name: str) -> Any: # noqa: ANN401
43
+ """Lazy load VerisConfig class."""
44
+ global _VerisConfig # noqa: PLW0603
45
+ if name == "VerisConfig":
46
+ if _VerisConfig is None:
47
+ try:
48
+ from .agents_wrapper import VerisConfig as _VerisConfig_impl # noqa: PLC0415
49
+
50
+ _VerisConfig = _VerisConfig_impl
51
+ except ImportError as e:
52
+ error_msg = (
53
+ "The 'VerisConfig' class requires additional dependencies. "
54
+ "Please install them with: pip install veris-ai[agents]"
55
+ )
56
+ raise ImportError(error_msg) from e
57
+ return _VerisConfig
58
+ msg = f"module {__name__!r} has no attribute {name!r}"
59
+ raise AttributeError(msg)
60
+
10
61
 
11
62
  __all__ = [
12
63
  "veris",
@@ -14,4 +65,6 @@ __all__ = [
14
65
  "ResponseExpectation",
15
66
  "init_observability",
16
67
  "instrument_fastapi_app",
68
+ "veris_runner",
69
+ "VerisConfig",
17
70
  ]
@@ -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)
veris_ai/api_client.py ADDED
@@ -0,0 +1,60 @@
1
+ """Centralized API client for VERIS simulation endpoints."""
2
+
3
+ import logging
4
+ import os
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class SimulatorAPIClient:
13
+ """Centralized client for making requests to VERIS simulation endpoints."""
14
+
15
+ def __init__(self) -> None:
16
+ """Initialize the API client with configuration from environment variables."""
17
+ self.base_url = os.getenv("VERIS_API_URL", "https://simulation.api.veris.ai/")
18
+ self.api_key = os.getenv("VERIS_API_KEY")
19
+ self.timeout = float(os.getenv("VERIS_MOCK_TIMEOUT", "90.0"))
20
+
21
+ def _build_headers(self) -> dict[str, str] | None:
22
+ """Build headers including OpenTelemetry tracing and API key."""
23
+ headers: dict[str, str] | None = None
24
+ # Add API key header if available
25
+ if self.api_key:
26
+ if headers is None:
27
+ headers = {}
28
+ headers["x-api-key"] = self.api_key
29
+
30
+ return headers
31
+
32
+ def post(self, endpoint: str, payload: dict[str, Any]) -> Any: # noqa: ANN401
33
+ """Make a synchronous POST request to the specified endpoint."""
34
+ headers = self._build_headers()
35
+ with httpx.Client(timeout=self.timeout) as client:
36
+ response = client.post(endpoint, json=payload, headers=headers)
37
+ response.raise_for_status()
38
+ return response.json() if response.content else None
39
+
40
+ @property
41
+ def tool_mock_endpoint(self) -> str:
42
+ """Get the tool mock endpoint URL."""
43
+ return f"{self.base_url}/v2/tool_mock"
44
+
45
+ def get_log_tool_call_endpoint(self, session_id: str) -> str:
46
+ """Get the log tool call endpoint URL."""
47
+ return f"{self.base_url}/v2/simulations/{session_id}/log_tool_call"
48
+
49
+ def get_log_tool_response_endpoint(self, session_id: str) -> str:
50
+ """Get the log tool response endpoint URL."""
51
+ return f"{self.base_url}/v2/simulations/{session_id}/log_tool_response"
52
+
53
+
54
+ # Global singleton instance
55
+ _api_client = SimulatorAPIClient()
56
+
57
+
58
+ def get_api_client() -> SimulatorAPIClient:
59
+ """Get the global API client instance."""
60
+ return _api_client
veris_ai/logging.py CHANGED
@@ -2,152 +2,45 @@
2
2
 
3
3
  import json
4
4
  import logging
5
- import os
6
5
  from typing import Any
7
6
 
8
- import httpx
7
+ from veris_ai.api_client import get_api_client
9
8
 
10
9
  logger = logging.getLogger(__name__)
11
10
 
12
11
 
13
- async def log_tool_call_async(
12
+ def log_tool_call(
14
13
  session_id: str,
15
14
  function_name: str,
16
- parameters: dict[str, Any],
17
- docstring: str,
18
- ) -> None:
19
- """Log tool call asynchronously to the VERIS logging endpoint."""
20
- base_url = os.getenv("VERIS_ENDPOINT_URL")
21
- if not base_url:
22
- logger.warning("VERIS_ENDPOINT_URL not set, skipping tool call logging")
23
- return
24
- base_url = base_url.rstrip("/")
25
-
26
- endpoint = f"{base_url}/api/v2/simulations/{session_id}/log_tool_call"
27
- payload = {
28
- "function_name": function_name,
29
- "parameters": parameters,
30
- "docstring": docstring,
31
- }
32
-
33
- timeout = float(os.getenv("VERIS_MOCK_TIMEOUT", "90.0"))
34
-
35
- try:
36
- headers: dict[str, str] | None = None
37
- try:
38
- from opentelemetry.propagate import get_global_textmap
39
-
40
- headers = {}
41
- get_global_textmap().inject(headers)
42
- except Exception: # pragma: no cover - otel optional
43
- headers = None
44
-
45
- async with httpx.AsyncClient(timeout=timeout) as client:
46
- response = await client.post(endpoint, json=payload, headers=headers)
47
- response.raise_for_status()
48
- logger.debug(f"Tool call logged for {function_name}")
49
- except Exception as e:
50
- logger.warning(f"Failed to log tool call for {function_name}: {e}")
51
-
52
-
53
- def log_tool_call_sync(
54
- session_id: str,
55
- function_name: str,
56
- parameters: dict[str, Any],
15
+ parameters: str,
57
16
  docstring: str,
58
17
  ) -> None:
59
18
  """Log tool call synchronously to the VERIS logging endpoint."""
60
- base_url = os.getenv("VERIS_ENDPOINT_URL")
61
- if not base_url:
62
- logger.warning("VERIS_ENDPOINT_URL not set, skipping tool call logging")
63
- return
64
-
65
- endpoint = f"{base_url}/api/v2/simulations/{session_id}/log_tool_call"
19
+ api_client = get_api_client()
20
+ endpoint = api_client.get_log_tool_call_endpoint(session_id)
66
21
  payload = {
67
22
  "function_name": function_name,
68
- "parameters": parameters,
23
+ "parameters": json.loads(parameters),
69
24
  "docstring": docstring,
70
25
  }
71
-
72
- timeout = float(os.getenv("VERIS_MOCK_TIMEOUT", "90.0"))
73
-
74
26
  try:
75
- headers: dict[str, str] | None = None
76
- try:
77
- from opentelemetry.propagate import get_global_textmap # type: ignore[import-not-found]
78
-
79
- headers = {}
80
- get_global_textmap().inject(headers)
81
- except Exception: # pragma: no cover - otel optional
82
- headers = None
83
-
84
- with httpx.Client(timeout=timeout) as client:
85
- response = client.post(endpoint, json=payload, headers=headers)
86
- response.raise_for_status()
87
- logger.debug(f"Tool call logged for {function_name}")
27
+ api_client.post(endpoint, payload)
28
+ logger.debug(f"Tool call logged for {function_name}")
88
29
  except Exception as e:
89
30
  logger.warning(f"Failed to log tool call for {function_name}: {e}")
90
31
 
91
32
 
92
- async def log_tool_response_async(session_id: str, response: Any) -> None: # noqa: ANN401
93
- """Log tool response asynchronously to the VERIS logging endpoint."""
94
- base_url = os.getenv("VERIS_ENDPOINT_URL")
95
- if not base_url:
96
- logger.warning("VERIS_ENDPOINT_URL not set, skipping tool response logging")
97
- return
98
-
99
- endpoint = f"{base_url}/api/v2/simulations/{session_id}/log_tool_response"
100
- payload = {
101
- "response": json.dumps(response, default=str),
102
- }
103
-
104
- timeout = float(os.getenv("VERIS_MOCK_TIMEOUT", "90.0"))
105
-
106
- try:
107
- headers: dict[str, str] | None = None
108
- try:
109
- from opentelemetry.propagate import get_global_textmap # type: ignore[import-not-found]
110
-
111
- headers = {}
112
- get_global_textmap().inject(headers)
113
- except Exception: # pragma: no cover - otel optional
114
- headers = None
115
-
116
- async with httpx.AsyncClient(timeout=timeout) as client:
117
- http_response = await client.post(endpoint, json=payload, headers=headers)
118
- http_response.raise_for_status()
119
- logger.debug("Tool response logged")
120
- except Exception as e:
121
- logger.warning(f"Failed to log tool response: {e}")
122
-
123
-
124
- 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
125
34
  """Log tool response synchronously to the VERIS logging endpoint."""
126
- base_url = os.getenv("VERIS_ENDPOINT_URL")
127
- if not base_url:
128
- logger.warning("VERIS_ENDPOINT_URL not set, skipping tool response logging")
129
- return
35
+ api_client = get_api_client()
36
+ endpoint = api_client.get_log_tool_response_endpoint(session_id)
130
37
 
131
- endpoint = f"{base_url}/api/v2/simulations/{session_id}/log_tool_response"
132
38
  payload = {
133
39
  "response": json.dumps(response, default=str),
134
40
  }
135
41
 
136
- timeout = float(os.getenv("VERIS_MOCK_TIMEOUT", "90.0"))
137
-
138
42
  try:
139
- headers: dict[str, str] | None = None
140
- try:
141
- from opentelemetry.propagate import get_global_textmap # type: ignore[import-not-found]
142
-
143
- headers = {}
144
- get_global_textmap().inject(headers)
145
- except Exception: # pragma: no cover - otel optional
146
- headers = None
147
-
148
- with httpx.Client(timeout=timeout) as client:
149
- http_response = client.post(endpoint, json=payload, headers=headers)
150
- http_response.raise_for_status()
151
- logger.debug("Tool response logged")
43
+ api_client.post(endpoint, payload)
44
+ logger.debug("Tool response logged")
152
45
  except Exception as e:
153
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/observability.py CHANGED
@@ -7,12 +7,13 @@ enable consistent tracing without duplicating setup code.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- import os
11
-
12
10
  from fastapi import FastAPI
11
+ from logging import getLogger
12
+
13
+ logger = getLogger(__name__)
13
14
 
14
15
 
15
- def init_observability(service_name: str | None = None) -> None:
16
+ def init_observability() -> None: # noqa: PLR0912
16
17
  """Initialize tracing/export and set W3C propagation.
17
18
 
18
19
  - Initializes Traceloop if available (acts as OTel bootstrap/exporter)
@@ -22,17 +23,14 @@ def init_observability(service_name: str | None = None) -> None:
22
23
 
23
24
  This function is safe to call even if instrumentation packages are not installed.
24
25
  """
25
-
26
- resolved_service_name = service_name or os.getenv("VERIS_SERVICE_NAME", "veris-service")
27
-
28
- # Initialize Traceloop SDK first (acts as OTel bootstrap)
29
26
  try:
30
- from traceloop.sdk import Traceloop # type: ignore[import-not-found, import-untyped]
27
+ import logfire
31
28
 
32
- Traceloop.init(app_name=resolved_service_name, disable_batch=True)
29
+ logfire.configure(scrubbing=False)
30
+ logfire.instrument_openai_agents()
33
31
  except Exception as e:
34
32
  # Tracing is optional; continue without Traceloop
35
- msg = "Traceloop not found: " + str(e)
33
+ msg = "Logfire not found: " + str(e)
36
34
  raise RuntimeError(msg) from e
37
35
 
38
36
  # Ensure W3C propagation (TraceContext + optional Baggage), tolerant to OTel versions
veris_ai/tool_mock.py CHANGED
@@ -1,7 +1,6 @@
1
1
  import inspect
2
2
  import json
3
3
  import logging
4
- import os
5
4
  from collections.abc import Callable
6
5
  from contextlib import suppress
7
6
  from contextvars import ContextVar
@@ -13,16 +12,14 @@ from typing import (
13
12
  get_type_hints,
14
13
  )
15
14
 
16
- import httpx
17
15
 
18
16
  from veris_ai.logging import (
19
- log_tool_call_async,
20
- log_tool_call_sync,
21
- log_tool_response_async,
22
- log_tool_response_sync,
17
+ log_tool_call,
18
+ log_tool_response,
23
19
  )
24
- from veris_ai.models import ResponseExpectation
25
- from veris_ai.utils import convert_to_type, extract_json_schema
20
+ from veris_ai.models import ResponseExpectation, ToolCallOptions
21
+ from veris_ai.api_client import get_api_client
22
+ from veris_ai.utils import convert_to_type, extract_json_schema, get_function_parameters
26
23
 
27
24
  logger = logging.getLogger(__name__)
28
25
 
@@ -98,243 +95,109 @@ class VerisSDK:
98
95
  **params_dict,
99
96
  )
100
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
+
101
145
  def mock( # noqa: C901, PLR0915
102
146
  self,
103
- mode: Literal["tool", "function", "spy"] = "tool",
147
+ mode: Literal["tool", "function"] = "tool",
104
148
  expects_response: bool | None = None,
105
149
  cache_response: bool | None = None,
106
150
  ) -> Callable:
107
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
+ )
108
163
 
109
164
  def decorator(func: Callable) -> Callable: # noqa: C901, PLR0915
110
165
  """Decorator for mocking tool calls."""
111
- # Check if the original function is async
112
166
  is_async = inspect.iscoroutinefunction(func)
113
167
 
114
- def create_mock_payload(
115
- *args: tuple[object, ...],
116
- **kwargs: dict[str, object],
117
- ) -> tuple[dict[str, Any], Any]:
118
- """Create the mock payload - shared logic for both sync and async."""
119
- sig = inspect.signature(func)
120
- type_hints = get_type_hints(func)
121
-
122
- # Extract return type object (not just the name)
123
- return_type_obj = type_hints.pop("return", Any)
124
- # Create parameter info
125
- params_info = {}
126
- bound_args = sig.bind(*args, **kwargs)
127
- bound_args.apply_defaults()
128
- _ = bound_args.arguments.pop("ctx", None)
129
- _ = bound_args.arguments.pop("self", None)
130
- _ = bound_args.arguments.pop("cls", None)
131
-
132
- for param_name, param_value in bound_args.arguments.items():
133
- params_info[param_name] = {
134
- "value": str(param_value),
135
- "type": str(type_hints.get(param_name, Any)),
136
- }
137
- # Get function docstring
138
- docstring = inspect.getdoc(func) or ""
139
- nonlocal expects_response
140
- if expects_response is None and mode == "function":
141
- expects_response = False
142
- # Prepare payload
143
- # Convert expects_response to response_expectation enum
144
- if expects_response is False:
145
- response_expectation = ResponseExpectation.NONE
146
- elif expects_response is True:
147
- response_expectation = ResponseExpectation.REQUIRED
148
- else:
149
- response_expectation = ResponseExpectation.AUTO
150
-
151
- payload = {
152
- "session_id": self.session_id,
153
- "response_expectation": response_expectation.value,
154
- "cache_response": bool(cache_response) if cache_response is not None else False,
155
- "tool_call": {
156
- "function_name": func.__name__,
157
- "parameters": params_info,
158
- "return_type": json.dumps(extract_json_schema(return_type_obj)),
159
- "docstring": docstring,
160
- },
161
- }
162
-
163
- return payload, return_type_obj
164
-
165
168
  @wraps(func)
166
169
  async def async_wrapper(
167
170
  *args: tuple[object, ...],
168
- **kwargs: dict[str, object],
171
+ **kwargs: Any, # noqa: ANN401
169
172
  ) -> object:
170
- # Check if we're in simulation mode
171
- if not self.session_id:
172
- # If not in simulation mode, execute the original function
173
+ """Async wrapper."""
174
+ session_id = _session_id_context.get()
175
+ if not session_id:
173
176
  return await func(*args, **kwargs)
174
-
175
- async def _execute_mock_logic(session_id: str) -> object:
176
- # Handle spy mode - execute original function and log
177
- if mode == "spy":
178
- logger.info(f"Spying on function: {func.__name__}")
179
-
180
- # Log the tool call
181
- sig = inspect.signature(func)
182
- bound_args = sig.bind(*args, **kwargs)
183
- bound_args.apply_defaults()
184
- _ = bound_args.arguments.pop("ctx", None)
185
- _ = bound_args.arguments.pop("self", None)
186
- _ = bound_args.arguments.pop("cls", None)
187
-
188
- await log_tool_call_async(
189
- session_id=session_id,
190
- function_name=func.__name__,
191
- parameters=bound_args.arguments,
192
- docstring=inspect.getdoc(func) or "",
193
- )
194
-
195
- # Execute the original function
196
- result = await func(*args, **kwargs)
197
-
198
- # Log the response
199
- await log_tool_response_async(session_id=session_id, response=result)
200
-
201
- return result
202
-
203
- # Regular mock mode
204
- base_url = os.getenv("VERIS_ENDPOINT_URL")
205
- if not base_url:
206
- error_msg = "VERIS_ENDPOINT_URL environment variable is not set"
207
- raise ValueError(error_msg)
208
- endpoint = f"{base_url.rstrip('/')}/api/v2/tool_mock"
209
- # Default timeout of 30 seconds
210
- timeout = float(os.getenv("VERIS_MOCK_TIMEOUT", "90.0"))
211
-
212
- logger.info(f"Simulating function: {func.__name__}")
213
- payload, return_type_obj = create_mock_payload(*args, **kwargs)
214
-
215
- # Send request to endpoint with timeout
216
- # Inject current trace context headers if OpenTelemetry is available
217
- headers: dict[str, str] | None = None
218
- try:
219
- from opentelemetry.propagate import get_global_textmap # type: ignore[import-not-found]
220
-
221
- headers = {}
222
- get_global_textmap().inject(headers)
223
- except Exception: # pragma: no cover - otel optional
224
- headers = None
225
-
226
- async with httpx.AsyncClient(timeout=timeout) as client:
227
- response = await client.post(endpoint, json=payload, headers=headers)
228
- response.raise_for_status()
229
- mock_result = response.json()
230
- logger.info(f"Mock response: {mock_result}")
231
-
232
- if isinstance(mock_result, str):
233
- with suppress(json.JSONDecodeError):
234
- mock_result = json.loads(mock_result)
235
- return convert_to_type(mock_result, return_type_obj)
236
- return convert_to_type(mock_result, return_type_obj)
237
-
238
- # Create a top-level span for the simulated mock call if OpenTelemetry is available
239
- try:
240
- from opentelemetry import trace # type: ignore[import-not-found]
241
-
242
- tracer = trace.get_tracer("veris_ai.tool_mock")
243
- span_name = f"mock.{func.__name__}"
244
- with tracer.start_as_current_span(span_name) as span: # type: ignore[attr-defined]
245
- span.set_attribute("veris_ai.session.id", self.session_id or "") # type: ignore[attr-defined]
246
- span.set_attribute("veris_ai.mock.mode", mode) # type: ignore[attr-defined]
247
- return await _execute_mock_logic(self.session_id)
248
- except Exception:
249
- # If OpenTelemetry is not available, run without span
250
- 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
+ )
251
184
 
252
185
  @wraps(func)
253
186
  def sync_wrapper(
254
187
  *args: tuple[object, ...],
255
- **kwargs: dict[str, object],
188
+ **kwargs: Any, # noqa: ANN401
256
189
  ) -> object:
257
- # Check if we're in simulation mode
258
- if not self.session_id:
259
- # If not in simulation mode, execute the original function
190
+ """Sync wrapper."""
191
+ session_id = _session_id_context.get()
192
+ if not session_id:
260
193
  return func(*args, **kwargs)
261
-
262
- def _execute_mock_logic(session_id: str) -> object:
263
- # Handle spy mode - execute original function and log
264
- if mode == "spy":
265
- logger.info(f"Spying on function: {func.__name__}")
266
-
267
- # Log the tool call
268
- sig = inspect.signature(func)
269
- bound_args = sig.bind(*args, **kwargs)
270
- bound_args.apply_defaults()
271
- _ = bound_args.arguments.pop("ctx", None)
272
- _ = bound_args.arguments.pop("self", None)
273
- _ = bound_args.arguments.pop("cls", None)
274
-
275
- log_tool_call_sync(
276
- session_id=session_id,
277
- function_name=func.__name__,
278
- parameters=bound_args.arguments,
279
- docstring=inspect.getdoc(func) or "",
280
- )
281
-
282
- # Execute the original function
283
- result = func(*args, **kwargs)
284
-
285
- # Log the response
286
- log_tool_response_sync(session_id=session_id, response=result)
287
-
288
- return result
289
-
290
- # Regular mock mode
291
- base_url = os.getenv("VERIS_ENDPOINT_URL")
292
- if not base_url:
293
- error_msg = "VERIS_ENDPOINT_URL environment variable is not set"
294
- raise ValueError(error_msg)
295
- endpoint = f"{base_url.rstrip('/')}/api/v2/tool_mock"
296
- # Default timeout of 30 seconds
297
- timeout = float(os.getenv("VERIS_MOCK_TIMEOUT", "90.0"))
298
-
299
- logger.info(f"Simulating function: {func.__name__}")
300
- payload, return_type_obj = create_mock_payload(*args, **kwargs)
301
-
302
- # Send request to endpoint with timeout (synchronous)
303
- # Inject current trace context headers if OpenTelemetry is available
304
- headers: dict[str, str] | None = None
305
- try:
306
- from opentelemetry.propagate import get_global_textmap # type: ignore[import-not-found]
307
-
308
- headers = {}
309
- get_global_textmap().inject(headers)
310
- except Exception: # pragma: no cover - otel optional
311
- headers = None
312
-
313
- with httpx.Client(timeout=timeout) as client:
314
- response = client.post(endpoint, json=payload, headers=headers)
315
- response.raise_for_status()
316
- mock_result = response.json()
317
- logger.info(f"Mock response: {mock_result}")
318
-
319
- if isinstance(mock_result, str):
320
- with suppress(json.JSONDecodeError):
321
- mock_result = json.loads(mock_result)
322
- return convert_to_type(mock_result, return_type_obj)
323
- return convert_to_type(mock_result, return_type_obj)
324
-
325
- # Create a top-level span for the simulated mock call if OpenTelemetry is available
326
- try:
327
- from opentelemetry import trace # type: ignore[import-not-found]
328
-
329
- tracer = trace.get_tracer("veris_ai.tool_mock")
330
- span_name = f"mock.{func.__name__}"
331
- with tracer.start_as_current_span(span_name) as span: # type: ignore[attr-defined]
332
- span.set_attribute("veris_ai.session.id", self.session_id or "") # type: ignore[attr-defined]
333
- span.set_attribute("veris_ai.mock.mode", mode) # type: ignore[attr-defined]
334
- return _execute_mock_logic(self.session_id)
335
- except Exception:
336
- # If OpenTelemetry is not available, run without span
337
- 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
+ )
338
201
 
339
202
  # Return the appropriate wrapper based on whether the function is async
340
203
  return async_wrapper if is_async else sync_wrapper
@@ -351,7 +214,7 @@ class VerisSDK:
351
214
  @wraps(func)
352
215
  async def async_wrapper(
353
216
  *args: tuple[object, ...],
354
- **kwargs: dict[str, object],
217
+ **kwargs: Any, # noqa: ANN401
355
218
  ) -> object:
356
219
  if not self.session_id:
357
220
  # If not in simulation mode, execute the original function
@@ -360,7 +223,7 @@ class VerisSDK:
360
223
  return return_value
361
224
 
362
225
  @wraps(func)
363
- def sync_wrapper(*args: tuple[object, ...], **kwargs: dict[str, object]) -> object:
226
+ def sync_wrapper(*args: tuple[object, ...], **kwargs: Any) -> object: # noqa: ANN401
364
227
  if not self.session_id:
365
228
  # If not in simulation mode, execute the original function
366
229
  return func(*args, **kwargs)
@@ -373,4 +236,46 @@ class VerisSDK:
373
236
  return decorator
374
237
 
375
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
+
376
281
  veris = VerisSDK()
veris_ai/utils.py CHANGED
@@ -1,8 +1,11 @@
1
+ import inspect
2
+ import json
1
3
  import sys
2
4
  import types
3
5
  import typing
4
6
  from contextlib import suppress
5
7
  from typing import Any, ForwardRef, Literal, NotRequired, Required, Union, get_args, get_origin
8
+ from collections.abc import Callable
6
9
 
7
10
  from pydantic import BaseModel
8
11
 
@@ -272,3 +275,16 @@ def extract_json_schema(target_type: Any) -> dict: # noqa: PLR0911, PLR0912, C9
272
275
 
273
276
  # Default case for unknown types
274
277
  return {"type": "object"}
278
+
279
+
280
+ def get_function_parameters(
281
+ func: Callable, args: tuple[object, ...], kwargs: dict[str, object]
282
+ ) -> str:
283
+ """Get the parameters for a function."""
284
+ sig = inspect.signature(func)
285
+ bound_args = sig.bind(*args, **kwargs)
286
+ bound_args.apply_defaults()
287
+ _ = bound_args.arguments.pop("ctx", None)
288
+ _ = bound_args.arguments.pop("self", None)
289
+ _ = bound_args.arguments.pop("cls", None)
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.7.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
@@ -9,6 +9,7 @@ License-Expression: MIT
9
9
  License-File: LICENSE
10
10
  Requires-Python: >=3.11
11
11
  Requires-Dist: httpx>=0.24.0
12
+ Requires-Dist: logfire>=4.3.3
12
13
  Requires-Dist: opentelemetry-api>=1.34.1
13
14
  Requires-Dist: opentelemetry-exporter-otlp>=1.34.1
14
15
  Requires-Dist: opentelemetry-instrumentation-fastapi>=0.55b1
@@ -19,11 +20,12 @@ Requires-Dist: opentelemetry-instrumentation>=0.55b1
19
20
  Requires-Dist: opentelemetry-sdk>=1.34.1
20
21
  Requires-Dist: pydantic>=2.0.0
21
22
  Requires-Dist: requests>=2.31.0
22
- Requires-Dist: traceloop-sdk>=0.45.4
23
+ Provides-Extra: agents
24
+ Requires-Dist: openai-agents>=0.0.1; extra == 'agents'
23
25
  Provides-Extra: dev
24
26
  Requires-Dist: black>=23.7.0; extra == 'dev'
25
27
  Requires-Dist: mypy>=1.5.1; extra == 'dev'
26
- Requires-Dist: openai-agents>=0.0.1; extra == 'dev'
28
+ Requires-Dist: openai-agents>=0.2.5; extra == 'dev'
27
29
  Requires-Dist: pre-commit>=3.3.3; extra == 'dev'
28
30
  Requires-Dist: pytest-asyncio>=0.21.1; extra == 'dev'
29
31
  Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
@@ -45,7 +47,7 @@ A Python package for Veris AI tools with simulation capabilities and FastAPI MCP
45
47
  ## Quick Reference
46
48
 
47
49
  **Purpose**: Tool mocking, tracing, and FastAPI MCP integration for AI agent development
48
- **Core Components**: [`tool_mock`](#function-mocking) • [`observability`](#sdk-observability-helpers) • [`fastapi_mcp`](#fastapi-mcp-integration) • [`jaeger_interface`](#jaeger-trace-interface)
50
+ **Core Components**: [`tool_mock`](#function-mocking) • [`api_client`](src/veris_ai/api_client.py) • [`observability`](#sdk-observability-helpers) • [`fastapi_mcp`](#fastapi-mcp-integration) • [`jaeger_interface`](#jaeger-trace-interface)
49
51
  **Deep Dive**: [`Module Architecture`](src/veris_ai/README.md) • [`Testing Guide`](tests/README.md) • [`Usage Examples`](examples/README.md)
50
52
  **Source of Truth**: Implementation details in [`src/veris_ai/`](src/veris_ai/) source code
51
53
 
@@ -84,13 +86,14 @@ from veris_ai import init_observability, instrument_fastapi_app # Provided by S
84
86
 
85
87
  | Variable | Purpose | Default |
86
88
  |----------|---------|---------|
87
- | `VERIS_ENDPOINT_URL` | Mock server endpoint | *Required* |
89
+ | `VERIS_API_KEY` | API authentication key | None |
88
90
  | `VERIS_MOCK_TIMEOUT` | Request timeout (seconds) | `90.0` |
89
91
  | `ENV` | Set to `"simulation"` for mock mode | Production |
90
- | `VERIS_SERVICE_NAME` | Tracing service name | Auto-detected |
91
- | `VERIS_OTLP_ENDPOINT` | OpenTelemetry collector | *Required for tracing* |
92
92
 
93
- **Configuration Details**: See [`src/veris_ai/tool_mock.py`](src/veris_ai/tool_mock.py) for environment handling logic.
93
+ **Advanced Configuration** (rarely needed):
94
+ - `VERIS_API_URL`: Override default API endpoint (defaults to production)
95
+
96
+ **Configuration Details**: See [`src/veris_ai/api_client.py`](src/veris_ai/api_client.py) for API configuration and [`src/veris_ai/tool_mock.py`](src/veris_ai/tool_mock.py) for environment handling logic.
94
97
 
95
98
 
96
99
  ### SDK Observability Helpers
@@ -102,7 +105,7 @@ from fastapi import FastAPI
102
105
  from veris_ai import init_observability, instrument_fastapi_app
103
106
 
104
107
  # Initialize tracing/export early (no-op if dependencies are absent)
105
- init_observability(service_name="my-customer-service")
108
+ init_observability()
106
109
 
107
110
  app = FastAPI()
108
111
 
@@ -110,6 +113,35 @@ app = FastAPI()
110
113
  instrument_fastapi_app(app)
111
114
  ```
112
115
 
116
+ #### Observability Environment
117
+
118
+ Set these environment variables to enable exporting traces via OTLP (Logfire) and ensure consistent service naming:
119
+
120
+ | Variable | Example | Notes |
121
+ |----------|---------|-------|
122
+ | `OTEL_SERVICE_NAME` | `simulation-server` | Should match `VERIS_SERVICE_NAME` used elsewhere to keep traces aligned |
123
+ | `OTEL_EXPORTER_OTLP_ENDPOINT` | `https://logfire-api.pydantic.dev` | OTLP HTTP endpoint |
124
+ | `LOGFIRE_TOKEN` | `FILL_IN` | Logfire API token used by the exporter |
125
+ | `OTEL_EXPORTER_OTLP_HEADERS` | `'Authorization=FILL_IN'` | Include quotes to preserve the `=`; often `Authorization=Bearer <LOGFIRE_TOKEN>` |
126
+
127
+ Quick setup example:
128
+
129
+ ```bash
130
+ export OTEL_SERVICE_NAME="simulation-server"
131
+ export OTEL_EXPORTER_OTLP_ENDPOINT="https://logfire-api.pydantic.dev"
132
+ export LOGFIRE_TOKEN="<your-token>"
133
+ export OTEL_EXPORTER_OTLP_HEADERS="Authorization=${LOGFIRE_TOKEN}"
134
+ ```
135
+
136
+ Then initialize in code early in your process:
137
+
138
+ ```python
139
+ from veris_ai import init_observability, instrument_fastapi_app
140
+ init_observability()
141
+ app = FastAPI()
142
+ instrument_fastapi_app(app)
143
+ ```
144
+
113
145
  What this enables:
114
146
  - Sets global W3C propagator (TraceContext + Baggage)
115
147
  - Optionally instruments FastAPI, requests, httpx, MCP client if installed
@@ -136,7 +168,7 @@ async def your_function(param1: str, param2: int) -> dict:
136
168
  return {"result": "actual implementation"}
137
169
 
138
170
  # Spy mode: Executes function but logs calls/responses
139
- @veris.mock(mode="spy")
171
+ @veris.spy()
140
172
  async def monitored_function(data: str) -> dict:
141
173
  return process_data(data)
142
174
 
@@ -222,7 +254,7 @@ pytest --cov=veris_ai # Test with coverage
222
254
 
223
255
  **Semantic Tag**: `module-architecture`
224
256
 
225
- **Core Modules**: `tool_mock` (mocking), `jaeger_interface` (trace queries), `utils` (schema conversion)
257
+ **Core Modules**: `tool_mock` (mocking), `api_client` (centralized API), `jaeger_interface` (trace queries), `utils` (schema conversion)
226
258
 
227
259
  **Complete Architecture**: See [`src/veris_ai/README.md`](src/veris_ai/README.md) for module overview, implementation flows, and configuration details.
228
260
 
@@ -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,15 +0,0 @@
1
- veris_ai/README.md,sha256=iMSwSIrBO2zEdUhImkhZucUnTNO0kSQ-_zxWjZCmp4I,2730
2
- veris_ai/__init__.py,sha256=Gh52-XfFpsxX37uqp8vuX0V3Np7f-Rlf5k3MMANu6e0,425
3
- veris_ai/logging.py,sha256=srEdVimcQSCsnwGhyzCydehD2JW1cQmwSRd3X20NQs0,5233
4
- veris_ai/models.py,sha256=6HINPxNFCakCVPcyEbUswWkXwb2K4lF0A8g8EvTMal4,213
5
- veris_ai/observability.py,sha256=fG5ixAiVDykLCdHUScKQNCsePLphKQ7PcaKkAGBVdF0,5113
6
- veris_ai/tool_mock.py,sha256=C1BANT3LCMKOIwJm_nOFuNdHnaO9_tbjeDTtBcI41RI,16512
7
- veris_ai/utils.py,sha256=aqFFNuNiBehil6874nOHtU7G_bWHbFpVuubcz2AIx6I,9267
8
- veris_ai/jaeger_interface/README.md,sha256=kd9rKcE5xf3EyNaiHu0tjn-0oES9sfaK6Ih-OhhTyCM,2821
9
- veris_ai/jaeger_interface/__init__.py,sha256=KD7NSiMYRG_2uF6dOLKkGG5lNQe4K9ptEwucwMT4_aw,1128
10
- veris_ai/jaeger_interface/client.py,sha256=yJrh86wRR0Dk3Gq12DId99WogcMIVbL0QQFqVSevvlE,8772
11
- veris_ai/jaeger_interface/models.py,sha256=e64VV6IvOEFuzRUgvDAMQFyOZMRb56I-PUPZLBZ3rX0,1864
12
- veris_ai-1.7.0.dist-info/METADATA,sha256=dmWFBpwU2MK9tjnJtlrkQdqnR9C0yn0Lhb1TMWb-XvA,8679
13
- veris_ai-1.7.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
14
- veris_ai-1.7.0.dist-info/licenses/LICENSE,sha256=2g4i20atAgtD5einaKzhQrIB-JrPhyQgD3bC0wkHcCI,1065
15
- veris_ai-1.7.0.dist-info/RECORD,,