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 +3 -7
- veris_ai/jaeger_interface/README.md +52 -24
- veris_ai/jaeger_interface/__init__.py +19 -6
- veris_ai/jaeger_interface/client.py +117 -14
- veris_ai/jaeger_interface/models.py +2 -77
- veris_ai/models.py +11 -0
- veris_ai/tool_mock.py +92 -31
- veris_ai/utils.py +1 -0
- {veris_ai-1.1.0.dist-info → veris_ai-1.3.0.dist-info}/METADATA +14 -5
- veris_ai-1.3.0.dist-info/RECORD +13 -0
- veris_ai-1.1.0.dist-info/RECORD +0 -12
- {veris_ai-1.1.0.dist-info → veris_ai-1.3.0.dist-info}/WHEEL +0 -0
- {veris_ai-1.1.0.dist-info → veris_ai-1.3.0.dist-info}/licenses/LICENSE +0 -0
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
|
|
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 SDK
|
|
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
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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(
|
|
78
|
-
| `get_trace(trace_id: str) -> GetTraceResponse` | Fetch a single trace by 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
|
-
### `
|
|
80
|
+
### `search()` Parameters
|
|
81
81
|
|
|
82
|
-
`
|
|
83
|
-
Jaeger’s `/api/traces` endpoint.
|
|
82
|
+
The `search()` method now uses a flattened parameter structure:
|
|
84
83
|
|
|
85
|
-
Parameter
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
##
|
|
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
|
|
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
|
-
|
|
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"
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
from typing import
|
|
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,
|
|
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(
|
|
71
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 don
|
|
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
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
112
|
-
async def wrapper(
|
|
108
|
+
def create_mock_payload(
|
|
113
109
|
*args: tuple[object, ...],
|
|
114
110
|
**kwargs: dict[str, object],
|
|
115
|
-
) ->
|
|
116
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
|
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
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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 Jaeger
|
|
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
|
|
440
|
+
from veris_ai.jaeger_interface import JaegerClient
|
|
441
441
|
|
|
442
442
|
client = JaegerClient("http://localhost:16686")
|
|
443
443
|
|
|
444
|
-
|
|
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,,
|
veris_ai-1.1.0.dist-info/RECORD
DELETED
|
@@ -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,,
|
|
File without changes
|
|
File without changes
|