veris-ai 1.1.0__py3-none-any.whl → 1.2.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,8 +5,9 @@ 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
9
8
  from .tool_mock import veris
9
+ from .jaeger_interface import JaegerClient
10
+ from .models import ResponseExpectation
10
11
 
11
12
  # Lazy import for modules with heavy dependencies
12
13
  _instrument = None
@@ -36,6 +37,6 @@ def instrument(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401
36
37
  __all__ = [
37
38
  "veris",
38
39
  "JaegerClient",
39
- "SearchQuery",
40
40
  "instrument",
41
+ "ResponseExpectation"
41
42
  ]
@@ -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
+ from typing import Any, Dict, List, Optional, Self
10
9
 
11
10
  import requests
12
11
 
13
- from .models import GetTraceResponse, SearchQuery, SearchResponse
12
+ from .models import GetTraceResponse, SearchResponse, Span, Tag, Trace
14
13
 
15
- if TYPE_CHECKING:
16
- from types import TracebackType
17
14
 
18
15
  __all__ = ["JaegerClient"]
19
16
 
@@ -63,20 +60,111 @@ 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
+ for key, value in span_tags.items():
73
+ if span_tag_dict.get(key) == value:
74
+ return True
75
+
76
+ return False
77
+
78
+ def _filter_spans(
79
+ self,
80
+ traces: List[Trace],
81
+ span_tags: Optional[Dict[str, Any]],
82
+ span_operations: Optional[List[str]] = None
83
+ ) -> List[Trace]:
84
+ """
85
+ Filter spans within traces based on span_tags (OR logic) and/or span_operations (OR logic).
86
+ If both are provided, a span must match at least one tag AND at least one operation.
87
+ """
88
+ if not span_tags and not span_operations:
89
+ return traces
90
+
91
+ filtered_traces = []
92
+ for trace in traces:
93
+ filtered_spans = []
94
+ for span in trace.spans:
95
+ tag_match = True
96
+ op_match = True
97
+
98
+ if span_tags:
99
+ tag_match = self._span_matches_tags(span, span_tags)
100
+ if span_operations:
101
+ op_match = span.operationName in span_operations
102
+
103
+ # If both filters are provided, require both to match (AND logic)
104
+ if tag_match and op_match:
105
+ filtered_spans.append(span)
106
+
107
+ if filtered_spans:
108
+ filtered_trace = Trace(
109
+ traceID=trace.traceID,
110
+ spans=filtered_spans,
111
+ process=trace.process,
112
+ warnings=trace.warnings
113
+ )
114
+ filtered_traces.append(filtered_trace)
115
+
116
+ return filtered_traces
117
+
66
118
  # ---------------------------------------------------------------------
67
119
  # Public API
68
120
  # ---------------------------------------------------------------------
69
121
 
70
- def search(self, query: SearchQuery) -> SearchResponse: # noqa: D401
71
- """Search traces using the *v1* ``/api/traces`` endpoint.
122
+ def search(
123
+ self,
124
+ service: Optional[str] = None,
125
+ *,
126
+ limit: Optional[int] = None,
127
+ tags: Optional[Dict[str, Any]] = None,
128
+ operation: Optional[str] = None,
129
+ span_tags: Optional[Dict[str, Any]] = None,
130
+ span_operations: Optional[List[str]] = None,
131
+ **kwargs: Any
132
+ ) -> SearchResponse: # noqa: D401
133
+ """Search traces using the *v1* ``/api/traces`` endpoint with optional span filtering.
72
134
 
73
135
  Args:
74
- query: :class:`~veris_ai.jaeger_interface.models.SearchQuery` instance.
136
+ service: Service name to search for. If not provided, searches across all services.
137
+ limit: Maximum number of traces to return.
138
+ tags: Dictionary of tag filters for trace-level filtering (AND-combined).
139
+ operation: Operation name to search for.
140
+ span_tags: Dictionary of tag filters for span-level filtering (OR-combined AND-combined with span_operations).
141
+ Applied client-side after retrieving traces.
142
+ span_operations: List of operation names to search for (OR-combined AND-combined with span_tags).
143
+ **kwargs: Additional parameters to pass to the Jaeger API.
75
144
 
76
145
  Returns:
77
- Parsed :class:`~veris_ai.jaeger_interface.models.SearchResponse` model.
146
+ Parsed :class:`~veris_ai.jaeger_interface.models.SearchResponse` model
147
+ with spans filtered according to span_tags if provided.
78
148
  """
79
- params = query.to_params()
149
+ # Build params for the Jaeger API (excluding span_tags)
150
+ params: Dict[str, Any] = {}
151
+
152
+ if service is not None:
153
+ params["service"] = service
154
+
155
+ if limit is not None:
156
+ params["limit"] = limit
157
+
158
+ if operation is not None:
159
+ params["operation"] = operation
160
+
161
+ if tags:
162
+ # Convert tags to JSON string as expected by Jaeger API
163
+ params["tags"] = json.dumps(tags)
164
+
165
+ # Add any additional parameters
166
+ params.update(kwargs)
167
+
80
168
  session, should_close = self._make_session()
81
169
  try:
82
170
  url = f"{self._base_url}/api/traces"
@@ -86,7 +174,19 @@ class JaegerClient: # noqa: D101
86
174
  finally:
87
175
  if should_close:
88
176
  session.close()
89
- return SearchResponse.model_validate(data) # type: ignore[arg-type]
177
+
178
+ # Parse the response
179
+ search_response = SearchResponse.model_validate(data) # type: ignore[arg-type]
180
+
181
+ # Apply span-level filtering if span_tags is provided
182
+ if span_tags or span_operations and search_response.data and isinstance(search_response.data, list):
183
+ filtered_traces = self._filter_spans(search_response.data, span_tags, span_operations)
184
+ search_response.data = filtered_traces
185
+ # Update the total to reflect filtered results
186
+ if search_response.total is not None:
187
+ search_response.total = len(filtered_traces)
188
+
189
+ return search_response
90
190
 
91
191
  def get_trace(self, trace_id: str) -> GetTraceResponse: # noqa: D401
92
192
  """Retrieve a single trace by *trace_id*.
@@ -125,7 +225,7 @@ class JaegerClient: # noqa: D101
125
225
  self,
126
226
  exc_type: type[BaseException] | None,
127
227
  exc: BaseException | None,
128
- tb: TracebackType | None,
228
+ tb: Any | None,
129
229
  ) -> None:
