veris-ai 1.1.0__py3-none-any.whl → 1.3.0__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/__init__.py CHANGED
@@ -5,7 +5,8 @@ from typing import Any
5
5
  __version__ = "0.1.0"
6
6
 
7
7
  # Import lightweight modules that only use base dependencies
8
- from .jaeger_interface import JaegerClient, SearchQuery
8
+ from .jaeger_interface import JaegerClient
9
+ from .models import ResponseExpectation
9
10
  from .tool_mock import veris
10
11
 
11
12
  # Lazy import for modules with heavy dependencies
@@ -33,9 +34,4 @@ def instrument(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401
33
34
  return _instrument(*args, **kwargs)
34
35
 
35
36
 
36
- __all__ = [
37
- "veris",
38
- "JaegerClient",
39
- "SearchQuery",
40
- "instrument",
41
- ]
37
+ __all__ = ["veris", "JaegerClient", "instrument", "ResponseExpectation"]
@@ -3,9 +3,10 @@
3
3
  This sub-package ships a **thin synchronous wrapper** around the
4
4
  [Jaeger Query Service](https://www.jaegertracing.io/docs/) HTTP API so
5
5
  that you can **search for and retrieve traces** directly from Python
6
- with minimal boilerplate.
6
+ with minimal boilerplate. It also provides **client-side span filtering**
7
+ capabilities for more granular control over the returned data.
7
8
 
8
- > The client relies on `requests` (already included in the SDKs
9
+ > The client relies on `requests` (already included in the SDK's
9
10
  > dependencies) and uses *pydantic* for full type-safety.
10
11
 
11
12
  ---
@@ -24,7 +25,7 @@ pip install veris-ai
24
25
  ## Quick-start
25
26
 
26
27
  ```python
27
- from veris_ai.jaeger_interface import JaegerClient, SearchQuery
28
+ from veris_ai.jaeger_interface import JaegerClient
28
29
  import json
29
30
  from veris_ai.jaeger_interface.models import Trace
30
31
  # Replace with the URL of your Jaeger Query Service instance
@@ -32,12 +33,11 @@ client = JaegerClient("http://localhost:16686")
32
33
 
33
34
  # --- 1. Search traces --------------------------------------------------
34
35
  resp = client.search(
35
- SearchQuery(
36
- service="veris-agent",
37
- limit=3,
38
- operation="CustomSpanData",
39
- tags={"bt.metadata.session_id":"oRL1_IhMP3s7T7mYrixCW"}
40
- )
36
+ service="veris-agent",
37
+ limit=10,
38
+ # operation="CustomSpanData",
39
+ tags={"veris.session_id":"088b5aaf-84bd-4768-9a62-5e981222a9f2"},
40
+ span_tags={"bt.metadata.model":"gpt-4.1-2025-04-14"}
41
41
  )
42
42
 
43
43
  # save to json
@@ -74,25 +74,53 @@ with open("detailed.json", "w") as f:
74
74
 
75
75
  | Method | Description |
76
76
  | -------- | ----------- |
77
- | `search(query: SearchQuery) -> SearchResponse` | Search for traces matching the given parameters (wrapper around `/api/traces`). |
78
- | `get_trace(trace_id: str) -> GetTraceResponse` | Fetch a single trace by ID (wrapper around `/api/traces/{id}`). |
77
+ | `search(service, *, limit=None, tags=None, operation=None, span_tags=None, **kwargs) -> SearchResponse` | Search for traces with optional span-level filtering. |
78
+ | `get_trace(trace_id: str) -> GetTraceResponse` | Fetch a single trace by ID (wrapper around `/api/traces/{id}`). |
79
79
 
80
- ### `SearchQuery`
80
+ ### `search()` Parameters
81
81
 
82
- `SearchQuery` is a *pydantic* model for building the query-string sent to
83
- Jaeger’s `/api/traces` endpoint.
82
+ The `search()` method now uses a flattened parameter structure:
84
83
 
85
- Parameter logic:
84
+ | Parameter | Type | Description |
85
+ | --------- | ---- | ----------- |
86
+ | `service` | `str` | Service name to search for. Optional - if not provided, searches across all services. |
87
+ | `limit` | `int` | Maximum number of traces to return. |
88
+ | `tags` | `Dict[str, Any]` | Trace-level tag filters (AND logic). A trace must have a span matching ALL tags. |
89
+ | `operation` | `str` | Filter by operation name. |
90
+ | `span_tags` | `Dict[str, Any]` | Span-level tag filters (OR logic). Returns only spans matching ANY of these tags. |
91
+ | `span_operations` | `List[str]` | Span-level operation name filters (OR logic). Returns only spans matching ANY of these operations. |
92
+ | `**kwargs` | `Any` | Additional parameters passed directly to Jaeger API. |
86
93
 
87
- * **service** – mandatory, sets the top-level service filter.
88
- * **operation** – optional; a trace is kept only if *any* of its spans has
89
- this exact `operationName`.
90
- * **tags** – dict of key/value filters; *all* pairs must match on a single
91
- span (logical AND). Example: `{"error": "true"}`.
92
- * **limit** – applied after all filters and trims the result list.
94
+ ### Filter Logic
93
95
 
94
- Any other fields are forwarded untouched, so you can experiment with new
95
- Jaeger parameters without waiting for an SDK update.
96
+ The interface provides two levels of filtering:
97
+
98
+ 1. **Trace-level filtering** (`tags` parameter):
99
+ - Sent directly to Jaeger API
100
+ - Uses AND logic: all tag key-value pairs must match on a single span
101
+ - Efficient server-side filtering
102
+
103
+ 2. **Span-level filtering** (`span_tags` parameter):
104
+ - Applied client-side after retrieving traces
105
+ - Uses OR logic: spans matching ANY of the provided tags are included
106
+ - Traces with no matching spans are excluded from results
107
+ - Useful for finding spans with specific characteristics across different traces
108
+
109
+ ### Example: Combining Filters
110
+
111
+ ```python
112
+ # Find traces in service that have errors, then filter to show only
113
+ # spans with specific HTTP status codes or database errors
114
+ traces = client.search(
115
+ service="my-api",
116
+ tags={"error": "true"}, # Trace must contain an error
117
+ span_tags={
118
+ "http.status_code": 500,
119
+ "http.status_code": 503,
120
+ "db.error": "connection_timeout"
121
+ } # Show only spans with these specific errors
122
+ )
123
+ ```
96
124
 
97
125
  ---
98
126
 
@@ -104,6 +132,6 @@ need API v3 support feel free to open an issue or contribution—thanks!
104
132
 
105
133
  ---
106
134
 
107
- ## Licence
135
+ ## License
108
136
 
109
137
  This package is released under the **MIT license**.
@@ -1,18 +1,32 @@
1
1
  """Jaeger interface for searching and retrieving traces.
2
2
 
3
3
  This sub-package provides a thin synchronous wrapper around the Jaeger
4
- Query Service HTTP API.
4
+ Query Service HTTP API with client-side span filtering capabilities.
5
5
 
6
6
  Typical usage example::
7
7
 
8
- from veris_ai.jaeger_interface import JaegerClient, SearchQuery
8
+ from veris_ai.jaeger_interface import JaegerClient
9
9
 
10
10
  client = JaegerClient("http://localhost:16686")
11
11
 
12
+ # Search traces with trace-level filters
12
13
  traces = client.search(
13
- SearchQuery(service="veris-agent", limit=20)
14
+ service="veris-agent",
15
+ limit=20,
16
+ tags={"error": "true"} # AND logic at trace level
14
17
  )
15
18
 
19
+ # Search with span-level filtering
20
+ traces_filtered = client.search(
21
+ service="veris-agent",
22
+ limit=20,
23
+ span_tags={
24
+ "http.status_code": 404,
25
+ "db.error": "timeout"
26
+ } # OR logic: spans with either tag are included
27
+ )
28
+
29
+ # Get a specific trace
16
30
  trace = client.get_trace(traces.data[0].traceID)
17
31
 
18
32
  The implementation uses *requests* under the hood and all public functions
@@ -20,7 +34,6 @@ are fully typed using *pydantic* models so that IDEs can provide proper
20
34
  autocomplete and type checking.
21
35
  """
22
36
 
23
- from .client import JaegerClient
24
- from .models import SearchQuery
37
+ from .client import JaegerClient as JaegerClient # noqa: F401
25
38
 
26
- __all__ = ["JaegerClient", "SearchQuery"]
39
+ __all__ = ["JaegerClient"]
@@ -4,16 +4,13 @@ This implementation keeps dependencies minimal while providing fully-typed
4
4
  *pydantic* models for both **request** and **response** bodies.
5
5
  """
6
6
 
7
- from __future__ import annotations
8
-
9
- from typing import TYPE_CHECKING, Self
7
+ import json
8
+ import types
9
+ from typing import Any, Self
10
10
 
11
11
  import requests
12
12
 
13
- from .models import GetTraceResponse, SearchQuery, SearchResponse
14
-
15
- if TYPE_CHECKING:
16
- from types import TracebackType
13
+ from .models import GetTraceResponse, SearchResponse, Span, Trace
17
14
 
18
15
  __all__ = ["JaegerClient"]
19
16
 
@@ -63,20 +60,110 @@ class JaegerClient: # noqa: D101
63
60
  session.headers.update(self._headers)
64
61
  return session, True
65
62
 
63
+ def _span_matches_tags(self, span: Span, span_tags: dict[str, Any]) -> bool:
64
+ """Check if a span matches any of the provided tags (OR logic)."""
65
+ if not span.tags or not span_tags:
66
+ return False
67
+
68
+ # Convert span tags to a dict for easier lookup
69
+ span_tag_dict = {tag.key: tag.value for tag in span.tags}
70
+
71
+ # OR logic: return True if ANY tag matches
72
+ return any(span_tag_dict.get(key) == value for key, value in span_tags.items())
73
+
74
+ def _filter_spans(
75
+ self,
76
+ traces: list[Trace],
77
+ span_tags: dict[str, Any] | None,
78
+ span_operations: list[str] | None = None,
79
+ ) -> list[Trace]:
80
+ """Filter spans within traces based on span_tags and/or span_operations.
81
+
82
+ Uses OR logic within each filter type. If both are provided, a span must
83
+ match at least one tag AND at least one operation.
84
+ """
85
+ if not span_tags and not span_operations:
86
+ return traces
87
+
88
+ filtered_traces = []
89
+ for trace in traces:
90
+ filtered_spans = []
91
+ for span in trace.spans:
92
+ tag_match = True
93
+ op_match = True
94
+
95
+ if span_tags:
96
+ tag_match = self._span_matches_tags(span, span_tags)
97
+ if span_operations:
98
+ op_match = span.operationName in span_operations
99
+
100
+ # If both filters are provided, require both to match (AND logic)
101
+ if tag_match and op_match:
102
+ filtered_spans.append(span)
103
+
104
+ if filtered_spans:
105
+ filtered_trace = Trace(
106
+ traceID=trace.traceID,
107
+ spans=filtered_spans,
108
+ process=trace.process,
109
+ warnings=trace.warnings,
110
+ )
111
+ filtered_traces.append(filtered_trace)
112
+
113
+ return filtered_traces
114
+
66
115
  # ---------------------------------------------------------------------
67
116
  # Public API
68
117
  # ---------------------------------------------------------------------
69
118
 
70
- def search(self, query: SearchQuery) -> SearchResponse: # noqa: D401
71
- """Search traces using the *v1* ``/api/traces`` endpoint.
119
+ def search( # noqa: PLR0913
120
+ self,
121
+ service: str | None = None,
122
+ *,
123
+ limit: int | None = None,
124
+ tags: dict[str, Any] | None = None,
125
+ operation: str | None = None,
126
+ span_tags: dict[str, Any] | None = None,
127
+ span_operations: list[str] | None = None,
128
+ **kwargs: Any, # noqa: ANN401
129
+ ) -> SearchResponse: # noqa: D401
130
+ """Search traces using the *v1* ``/api/traces`` endpoint with optional span filtering.
72
131
 
73
132
  Args:
74
- query: :class:`~veris_ai.jaeger_interface.models.SearchQuery` instance.
133
+ service: Service name to search for. If not provided, searches across all services.
134
+ limit: Maximum number of traces to return.
135
+ tags: Dictionary of tag filters for trace-level filtering (AND-combined).
136
+ operation: Operation name to search for.
137
+ span_tags: Dictionary of tag filters for span-level filtering.
138
+ Uses OR logic. Combined with span_operations using AND.
139
+ Applied client-side after retrieving traces.
140
+ span_operations: List of operation names to search for.
141
+ Uses OR logic. Combined with span_tags using AND.
142
+ **kwargs: Additional parameters to pass to the Jaeger API.
75
143
 
76
144
  Returns:
77
- Parsed :class:`~veris_ai.jaeger_interface.models.SearchResponse` model.
145
+ Parsed :class:`~veris_ai.jaeger_interface.models.SearchResponse` model
146
+ with spans filtered according to span_tags if provided.
78
147
  """
79
- params = query.to_params()
148
+ # Build params for the Jaeger API (excluding span_tags)
149
+ params: dict[str, Any] = {}
150
+
151
+ if service is not None:
152
+ params["service"] = service
153
+
154
+ if limit is not None:
155
+ params["limit"] = limit
156
+
157
+ if operation is not None:
158
+ params["operation"] = operation
159
+
160
+ if tags:
161
+ # Convert tags to JSON string as expected by Jaeger API
162
+ params["tags"] = json.dumps(tags)
163
+
164
+ # Add any additional parameters
165
+ params.update(kwargs)
166
+
80
167
  session, should_close = self._make_session()
81
168
  try:
82
169
  url = f"{self._base_url}/api/traces"
@@ -86,7 +173,23 @@ class JaegerClient: # noqa: D101
86
173
  finally:
87
174
  if should_close:
88
175
  session.close()
89
- return SearchResponse.model_validate(data) # type: ignore[arg-type]
176
+
177
+ # Parse the response
178
+ search_response = SearchResponse.model_validate(data) # type: ignore[arg-type]
179
+
180
+ # Apply span-level filtering if span_tags is provided
181
+ if (
182
+ (span_tags or span_operations)
183
+ and search_response.data
184
+ and isinstance(search_response.data, list)
185
+ ):
186
+ filtered_traces = self._filter_spans(search_response.data, span_tags, span_operations)
187
+ search_response.data = filtered_traces
188
+ # Update the total to reflect filtered results
189
+ if search_response.total is not None:
190
+ search_response.total = len(filtered_traces)
191
+
192
+ return search_response
90
193
 
91
194
  def get_trace(self, trace_id: str) -> GetTraceResponse: # noqa: D401
92
195
  """Retrieve a single trace by *trace_id*.
@@ -125,7 +228,7 @@ class JaegerClient: # noqa: D101
125
228
  self,
126
229
  exc_type: type[BaseException] | None,
127
230
  exc: BaseException | None,
128
- tb: TracebackType | None,
231
+ tb: types.TracebackType | None,
129
232
  ) -> None:
130
233
  """Exit the context manager."""
131
234
  # Only close if we created the session
@@ -1,9 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
- import json
4
3
  from typing import Any
5
4
 
6
- from pydantic import BaseModel, ConfigDict, Field, field_validator
5
+ from pydantic import BaseModel, ConfigDict, Field
7
6
 
8
7
  __all__ = [
9
8
  "Tag",
@@ -12,7 +11,6 @@ __all__ = [
12
11
  "Trace",
13
12
  "SearchResponse",
14
13
  "GetTraceResponse",
15
- "SearchQuery",
16
14
  ]
17
15
 
18
16
 
@@ -62,7 +60,7 @@ class _BaseResponse(BaseModel):
62
60
  errors: list[str] | None = None
63
61
 
64
62
  # Allow any additional keys returned by Jaeger so that nothing gets
65
- # silently dropped if the backend adds new fields we dont know about.
63
+ # silently dropped if the backend adds new fields we don't know about.
66
64
 
67
65
  model_config = ConfigDict(extra="allow")
68
66
 
@@ -78,76 +76,3 @@ class GetTraceResponse(_BaseResponse):
78
76
  """Response model for *get trace by id* requests."""
79
77
 
80
78
  # Same as base but alias for clarity
81
-
82
-
83
- # ---------------------------------------------------------------------------
84
- # Query models
85
- # ---------------------------------------------------------------------------
86
-
87
-
88
- class SearchQuery(BaseModel):
89
- """Minimal set of query parameters for the `/api/traces` endpoint.
90
-
91
- Parameter interaction rules:
92
-
93
- * **service** – global filter; *all* returned traces must belong to this
94
- service.
95
- * **operation** – optional secondary filter; returned traces must contain
96
- *at least one span* whose ``operationName`` equals the provided value.
97
- * **tags** – dictionary of key‒value pairs; each trace must include a span
98
- that matches **all** of the pairs (logical AND).
99
- * **limit** – applied *after* all other filters; truncates the final list
100
- of traces to the requested maximum.
101
-
102
- Any additional/unknown parameters are forwarded thanks to
103
- ``extra = "allow"`` – this keeps the model future-proof.
104
- """
105
-
106
- # NOTE: Only the fields that are reliably supported by Jaeger’s REST API and
107
- # work with the user’s deployment are kept. The model remains *open* to any
108
- # extra parameters thanks to `extra = "allow"`.
109
-
110
- service: str = Field(
111
- ...,
112
- description="Service name to search for. Example: 'veris-agent'",
113
- )
114
-
115
- limit: int | None = Field(
116
- None,
117
- description="Maximum number of traces to return. Example: 10",
118
- )
119
-
120
- tags: dict[str, Any] | None = Field(
121
- None,
122
- description=(
123
- "Dictionary of tag filters (AND-combined). "
124
- "Example: {'error': 'true', 'bt.metrics.time_to_first_token': '0.813544'}"
125
- ),
126
- )
127
-
128
- operation: str | None = Field(
129
- None,
130
- description="Operation name to search for. Example: 'process_chat_message'",
131
- )
132
-
133
- model_config = ConfigDict(
134
- extra="allow", # allow additional query params implicitly
135
- populate_by_name=True,
136
- str_to_lower=False,
137
- )
138
-
139
- @field_validator("tags", mode="before")
140
- @classmethod
141
- def _empty_to_none(cls, v: dict[str, Any] | None) -> dict[str, Any] | None: # noqa: D401, ANN102
142
- return v or None
143
-
144
- def to_params(self) -> dict[str, Any]: # noqa: D401
145
- """Translate the model into a *requests*/*httpx* compatible params dict."""
146
- # Dump using aliases so ``span_kind`` becomes ``spanKind`` automatically.
147
- params: dict[str, Any] = self.model_dump(exclude_none=True, by_alias=True)
148
-
149
- # Convert tags to a JSON string if necessary – this matches what the UI sends.
150
- if "tags" in params and isinstance(params["tags"], dict):
151
- params["tags"] = json.dumps(params["tags"])
152
-
153
- return params
veris_ai/models.py ADDED
@@ -0,0 +1,11 @@
1
+ """Models for the VERIS SDK."""
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class ResponseExpectation(str, Enum):
7
+ """Expected response behavior for tool mocking."""
8
+
9
+ AUTO = "auto"
10
+ REQUIRED = "required"
11
+ NONE = "none"
veris_ai/tool_mock.py CHANGED
@@ -15,6 +15,7 @@ from typing import (
15
15
 
16
16
  import httpx
17
17
 
18
+ from veris_ai.models import ResponseExpectation
18
19
  from veris_ai.utils import convert_to_type, extract_json_schema
19
20
 
20
21
  logger = logging.getLogger(__name__)
@@ -91,7 +92,7 @@ class VerisSDK:
91
92
  **params_dict,
92
93
  )
93
94
 
94
- def mock(
95
+ def mock( # noqa: C901, PLR0915
95
96
  self,
96
97
  mode: Literal["tool", "function"] = "tool",
97
98
  expects_response: bool | None = None,
@@ -99,26 +100,16 @@ class VerisSDK:
99
100
  ) -> Callable:
100
101
  """Decorator for mocking tool calls."""
101
102
 
102
- def decorator(func: Callable) -> Callable:
103
+ def decorator(func: Callable) -> Callable: # noqa: C901, PLR0915
103
104
  """Decorator for mocking tool calls."""
104
- endpoint = os.getenv("VERIS_MOCK_ENDPOINT_URL")
105
- if not endpoint:
106
- error_msg = "VERIS_MOCK_ENDPOINT_URL environment variable is not set"
107
- raise ValueError(error_msg)
108
- # Default timeout of 30 seconds
109
- timeout = float(os.getenv("VERIS_MOCK_TIMEOUT", "90.0"))
105
+ # Check if the original function is async
106
+ is_async = inspect.iscoroutinefunction(func)
110
107
 
111
- @wraps(func)
112
- async def wrapper(
108
+ def create_mock_payload(
113
109
  *args: tuple[object, ...],
114
110
  **kwargs: dict[str, object],
115
- ) -> object:
116
- # Check if we're in simulation mode
117
- env_mode = os.getenv("ENV", "").lower()
118
- if env_mode != "simulation":
119
- # If not in simulation mode, execute the original function
120
- return await func(*args, **kwargs)
121
- logger.info(f"Simulating function: {func.__name__}")
111
+ ) -> tuple[dict[str, Any], Any]:
112
+ """Create the mock payload - shared logic for both sync and async."""
122
113
  sig = inspect.signature(func)
123
114
  type_hints = get_type_hints(func)
124
115
 
@@ -143,10 +134,18 @@ class VerisSDK:
143
134
  if expects_response is None and mode == "function":
144
135
  expects_response = False
145
136
  # Prepare payload
137
+ # Convert expects_response to response_expectation enum
138
+ if expects_response is False:
139
+ response_expectation = ResponseExpectation.NONE
140
+ elif expects_response is True:
141
+ response_expectation = ResponseExpectation.REQUIRED
142
+ else:
143
+ response_expectation = ResponseExpectation.AUTO
144
+
146
145
  payload = {
147
146
  "session_id": self.session_id,
148
- "expects_response": expects_response,
149
- "cache_response": cache_response,
147
+ "response_expectation": response_expectation.value,
148
+ "cache_response": bool(cache_response) if cache_response is not None else False,
150
149
  "tool_call": {
151
150
  "function_name": func.__name__,
152
151
  "parameters": params_info,
@@ -155,6 +154,27 @@ class VerisSDK:
155
154
  },
156
155
  }
157
156
 
157
+ return payload, return_type_obj
158
+
159
+ @wraps(func)
160
+ async def async_wrapper(
161
+ *args: tuple[object, ...],
162
+ **kwargs: dict[str, object],
163
+ ) -> object:
164
+ # Check if we're in simulation mode
165
+ if not self.session_id:
166
+ # If not in simulation mode, execute the original function
167
+ return await func(*args, **kwargs)
168
+ endpoint = os.getenv("VERIS_MOCK_ENDPOINT_URL")
169
+ if not endpoint:
170
+ error_msg = "VERIS_MOCK_ENDPOINT_URL environment variable is not set"
171
+ raise ValueError(error_msg)
172
+ # Default timeout of 30 seconds
173
+ timeout = float(os.getenv("VERIS_MOCK_TIMEOUT", "90.0"))
174
+
175
+ logger.info(f"Simulating function: {func.__name__}")
176
+ payload, return_type_obj = create_mock_payload(*args, **kwargs)
177
+
158
178
  # Send request to endpoint with timeout
159
179
  async with httpx.AsyncClient(timeout=timeout) as client:
160
180
  response = await client.post(endpoint, json=payload)
@@ -162,36 +182,77 @@ class VerisSDK:
162
182
  mock_result = response.json()
163
183
  logger.info(f"Mock response: {mock_result}")
164
184
 
165
- # Convert the mock result to the expected return type
166
- if mode == "tool":
167
- return {"content": [{"type": "text", "text": mock_result}]}
168
- # Parse the mock result if it's a string
169
- # Extract result field for backwards compatibility
170
- # Parse the mock result if it's a string
171
185
  if isinstance(mock_result, str):
172
186
  with suppress(json.JSONDecodeError):
173
187
  mock_result = json.loads(mock_result)
174
188
  return convert_to_type(mock_result, return_type_obj)
175
189
  return convert_to_type(mock_result, return_type_obj)
176
190
 
177
- return wrapper
191
+ @wraps(func)
192
+ def sync_wrapper(
193
+ *args: tuple[object, ...],
194
+ **kwargs: dict[str, object],
195
+ ) -> object:
196
+ # Check if we're in simulation mode
197
+ if not self.session_id:
198
+ # If not in simulation mode, execute the original function
199
+ return func(*args, **kwargs)
200
+ endpoint = os.getenv("VERIS_MOCK_ENDPOINT_URL")
201
+ if not endpoint:
202
+ error_msg = "VERIS_MOCK_ENDPOINT_URL environment variable is not set"
203
+ raise ValueError(error_msg)
204
+ # Default timeout of 30 seconds
205
+ timeout = float(os.getenv("VERIS_MOCK_TIMEOUT", "90.0"))
206
+
207
+ logger.info(f"Simulating function: {func.__name__}")
208
+ payload, return_type_obj = create_mock_payload(*args, **kwargs)
209
+
210
+ # Send request to endpoint with timeout (synchronous)
211
+ with httpx.Client(timeout=timeout) as client:
212
+ response = client.post(endpoint, json=payload)
213
+ response.raise_for_status()
214
+ mock_result = response.json()
215
+ logger.info(f"Mock response: {mock_result}")
216
+
217
+ if isinstance(mock_result, str):
218
+ with suppress(json.JSONDecodeError):
219
+ mock_result = json.loads(mock_result)
220
+ return convert_to_type(mock_result, return_type_obj)
221
+ return convert_to_type(mock_result, return_type_obj)
222
+
223
+ # Return the appropriate wrapper based on whether the function is async
224
+ return async_wrapper if is_async else sync_wrapper
178
225
 
179
226
  return decorator
180
227
 
181
228
  def stub(self, return_value: Any) -> Callable: # noqa: ANN401
182
- """Decorator for stubbing tool calls."""
229
+ """Decorator for stubbing toolw calls."""
183
230
 
184
231
  def decorator(func: Callable) -> Callable:
232
+ # Check if the original function is async
233
+ is_async = inspect.iscoroutinefunction(func)
234
+
185
235
  @wraps(func)
186
- async def wrapper(*args: tuple[object, ...], **kwargs: dict[str, object]) -> object:
187
- env_mode = os.getenv("ENV", "").lower()
188
- if env_mode != "simulation":
236
+ async def async_wrapper(
237
+ *args: tuple[object, ...],
238
+ **kwargs: dict[str, object],
239
+ ) -> object:
240
+ if not self.session_id:
189
241
  # If not in simulation mode, execute the original function
190
242
  return await func(*args, **kwargs)
191
243
  logger.info(f"Simulating function: {func.__name__}")
192
244
  return return_value
193
245
 
194
- return wrapper
246
+ @wraps(func)
247
+ def sync_wrapper(*args: tuple[object, ...], **kwargs: dict[str, object]) -> object:
248
+ if not self.session_id:
249
+ # If not in simulation mode, execute the original function
250
+ return func(*args, **kwargs)
251
+ logger.info(f"Simulating function: {func.__name__}")
252
+ return return_value
253
+
254
+ # Return the appropriate wrapper based on whether the function is async
255
+ return async_wrapper if is_async else sync_wrapper
195
256
 
196
257
  return decorator
197
258
 
veris_ai/utils.py CHANGED
@@ -20,6 +20,7 @@ def convert_to_type(value: object, target_type: type) -> object:
20
20
  list: _convert_list,
21
21
  dict: _convert_dict,
22
22
  Union: _convert_union,
23
+ types.UnionType: _convert_union, # Handle Python 3.10+ union syntax (str | int)
23
24
  }
24
25
 
25
26
  # Use appropriate converter based on origin
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: veris-ai
3
- Version: 1.1.0
3
+ Version: 1.3.0
4
4
  Summary: A Python package for Veris AI tools
5
5
  Project-URL: Homepage, https://github.com/veris-ai/veris-python-sdk
6
6
  Project-URL: Bug Tracker, https://github.com/veris-ai/veris-python-sdk/issues
@@ -64,7 +64,7 @@ The SDK supports flexible import patterns to minimize dependencies:
64
64
 
65
65
  ```python
66
66
  # These imports only require base dependencies (httpx, pydantic, requests)
67
- from veris_ai import veris, JaegerClient, SearchQuery
67
+ from veris_ai import veris, JaegerClient
68
68
  ```
69
69
 
70
70
  ### Optional Imports (Require Extra Dependencies)
@@ -434,14 +434,23 @@ This project is licensed under the MIT License - see the LICENSE file for detail
434
434
  ## Jaeger Trace Interface
435
435
 
436
436
  A lightweight, fully-typed wrapper around the Jaeger **Query Service** HTTP API lives under `veris_ai.jaeger_interface`.
437
- It allows you to **search and retrieve traces** from both Jaegers default storage back-ends as well as **OpenSearch**, which powers the official Jaeger UI search page.
437
+ It allows you to **search and retrieve traces** from both Jaeger's default storage back-ends as well as **OpenSearch**, with additional **client-side span filtering** capabilities.
438
438
 
439
439
  ```python
440
- from veris_ai.jaeger_interface import JaegerClient, SearchQuery
440
+ from veris_ai.jaeger_interface import JaegerClient
441
441
 
442
442
  client = JaegerClient("http://localhost:16686")
443
443
 
444
- traces = client.search(SearchQuery(service="veris-agent", limit=2))
444
+ # Search with trace-level filters (server-side)
445
+ traces = client.search(service="veris-agent", limit=2, tags={"error": "true"})
446
+
447
+ # Search with span-level filters (client-side, OR logic)
448
+ filtered = client.search(
449
+ service="veris-agent",
450
+ limit=10,
451
+ span_tags={"http.status_code": 500, "db.error": "timeout"}
452
+ )
453
+
445
454
  first_trace = client.get_trace(traces.data[0].traceID)
446
455
  ```
447
456
 
@@ -0,0 +1,13 @@
1
+ veris_ai/__init__.py,sha256=Vp5yf9ZRchw0mmPwrRuNebI5cb2NGwpJGfr41l86q1U,1177
2
+ veris_ai/braintrust_tracing.py,sha256=0i-HR6IuK3-Q5ujMjT1FojxESRZLgqEvJ44bfJfDHaw,11938
3
+ veris_ai/models.py,sha256=6HINPxNFCakCVPcyEbUswWkXwb2K4lF0A8g8EvTMal4,213
4
+ veris_ai/tool_mock.py,sha256=U4xjINEZbtjutb_Vtr_So1ENNrU9kDROSyNUU5RL1Jc,10610
5
+ veris_ai/utils.py,sha256=3R2J9ko5t1UATiF1R6Hox9IPbtUz59EHsMDFg1-i7sk,9208
6
+ veris_ai/jaeger_interface/README.md,sha256=te5z3MWLqd2ECVV-a0MImBwTKRgQuuSuDB3bwIIsHi0,4437
7
+ veris_ai/jaeger_interface/__init__.py,sha256=d873a0zq3eUYU2Y77MtdjCwIARjAsAP7WDqGXDMWpYs,1158
8
+ veris_ai/jaeger_interface/client.py,sha256=yJrh86wRR0Dk3Gq12DId99WogcMIVbL0QQFqVSevvlE,8772
9
+ veris_ai/jaeger_interface/models.py,sha256=e64VV6IvOEFuzRUgvDAMQFyOZMRb56I-PUPZLBZ3rX0,1864
10
+ veris_ai-1.3.0.dist-info/METADATA,sha256=FD5DuzpYTZKjDmsPOb897HIMdOzS4l1UNTKh_68d29Y,13832
11
+ veris_ai-1.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
12
+ veris_ai-1.3.0.dist-info/licenses/LICENSE,sha256=2g4i20atAgtD5einaKzhQrIB-JrPhyQgD3bC0wkHcCI,1065
13
+ veris_ai-1.3.0.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- veris_ai/__init__.py,sha256=D7IkmgwQmucTTqqgRu-kOs5JTLLAA0ts3x91k4kyK2Y,1161
2
- veris_ai/braintrust_tracing.py,sha256=0i-HR6IuK3-Q5ujMjT1FojxESRZLgqEvJ44bfJfDHaw,11938
3
- veris_ai/tool_mock.py,sha256=PkI90tHMtmLj8shp8ytmOlrKc53M4kY-1YU0y4muTmU,7645
4
- veris_ai/utils.py,sha256=AjKfsCdpatUqO6iPcqcN_V5JFWo25MfF-xchwCnx3Ws,9119
5
- veris_ai/jaeger_interface/README.md,sha256=mu-XdBm3FM8MqyLEpZdjIE1lbgsvWszk2ehvez2p-8w,3055
6
- veris_ai/jaeger_interface/__init__.py,sha256=sWwt5M52Hi1vltNXmjSEpIHvA1NqsXymV8MiiGSedlg,735
7
- veris_ai/jaeger_interface/client.py,sha256=GWYFS0sgTZShQOi7Ndz504vyzCIKi6Kb_ZAMKskdjgE,4782
8
- veris_ai/jaeger_interface/models.py,sha256=Tnx_817dCNUJ1bM9dv-45AhK017AL8qgOfgE6ne9HUU,4606
9
- veris_ai-1.1.0.dist-info/METADATA,sha256=FzO5tRUgWZ5OXqqeUpz_uHpou5I4mc000n2jTjMja_g,13597
10
- veris_ai-1.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
11
- veris_ai-1.1.0.dist-info/licenses/LICENSE,sha256=2g4i20atAgtD5einaKzhQrIB-JrPhyQgD3bC0wkHcCI,1065
12
- veris_ai-1.1.0.dist-info/RECORD,,