veris-ai 1.0.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 +36 -1
- veris_ai/braintrust_tracing.py +282 -0
- veris_ai/jaeger_interface/README.md +137 -0
- veris_ai/jaeger_interface/__init__.py +39 -0
- veris_ai/jaeger_interface/client.py +233 -0
- veris_ai/jaeger_interface/models.py +79 -0
- veris_ai/models.py +11 -0
- veris_ai/tool_mock.py +162 -64
- veris_ai/utils.py +201 -1
- veris_ai-1.2.0.dist-info/METADATA +457 -0
- veris_ai-1.2.0.dist-info/RECORD +13 -0
- veris_ai-1.0.0.dist-info/METADATA +0 -239
- veris_ai-1.0.0.dist-info/RECORD +0 -7
- {veris_ai-1.0.0.dist-info → veris_ai-1.2.0.dist-info}/WHEEL +0 -0
- {veris_ai-1.0.0.dist-info → veris_ai-1.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Synchronous Jaeger Query Service client built on **requests**.
|
|
2
|
+
|
|
3
|
+
This implementation keeps dependencies minimal while providing fully-typed
|
|
4
|
+
*pydantic* models for both **request** and **response** bodies.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from typing import Any, Dict, List, Optional, Self
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
from .models import GetTraceResponse, SearchResponse, Span, Tag, Trace
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = ["JaegerClient"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class JaegerClient: # noqa: D101
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
base_url: str,
|
|
22
|
+
*,
|
|
23
|
+
timeout: float | None = 10.0,
|
|
24
|
+
session: requests.Session | None = None,
|
|
25
|
+
headers: dict[str, str] | None = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Create a new *JaegerClient* instance.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
base_url: Base URL of the Jaeger Query Service (e.g. ``http://localhost:16686``).
|
|
31
|
+
timeout: Request timeout in **seconds** (applied to every call).
|
|
32
|
+
session: Optional pre-configured :class:`requests.Session` to reuse.
|
|
33
|
+
headers: Optional default headers to send with every request.
|
|
34
|
+
"""
|
|
35
|
+
# Normalise to avoid trailing slash duplicates
|
|
36
|
+
self._base_url = base_url.rstrip("/")
|
|
37
|
+
self._timeout = timeout
|
|
38
|
+
self._external_session = session # If provided we won't close it
|
|
39
|
+
self._headers = headers or {}
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------
|
|
42
|
+
# Internal helpers
|
|
43
|
+
# ---------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
def _make_session(self) -> tuple[requests.Session, bool]: # noqa: D401
|
|
46
|
+
"""Return a *(session, should_close)* tuple.
|
|
47
|
+
|
|
48
|
+
If an external session was supplied we **must not** close it after the
|
|
49
|
+
request, hence the boolean flag letting callers know whether they are
|
|
50
|
+
responsible for closing the session.
|
|
51
|
+
"""
|
|
52
|
+
if self._external_session is not None:
|
|
53
|
+
return self._external_session, False
|
|
54
|
+
|
|
55
|
+
# Reuse the session opened via the context manager if available
|
|
56
|
+
if hasattr(self, "_session_ctx"):
|
|
57
|
+
return self._session_ctx, False
|
|
58
|
+
|
|
59
|
+
session = requests.Session()
|
|
60
|
+
session.headers.update(self._headers)
|
|
61
|
+
return session, True
|
|
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
|
+
|
|
118
|
+
# ---------------------------------------------------------------------
|
|
119
|
+
# Public API
|
|
120
|
+
# ---------------------------------------------------------------------
|
|
121
|
+
|
|
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.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
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.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Parsed :class:`~veris_ai.jaeger_interface.models.SearchResponse` model
|
|
147
|
+
with spans filtered according to span_tags if provided.
|
|
148
|
+
"""
|
|
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
|
+
|
|
168
|
+
session, should_close = self._make_session()
|
|
169
|
+
try:
|
|
170
|
+
url = f"{self._base_url}/api/traces"
|
|
171
|
+
response = session.get(url, params=params, timeout=self._timeout)
|
|
172
|
+
response.raise_for_status()
|
|
173
|
+
data = response.json()
|
|
174
|
+
finally:
|
|
175
|
+
if should_close:
|
|
176
|
+
session.close()
|
|
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
|
|
190
|
+
|
|
191
|
+
def get_trace(self, trace_id: str) -> GetTraceResponse: # noqa: D401
|
|
192
|
+
"""Retrieve a single trace by *trace_id*.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
trace_id: The Jaeger trace identifier.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Parsed :class:`~veris_ai.jaeger_interface.models.GetTraceResponse` model.
|
|
199
|
+
"""
|
|
200
|
+
if not trace_id:
|
|
201
|
+
error_msg = "trace_id must be non-empty"
|
|
202
|
+
raise ValueError(error_msg)
|
|
203
|
+
|
|
204
|
+
session, should_close = self._make_session()
|
|
205
|
+
try:
|
|
206
|
+
url = f"{self._base_url}/api/traces/{trace_id}"
|
|
207
|
+
response = session.get(url, timeout=self._timeout)
|
|
208
|
+
response.raise_for_status()
|
|
209
|
+
data = response.json()
|
|
210
|
+
finally:
|
|
211
|
+
if should_close:
|
|
212
|
+
session.close()
|
|
213
|
+
return GetTraceResponse.model_validate(data) # type: ignore[arg-type]
|
|
214
|
+
|
|
215
|
+
# ------------------------------------------------------------------
|
|
216
|
+
# Context-manager helpers (optional but convenient)
|
|
217
|
+
# ------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
def __enter__(self) -> Self:
|
|
220
|
+
"""Enter the context manager."""
|
|
221
|
+
self._session_ctx, self._should_close_ctx = self._make_session()
|
|
222
|
+
return self
|
|
223
|
+
|
|
224
|
+
def __exit__(
|
|
225
|
+
self,
|
|
226
|
+
exc_type: type[BaseException] | None,
|
|
227
|
+
exc: BaseException | None,
|
|
228
|
+
tb: Any | None,
|
|
229
|
+
) -> None:
|
|
230
|
+
"""Exit the context manager."""
|
|
231
|
+
# Only close if we created the session
|
|
232
|
+
if getattr(self, "_should_close_ctx", False):
|
|
233
|
+
self._session_ctx.close()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Tag",
|
|
10
|
+
"Process",
|
|
11
|
+
"Span",
|
|
12
|
+
"Trace",
|
|
13
|
+
"SearchResponse",
|
|
14
|
+
"GetTraceResponse",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Tag(BaseModel):
|
|
19
|
+
"""A Jaeger tag key/value pair."""
|
|
20
|
+
|
|
21
|
+
key: str
|
|
22
|
+
value: Any
|
|
23
|
+
type: str | None = None # Jaeger uses an optional *type* field in v1
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Process(BaseModel):
|
|
27
|
+
"""Represents the *process* section of a Jaeger trace."""
|
|
28
|
+
|
|
29
|
+
serviceName: str = Field(alias="serviceName") # noqa: N815
|
|
30
|
+
tags: list[Tag] | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Span(BaseModel):
|
|
34
|
+
"""Represents a single Jaeger span."""
|
|
35
|
+
|
|
36
|
+
traceID: str # noqa: N815
|
|
37
|
+
spanID: str # noqa: N815
|
|
38
|
+
operationName: str # noqa: N815
|
|
39
|
+
startTime: int # noqa: N815
|
|
40
|
+
duration: int
|
|
41
|
+
tags: list[Tag] | None = None
|
|
42
|
+
references: list[dict[str, Any]] | None = None
|
|
43
|
+
processID: str | None = None # noqa: N815
|
|
44
|
+
|
|
45
|
+
model_config = ConfigDict(extra="allow")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Trace(BaseModel):
|
|
49
|
+
"""A full Jaeger trace as returned by the Query API."""
|
|
50
|
+
|
|
51
|
+
traceID: str # noqa: N815
|
|
52
|
+
spans: list[Span]
|
|
53
|
+
process: Process | dict[str, Process] | None = None
|
|
54
|
+
warnings: list[str] | None = None
|
|
55
|
+
|
|
56
|
+
model_config = ConfigDict(extra="allow")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class _BaseResponse(BaseModel):
|
|
60
|
+
data: list[Trace] | Trace | None = None
|
|
61
|
+
errors: list[str] | None = None
|
|
62
|
+
|
|
63
|
+
# Allow any additional keys returned by Jaeger so that nothing gets
|
|
64
|
+
# silently dropped if the backend adds new fields we don't know about.
|
|
65
|
+
|
|
66
|
+
model_config = ConfigDict(extra="allow")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class SearchResponse(_BaseResponse):
|
|
70
|
+
"""Response model for *search* or *find traces* requests."""
|
|
71
|
+
|
|
72
|
+
total: int | None = None
|
|
73
|
+
limit: int | None = None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class GetTraceResponse(_BaseResponse):
|
|
77
|
+
"""Response model for *get trace by id* requests."""
|
|
78
|
+
|
|
79
|
+
# Same as base but alias for clarity
|
veris_ai/models.py
ADDED
veris_ai/tool_mock.py
CHANGED
|
@@ -8,13 +8,15 @@ from contextvars import ContextVar
|
|
|
8
8
|
from functools import wraps
|
|
9
9
|
from typing import (
|
|
10
10
|
Any,
|
|
11
|
+
Literal,
|
|
11
12
|
TypeVar,
|
|
12
13
|
get_type_hints,
|
|
13
14
|
)
|
|
14
15
|
|
|
15
16
|
import httpx
|
|
16
17
|
|
|
17
|
-
from veris_ai.
|
|
18
|
+
from veris_ai.models import ResponseExpectation
|
|
19
|
+
from veris_ai.utils import convert_to_type, extract_json_schema
|
|
18
20
|
|
|
19
21
|
logger = logging.getLogger(__name__)
|
|
20
22
|
|
|
@@ -90,72 +92,168 @@ class VerisSDK:
|
|
|
90
92
|
**params_dict,
|
|
91
93
|
)
|
|
92
94
|
|
|
93
|
-
def mock(
|
|
95
|
+
def mock(
|
|
96
|
+
self,
|
|
97
|
+
mode: Literal["tool", "function"] = "tool",
|
|
98
|
+
expects_response: bool | None = None,
|
|
99
|
+
cache_response: bool | None = None,
|
|
100
|
+
) -> Callable:
|
|
94
101
|
"""Decorator for mocking tool calls."""
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
102
|
+
|
|
103
|
+
def decorator(func: Callable) -> Callable:
|
|
104
|
+
"""Decorator for mocking tool calls."""
|
|
105
|
+
endpoint = os.getenv("VERIS_MOCK_ENDPOINT_URL")
|
|
106
|
+
if not endpoint:
|
|
107
|
+
error_msg = "VERIS_MOCK_ENDPOINT_URL environment variable is not set"
|
|
108
|
+
raise ValueError(error_msg)
|
|
109
|
+
# Default timeout of 30 seconds
|
|
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)
|
|
114
|
+
|
|
115
|
+
def create_mock_payload(
|
|
116
|
+
*args: tuple[object, ...],
|
|
117
|
+
**kwargs: dict[str, object],
|
|
118
|
+
) -> tuple[dict[str, Any], Any]:
|
|
119
|
+
"""Create the mock payload - shared logic for both sync and async."""
|
|
120
|
+
sig = inspect.signature(func)
|
|
121
|
+
type_hints = get_type_hints(func)
|
|
122
|
+
|
|
123
|
+
# Extract return type object (not just the name)
|
|
124
|
+
return_type_obj = type_hints.pop("return", Any)
|
|
125
|
+
# Create parameter info
|
|
126
|
+
params_info = {}
|
|
127
|
+
bound_args = sig.bind(*args, **kwargs)
|
|
128
|
+
bound_args.apply_defaults()
|
|
129
|
+
_ = bound_args.arguments.pop("ctx", None)
|
|
130
|
+
_ = bound_args.arguments.pop("self", None)
|
|
131
|
+
_ = bound_args.arguments.pop("cls", None)
|
|
132
|
+
|
|
133
|
+
for param_name, param_value in bound_args.arguments.items():
|
|
134
|
+
params_info[param_name] = {
|
|
135
|
+
"value": str(param_value),
|
|
136
|
+
"type": str(type_hints.get(param_name, Any)),
|
|
137
|
+
}
|
|
138
|
+
# Get function docstring
|
|
139
|
+
docstring = inspect.getdoc(func) or ""
|
|
140
|
+
nonlocal expects_response
|
|
141
|
+
if expects_response is None and mode == "function":
|
|
142
|
+
expects_response = False
|
|
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
|
+
|
|
152
|
+
payload = {
|
|
153
|
+
"session_id": self.session_id,
|
|
154
|
+
"response_expectation": response_expectation.value,
|
|
155
|
+
"cache_response": bool(cache_response) if cache_response is not None else False,
|
|
156
|
+
"tool_call": {
|
|
157
|
+
"function_name": func.__name__,
|
|
158
|
+
"parameters": params_info,
|
|
159
|
+
"return_type": json.dumps(extract_json_schema(return_type_obj)),
|
|
160
|
+
"docstring": docstring,
|
|
161
|
+
},
|
|
128
162
|
}
|
|
129
163
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
+
|
|
180
|
+
# Send request to endpoint with timeout
|
|
181
|
+
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
182
|
+
response = await client.post(endpoint, json=payload)
|
|
183
|
+
response.raise_for_status()
|
|
184
|
+
mock_result = response.json()
|
|
185
|
+
logger.info(f"Mock response: {mock_result}")
|
|
186
|
+
|
|
187
|
+
if isinstance(mock_result, str):
|
|
188
|
+
with suppress(json.JSONDecodeError):
|
|
189
|
+
mock_result = json.loads(mock_result)
|
|
190
|
+
return convert_to_type(mock_result, return_type_obj)
|
|
191
|
+
return convert_to_type(mock_result, return_type_obj)
|
|
192
|
+
|
|
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
|
|
222
|
+
|
|
223
|
+
return decorator
|
|
224
|
+
|
|
225
|
+
def stub(self, return_value: Any) -> Callable: # noqa: ANN401
|
|
226
|
+
"""Decorator for stubbing toolw calls."""
|
|
227
|
+
|
|
228
|
+
def decorator(func: Callable) -> Callable:
|
|
229
|
+
# Check if the original function is async
|
|
230
|
+
is_async = inspect.iscoroutinefunction(func)
|
|
231
|
+
|
|
232
|
+
@wraps(func)
|
|
233
|
+
async def async_wrapper(
|
|
234
|
+
*args: tuple[object, ...],
|
|
235
|
+
**kwargs: dict[str, object],
|
|
236
|
+
) -> object:
|
|
237
|
+
env_mode = os.getenv("ENV", "").lower()
|
|
238
|
+
if env_mode != "simulation":
|
|
239
|
+
# If not in simulation mode, execute the original function
|
|
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)
|
|
250
|
+
logger.info(f"Simulating function: {func.__name__}")
|
|
251
|
+
return return_value
|
|
252
|
+
|
|
253
|
+
# Return the appropriate wrapper based on whether the function is async
|
|
254
|
+
return async_wrapper if is_async else sync_wrapper
|
|
255
|
+
|
|
256
|
+
return decorator
|
|
159
257
|
|
|
160
258
|
|
|
161
259
|
veris = VerisSDK()
|