130
230
  """Exit the context manager."""
131
231
  # Only close if we created the session
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import json
4
4
  from typing import Any
5
5
 
6
- from pydantic import BaseModel, ConfigDict, Field, field_validator
6
+ from pydantic import BaseModel, Field, ConfigDict
7
7
 
8
8
  __all__ = [
9
9
  "Tag",
@@ -12,7 +12,6 @@ __all__ = [
12
12
  "Trace",
13
13
  "SearchResponse",
14
14
  "GetTraceResponse",
15
- "SearchQuery",
16
15
  ]
17
16
 
18
17
 
@@ -62,7 +61,7 @@ class _BaseResponse(BaseModel):
62
61
  errors: list[str] | None = None
63
62
 
64
63
  # Allow any additional keys returned by Jaeger so that nothing gets
65
- # silently dropped if the backend adds new fields we dont know about.
64
+ # silently dropped if the backend adds new fields we don't know about.
66
65
 
67
66
  model_config = ConfigDict(extra="allow")
68
67
 
@@ -78,76 +77,3 @@ class GetTraceResponse(_BaseResponse):
78
77
  """Response model for *get trace by id* requests."""
79
78
 
80
79
  # 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__)
@@ -107,18 +108,15 @@ class VerisSDK:
107
108
  raise ValueError(error_msg)
108
109
  # Default timeout of 30 seconds
109
110
  timeout = float(os.getenv("VERIS_MOCK_TIMEOUT", "90.0"))
111
+
112
+ # Check if the original function is async
113
+ is_async = inspect.iscoroutinefunction(func)
110
114
 
111
- @wraps(func)
112
- async def wrapper(
115
+ def create_mock_payload(
113
116
  *args: tuple[object, ...],
114
117
  **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__}")
118
+ ) -> tuple[dict[str, Any], Any]:
119
+ """Create the mock payload - shared logic for both sync and async."""
122
120
  sig = inspect.signature(func)
123
121
  type_hints = get_type_hints(func)
124
122
 
@@ -143,10 +141,18 @@ class VerisSDK:
143
141
  if expects_response is None and mode == "function":
144
142
  expects_response = False
145
143
  # Prepare payload
144
+ # Convert expects_response to response_expectation enum
145
+ if expects_response is False:
146
+ response_expectation = ResponseExpectation.NONE
147
+ elif expects_response is True:
148
+ response_expectation = ResponseExpectation.REQUIRED
149
+ else:
150
+ response_expectation = ResponseExpectation.AUTO
151
+
146
152
  payload = {
147
153
  "session_id": self.session_id,
148
- "expects_response": expects_response,
149
- "cache_response": cache_response,
154
+ "response_expectation": response_expectation.value,
155
+ "cache_response": bool(cache_response) if cache_response is not None else False,
150
156
  "tool_call": {
151
157
  "function_name": func.__name__,
152
158
  "parameters": params_info,
@@ -155,6 +161,22 @@ class VerisSDK:
155
161
  },
156
162
  }
157
163
 
164
+ return payload, return_type_obj
165
+
166
+ @wraps(func)
167
+ async def async_wrapper(
168
+ *args: tuple[object, ...],
169
+ **kwargs: dict[str, object],
170
+ ) -> object:
171
+ # Check if we're in simulation mode
172
+ env_mode = os.getenv("ENV", "").lower()
173
+ if env_mode != "simulation":
174
+ # If not in simulation mode, execute the original function
175
+ return await func(*args, **kwargs)
176
+
177
+ logger.info(f"Simulating function: {func.__name__}")
178
+ payload, return_type_obj = create_mock_payload(*args, **kwargs)
179
+
158
180
  # Send request to endpoint with timeout
159
181
  async with httpx.AsyncClient(timeout=timeout) as client:
160
182
  response = await client.post(endpoint, json=payload)
@@ -162,36 +184,74 @@ class VerisSDK:
162
184
  mock_result = response.json()
163
185
  logger.info(f"Mock response: {mock_result}")
164
186
 
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
187
  if isinstance(mock_result, str):
172
188
  with suppress(json.JSONDecodeError):
173
189
  mock_result = json.loads(mock_result)
174
190
  return convert_to_type(mock_result, return_type_obj)
175
191
  return convert_to_type(mock_result, return_type_obj)
176
192
 
177
- return wrapper
193
+ @wraps(func)
194
+ def sync_wrapper(
195
+ *args: tuple[object, ...],
196
+ **kwargs: dict[str, object],
197
+ ) -> object:
198
+ # Check if we're in simulation mode
199
+ env_mode = os.getenv("ENV", "").lower()
200
+ if env_mode != "simulation":
201
+ # If not in simulation mode, execute the original function
202
+ return func(*args, **kwargs)
203
+
204
+ logger.info(f"Simulating function: {func.__name__}")
205
+ payload, return_type_obj = create_mock_payload(*args, **kwargs)
206
+
207
+ # Send request to endpoint with timeout (synchronous)
208
+ with httpx.Client(timeout=timeout) as client:
209
+ response = client.post(endpoint, json=payload)
210
+ response.raise_for_status()
211
+ mock_result = response.json()
212
+ logger.info(f"Mock response: {mock_result}")
213
+
214
+ if isinstance(mock_result, str):
215
+ with suppress(json.JSONDecodeError):
216
+ mock_result = json.loads(mock_result)
217
+ return convert_to_type(mock_result, return_type_obj)
218
+ return convert_to_type(mock_result, return_type_obj)
219
+
220
+ # Return the appropriate wrapper based on whether the function is async
221
+ return async_wrapper if is_async else sync_wrapper
178
222
 
179
223
  return decorator
180
224
 
181
225
  def stub(self, return_value: Any) -> Callable: # noqa: ANN401
182
- """Decorator for stubbing tool calls."""
226
+ """Decorator for stubbing toolw calls."""
183
227
 
184
228
  def decorator(func: Callable) -> Callable:
229
+ # Check if the original function is async
230
+ is_async = inspect.iscoroutinefunction(func)
231
+
185
232
  @wraps(func)
186
- async def wrapper(*args: tuple[object, ...], **kwargs: dict[str, object]) -> object:
233
+ async def async_wrapper(
234
+ *args: tuple[object, ...],
235
+ **kwargs: dict[str, object],
236
+ ) -> object:
187
237
  env_mode = os.getenv("ENV", "").lower()
188
238
  if env_mode != "simulation":
189
239
  # If not in simulation mode, execute the original function
190
- return await func(*args, **kwargs)
240
+ return func(*args, **kwargs)
241
+ logger.info(f"Simulating function: {func.__name__}")
242
+ return return_value
243
+
244
+ @wraps(func)
245
+ def sync_wrapper(*args: tuple[object, ...], **kwargs: dict[str, object]) -> object:
246
+ env_mode = os.getenv("ENV", "").lower()
247
+ if env_mode != "simulation":
248
+ # If not in simulation mode, execute the original function
249
+ return func(*args, **kwargs)
191
250
  logger.info(f"Simulating function: {func.__name__}")
192
251
  return return_value
193
252
 
194
- return wrapper
253
+ # Return the appropriate wrapper based on whether the function is async
254
+ return async_wrapper if is_async else sync_wrapper
195
255
 
196
256
  return decorator
197
257
 
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.2.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=FkpF1jAEX9y5mZjICuxv0-uc_pTgWuCzK5D5fEQ-b8s,1195
2
+ veris_ai/braintrust_tracing.py,sha256=0i-HR6IuK3-Q5ujMjT1FojxESRZLgqEvJ44bfJfDHaw,11938
3
+ veris_ai/models.py,sha256=wj1avahszKrErFbWVar9xvFrp_pVkQUCaJUc_gFzu3k,216
4
+ veris_ai/tool_mock.py,sha256=UO-6tYvfreLXfOxHFKFl-GJ1nI7bLoz93uuv_yBehWc,10507
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=AU9qP_pzutwantcLiKaml5JW_AHrXHH7X-ALYVkbNZc,8777
9
+ veris_ai/jaeger_interface/models.py,sha256=ZYL2vlJbObAiAowsashuAuT6tUV9HaEmGKuxLPi_ye8,1876
10
+ veris_ai-1.2.0.dist-info/METADATA,sha256=ZmgJqKZUnL2G0ANQxX6SiYX2UK3HqeG7FjGgCttAQPM,13832
11
+ veris_ai-1.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
12
+ veris_ai-1.2.0.dist-info/licenses/LICENSE,sha256=2g4i20atAgtD5einaKzhQrIB-JrPhyQgD3bC0wkHcCI,1065
13
+ veris_ai-1.2.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,,