veris-ai 1.5.0__py3-none-any.whl → 1.7.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/README.md +0 -7
- veris_ai/__init__.py +9 -29
- veris_ai/jaeger_interface/__init__.py +1 -1
- veris_ai/logging.py +43 -6
- veris_ai/observability.py +133 -0
- veris_ai/tool_mock.py +152 -102
- {veris_ai-1.5.0.dist-info → veris_ai-1.7.0.dist-info}/METADATA +33 -18
- veris_ai-1.7.0.dist-info/RECORD +15 -0
- veris_ai/braintrust_tracing.py +0 -282
- veris_ai-1.5.0.dist-info/RECORD +0 -15
- {veris_ai-1.5.0.dist-info → veris_ai-1.7.0.dist-info}/WHEEL +0 -0
- {veris_ai-1.5.0.dist-info → veris_ai-1.7.0.dist-info}/licenses/LICENSE +0 -0
veris_ai/README.md
CHANGED
|
@@ -15,7 +15,6 @@ This module contains the core implementation of the Veris AI Python SDK. Each co
|
|
|
15
15
|
| Module | Purpose | Key Classes/Functions | Lines |
|
|
16
16
|
|--------|---------|----------------------|-------|
|
|
17
17
|
| [`tool_mock.py`](tool_mock.py) | Function mocking & FastAPI MCP | `VerisSDK`, `@mock`, `@stub` | 327 |
|
|
18
|
-
| [`braintrust_tracing.py`](braintrust_tracing.py) | Dual tracing instrumentation | `instrument()` | 283 |
|
|
19
18
|
| [`utils.py`](utils.py) | Type utilities & JSON schema | `extract_json_schema()` | 272 |
|
|
20
19
|
| [`logging.py`](logging.py) | Logging configuration | `setup_logging()` | 116 |
|
|
21
20
|
| [`models.py`](models.py) | Data models | Type definitions | 12 |
|
|
@@ -40,12 +39,6 @@ This module contains the core implementation of the Veris AI Python SDK. Each co
|
|
|
40
39
|
|
|
41
40
|
**Implementation**: [`tool_mock.py:250-300`](tool_mock.py)
|
|
42
41
|
|
|
43
|
-
### Tracing Flow
|
|
44
|
-
1. **Dual Setup**: Braintrust + OpenTelemetry instrumentation
|
|
45
|
-
2. **Session Tagging**: Bearer tokens → session IDs
|
|
46
|
-
3. **Span Attribution**: All operations tagged with `veris.session_id`
|
|
47
|
-
|
|
48
|
-
**Implementation**: [`braintrust_tracing.py:50-150`](braintrust_tracing.py)
|
|
49
42
|
|
|
50
43
|
## Configuration
|
|
51
44
|
|
veris_ai/__init__.py
CHANGED
|
@@ -1,37 +1,17 @@
|
|
|
1
1
|
"""Veris AI Python SDK."""
|
|
2
2
|
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
3
|
__version__ = "0.1.0"
|
|
6
4
|
|
|
7
5
|
# Import lightweight modules that only use base dependencies
|
|
8
6
|
from .jaeger_interface import JaegerClient
|
|
9
7
|
from .models import ResponseExpectation
|
|
10
8
|
from .tool_mock import veris
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
""
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
pip install veris-ai[instrument]
|
|
21
|
-
"""
|
|
22
|
-
global _instrument # noqa: PLW0603
|
|
23
|
-
if _instrument is None:
|
|
24
|
-
try:
|
|
25
|
-
from .braintrust_tracing import instrument as _instrument_impl # noqa: PLC0415
|
|
26
|
-
|
|
27
|
-
_instrument = _instrument_impl
|
|
28
|
-
except ImportError as e:
|
|
29
|
-
error_msg = (
|
|
30
|
-
"The 'instrument' function requires additional dependencies. "
|
|
31
|
-
"Please install them with: pip install veris-ai[instrument]"
|
|
32
|
-
)
|
|
33
|
-
raise ImportError(error_msg) from e
|
|
34
|
-
return _instrument(*args, **kwargs)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
__all__ = ["veris", "JaegerClient", "instrument", "ResponseExpectation"]
|
|
9
|
+
from .observability import init_observability, instrument_fastapi_app
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"veris",
|
|
13
|
+
"JaegerClient",
|
|
14
|
+
"ResponseExpectation",
|
|
15
|
+
"init_observability",
|
|
16
|
+
"instrument_fastapi_app",
|
|
17
|
+
]
|
veris_ai/logging.py
CHANGED
|
@@ -21,6 +21,7 @@ async def log_tool_call_async(
|
|
|
21
21
|
if not base_url:
|
|
22
22
|
logger.warning("VERIS_ENDPOINT_URL not set, skipping tool call logging")
|
|
23
23
|
return
|
|
24
|
+
base_url = base_url.rstrip("/")
|
|
24
25
|
|
|
25
26
|
endpoint = f"{base_url}/api/v2/simulations/{session_id}/log_tool_call"
|
|
26
27
|
payload = {
|
|
@@ -32,8 +33,17 @@ async def log_tool_call_async(
|
|
|
32
33
|
timeout = float(os.getenv("VERIS_MOCK_TIMEOUT", "90.0"))
|
|
33
34
|
|
|
34
35
|
try:
|
|
36
|
+
headers: dict[str, str] | None = None
|
|
37
|
+
try:
|
|
38
|
+
from opentelemetry.propagate import get_global_textmap
|
|
39
|
+
|
|
40
|
+
headers = {}
|
|
41
|
+
get_global_textmap().inject(headers)
|
|
42
|
+
except Exception: # pragma: no cover - otel optional
|
|
43
|
+
headers = None
|
|
44
|
+
|
|
35
45
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
36
|
-
response = await client.post(endpoint, json=payload)
|
|
46
|
+
response = await client.post(endpoint, json=payload, headers=headers)
|
|
37
47
|
response.raise_for_status()
|
|
38
48
|
logger.debug(f"Tool call logged for {function_name}")
|
|
39
49
|
except Exception as e:
|
|
@@ -62,15 +72,24 @@ def log_tool_call_sync(
|
|
|
62
72
|
timeout = float(os.getenv("VERIS_MOCK_TIMEOUT", "90.0"))
|
|
63
73
|
|
|
64
74
|
try:
|
|
75
|
+
headers: dict[str, str] | None = None
|
|
76
|
+
try:
|
|
77
|
+
from opentelemetry.propagate import get_global_textmap # type: ignore[import-not-found]
|
|
78
|
+
|
|
79
|
+
headers = {}
|
|
80
|
+
get_global_textmap().inject(headers)
|
|
81
|
+
except Exception: # pragma: no cover - otel optional
|
|
82
|
+
headers = None
|
|
83
|
+
|
|
65
84
|
with httpx.Client(timeout=timeout) as client:
|
|
66
|
-
response = client.post(endpoint, json=payload)
|
|
85
|
+
response = client.post(endpoint, json=payload, headers=headers)
|
|
67
86
|
response.raise_for_status()
|
|
68
87
|
logger.debug(f"Tool call logged for {function_name}")
|
|
69
88
|
except Exception as e:
|
|
70
89
|
logger.warning(f"Failed to log tool call for {function_name}: {e}")
|
|
71
90
|
|
|
72
91
|
|
|
73
|
-
async def log_tool_response_async(session_id: str, response:
|
|
92
|
+
async def log_tool_response_async(session_id: str, response: Any) -> None: # noqa: ANN401
|
|
74
93
|
"""Log tool response asynchronously to the VERIS logging endpoint."""
|
|
75
94
|
base_url = os.getenv("VERIS_ENDPOINT_URL")
|
|
76
95
|
if not base_url:
|
|
@@ -85,15 +104,24 @@ async def log_tool_response_async(session_id: str, response: object) -> None:
|
|
|
85
104
|
timeout = float(os.getenv("VERIS_MOCK_TIMEOUT", "90.0"))
|
|
86
105
|
|
|
87
106
|
try:
|
|
107
|
+
headers: dict[str, str] | None = None
|
|
108
|
+
try:
|
|
109
|
+
from opentelemetry.propagate import get_global_textmap # type: ignore[import-not-found]
|
|
110
|
+
|
|
111
|
+
headers = {}
|
|
112
|
+
get_global_textmap().inject(headers)
|
|
113
|
+
except Exception: # pragma: no cover - otel optional
|
|
114
|
+
headers = None
|
|
115
|
+
|
|
88
116
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
89
|
-
http_response = await client.post(endpoint, json=payload)
|
|
117
|
+
http_response = await client.post(endpoint, json=payload, headers=headers)
|
|
90
118
|
http_response.raise_for_status()
|
|
91
119
|
logger.debug("Tool response logged")
|
|
92
120
|
except Exception as e:
|
|
93
121
|
logger.warning(f"Failed to log tool response: {e}")
|
|
94
122
|
|
|
95
123
|
|
|
96
|
-
def log_tool_response_sync(session_id: str, response:
|
|
124
|
+
def log_tool_response_sync(session_id: str, response: Any) -> None: # noqa: ANN401
|
|
97
125
|
"""Log tool response synchronously to the VERIS logging endpoint."""
|
|
98
126
|
base_url = os.getenv("VERIS_ENDPOINT_URL")
|
|
99
127
|
if not base_url:
|
|
@@ -108,8 +136,17 @@ def log_tool_response_sync(session_id: str, response: object) -> None:
|
|
|
108
136
|
timeout = float(os.getenv("VERIS_MOCK_TIMEOUT", "90.0"))
|
|
109
137
|
|
|
110
138
|
try:
|
|
139
|
+
headers: dict[str, str] | None = None
|
|
140
|
+
try:
|
|
141
|
+
from opentelemetry.propagate import get_global_textmap # type: ignore[import-not-found]
|
|
142
|
+
|
|
143
|
+
headers = {}
|
|
144
|
+
get_global_textmap().inject(headers)
|
|
145
|
+
except Exception: # pragma: no cover - otel optional
|
|
146
|
+
headers = None
|
|
147
|
+
|
|
111
148
|
with httpx.Client(timeout=timeout) as client:
|
|
112
|
-
http_response = client.post(endpoint, json=payload)
|
|
149
|
+
http_response = client.post(endpoint, json=payload, headers=headers)
|
|
113
150
|
http_response.raise_for_status()
|
|
114
151
|
logger.debug("Tool response logged")
|
|
115
152
|
except Exception as e:
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Observability helpers for services using veris-ai.
|
|
2
|
+
|
|
3
|
+
Provides optional-safe initialization for OpenTelemetry propagation/export and
|
|
4
|
+
client/server instrumentation. Services can import and call these helpers to
|
|
5
|
+
enable consistent tracing without duplicating setup code.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
from fastapi import FastAPI
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def init_observability(service_name: str | None = None) -> None:
|
|
16
|
+
"""Initialize tracing/export and set W3C propagation.
|
|
17
|
+
|
|
18
|
+
- Initializes Traceloop if available (acts as OTel bootstrap/exporter)
|
|
19
|
+
- Sets global propagator to TraceContext + Baggage (W3C)
|
|
20
|
+
- Instruments MCP, requests, httpx if instrumentation packages are present
|
|
21
|
+
- Adds a request hook to capture outbound traceparent for debugging
|
|
22
|
+
|
|
23
|
+
This function is safe to call even if instrumentation packages are not installed.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
resolved_service_name = service_name or os.getenv("VERIS_SERVICE_NAME", "veris-service")
|
|
27
|
+
|
|
28
|
+
# Initialize Traceloop SDK first (acts as OTel bootstrap)
|
|
29
|
+
try:
|
|
30
|
+
from traceloop.sdk import Traceloop # type: ignore[import-not-found, import-untyped]
|
|
31
|
+
|
|
32
|
+
Traceloop.init(app_name=resolved_service_name, disable_batch=True)
|
|
33
|
+
except Exception as e:
|
|
34
|
+
# Tracing is optional; continue without Traceloop
|
|
35
|
+
msg = "Traceloop not found: " + str(e)
|
|
36
|
+
raise RuntimeError(msg) from e
|
|
37
|
+
|
|
38
|
+
# Ensure W3C propagation (TraceContext + optional Baggage), tolerant to OTel versions
|
|
39
|
+
try:
|
|
40
|
+
from opentelemetry.propagate import set_global_textmap
|
|
41
|
+
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
|
|
42
|
+
from collections.abc import Mapping # noqa: TC003
|
|
43
|
+
from opentelemetry.trace import Span
|
|
44
|
+
from requests import PreparedRequest
|
|
45
|
+
import httpx
|
|
46
|
+
|
|
47
|
+
# Import baggage propagator
|
|
48
|
+
baggage = None
|
|
49
|
+
try:
|
|
50
|
+
from opentelemetry.baggage.propagation import W3CBaggagePropagator
|
|
51
|
+
|
|
52
|
+
baggage = W3CBaggagePropagator()
|
|
53
|
+
except Exception as e:
|
|
54
|
+
msg = "OpenTelemetry not found: " + str(e)
|
|
55
|
+
raise RuntimeError(msg) from e
|
|
56
|
+
|
|
57
|
+
# Import composite propagator
|
|
58
|
+
try:
|
|
59
|
+
from opentelemetry.propagators.composite import CompositeHTTPPropagator
|
|
60
|
+
|
|
61
|
+
propagators: list[W3CBaggagePropagator | TraceContextTextMapPropagator] = [
|
|
62
|
+
TraceContextTextMapPropagator()
|
|
63
|
+
]
|
|
64
|
+
if baggage:
|
|
65
|
+
propagators.append(baggage)
|
|
66
|
+
set_global_textmap(CompositeHTTPPropagator(propagators))
|
|
67
|
+
except Exception as e:
|
|
68
|
+
msg = "OpenTelemetry not found: " + str(e)
|
|
69
|
+
raise RuntimeError(msg) from e
|
|
70
|
+
except Exception as e:
|
|
71
|
+
# OpenTelemetry not installed or incompatible; continue without changing global propagator
|
|
72
|
+
msg = "OpenTelemetry not found: " + str(e)
|
|
73
|
+
raise RuntimeError(msg) from e
|
|
74
|
+
|
|
75
|
+
# Instrument HTTP clients and capture outbound traceparent for debugging
|
|
76
|
+
def _log_request_headers(
|
|
77
|
+
span: Span, request: PreparedRequest | httpx.Request | dict[str, Mapping[str, str]]
|
|
78
|
+
) -> None:
|
|
79
|
+
try:
|
|
80
|
+
traceparent = None
|
|
81
|
+
if hasattr(request, "headers"):
|
|
82
|
+
traceparent = request.headers.get("traceparent")
|
|
83
|
+
elif isinstance(request, dict):
|
|
84
|
+
traceparent = request.get("headers", {}).get("traceparent")
|
|
85
|
+
if traceparent:
|
|
86
|
+
span.set_attribute("debug.traceparent", traceparent)
|
|
87
|
+
except Exception as e:
|
|
88
|
+
msg = "OpenTelemetry not found: " + str(e)
|
|
89
|
+
raise RuntimeError(msg) from e
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
from opentelemetry.instrumentation.requests import (
|
|
93
|
+
RequestsInstrumentor, # type: ignore[import-not-found]
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
RequestsInstrumentor().instrument(request_hook=_log_request_headers)
|
|
97
|
+
except Exception as e:
|
|
98
|
+
msg = "OpenTelemetry not found: " + str(e)
|
|
99
|
+
raise RuntimeError(msg) from e
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
from opentelemetry.instrumentation.httpx import (
|
|
103
|
+
HTTPXClientInstrumentor, # type: ignore[import-not-found]
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
HTTPXClientInstrumentor().instrument(request_hook=_log_request_headers)
|
|
107
|
+
except Exception as e:
|
|
108
|
+
msg = "OpenTelemetry not found: " + str(e)
|
|
109
|
+
raise RuntimeError(msg) from e
|
|
110
|
+
|
|
111
|
+
# Optionally enable MCP-specific spans
|
|
112
|
+
try:
|
|
113
|
+
from opentelemetry.instrumentation.mcp import McpInstrumentor # type: ignore[import-not-found]
|
|
114
|
+
|
|
115
|
+
McpInstrumentor().instrument()
|
|
116
|
+
except Exception as e:
|
|
117
|
+
msg = "OpenTelemetry not found: " + str(e)
|
|
118
|
+
raise RuntimeError(msg) from e
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def instrument_fastapi_app(app: FastAPI) -> None:
|
|
122
|
+
"""Instrument a FastAPI app so inbound HTTP requests continue W3C traces.
|
|
123
|
+
|
|
124
|
+
Safe to call even if the fastapi instrumentation package is not installed.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor # type: ignore[import-not-found]
|
|
129
|
+
|
|
130
|
+
FastAPIInstrumentor.instrument_app(app)
|
|
131
|
+
except Exception as e:
|
|
132
|
+
msg = "OpenTelemetry not found: " + str(e)
|
|
133
|
+
raise RuntimeError(msg) from e
|
veris_ai/tool_mock.py
CHANGED
|
@@ -172,57 +172,82 @@ class VerisSDK:
|
|
|
172
172
|
# If not in simulation mode, execute the original function
|
|
173
173
|
return await func(*args, **kwargs)
|
|
174
174
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
175
|
+
async def _execute_mock_logic(session_id: str) -> object:
|
|
176
|
+
# Handle spy mode - execute original function and log
|
|
177
|
+
if mode == "spy":
|
|
178
|
+
logger.info(f"Spying on function: {func.__name__}")
|
|
179
|
+
|
|
180
|
+
# Log the tool call
|
|
181
|
+
sig = inspect.signature(func)
|
|
182
|
+
bound_args = sig.bind(*args, **kwargs)
|
|
183
|
+
bound_args.apply_defaults()
|
|
184
|
+
_ = bound_args.arguments.pop("ctx", None)
|
|
185
|
+
_ = bound_args.arguments.pop("self", None)
|
|
186
|
+
_ = bound_args.arguments.pop("cls", None)
|
|
187
|
+
|
|
188
|
+
await log_tool_call_async(
|
|
189
|
+
session_id=session_id,
|
|
190
|
+
function_name=func.__name__,
|
|
191
|
+
parameters=bound_args.arguments,
|
|
192
|
+
docstring=inspect.getdoc(func) or "",
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Execute the original function
|
|
196
|
+
result = await func(*args, **kwargs)
|
|
197
|
+
|
|
198
|
+
# Log the response
|
|
199
|
+
await log_tool_response_async(session_id=session_id, response=result)
|
|
200
|
+
|
|
201
|
+
return result
|
|
202
|
+
|
|
203
|
+
# Regular mock mode
|
|
204
|
+
base_url = os.getenv("VERIS_ENDPOINT_URL")
|
|
205
|
+
if not base_url:
|
|
206
|
+
error_msg = "VERIS_ENDPOINT_URL environment variable is not set"
|
|
207
|
+
raise ValueError(error_msg)
|
|
208
|
+
endpoint = f"{base_url.rstrip('/')}/api/v2/tool_mock"
|
|
209
|
+
# Default timeout of 30 seconds
|
|
210
|
+
timeout = float(os.getenv("VERIS_MOCK_TIMEOUT", "90.0"))
|
|
211
|
+
|
|
212
|
+
logger.info(f"Simulating function: {func.__name__}")
|
|
213
|
+
payload, return_type_obj = create_mock_payload(*args, **kwargs)
|
|
214
|
+
|
|
215
|
+
# Send request to endpoint with timeout
|
|
216
|
+
# Inject current trace context headers if OpenTelemetry is available
|
|
217
|
+
headers: dict[str, str] | None = None
|
|
218
|
+
try:
|
|
219
|
+
from opentelemetry.propagate import get_global_textmap # type: ignore[import-not-found]
|
|
220
|
+
|
|
221
|
+
headers = {}
|
|
222
|
+
get_global_textmap().inject(headers)
|
|
223
|
+
except Exception: # pragma: no cover - otel optional
|
|
224
|
+
headers = None
|
|
225
|
+
|
|
226
|
+
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
227
|
+
response = await client.post(endpoint, json=payload, headers=headers)
|
|
228
|
+
response.raise_for_status()
|
|
229
|
+
mock_result = response.json()
|
|
230
|
+
logger.info(f"Mock response: {mock_result}")
|
|
231
|
+
|
|
232
|
+
if isinstance(mock_result, str):
|
|
233
|
+
with suppress(json.JSONDecodeError):
|
|
234
|
+
mock_result = json.loads(mock_result)
|
|
235
|
+
return convert_to_type(mock_result, return_type_obj)
|
|
236
|
+
return convert_to_type(mock_result, return_type_obj)
|
|
237
|
+
|
|
238
|
+
# Create a top-level span for the simulated mock call if OpenTelemetry is available
|
|
239
|
+
try:
|
|
240
|
+
from opentelemetry import trace # type: ignore[import-not-found]
|
|
241
|
+
|
|
242
|
+
tracer = trace.get_tracer("veris_ai.tool_mock")
|
|
243
|
+
span_name = f"mock.{func.__name__}"
|
|
244
|
+
with tracer.start_as_current_span(span_name) as span: # type: ignore[attr-defined]
|
|
245
|
+
span.set_attribute("veris_ai.session.id", self.session_id or "") # type: ignore[attr-defined]
|
|
246
|
+
span.set_attribute("veris_ai.mock.mode", mode) # type: ignore[attr-defined]
|
|
247
|
+
return await _execute_mock_logic(self.session_id)
|
|
248
|
+
except Exception:
|
|
249
|
+
# If OpenTelemetry is not available, run without span
|
|
250
|
+
return await _execute_mock_logic(self.session_id)
|
|
226
251
|
|
|
227
252
|
@wraps(func)
|
|
228
253
|
def sync_wrapper(
|
|
@@ -234,57 +259,82 @@ class VerisSDK:
|
|
|
234
259
|
# If not in simulation mode, execute the original function
|
|
235
260
|
return func(*args, **kwargs)
|
|
236
261
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
262
|
+
def _execute_mock_logic(session_id: str) -> object:
|
|
263
|
+
# Handle spy mode - execute original function and log
|
|
264
|
+
if mode == "spy":
|
|
265
|
+
logger.info(f"Spying on function: {func.__name__}")
|
|
266
|
+
|
|
267
|
+
# Log the tool call
|
|
268
|
+
sig = inspect.signature(func)
|
|
269
|
+
bound_args = sig.bind(*args, **kwargs)
|
|
270
|
+
bound_args.apply_defaults()
|
|
271
|
+
_ = bound_args.arguments.pop("ctx", None)
|
|
272
|
+
_ = bound_args.arguments.pop("self", None)
|
|
273
|
+
_ = bound_args.arguments.pop("cls", None)
|
|
274
|
+
|
|
275
|
+
log_tool_call_sync(
|
|
276
|
+
session_id=session_id,
|
|
277
|
+
function_name=func.__name__,
|
|
278
|
+
parameters=bound_args.arguments,
|
|
279
|
+
docstring=inspect.getdoc(func) or "",
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Execute the original function
|
|
283
|
+
result = func(*args, **kwargs)
|
|
284
|
+
|
|
285
|
+
# Log the response
|
|
286
|
+
log_tool_response_sync(session_id=session_id, response=result)
|
|
287
|
+
|
|
288
|
+
return result
|
|
289
|
+
|
|
290
|
+
# Regular mock mode
|
|
291
|
+
base_url = os.getenv("VERIS_ENDPOINT_URL")
|
|
292
|
+
if not base_url:
|
|
293
|
+
error_msg = "VERIS_ENDPOINT_URL environment variable is not set"
|
|
294
|
+
raise ValueError(error_msg)
|
|
295
|
+
endpoint = f"{base_url.rstrip('/')}/api/v2/tool_mock"
|
|
296
|
+
# Default timeout of 30 seconds
|
|
297
|
+
timeout = float(os.getenv("VERIS_MOCK_TIMEOUT", "90.0"))
|
|
298
|
+
|
|
299
|
+
logger.info(f"Simulating function: {func.__name__}")
|
|
300
|
+
payload, return_type_obj = create_mock_payload(*args, **kwargs)
|
|
301
|
+
|
|
302
|
+
# Send request to endpoint with timeout (synchronous)
|
|
303
|
+
# Inject current trace context headers if OpenTelemetry is available
|
|
304
|
+
headers: dict[str, str] | None = None
|
|
305
|
+
try:
|
|
306
|
+
from opentelemetry.propagate import get_global_textmap # type: ignore[import-not-found]
|
|
307
|
+
|
|
308
|
+
headers = {}
|
|
309
|
+
get_global_textmap().inject(headers)
|
|
310
|
+
except Exception: # pragma: no cover - otel optional
|
|
311
|
+
headers = None
|
|
312
|
+
|
|
313
|
+
with httpx.Client(timeout=timeout) as client:
|
|
314
|
+
response = client.post(endpoint, json=payload, headers=headers)
|
|
315
|
+
response.raise_for_status()
|
|
316
|
+
mock_result = response.json()
|
|
317
|
+
logger.info(f"Mock response: {mock_result}")
|
|
318
|
+
|
|
319
|
+
if isinstance(mock_result, str):
|
|
320
|
+
with suppress(json.JSONDecodeError):
|
|
321
|
+
mock_result = json.loads(mock_result)
|
|
322
|
+
return convert_to_type(mock_result, return_type_obj)
|
|
323
|
+
return convert_to_type(mock_result, return_type_obj)
|
|
324
|
+
|
|
325
|
+
# Create a top-level span for the simulated mock call if OpenTelemetry is available
|
|
326
|
+
try:
|
|
327
|
+
from opentelemetry import trace # type: ignore[import-not-found]
|
|
328
|
+
|
|
329
|
+
tracer = trace.get_tracer("veris_ai.tool_mock")
|
|
330
|
+
span_name = f"mock.{func.__name__}"
|
|
331
|
+
with tracer.start_as_current_span(span_name) as span: # type: ignore[attr-defined]
|
|
332
|
+
span.set_attribute("veris_ai.session.id", self.session_id or "") # type: ignore[attr-defined]
|
|
333
|
+
span.set_attribute("veris_ai.mock.mode", mode) # type: ignore[attr-defined]
|
|
334
|
+
return _execute_mock_logic(self.session_id)
|
|
335
|
+
except Exception:
|
|
336
|
+
# If OpenTelemetry is not available, run without span
|
|
337
|
+
return _execute_mock_logic(self.session_id)
|
|
288
338
|
|
|
289
339
|
# Return the appropriate wrapper based on whether the function is async
|
|
290
340
|
return async_wrapper if is_async else sync_wrapper
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: veris-ai
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.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
|
|
@@ -9,8 +9,17 @@ License-Expression: MIT
|
|
|
9
9
|
License-File: LICENSE
|
|
10
10
|
Requires-Python: >=3.11
|
|
11
11
|
Requires-Dist: httpx>=0.24.0
|
|
12
|
+
Requires-Dist: opentelemetry-api>=1.34.1
|
|
13
|
+
Requires-Dist: opentelemetry-exporter-otlp>=1.34.1
|
|
14
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.55b1
|
|
15
|
+
Requires-Dist: opentelemetry-instrumentation-httpx>=0.55b1
|
|
16
|
+
Requires-Dist: opentelemetry-instrumentation-mcp>=0.44.1
|
|
17
|
+
Requires-Dist: opentelemetry-instrumentation-requests>=0.55b1
|
|
18
|
+
Requires-Dist: opentelemetry-instrumentation>=0.55b1
|
|
19
|
+
Requires-Dist: opentelemetry-sdk>=1.34.1
|
|
12
20
|
Requires-Dist: pydantic>=2.0.0
|
|
13
21
|
Requires-Dist: requests>=2.31.0
|
|
22
|
+
Requires-Dist: traceloop-sdk>=0.45.4
|
|
14
23
|
Provides-Extra: dev
|
|
15
24
|
Requires-Dist: black>=23.7.0; extra == 'dev'
|
|
16
25
|
Requires-Dist: mypy>=1.5.1; extra == 'dev'
|
|
@@ -24,12 +33,7 @@ Provides-Extra: fastapi
|
|
|
24
33
|
Requires-Dist: fastapi; extra == 'fastapi'
|
|
25
34
|
Requires-Dist: fastapi-mcp>=0.4.0; extra == 'fastapi'
|
|
26
35
|
Provides-Extra: instrument
|
|
27
|
-
Requires-Dist: braintrust; extra == 'instrument'
|
|
28
36
|
Requires-Dist: opentelemetry-api; extra == 'instrument'
|
|
29
|
-
Requires-Dist: opentelemetry-exporter-otlp; extra == 'instrument'
|
|
30
|
-
Requires-Dist: opentelemetry-exporter-otlp-proto-common; extra == 'instrument'
|
|
31
|
-
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc; extra == 'instrument'
|
|
32
|
-
Requires-Dist: opentelemetry-exporter-otlp-proto-http; extra == 'instrument'
|
|
33
37
|
Requires-Dist: opentelemetry-sdk; extra == 'instrument'
|
|
34
38
|
Requires-Dist: wrapt; extra == 'instrument'
|
|
35
39
|
Description-Content-Type: text/markdown
|
|
@@ -41,7 +45,7 @@ A Python package for Veris AI tools with simulation capabilities and FastAPI MCP
|
|
|
41
45
|
## Quick Reference
|
|
42
46
|
|
|
43
47
|
**Purpose**: Tool mocking, tracing, and FastAPI MCP integration for AI agent development
|
|
44
|
-
**Core Components**: [`tool_mock`](#function-mocking) • [`
|
|
48
|
+
**Core Components**: [`tool_mock`](#function-mocking) • [`observability`](#sdk-observability-helpers) • [`fastapi_mcp`](#fastapi-mcp-integration) • [`jaeger_interface`](#jaeger-trace-interface)
|
|
45
49
|
**Deep Dive**: [`Module Architecture`](src/veris_ai/README.md) • [`Testing Guide`](tests/README.md) • [`Usage Examples`](examples/README.md)
|
|
46
50
|
**Source of Truth**: Implementation details in [`src/veris_ai/`](src/veris_ai/) source code
|
|
47
51
|
|
|
@@ -58,7 +62,7 @@ uv add "veris-ai[dev,fastapi,instrument]"
|
|
|
58
62
|
**Installation Profiles**:
|
|
59
63
|
- `dev`: Development tools (ruff, pytest, mypy)
|
|
60
64
|
- `fastapi`: FastAPI MCP integration
|
|
61
|
-
- `
|
|
65
|
+
- `observability`: OpenTelemetry tracing
|
|
62
66
|
|
|
63
67
|
## Import Patterns
|
|
64
68
|
|
|
@@ -69,7 +73,7 @@ uv add "veris-ai[dev,fastapi,instrument]"
|
|
|
69
73
|
from veris_ai import veris, JaegerClient
|
|
70
74
|
|
|
71
75
|
# Optional features (require extras)
|
|
72
|
-
from veris_ai import
|
|
76
|
+
from veris_ai import init_observability, instrument_fastapi_app # Provided by SDK observability helpers
|
|
73
77
|
```
|
|
74
78
|
|
|
75
79
|
**Complete Import Strategies**: See [`examples/README.md`](examples/README.md) for five different import approaches, conditional features, and integration patterns.
|
|
@@ -88,22 +92,33 @@ from veris_ai import braintrust_tracing # Requires [instrument]
|
|
|
88
92
|
|
|
89
93
|
**Configuration Details**: See [`src/veris_ai/tool_mock.py`](src/veris_ai/tool_mock.py) for environment handling logic.
|
|
90
94
|
|
|
91
|
-
## Tracing Integration
|
|
92
95
|
|
|
93
|
-
|
|
96
|
+
### SDK Observability Helpers
|
|
94
97
|
|
|
95
|
-
|
|
98
|
+
The SDK provides optional-safe observability helpers that standardize OpenTelemetry setup and W3C context propagation across services.
|
|
96
99
|
|
|
97
100
|
```python
|
|
98
|
-
from
|
|
101
|
+
from fastapi import FastAPI
|
|
102
|
+
from veris_ai import init_observability, instrument_fastapi_app
|
|
103
|
+
|
|
104
|
+
# Initialize tracing/export early (no-op if dependencies are absent)
|
|
105
|
+
init_observability(service_name="my-customer-service")
|
|
106
|
+
|
|
107
|
+
app = FastAPI()
|
|
99
108
|
|
|
100
|
-
#
|
|
101
|
-
|
|
109
|
+
# Ensure inbound HTTP requests continue W3C traces
|
|
110
|
+
instrument_fastapi_app(app)
|
|
102
111
|
```
|
|
103
112
|
|
|
104
|
-
|
|
113
|
+
What this enables:
|
|
114
|
+
- Sets global W3C propagator (TraceContext + Baggage)
|
|
115
|
+
- Optionally instruments FastAPI, requests, httpx, MCP client if installed
|
|
116
|
+
- Includes request hooks to attach outbound `traceparent` on HTTP calls for continuity
|
|
105
117
|
|
|
106
|
-
|
|
118
|
+
End-to-end propagation with the simulator:
|
|
119
|
+
- The simulator injects W3C headers when connecting to your FastAPI MCP endpoints
|
|
120
|
+
- The SDK injects W3C headers on `/api/v2/tool_mock` and logging requests back to the simulator
|
|
121
|
+
- Result: customer agent spans and tool mocks appear under the same distributed trace
|
|
107
122
|
|
|
108
123
|
## Function Mocking
|
|
109
124
|
|
|
@@ -207,7 +222,7 @@ pytest --cov=veris_ai # Test with coverage
|
|
|
207
222
|
|
|
208
223
|
**Semantic Tag**: `module-architecture`
|
|
209
224
|
|
|
210
|
-
**Core Modules**: `tool_mock` (mocking), `jaeger_interface` (trace queries), `
|
|
225
|
+
**Core Modules**: `tool_mock` (mocking), `jaeger_interface` (trace queries), `utils` (schema conversion)
|
|
211
226
|
|
|
212
227
|
**Complete Architecture**: See [`src/veris_ai/README.md`](src/veris_ai/README.md) for module overview, implementation flows, and configuration details.
|
|
213
228
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
veris_ai/README.md,sha256=iMSwSIrBO2zEdUhImkhZucUnTNO0kSQ-_zxWjZCmp4I,2730
|
|
2
|
+
veris_ai/__init__.py,sha256=Gh52-XfFpsxX37uqp8vuX0V3Np7f-Rlf5k3MMANu6e0,425
|
|
3
|
+
veris_ai/logging.py,sha256=srEdVimcQSCsnwGhyzCydehD2JW1cQmwSRd3X20NQs0,5233
|
|
4
|
+
veris_ai/models.py,sha256=6HINPxNFCakCVPcyEbUswWkXwb2K4lF0A8g8EvTMal4,213
|
|
5
|
+
veris_ai/observability.py,sha256=fG5ixAiVDykLCdHUScKQNCsePLphKQ7PcaKkAGBVdF0,5113
|
|
6
|
+
veris_ai/tool_mock.py,sha256=C1BANT3LCMKOIwJm_nOFuNdHnaO9_tbjeDTtBcI41RI,16512
|
|
7
|
+
veris_ai/utils.py,sha256=aqFFNuNiBehil6874nOHtU7G_bWHbFpVuubcz2AIx6I,9267
|
|
8
|
+
veris_ai/jaeger_interface/README.md,sha256=kd9rKcE5xf3EyNaiHu0tjn-0oES9sfaK6Ih-OhhTyCM,2821
|
|
9
|
+
veris_ai/jaeger_interface/__init__.py,sha256=KD7NSiMYRG_2uF6dOLKkGG5lNQe4K9ptEwucwMT4_aw,1128
|
|
10
|
+
veris_ai/jaeger_interface/client.py,sha256=yJrh86wRR0Dk3Gq12DId99WogcMIVbL0QQFqVSevvlE,8772
|
|
11
|
+
veris_ai/jaeger_interface/models.py,sha256=e64VV6IvOEFuzRUgvDAMQFyOZMRb56I-PUPZLBZ3rX0,1864
|
|
12
|
+
veris_ai-1.7.0.dist-info/METADATA,sha256=dmWFBpwU2MK9tjnJtlrkQdqnR9C0yn0Lhb1TMWb-XvA,8679
|
|
13
|
+
veris_ai-1.7.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
14
|
+
veris_ai-1.7.0.dist-info/licenses/LICENSE,sha256=2g4i20atAgtD5einaKzhQrIB-JrPhyQgD3bC0wkHcCI,1065
|
|
15
|
+
veris_ai-1.7.0.dist-info/RECORD,,
|
veris_ai/braintrust_tracing.py
DELETED
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
"""Non-invasive Braintrust + Jaeger (OTEL) instrumentation helper for the `openai-agents` SDK.
|
|
2
|
-
|
|
3
|
-
Typical usage
|
|
4
|
-
-------------
|
|
5
|
-
>>> from our_sdk import braintrust_tracing
|
|
6
|
-
>>> braintrust_tracing.instrument(service_name="openai-agent")
|
|
7
|
-
|
|
8
|
-
After calling :func:`instrument`, any later call to
|
|
9
|
-
``agents.set_trace_processors([...])`` will be transparently patched so that:
|
|
10
|
-
• the list always contains a BraintrustTracingProcessor (for Braintrust UI)
|
|
11
|
-
• *and* an OpenTelemetry bridge processor that mirrors every span to the
|
|
12
|
-
global OTEL tracer provider (Jaeger by default).
|
|
13
|
-
|
|
14
|
-
Goal: deliver full OTEL compatibility while keeping the official Braintrust
|
|
15
|
-
SDK integration unchanged – **no code modifications required** besides the
|
|
16
|
-
single `instrument()` call.
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
from __future__ import annotations
|
|
20
|
-
|
|
21
|
-
import json
|
|
22
|
-
import logging
|
|
23
|
-
import os
|
|
24
|
-
from typing import Any, cast
|
|
25
|
-
|
|
26
|
-
import wrapt # type: ignore[import-untyped, import-not-found]
|
|
27
|
-
from braintrust.wrappers.openai import (
|
|
28
|
-
BraintrustTracingProcessor, # type: ignore[import-untyped, import-not-found]
|
|
29
|
-
)
|
|
30
|
-
from opentelemetry import context as otel_context # type: ignore[import-untyped, import-not-found]
|
|
31
|
-
from opentelemetry import trace # type: ignore[import-untyped, import-not-found]
|
|
32
|
-
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
|
|
33
|
-
OTLPSpanExporter, # type: ignore[import-untyped, import-not-found]
|
|
34
|
-
)
|
|
35
|
-
from opentelemetry.sdk.resources import ( # type: ignore[import-untyped, import-not-found]
|
|
36
|
-
SERVICE_NAME,
|
|
37
|
-
Resource,
|
|
38
|
-
)
|
|
39
|
-
from opentelemetry.sdk.trace import TracerProvider # type: ignore[import-untyped, import-not-found]
|
|
40
|
-
from opentelemetry.sdk.trace.export import (
|
|
41
|
-
BatchSpanProcessor, # type: ignore[import-untyped, import-not-found]
|
|
42
|
-
)
|
|
43
|
-
from opentelemetry.trace import SpanKind # type: ignore[import-untyped, import-not-found]
|
|
44
|
-
|
|
45
|
-
from veris_ai.tool_mock import _session_id_context
|
|
46
|
-
|
|
47
|
-
# ---------------------------------------------------------------------------
|
|
48
|
-
# Optional import of *agents* – we fail lazily at runtime if missing.
|
|
49
|
-
# ---------------------------------------------------------------------------
|
|
50
|
-
try:
|
|
51
|
-
import agents # type: ignore[import-untyped] # noqa: TC002
|
|
52
|
-
from agents import TracingProcessor # type: ignore[import-untyped]
|
|
53
|
-
|
|
54
|
-
try:
|
|
55
|
-
from agents.tracing import get_trace_provider # type: ignore[import-untyped]
|
|
56
|
-
except ImportError:
|
|
57
|
-
# Fallback for newer versions that have GLOBAL_TRACE_PROVIDER instead
|
|
58
|
-
from agents.tracing import ( # type: ignore[import-untyped, attr-defined, import-not-found]
|
|
59
|
-
GLOBAL_TRACE_PROVIDER, # type: ignore[import-untyped, attr-defined, import-not-found]
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
get_trace_provider = lambda: GLOBAL_TRACE_PROVIDER # type: ignore[no-any-return] # noqa: E731
|
|
63
|
-
except ModuleNotFoundError as exc: # pragma: no cover
|
|
64
|
-
_IMPORT_ERR: ModuleNotFoundError | None = exc
|
|
65
|
-
TracingProcessor = object # type: ignore[assignment, misc]
|
|
66
|
-
get_trace_provider = None # type: ignore[assignment]
|
|
67
|
-
else:
|
|
68
|
-
_IMPORT_ERR = None
|
|
69
|
-
|
|
70
|
-
__all__ = ["instrument"]
|
|
71
|
-
|
|
72
|
-
logger = logging.getLogger(__name__)
|
|
73
|
-
|
|
74
|
-
# ---------------------------------------------------------------------------
|
|
75
|
-
# Internal helper – OTEL bridge processor
|
|
76
|
-
# ---------------------------------------------------------------------------
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
class AgentsOTELBridgeProcessor(TracingProcessor): # type: ignore[misc]
|
|
80
|
-
"""Mirrors every Agents span into a dedicated OTEL tracer provider."""
|
|
81
|
-
|
|
82
|
-
def __init__(
|
|
83
|
-
self,
|
|
84
|
-
braintrust_processor: BraintrustTracingProcessor,
|
|
85
|
-
*,
|
|
86
|
-
service_name: str, # noqa: ARG002
|
|
87
|
-
tracer_provider: trace.TracerProvider,
|
|
88
|
-
) -> None: # noqa: D401,E501
|
|
89
|
-
self._braintrust = braintrust_processor
|
|
90
|
-
self._tracer = tracer_provider.get_tracer(__name__)
|
|
91
|
-
self._otel_spans: dict[str, trace.Span] = {}
|
|
92
|
-
self._provider = tracer_provider
|
|
93
|
-
|
|
94
|
-
# ----------------------------- utils ---------------------------------
|
|
95
|
-
@staticmethod
|
|
96
|
-
def _flatten(prefix: str, obj: Any, out: dict[str, Any]) -> None: # noqa: PLR0911, ANN401
|
|
97
|
-
"""Flatten complex objects into OTEL-compatible primitives."""
|
|
98
|
-
if isinstance(obj, dict):
|
|
99
|
-
for k, v in obj.items():
|
|
100
|
-
AgentsOTELBridgeProcessor._flatten(f"{prefix}.{k}" if prefix else str(k), v, out)
|
|
101
|
-
elif isinstance(obj, str | int | float | bool) or obj is None:
|
|
102
|
-
out[prefix] = obj
|
|
103
|
-
elif isinstance(obj, list | tuple):
|
|
104
|
-
try:
|
|
105
|
-
if all(isinstance(i, str | int | float | bool) or i is None for i in obj):
|
|
106
|
-
out[prefix] = list(obj)
|
|
107
|
-
else:
|
|
108
|
-
out[prefix] = json.dumps(obj, default=str)
|
|
109
|
-
except Exception: # pragma: no cover – defensive
|
|
110
|
-
out[prefix] = json.dumps(obj, default=str)
|
|
111
|
-
else:
|
|
112
|
-
out[prefix] = str(obj)
|
|
113
|
-
|
|
114
|
-
def _log_data_attributes(self, span_obj: agents.tracing.Span) -> dict[str, Any]: # type: ignore[name-defined]
|
|
115
|
-
data = self._braintrust._log_data(span_obj) # pyright: ignore[reportPrivateUsage] # noqa: SLF001
|
|
116
|
-
flat: dict[str, Any] = {}
|
|
117
|
-
self._flatten("bt", data, flat)
|
|
118
|
-
|
|
119
|
-
# Add session_id if available
|
|
120
|
-
session_id = _session_id_context.get()
|
|
121
|
-
if session_id:
|
|
122
|
-
flat["veris.session_id"] = session_id
|
|
123
|
-
|
|
124
|
-
return {k: v for k, v in flat.items() if v is not None}
|
|
125
|
-
|
|
126
|
-
# --------------------- Agents lifecycle hooks ------------------------
|
|
127
|
-
def on_trace_start(self, trace_obj: Any) -> None: # noqa: ANN401, D102
|
|
128
|
-
# Get session_id at trace start
|
|
129
|
-
session_id = _session_id_context.get()
|
|
130
|
-
attributes = {"veris.session_id": session_id} if session_id else {}
|
|
131
|
-
|
|
132
|
-
otel_span = self._tracer.start_span(
|
|
133
|
-
name=trace_obj.name or "agent-trace",
|
|
134
|
-
kind=SpanKind.INTERNAL,
|
|
135
|
-
attributes=attributes,
|
|
136
|
-
)
|
|
137
|
-
self._otel_spans[trace_obj.trace_id] = otel_span
|
|
138
|
-
logger.info(f"VERIS AI BraintrustTracingProcessor: on_trace_start: {trace_obj.trace_id}")
|
|
139
|
-
|
|
140
|
-
def on_trace_end(self, trace_obj: Any) -> None: # noqa: ANN401, D102
|
|
141
|
-
span = self._otel_spans.pop(trace_obj.trace_id, None)
|
|
142
|
-
if span:
|
|
143
|
-
span.end()
|
|
144
|
-
|
|
145
|
-
def on_span_start(self, span: Any) -> None: # noqa: ANN401, D102
|
|
146
|
-
parent_otel = (
|
|
147
|
-
self._otel_spans.get(span.parent_id)
|
|
148
|
-
if span.parent_id
|
|
149
|
-
else self._otel_spans.get(span.trace_id)
|
|
150
|
-
)
|
|
151
|
-
parent_ctx = (
|
|
152
|
-
trace.set_span_in_context(parent_otel) if parent_otel else otel_context.get_current()
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
# Get session_id at span start
|
|
156
|
-
session_id = _session_id_context.get()
|
|
157
|
-
attributes = {"veris.session_id": session_id} if session_id else {}
|
|
158
|
-
|
|
159
|
-
child = self._tracer.start_span(
|
|
160
|
-
name=span.span_data.__class__.__name__,
|
|
161
|
-
context=parent_ctx,
|
|
162
|
-
kind=SpanKind.INTERNAL,
|
|
163
|
-
attributes=attributes,
|
|
164
|
-
)
|
|
165
|
-
self._otel_spans[span.span_id] = child
|
|
166
|
-
logger.info(f"VERIS AI BraintrustTracingProcessor: on_span_start: {span.span_id}")
|
|
167
|
-
|
|
168
|
-
def on_span_end(self, span: Any) -> None: # noqa: ANN401, D102
|
|
169
|
-
child = self._otel_spans.pop(span.span_id, None)
|
|
170
|
-
logger.info(f"VERIS AI BraintrustTracingProcessor: on_span_end: {span.span_id}")
|
|
171
|
-
if child:
|
|
172
|
-
for k, v in self._log_data_attributes(span).items():
|
|
173
|
-
try:
|
|
174
|
-
child.set_attribute(k, v)
|
|
175
|
-
except Exception: # pragma: no cover – bad value type # noqa: S112
|
|
176
|
-
continue
|
|
177
|
-
child.end()
|
|
178
|
-
|
|
179
|
-
# --------------------- house-keeping ---------------------------------
|
|
180
|
-
def shutdown(self) -> None: # noqa: D401
|
|
181
|
-
provider = cast("TracerProvider", self._provider)
|
|
182
|
-
provider.shutdown() # type: ignore[attr-defined]
|
|
183
|
-
|
|
184
|
-
def force_flush(self) -> None: # noqa: D401
|
|
185
|
-
provider = cast("TracerProvider", self._provider)
|
|
186
|
-
provider.force_flush() # type: ignore[attr-defined]
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
# ---------------------------------------------------------------------------
|
|
190
|
-
# Public entry point
|
|
191
|
-
# ---------------------------------------------------------------------------
|
|
192
|
-
|
|
193
|
-
_PATCHED: bool = False # ensure idempotent patching
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
def instrument(
|
|
197
|
-
*,
|
|
198
|
-
service_name: str | None = None,
|
|
199
|
-
otlp_endpoint: str | None = None,
|
|
200
|
-
) -> None:
|
|
201
|
-
"""Bootstrap Braintrust + OTEL instrumentation and patch Agents SDK.
|
|
202
|
-
|
|
203
|
-
Invoke once at any point before `set_trace_processors` is called.
|
|
204
|
-
"""
|
|
205
|
-
global _PATCHED # noqa: PLW0603
|
|
206
|
-
if _PATCHED:
|
|
207
|
-
return # already done
|
|
208
|
-
|
|
209
|
-
if _IMPORT_ERR is not None or get_trace_provider is None: # pragma: no cover
|
|
210
|
-
error_msg = "The `agents` package is required but not installed"
|
|
211
|
-
raise RuntimeError(error_msg) from _IMPORT_ERR
|
|
212
|
-
|
|
213
|
-
# ------------------ 0. Validate inputs -----------------------------
|
|
214
|
-
# Resolve service name ─ explicit argument → env var → error
|
|
215
|
-
if not service_name or not str(service_name).strip():
|
|
216
|
-
service_name = os.getenv("VERIS_SERVICE_NAME")
|
|
217
|
-
|
|
218
|
-
if not service_name or not str(service_name).strip():
|
|
219
|
-
error_msg = (
|
|
220
|
-
"`service_name` must be provided either as an argument or via the "
|
|
221
|
-
"VERIS_SERVICE_NAME environment variable"
|
|
222
|
-
)
|
|
223
|
-
raise ValueError(error_msg)
|
|
224
|
-
|
|
225
|
-
# Resolve OTLP endpoint ─ explicit argument → env var → error
|
|
226
|
-
if not otlp_endpoint or not str(otlp_endpoint).strip():
|
|
227
|
-
otlp_endpoint = os.getenv("VERIS_OTLP_ENDPOINT")
|
|
228
|
-
|
|
229
|
-
if not otlp_endpoint or not str(otlp_endpoint).strip():
|
|
230
|
-
error_msg = (
|
|
231
|
-
"`otlp_endpoint` must be provided either as an argument or via the "
|
|
232
|
-
"VERIS_OTLP_ENDPOINT environment variable"
|
|
233
|
-
)
|
|
234
|
-
raise ValueError(error_msg)
|
|
235
|
-
|
|
236
|
-
logger.info(f"service_name: {service_name}")
|
|
237
|
-
logger.info(f"otlp_endpoint: {otlp_endpoint}")
|
|
238
|
-
|
|
239
|
-
# ------------------ 1. Configure OTEL provider ---------------------
|
|
240
|
-
# We create our own provider instance and do NOT set it globally.
|
|
241
|
-
# This avoids conflicts with any other OTEL setup in the application.
|
|
242
|
-
otel_provider = TracerProvider(resource=Resource.create({SERVICE_NAME: service_name}))
|
|
243
|
-
otel_provider.add_span_processor(
|
|
244
|
-
BatchSpanProcessor(OTLPSpanExporter(endpoint=otlp_endpoint, insecure=True)),
|
|
245
|
-
)
|
|
246
|
-
|
|
247
|
-
# ------------------ 2. Define wrapper for patching -------------------
|
|
248
|
-
def _wrapper(wrapped: Any, instance: Any, args: Any, kwargs: Any) -> Any: # noqa: ANN401, ARG001
|
|
249
|
-
"""This function wraps `TraceProvider.set_processors`."""
|
|
250
|
-
processors = args[0] if args else []
|
|
251
|
-
|
|
252
|
-
# Find the user's Braintrust processor to pass to our bridge.
|
|
253
|
-
bt_processor = next(
|
|
254
|
-
(p for p in processors if isinstance(p, BraintrustTracingProcessor)), None
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
# If no Braintrust processor is present, our bridge is useless.
|
|
258
|
-
# Also, if a bridge is already there, don't add another one.
|
|
259
|
-
has_bridge = any(isinstance(p, AgentsOTELBridgeProcessor) for p in processors)
|
|
260
|
-
if not bt_processor or has_bridge:
|
|
261
|
-
return wrapped(*args, **kwargs)
|
|
262
|
-
|
|
263
|
-
# Create the bridge and add it to the list of processors.
|
|
264
|
-
bridge = AgentsOTELBridgeProcessor(
|
|
265
|
-
bt_processor,
|
|
266
|
-
service_name=service_name,
|
|
267
|
-
tracer_provider=otel_provider,
|
|
268
|
-
)
|
|
269
|
-
new_processors = list(processors) + [bridge]
|
|
270
|
-
|
|
271
|
-
# Call the original function with the augmented list.
|
|
272
|
-
new_args = (new_processors,) + args[1:]
|
|
273
|
-
logger.info(f"VERIS AI BraintrustTracingProcessor: {new_args}")
|
|
274
|
-
return wrapped(*new_args, **kwargs)
|
|
275
|
-
|
|
276
|
-
# ------------------ 3. Patch the provider instance -------------------
|
|
277
|
-
# This is more robust than patching the function, as it's independent
|
|
278
|
-
# of how the user imports `set_trace_processors`.
|
|
279
|
-
provider_instance = get_trace_provider()
|
|
280
|
-
wrapt.wrap_function_wrapper(provider_instance, "set_processors", _wrapper)
|
|
281
|
-
|
|
282
|
-
_PATCHED = True
|
veris_ai-1.5.0.dist-info/RECORD
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
veris_ai/README.md,sha256=q0Qn6gu85HWU189SmyOygY_mkNj0l10wlAVy9kj4udo,3120
|
|
2
|
-
veris_ai/__init__.py,sha256=Vp5yf9ZRchw0mmPwrRuNebI5cb2NGwpJGfr41l86q1U,1177
|
|
3
|
-
veris_ai/braintrust_tracing.py,sha256=0i-HR6IuK3-Q5ujMjT1FojxESRZLgqEvJ44bfJfDHaw,11938
|
|
4
|
-
veris_ai/logging.py,sha256=2855E8s_k6yTnzFK10EQR4WQngQk3VuWFPJW-MYiw3k,3837
|
|
5
|
-
veris_ai/models.py,sha256=6HINPxNFCakCVPcyEbUswWkXwb2K4lF0A8g8EvTMal4,213
|
|
6
|
-
veris_ai/tool_mock.py,sha256=p10k6dCZAIqG8YoefR3QyvtkInnWNPMRO_07LI5Wks0,13218
|
|
7
|
-
veris_ai/utils.py,sha256=aqFFNuNiBehil6874nOHtU7G_bWHbFpVuubcz2AIx6I,9267
|
|
8
|
-
veris_ai/jaeger_interface/README.md,sha256=kd9rKcE5xf3EyNaiHu0tjn-0oES9sfaK6Ih-OhhTyCM,2821
|
|
9
|
-
veris_ai/jaeger_interface/__init__.py,sha256=d873a0zq3eUYU2Y77MtdjCwIARjAsAP7WDqGXDMWpYs,1158
|
|
10
|
-
veris_ai/jaeger_interface/client.py,sha256=yJrh86wRR0Dk3Gq12DId99WogcMIVbL0QQFqVSevvlE,8772
|
|
11
|
-
veris_ai/jaeger_interface/models.py,sha256=e64VV6IvOEFuzRUgvDAMQFyOZMRb56I-PUPZLBZ3rX0,1864
|
|
12
|
-
veris_ai-1.5.0.dist-info/METADATA,sha256=l-x_bBgu3DWDVgG4IFeek26y1U1ANP6F4Cz-yrm9beQ,8151
|
|
13
|
-
veris_ai-1.5.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
14
|
-
veris_ai-1.5.0.dist-info/licenses/LICENSE,sha256=2g4i20atAgtD5einaKzhQrIB-JrPhyQgD3bC0wkHcCI,1065
|
|
15
|
-
veris_ai-1.5.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|