variably-sdk 2.12.0__tar.gz → 2.13.0__tar.gz
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.
- {variably_sdk-2.12.0 → variably_sdk-2.13.0}/PKG-INFO +7 -2
- {variably_sdk-2.12.0 → variably_sdk-2.13.0}/README.md +1 -1
- {variably_sdk-2.12.0 → variably_sdk-2.13.0}/pyproject.toml +10 -1
- {variably_sdk-2.12.0 → variably_sdk-2.13.0}/src/variably/__init__.py +2 -0
- {variably_sdk-2.12.0 → variably_sdk-2.13.0}/src/variably/client.py +92 -19
- {variably_sdk-2.12.0 → variably_sdk-2.13.0}/src/variably/config.py +10 -3
- {variably_sdk-2.12.0 → variably_sdk-2.13.0}/src/variably/errors.py +19 -1
- {variably_sdk-2.12.0 → variably_sdk-2.13.0}/src/variably/http_client.py +50 -3
- {variably_sdk-2.12.0 → variably_sdk-2.13.0}/src/variably/integrations/__init__.py +8 -1
- variably_sdk-2.13.0/src/variably/integrations/adk.py +563 -0
- variably_sdk-2.13.0/src/variably/integrations/otel.py +112 -0
- {variably_sdk-2.12.0 → variably_sdk-2.13.0}/src/variably/observe.py +8 -3
- {variably_sdk-2.12.0 → variably_sdk-2.13.0}/src/variably/version.py +1 -1
- {variably_sdk-2.12.0 → variably_sdk-2.13.0}/src/variably_sdk.egg-info/PKG-INFO +7 -2
- {variably_sdk-2.12.0 → variably_sdk-2.13.0}/src/variably_sdk.egg-info/SOURCES.txt +4 -0
- {variably_sdk-2.12.0 → variably_sdk-2.13.0}/src/variably_sdk.egg-info/requires.txt +9 -0
- variably_sdk-2.13.0/tests/test_integrations_adk.py +252 -0
- variably_sdk-2.13.0/tests/test_integrations_otel.py +67 -0
- {variably_sdk-2.12.0 → variably_sdk-2.13.0}/setup.cfg +0 -0
- {variably_sdk-2.12.0 → variably_sdk-2.13.0}/src/variably/assignment.py +0 -0
- {variably_sdk-2.12.0 → variably_sdk-2.13.0}/src/variably/cache.py +0 -0
- {variably_sdk-2.12.0 → variably_sdk-2.13.0}/src/variably/integrations/_common.py +0 -0
- {variably_sdk-2.12.0 → variably_sdk-2.13.0}/src/variably/integrations/langgraph.py +0 -0
- {variably_sdk-2.12.0 → variably_sdk-2.13.0}/src/variably/logger.py +0 -0
- {variably_sdk-2.12.0 → variably_sdk-2.13.0}/src/variably/prompt_config.py +0 -0
- {variably_sdk-2.12.0 → variably_sdk-2.13.0}/src/variably/types.py +0 -0
- {variably_sdk-2.12.0 → variably_sdk-2.13.0}/src/variably_sdk.egg-info/dependency_links.txt +0 -0
- {variably_sdk-2.12.0 → variably_sdk-2.13.0}/src/variably_sdk.egg-info/top_level.txt +0 -0
- {variably_sdk-2.12.0 → variably_sdk-2.13.0}/tests/test_integrations_langgraph.py +0 -0
- {variably_sdk-2.12.0 → variably_sdk-2.13.0}/tests/test_prompt_assignment.py +0 -0
- {variably_sdk-2.12.0 → variably_sdk-2.13.0}/tests/test_streaming.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: variably-sdk
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.13.0
|
|
4
4
|
Summary: Official Python SDK for Variably feature flags, LLM experimentation, and prompt optimization platform
|
|
5
5
|
Author-email: Variably <support@variably.com>
|
|
6
6
|
License: MIT
|
|
@@ -28,6 +28,11 @@ Requires-Dist: requests>=2.25.0
|
|
|
28
28
|
Requires-Dist: typing-extensions>=3.7.4; python_version < "3.8"
|
|
29
29
|
Provides-Extra: langgraph
|
|
30
30
|
Requires-Dist: langgraph>=0.1; extra == "langgraph"
|
|
31
|
+
Provides-Extra: adk
|
|
32
|
+
Requires-Dist: google-adk<3,>=2.0; python_version >= "3.10" and extra == "adk"
|
|
33
|
+
Provides-Extra: otel
|
|
34
|
+
Requires-Dist: opentelemetry-sdk<2,>=1.20; extra == "otel"
|
|
35
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http<2,>=1.20; extra == "otel"
|
|
31
36
|
Provides-Extra: dev
|
|
32
37
|
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
33
38
|
Requires-Dist: pytest-asyncio>=0.18.0; extra == "dev"
|
|
@@ -67,7 +72,7 @@ No experiment setup. No prompt migration. Just log and see scores.
|
|
|
67
72
|
|
|
68
73
|
```bash
|
|
69
74
|
VARIABLY_API_KEY=vbl_your_key_here
|
|
70
|
-
VARIABLY_BASE_URL=https://api.variably.
|
|
75
|
+
VARIABLY_BASE_URL=https://api.variably.dev
|
|
71
76
|
```
|
|
72
77
|
|
|
73
78
|
### 2. Add one line after your LLM call
|
|
@@ -23,7 +23,7 @@ No experiment setup. No prompt migration. Just log and see scores.
|
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
25
|
VARIABLY_API_KEY=vbl_your_key_here
|
|
26
|
-
VARIABLY_BASE_URL=https://api.variably.
|
|
26
|
+
VARIABLY_BASE_URL=https://api.variably.dev
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
### 2. Add one line after your LLM call
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "variably-sdk"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.13.0"
|
|
8
8
|
description = "Official Python SDK for Variably feature flags, LLM experimentation, and prompt optimization platform"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.7"
|
|
@@ -39,6 +39,15 @@ dependencies = [
|
|
|
39
39
|
langgraph = [
|
|
40
40
|
"langgraph>=0.1",
|
|
41
41
|
]
|
|
42
|
+
# Google ADK / Vertex AI Agent Engine plugin (#102). ADK itself needs Python >= 3.10.
|
|
43
|
+
adk = [
|
|
44
|
+
"google-adk>=2.0,<3; python_version>='3.10'",
|
|
45
|
+
]
|
|
46
|
+
# OpenTelemetry exporter: send existing OTel traces to be evaluated.
|
|
47
|
+
otel = [
|
|
48
|
+
"opentelemetry-sdk>=1.20,<2",
|
|
49
|
+
"opentelemetry-exporter-otlp-proto-http>=1.20,<2",
|
|
50
|
+
]
|
|
42
51
|
dev = [
|
|
43
52
|
"pytest>=6.0",
|
|
44
53
|
"pytest-asyncio>=0.18.0",
|
|
@@ -8,6 +8,7 @@ from .client import VariablyClient
|
|
|
8
8
|
from .config import VariablyConfig
|
|
9
9
|
from .types import UserContext, Event, FlagResult, PromptVariant, EvaluateResponse, StreamingEvaluateResponse, SubmitResponseResult
|
|
10
10
|
from .errors import (
|
|
11
|
+
FlagNotFoundError,
|
|
11
12
|
VariablyError,
|
|
12
13
|
NetworkError,
|
|
13
14
|
AuthenticationError,
|
|
@@ -20,6 +21,7 @@ from .observe import observe
|
|
|
20
21
|
from .version import __version__
|
|
21
22
|
|
|
22
23
|
__all__ = [
|
|
24
|
+
"FlagNotFoundError",
|
|
23
25
|
"VariablyClient",
|
|
24
26
|
"VariablyConfig",
|
|
25
27
|
"UserContext",
|
|
@@ -7,7 +7,7 @@ from typing import Any, Dict, List, Optional, Union
|
|
|
7
7
|
|
|
8
8
|
from .cache import CacheManager, NoOpCache
|
|
9
9
|
from .config import VariablyConfig
|
|
10
|
-
from .errors import ConfigurationError, VariablyError
|
|
10
|
+
from .errors import ConfigurationError, FlagNotFoundError, VariablyError
|
|
11
11
|
from .http_client import HTTPClient
|
|
12
12
|
from .logger import VariablyLogger, create_logger
|
|
13
13
|
from .prompt_config import PromptConfigStore, http_config_fetcher
|
|
@@ -24,6 +24,10 @@ from .types import (
|
|
|
24
24
|
)
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
#: Reason the API returns when a project has no such flag.
|
|
28
|
+
FLAG_NOT_FOUND_REASON = "gate_not_found"
|
|
29
|
+
|
|
30
|
+
|
|
27
31
|
class VariablyClient:
|
|
28
32
|
"""Main Variably SDK client for feature flags and experimentation."""
|
|
29
33
|
|
|
@@ -155,10 +159,10 @@ class VariablyClient:
|
|
|
155
159
|
raise ConfigurationError(
|
|
156
160
|
"Flag key must be a non-empty string", "flag_key"
|
|
157
161
|
)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
+
# A user id is optional. Server-rendered callers evaluate before a user
|
|
163
|
+
# is known and expect the flag's default rather than an exception; the API
|
|
164
|
+
# accepts an anonymous context, so rejecting it here would make the SDK
|
|
165
|
+
# stricter than the service it wraps.
|
|
162
166
|
|
|
163
167
|
# Check cache first
|
|
164
168
|
cache_key = self._generate_cache_key(flag_key, user_context)
|
|
@@ -167,7 +171,7 @@ class VariablyClient:
|
|
|
167
171
|
self.metrics.cache_hits += 1
|
|
168
172
|
self.logger.debug(
|
|
169
173
|
"Flag evaluation cache hit",
|
|
170
|
-
{"flag_key": flag_key, "user_id": user_context
|
|
174
|
+
{"flag_key": flag_key, "user_id": user_context.get("user_id", "")},
|
|
171
175
|
)
|
|
172
176
|
cached.cache_hit = True
|
|
173
177
|
return cached
|
|
@@ -176,10 +180,15 @@ class VariablyClient:
|
|
|
176
180
|
|
|
177
181
|
# Evaluate via API
|
|
178
182
|
response = self.http_client.evaluate_flag(flag_key, user_context)
|
|
179
|
-
|
|
183
|
+
|
|
184
|
+
if response.get("reason") == FLAG_NOT_FOUND_REASON:
|
|
185
|
+
raise FlagNotFoundError(flag_key)
|
|
186
|
+
|
|
187
|
+
value = response["value"]
|
|
188
|
+
|
|
180
189
|
result = FlagResult(
|
|
181
190
|
key=flag_key,
|
|
182
|
-
value=
|
|
191
|
+
value=value,
|
|
183
192
|
reason=response.get("reason", "api_evaluation"),
|
|
184
193
|
rule_id=response.get("rule_id"),
|
|
185
194
|
cache_hit=False,
|
|
@@ -193,13 +202,17 @@ class VariablyClient:
|
|
|
193
202
|
"Flag evaluation successful",
|
|
194
203
|
{
|
|
195
204
|
"flag_key": flag_key,
|
|
196
|
-
"value":
|
|
197
|
-
"user_id": user_context
|
|
205
|
+
"value": value,
|
|
206
|
+
"user_id": user_context.get("user_id", ""),
|
|
198
207
|
},
|
|
199
208
|
)
|
|
200
209
|
|
|
201
210
|
return result
|
|
202
211
|
|
|
212
|
+
except FlagNotFoundError:
|
|
213
|
+
# A missing flag is a configuration error and must reach the caller.
|
|
214
|
+
# Only transient failures below degrade to the default.
|
|
215
|
+
raise
|
|
203
216
|
except Exception as error:
|
|
204
217
|
self.logger.error(
|
|
205
218
|
"Flag evaluation failed",
|
|
@@ -306,10 +319,10 @@ class VariablyClient:
|
|
|
306
319
|
raise ConfigurationError(
|
|
307
320
|
"Gate key must be a non-empty string", "gate_key"
|
|
308
321
|
)
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
322
|
+
# A user id is optional. Server-rendered callers evaluate before a user
|
|
323
|
+
# is known and expect the flag's default rather than an exception; the API
|
|
324
|
+
# accepts an anonymous context, so rejecting it here would make the SDK
|
|
325
|
+
# stricter than the service it wraps.
|
|
313
326
|
|
|
314
327
|
# Check cache first
|
|
315
328
|
cache_key = self._generate_gate_cache_key(gate_key, user_context)
|
|
@@ -318,7 +331,7 @@ class VariablyClient:
|
|
|
318
331
|
self.metrics.cache_hits += 1
|
|
319
332
|
self.logger.debug(
|
|
320
333
|
"Gate evaluation cache hit",
|
|
321
|
-
{"gate_key": gate_key, "user_id": user_context
|
|
334
|
+
{"gate_key": gate_key, "user_id": user_context.get("user_id", "")},
|
|
322
335
|
)
|
|
323
336
|
return cached.value
|
|
324
337
|
|
|
@@ -342,8 +355,8 @@ class VariablyClient:
|
|
|
342
355
|
"Gate evaluation successful",
|
|
343
356
|
{
|
|
344
357
|
"gate_key": gate_key,
|
|
345
|
-
"value":
|
|
346
|
-
"user_id": user_context
|
|
358
|
+
"value": value,
|
|
359
|
+
"user_id": user_context.get("user_id", ""),
|
|
347
360
|
},
|
|
348
361
|
)
|
|
349
362
|
|
|
@@ -359,7 +372,67 @@ class VariablyClient:
|
|
|
359
372
|
},
|
|
360
373
|
)
|
|
361
374
|
return False # Default to false for gates
|
|
362
|
-
|
|
375
|
+
|
|
376
|
+
def evaluate_gates(
|
|
377
|
+
self, gate_keys: List[str], user_context: UserContext
|
|
378
|
+
) -> Dict[str, bool]:
|
|
379
|
+
"""
|
|
380
|
+
Evaluate multiple feature gates.
|
|
381
|
+
|
|
382
|
+
Mirrors the Go SDK's ``EvaluateGates`` so both SDKs expose the same
|
|
383
|
+
feature-flag surface.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
gate_keys: Feature gate keys
|
|
387
|
+
user_context: User context for access control
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
Mapping of gate key to whether access is granted. A gate that fails to
|
|
391
|
+
evaluate is False, matching ``evaluate_gate``.
|
|
392
|
+
"""
|
|
393
|
+
results: Dict[str, bool] = {}
|
|
394
|
+
for gate_key in gate_keys:
|
|
395
|
+
results[gate_key] = self.evaluate_gate(gate_key, user_context)
|
|
396
|
+
return results
|
|
397
|
+
|
|
398
|
+
def all_flags(self, user_context: UserContext) -> Dict[str, FlagResult]:
|
|
399
|
+
"""
|
|
400
|
+
Evaluate every flag in the project for one user context.
|
|
401
|
+
|
|
402
|
+
The caller does not supply flag keys: the server discovers them, so a newly
|
|
403
|
+
created flag reaches the SDK without a release. This is what a process should
|
|
404
|
+
call on start-up, and what a server-side renderer should use to build the
|
|
405
|
+
snapshot it hands to a browser client.
|
|
406
|
+
|
|
407
|
+
A flag missing from the result has no definition in this project. Callers
|
|
408
|
+
should treat that as "unset" rather than False, so a not-yet-created flag is
|
|
409
|
+
distinguishable from one deliberately turned off.
|
|
410
|
+
"""
|
|
411
|
+
response = self.http_client.evaluate_all_flags(user_context)
|
|
412
|
+
|
|
413
|
+
results: Dict[str, FlagResult] = {}
|
|
414
|
+
for flag_key, result in (response.get("flags") or {}).items():
|
|
415
|
+
results[flag_key] = FlagResult(
|
|
416
|
+
key=flag_key,
|
|
417
|
+
value=result.get("value"),
|
|
418
|
+
reason=result.get("reason", "api_evaluation"),
|
|
419
|
+
rule_id=result.get("rule_id"),
|
|
420
|
+
variation=result.get("variant_id"),
|
|
421
|
+
cache_hit=False,
|
|
422
|
+
)
|
|
423
|
+
return results
|
|
424
|
+
|
|
425
|
+
def validate_flags(
|
|
426
|
+
self, expected_keys: List[str], user_context: Optional[UserContext] = None
|
|
427
|
+
) -> List[str]:
|
|
428
|
+
"""Report which of ``expected_keys`` the project does not define.
|
|
429
|
+
|
|
430
|
+
Call this at start-up: it surfaces every missing flag at once, before a
|
|
431
|
+
request path can hit whichever one happens to be read first.
|
|
432
|
+
"""
|
|
433
|
+
defined = self.all_flags(user_context or {"user_id": ""})
|
|
434
|
+
return [key for key in expected_keys if key not in defined]
|
|
435
|
+
|
|
363
436
|
def track(self, event: Event) -> None:
|
|
364
437
|
"""
|
|
365
438
|
Track an analytics event.
|
|
@@ -921,7 +994,7 @@ class VariablyClient:
|
|
|
921
994
|
# Create a stable hash of user context
|
|
922
995
|
context_str = json.dumps(
|
|
923
996
|
{
|
|
924
|
-
"user_id": user_context
|
|
997
|
+
"user_id": user_context.get("user_id", ""),
|
|
925
998
|
"attributes": user_context.get("attributes", {}),
|
|
926
999
|
},
|
|
927
1000
|
sort_keys=True,
|
|
@@ -25,7 +25,9 @@ class VariablyConfig:
|
|
|
25
25
|
|
|
26
26
|
Args:
|
|
27
27
|
api_key: API key for authentication
|
|
28
|
-
base_url: Base URL for the API
|
|
28
|
+
base_url: Base URL for the API. REQUIRED — no default (e.g. your
|
|
29
|
+
self-hosted https://api.your-domain.com). Fail-closed by design so
|
|
30
|
+
evaluation data is never sent to an unintended host.
|
|
29
31
|
environment: Environment name (development, staging, production)
|
|
30
32
|
timeout: Request timeout in milliseconds (default: 5000)
|
|
31
33
|
retry_attempts: Number of retry attempts (default: 3)
|
|
@@ -35,9 +37,14 @@ class VariablyConfig:
|
|
|
35
37
|
"""
|
|
36
38
|
if not api_key:
|
|
37
39
|
raise ValueError("API key is required")
|
|
38
|
-
|
|
40
|
+
if not base_url:
|
|
41
|
+
raise ValueError(
|
|
42
|
+
"base_url is required — set VARIABLY_BASE_URL to your API endpoint. "
|
|
43
|
+
"No default, so evaluation data is never sent to an unintended host."
|
|
44
|
+
)
|
|
45
|
+
|
|
39
46
|
self.api_key = api_key
|
|
40
|
-
self.base_url = base_url
|
|
47
|
+
self.base_url = base_url
|
|
41
48
|
self.environment = environment or "development"
|
|
42
49
|
self.timeout = timeout / 1000.0 # Convert to seconds
|
|
43
50
|
self.retry_attempts = retry_attempts
|
|
@@ -58,4 +58,22 @@ class ConfigurationError(VariablyError):
|
|
|
58
58
|
|
|
59
59
|
def __init__(self, message: str, parameter: str, cause: Exception = None):
|
|
60
60
|
super().__init__(message, cause)
|
|
61
|
-
self.parameter = parameter
|
|
61
|
+
self.parameter = parameter
|
|
62
|
+
|
|
63
|
+
class FlagNotFoundError(VariablyError):
|
|
64
|
+
"""Raised when a flag is not defined in the project.
|
|
65
|
+
|
|
66
|
+
This is a configuration error, not a value. Returning the caller's default
|
|
67
|
+
here would make a typo, an unseeded flag, or a half-finished migration look
|
|
68
|
+
exactly like a flag that is deliberately off — the application would run on
|
|
69
|
+
defaults and appear healthy. Network failures are different and still fall
|
|
70
|
+
back to the default: those are transient, this one is not.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, flag_key: str):
|
|
74
|
+
self.flag_key = flag_key
|
|
75
|
+
super().__init__(
|
|
76
|
+
f'Variably: flag "{flag_key}" is not defined in this project. '
|
|
77
|
+
"Create it, or check the key for a typo — a missing flag is not "
|
|
78
|
+
'treated as "off".'
|
|
79
|
+
)
|
|
@@ -31,6 +31,40 @@ from .types import (
|
|
|
31
31
|
)
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
|
|
35
|
+
def to_sdk_context(context: UserContext) -> Dict[str, Any]:
|
|
36
|
+
"""Map a UserContext onto the wire shape the API expects.
|
|
37
|
+
|
|
38
|
+
UserContext is snake_case (``user_id``) but the API binds camelCase
|
|
39
|
+
(``userId``). Passing the dict through unmapped drops the user id silently:
|
|
40
|
+
every evaluation then looks anonymous, so overrides and targeting rules are
|
|
41
|
+
ignored and the caller gets a plausible-but-wrong answer rather than an error.
|
|
42
|
+
"""
|
|
43
|
+
mapping = {
|
|
44
|
+
"user_id": "userId",
|
|
45
|
+
"email": "email",
|
|
46
|
+
"country": "country",
|
|
47
|
+
"language": "language",
|
|
48
|
+
"platform": "platform",
|
|
49
|
+
"version": "version",
|
|
50
|
+
"ip_address": "ipAddress",
|
|
51
|
+
"user_agent": "userAgent",
|
|
52
|
+
"session_id": "sessionId",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
payload: Dict[str, Any] = {}
|
|
56
|
+
for snake, camel in mapping.items():
|
|
57
|
+
value = context.get(snake)
|
|
58
|
+
if value:
|
|
59
|
+
payload[camel] = value
|
|
60
|
+
|
|
61
|
+
attributes = context.get("attributes")
|
|
62
|
+
if attributes:
|
|
63
|
+
payload["attributes"] = attributes
|
|
64
|
+
|
|
65
|
+
return payload
|
|
66
|
+
|
|
67
|
+
|
|
34
68
|
class HTTPClient:
|
|
35
69
|
"""HTTP client for Variably API with retry logic and error handling."""
|
|
36
70
|
|
|
@@ -69,7 +103,7 @@ class HTTPClient:
|
|
|
69
103
|
"""Evaluate a single feature flag."""
|
|
70
104
|
request_data: FlagEvaluationRequest = {
|
|
71
105
|
"flag_key": flag_key,
|
|
72
|
-
"context": context,
|
|
106
|
+
"context": to_sdk_context(context),
|
|
73
107
|
}
|
|
74
108
|
|
|
75
109
|
response = self._make_request(
|
|
@@ -87,7 +121,7 @@ class HTTPClient:
|
|
|
87
121
|
"""Evaluate multiple feature flags in batch."""
|
|
88
122
|
request_data: BatchFlagEvaluationRequest = {
|
|
89
123
|
"flag_keys": flag_keys,
|
|
90
|
-
"context": context,
|
|
124
|
+
"context": to_sdk_context(context),
|
|
91
125
|
}
|
|
92
126
|
|
|
93
127
|
response = self._make_request(
|
|
@@ -99,11 +133,24 @@ class HTTPClient:
|
|
|
99
133
|
|
|
100
134
|
return response
|
|
101
135
|
|
|
136
|
+
def evaluate_all_flags(self, context: UserContext) -> Dict[str, Any]:
|
|
137
|
+
"""Evaluate every flag in the project for one context.
|
|
138
|
+
|
|
139
|
+
No flag keys are sent: the server discovers them, so a newly created flag
|
|
140
|
+
reaches the SDK without a client release.
|
|
141
|
+
"""
|
|
142
|
+
return self._make_request(
|
|
143
|
+
"POST",
|
|
144
|
+
"/api/v1/sdk/evaluate/all",
|
|
145
|
+
json={"context": to_sdk_context(context)},
|
|
146
|
+
retryable=True,
|
|
147
|
+
)
|
|
148
|
+
|
|
102
149
|
def evaluate_gate(self, gate_key: str, context: UserContext) -> GateEvaluationResponse:
|
|
103
150
|
"""Evaluate a feature gate."""
|
|
104
151
|
request_data: GateEvaluationRequest = {
|
|
105
152
|
"gate_key": gate_key,
|
|
106
|
-
"context": context,
|
|
153
|
+
"context": to_sdk_context(context),
|
|
107
154
|
}
|
|
108
155
|
|
|
109
156
|
response = self._make_request(
|
|
@@ -6,10 +6,17 @@ deterministic cross-step evaluation with little or no per-node code:
|
|
|
6
6
|
from variably.integrations.langgraph import instrument
|
|
7
7
|
instrument(graph, session_id_key="session_id")
|
|
8
8
|
|
|
9
|
+
from variably.integrations.adk import VariablyPlugin # Google ADK / Agent Engine
|
|
10
|
+
Runner(agent=root_agent, ..., plugins=[VariablyPlugin()])
|
|
11
|
+
|
|
12
|
+
from variably.integrations.otel import instrument # any OpenTelemetry tracing
|
|
13
|
+
instrument()
|
|
14
|
+
|
|
9
15
|
Each integration maps the framework's node/agent identity to ``agent_name``,
|
|
10
16
|
detects the terminal node(s) as ``is_aggregator``, pulls session id + context
|
|
11
17
|
from the framework's state, and logs via ``variably.observe`` — always
|
|
12
18
|
best-effort, never raising into the customer's workflow.
|
|
13
19
|
|
|
14
|
-
Install extras per framework, e.g. ``pip install variably-sdk[langgraph]
|
|
20
|
+
Install extras per framework, e.g. ``pip install variably-sdk[langgraph]`` or
|
|
21
|
+
``pip install "variably-sdk[adk]"`` or ``pip install "variably-sdk[otel]"``.
|
|
15
22
|
"""
|