variably-sdk 2.4.0__tar.gz → 2.6.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.
Files changed (21) hide show
  1. {variably_sdk-2.4.0 → variably_sdk-2.6.0}/PKG-INFO +1 -1
  2. {variably_sdk-2.4.0 → variably_sdk-2.6.0}/pyproject.toml +1 -1
  3. {variably_sdk-2.4.0 → variably_sdk-2.6.0}/src/variably/__init__.py +2 -0
  4. {variably_sdk-2.4.0 → variably_sdk-2.6.0}/src/variably/client.py +4 -0
  5. variably_sdk-2.6.0/src/variably/observe.py +257 -0
  6. {variably_sdk-2.4.0 → variably_sdk-2.6.0}/src/variably/version.py +1 -1
  7. {variably_sdk-2.4.0 → variably_sdk-2.6.0}/src/variably_sdk.egg-info/PKG-INFO +1 -1
  8. {variably_sdk-2.4.0 → variably_sdk-2.6.0}/src/variably_sdk.egg-info/SOURCES.txt +1 -0
  9. {variably_sdk-2.4.0 → variably_sdk-2.6.0}/README.md +0 -0
  10. {variably_sdk-2.4.0 → variably_sdk-2.6.0}/setup.cfg +0 -0
  11. {variably_sdk-2.4.0 → variably_sdk-2.6.0}/setup.py +0 -0
  12. {variably_sdk-2.4.0 → variably_sdk-2.6.0}/src/variably/cache.py +0 -0
  13. {variably_sdk-2.4.0 → variably_sdk-2.6.0}/src/variably/config.py +0 -0
  14. {variably_sdk-2.4.0 → variably_sdk-2.6.0}/src/variably/errors.py +0 -0
  15. {variably_sdk-2.4.0 → variably_sdk-2.6.0}/src/variably/http_client.py +0 -0
  16. {variably_sdk-2.4.0 → variably_sdk-2.6.0}/src/variably/logger.py +0 -0
  17. {variably_sdk-2.4.0 → variably_sdk-2.6.0}/src/variably/types.py +0 -0
  18. {variably_sdk-2.4.0 → variably_sdk-2.6.0}/src/variably_sdk.egg-info/dependency_links.txt +0 -0
  19. {variably_sdk-2.4.0 → variably_sdk-2.6.0}/src/variably_sdk.egg-info/requires.txt +0 -0
  20. {variably_sdk-2.4.0 → variably_sdk-2.6.0}/src/variably_sdk.egg-info/top_level.txt +0 -0
  21. {variably_sdk-2.4.0 → variably_sdk-2.6.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.4.0
3
+ Version: 2.6.0
4
4
  Summary: Official Python SDK for Variably feature flags, LLM experimentation, and prompt optimization platform
5
5
  Author: Variably
6
6
  Author-email: Variably <support@variably.com>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "variably-sdk"
7
- version = "2.4.0"
7
+ version = "2.6.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"
@@ -16,6 +16,7 @@ from .errors import (
16
16
  TimeoutError,
17
17
  ConfigurationError,
18
18
  )
19
+ from .observe import observe
19
20
  from .version import __version__
20
21
 
21
22
  __all__ = [
@@ -35,5 +36,6 @@ __all__ = [
35
36
  "RateLimitError",
36
37
  "TimeoutError",
37
38
  "ConfigurationError",
39
+ "observe",
38
40
  "__version__",
39
41
  ]
@@ -612,6 +612,7 @@ class VariablyClient:
612
612
  latency_ms: Optional[int] = None,
613
613
  prompt_tokens: Optional[int] = None,
614
614
  completion_tokens: Optional[int] = None,
615
+ cost: Optional[float] = None,
615
616
  metadata: Optional[Dict[str, Any]] = None,
616
617
  ) -> LogResult:
617
618
  """Log a prompt/response pair for 43-dimension evaluation (Observe Mode).
@@ -633,6 +634,7 @@ class VariablyClient:
633
634
  latency_ms: LLM call latency in milliseconds.
634
635
  prompt_tokens: Number of input tokens.
635
636
  completion_tokens: Number of output tokens.
637
+ cost: Cost in USD for this LLM call.
636
638
  metadata: Arbitrary key-value metadata.
637
639
 
638
640
  Returns:
@@ -673,6 +675,8 @@ class VariablyClient:
673
675
  data["prompt_tokens"] = prompt_tokens
674
676
  if completion_tokens is not None:
675
677
  data["completion_tokens"] = completion_tokens
678
+ if cost is not None:
679
+ data["cost"] = cost
676
680
  if metadata:
677
681
  data["metadata"] = metadata
678
682
 
@@ -0,0 +1,257 @@
1
+ """
2
+ Variably Observe Mode — drop-in integration for multi-dimension LLM evaluation.
3
+
4
+ Usage:
5
+ from variably import observe
6
+
7
+ observe(prompt=user_query, response=llm_response, provider_response=completion)
8
+
9
+ Never raises. Returns {"observation_id": "...", "status": "queued"} or None.
10
+ """
11
+
12
+ import os
13
+ import sys
14
+ import threading
15
+ from typing import Any, Dict, List, Optional
16
+
17
+ from .client import VariablyClient
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Lazy singleton client
21
+ # ---------------------------------------------------------------------------
22
+
23
+ _client: Optional[VariablyClient] = None
24
+ _client_lock = threading.Lock()
25
+
26
+
27
+ def _get_client() -> Optional[VariablyClient]:
28
+ """Return a shared VariablyClient, creating it on first call. Thread-safe."""
29
+ global _client
30
+ if _client is not None:
31
+ return _client
32
+
33
+ with _client_lock:
34
+ # Double-check after acquiring lock
35
+ if _client is not None:
36
+ return _client
37
+
38
+ # Try loading .env if python-dotenv is available
39
+ try:
40
+ from dotenv import load_dotenv # type: ignore[import-untyped]
41
+
42
+ load_dotenv()
43
+ except ImportError:
44
+ pass
45
+
46
+ api_key = os.environ.get("VARIABLY_API_KEY")
47
+ if not api_key:
48
+ return None
49
+
50
+ base_url = os.environ.get(
51
+ "VARIABLY_BASE_URL", "https://api.variably.tech"
52
+ )
53
+
54
+ try:
55
+ _client = VariablyClient(
56
+ {
57
+ "api_key": api_key,
58
+ "base_url": base_url,
59
+ "log_level": os.environ.get("VARIABLY_LOG_LEVEL", "WARNING"),
60
+ }
61
+ )
62
+ return _client
63
+ except Exception as exc:
64
+ print(
65
+ f"[variably.observe] Failed to initialize client: {exc}",
66
+ file=sys.stderr,
67
+ )
68
+ return None
69
+
70
+
71
+ # ---------------------------------------------------------------------------
72
+ # Provider response auto-extraction (duck-typed, no SDK imports needed)
73
+ # ---------------------------------------------------------------------------
74
+
75
+
76
+ def _extract_from_provider(provider_response: Any) -> Dict[str, Any]:
77
+ """Extract model, tokens, and provider from a raw LLM response object.
78
+
79
+ Supports OpenAI, Anthropic, and Google/Gemini response shapes via duck typing.
80
+ No provider SDK imports required.
81
+ """
82
+ extracted: Dict[str, Any] = {}
83
+
84
+ if provider_response is None:
85
+ return extracted
86
+
87
+ # --- Model name ---
88
+ model = getattr(provider_response, "model", None)
89
+ if model:
90
+ extracted["model"] = str(model)
91
+
92
+ # --- OpenAI-style: resp.usage.prompt_tokens / completion_tokens ---
93
+ usage = getattr(provider_response, "usage", None)
94
+ if usage is not None:
95
+ pt = getattr(usage, "prompt_tokens", None)
96
+ ct = getattr(usage, "completion_tokens", None)
97
+ if pt is not None:
98
+ extracted["prompt_tokens"] = int(pt)
99
+ if ct is not None:
100
+ extracted["completion_tokens"] = int(ct)
101
+
102
+ # Anthropic-style: resp.usage.input_tokens / output_tokens
103
+ if pt is None:
104
+ it = getattr(usage, "input_tokens", None)
105
+ if it is not None:
106
+ extracted["prompt_tokens"] = int(it)
107
+ if ct is None:
108
+ ot = getattr(usage, "output_tokens", None)
109
+ if ot is not None:
110
+ extracted["completion_tokens"] = int(ot)
111
+
112
+ # --- Google/Gemini-style: resp.usage_metadata ---
113
+ usage_meta = getattr(provider_response, "usage_metadata", None)
114
+ if usage_meta is not None and usage is None:
115
+ ptc = getattr(usage_meta, "prompt_token_count", None)
116
+ ctc = getattr(usage_meta, "candidates_token_count", None)
117
+ if ptc is not None:
118
+ extracted["prompt_tokens"] = int(ptc)
119
+ if ctc is not None:
120
+ extracted["completion_tokens"] = int(ctc)
121
+
122
+ # --- Auto-detect provider from model name ---
123
+ m = extracted.get("model", "")
124
+ if m:
125
+ m_lower = m.lower()
126
+ if any(k in m_lower for k in ("gpt-", "o1-", "o3-", "chatgpt")):
127
+ extracted["provider"] = "openai"
128
+ elif "claude" in m_lower:
129
+ extracted["provider"] = "anthropic"
130
+ elif any(k in m_lower for k in ("gemini", "palm")):
131
+ extracted["provider"] = "google"
132
+
133
+ return extracted
134
+
135
+
136
+ # ---------------------------------------------------------------------------
137
+ # Public API
138
+ # ---------------------------------------------------------------------------
139
+
140
+
141
+ def observe(
142
+ prompt: str,
143
+ response: str,
144
+ *,
145
+ # Auto-extraction from provider response object
146
+ provider_response: Any = None,
147
+ # Manual overrides (take precedence over auto-extracted)
148
+ model: Optional[str] = None,
149
+ provider: Optional[str] = None,
150
+ latency_ms: Optional[int] = None,
151
+ prompt_tokens: Optional[int] = None,
152
+ completion_tokens: Optional[int] = None,
153
+ cost: Optional[float] = None,
154
+ # RAG context
155
+ reference_materials: Optional[List[Dict[str, Any]]] = None,
156
+ retrieval_query: Optional[str] = None,
157
+ # Multi-turn context
158
+ conversation_history: Optional[List[Dict[str, str]]] = None,
159
+ # Metadata
160
+ tags: Optional[List[str]] = None,
161
+ user_id: Optional[str] = None,
162
+ session_id: Optional[str] = None,
163
+ metadata: Optional[Dict[str, Any]] = None,
164
+ ) -> Optional[Dict[str, Any]]:
165
+ """Log a prompt/response pair to Variably for multi-dimension evaluation.
166
+
167
+ Never raises. Returns ``{"observation_id": "...", "status": "queued"}``
168
+ on success, or ``None`` on any error (missing API key, network failure, etc.).
169
+
170
+ Args:
171
+ prompt: The user's input / question sent to the LLM.
172
+ response: The LLM's generated response text.
173
+ provider_response: Raw OpenAI / Anthropic / Google response object.
174
+ Model name, token counts, and provider are auto-extracted via
175
+ duck typing — no provider SDK imports needed.
176
+ model: Model name override (e.g. ``"gpt-4o"``).
177
+ Takes precedence over auto-extracted value.
178
+ provider: Provider override (e.g. ``"openai"``).
179
+ Takes precedence over auto-detected value.
180
+ latency_ms: Response generation time in milliseconds.
181
+ prompt_tokens: Input token count override.
182
+ Takes precedence over auto-extracted value.
183
+ completion_tokens: Output token count override.
184
+ Takes precedence over auto-extracted value.
185
+ cost: Cost in USD for this LLM call.
186
+ reference_materials: RAG chunks for grounding / hallucination scoring.
187
+ Each dict should have ``"id"``, ``"content"``, and optionally
188
+ ``"source"``.
189
+ retrieval_query: The query sent to your retriever (enables retrieval
190
+ quality scoring).
191
+ conversation_history: Prior turns for coherence scoring.
192
+ Each dict should have ``"role"`` and ``"content"``.
193
+ tags: Grouping labels, e.g. ``["production", "rag"]``.
194
+ user_id: Your end-user's ID.
195
+ session_id: Conversation session ID (groups multi-turn observations).
196
+ metadata: Arbitrary key-value data.
197
+
198
+ Returns:
199
+ Dict with ``observation_id`` and ``status`` on success, or ``None``.
200
+ """
201
+ try:
202
+ client = _get_client()
203
+ if client is None:
204
+ return None
205
+
206
+ # Auto-extract from provider response; manual params override
207
+ auto = _extract_from_provider(provider_response)
208
+
209
+ resolved_model = model or auto.get("model")
210
+ resolved_provider = provider or auto.get("provider")
211
+ resolved_prompt_tokens = (
212
+ prompt_tokens if prompt_tokens is not None else auto.get("prompt_tokens")
213
+ )
214
+ resolved_completion_tokens = (
215
+ completion_tokens
216
+ if completion_tokens is not None
217
+ else auto.get("completion_tokens")
218
+ )
219
+
220
+ result = client.log(
221
+ prompt=prompt,
222
+ response=response,
223
+ model=resolved_model,
224
+ provider=resolved_provider,
225
+ latency_ms=latency_ms,
226
+ prompt_tokens=resolved_prompt_tokens,
227
+ completion_tokens=resolved_completion_tokens,
228
+ cost=cost,
229
+ reference_materials=reference_materials,
230
+ retrieval_query=retrieval_query,
231
+ conversation_history=conversation_history,
232
+ tags=tags,
233
+ user_id=user_id,
234
+ session_id=session_id,
235
+ metadata=metadata,
236
+ )
237
+
238
+ return {
239
+ "observation_id": result.observation_id,
240
+ "status": result.status,
241
+ }
242
+
243
+ except Exception as exc:
244
+ print(f"[variably.observe] {exc}", file=sys.stderr)
245
+ return None
246
+
247
+
248
+ def reset() -> None:
249
+ """Reset the singleton client. Useful for testing or reconfiguration."""
250
+ global _client
251
+ with _client_lock:
252
+ if _client is not None:
253
+ try:
254
+ _client.close()
255
+ except Exception:
256
+ pass
257
+ _client = None
@@ -1,3 +1,3 @@
1
1
  """Version information for Variably SDK."""
2
2
 
3
- __version__ = "2.3.0"
3
+ __version__ = "2.6.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: variably-sdk
3
- Version: 2.4.0
3
+ Version: 2.6.0
4
4
  Summary: Official Python SDK for Variably feature flags, LLM experimentation, and prompt optimization platform
5
5
  Author: Variably
6
6
  Author-email: Variably <support@variably.com>
@@ -8,6 +8,7 @@ src/variably/config.py
8
8
  src/variably/errors.py
9
9
  src/variably/http_client.py
10
10
  src/variably/logger.py
11
+ src/variably/observe.py
11
12
  src/variably/types.py
12
13
  src/variably/version.py
13
14
  src/variably_sdk.egg-info/PKG-INFO
File without changes
File without changes
File without changes