variably-sdk 2.6.0__tar.gz → 2.7.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.6.0 → variably_sdk-2.7.0}/PKG-INFO +143 -37
  2. {variably_sdk-2.6.0 → variably_sdk-2.7.0}/README.md +142 -36
  3. {variably_sdk-2.6.0 → variably_sdk-2.7.0}/pyproject.toml +1 -1
  4. {variably_sdk-2.6.0 → variably_sdk-2.7.0}/src/variably/client.py +51 -33
  5. {variably_sdk-2.6.0 → variably_sdk-2.7.0}/src/variably/observe.py +72 -42
  6. {variably_sdk-2.6.0 → variably_sdk-2.7.0}/src/variably/version.py +1 -1
  7. {variably_sdk-2.6.0 → variably_sdk-2.7.0}/src/variably_sdk.egg-info/PKG-INFO +143 -37
  8. {variably_sdk-2.6.0 → variably_sdk-2.7.0}/setup.cfg +0 -0
  9. {variably_sdk-2.6.0 → variably_sdk-2.7.0}/setup.py +0 -0
  10. {variably_sdk-2.6.0 → variably_sdk-2.7.0}/src/variably/__init__.py +0 -0
  11. {variably_sdk-2.6.0 → variably_sdk-2.7.0}/src/variably/cache.py +0 -0
  12. {variably_sdk-2.6.0 → variably_sdk-2.7.0}/src/variably/config.py +0 -0
  13. {variably_sdk-2.6.0 → variably_sdk-2.7.0}/src/variably/errors.py +0 -0
  14. {variably_sdk-2.6.0 → variably_sdk-2.7.0}/src/variably/http_client.py +0 -0
  15. {variably_sdk-2.6.0 → variably_sdk-2.7.0}/src/variably/logger.py +0 -0
  16. {variably_sdk-2.6.0 → variably_sdk-2.7.0}/src/variably/types.py +0 -0
  17. {variably_sdk-2.6.0 → variably_sdk-2.7.0}/src/variably_sdk.egg-info/SOURCES.txt +0 -0
  18. {variably_sdk-2.6.0 → variably_sdk-2.7.0}/src/variably_sdk.egg-info/dependency_links.txt +0 -0
  19. {variably_sdk-2.6.0 → variably_sdk-2.7.0}/src/variably_sdk.egg-info/requires.txt +0 -0
  20. {variably_sdk-2.6.0 → variably_sdk-2.7.0}/src/variably_sdk.egg-info/top_level.txt +0 -0
  21. {variably_sdk-2.6.0 → variably_sdk-2.7.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.6.0
3
+ Version: 2.7.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>
@@ -45,7 +45,7 @@ Dynamic: requires-python
45
45
 
46
46
  # Variably Python SDK
47
47
 
48
- Official Python SDK for Variably — feature flags, LLM experimentation, and prompt optimization.
48
+ Official Python SDK for Variably — LLM evaluation, experimentation, and prompt optimization.
49
49
 
50
50
  ## Installation
51
51
 
@@ -53,52 +53,158 @@ Official Python SDK for Variably — feature flags, LLM experimentation, and pro
53
53
  pip install variably-sdk
54
54
  ```
55
55
 
56
- ## Quick Start
56
+ For Docker/Kubernetes deployments, add to your `requirements.txt`:
57
+
58
+ ```
59
+ variably-sdk>=2.6.1
60
+ ```
61
+ ## Quick Start — Observe Mode
62
+
63
+ Add **one line** to your existing LLM app and get **multi-dimension evaluation** across 40+ metrics in 6 categories: Quality, Safety, Semantic, Grounding, Coherence, and Advanced.
64
+
65
+ No experiment setup. No prompt migration. Just log and see scores.
66
+
67
+ ### 1. Set your environment variables
68
+
69
+ ```bash
70
+ VARIABLY_API_KEY=vbl_your_key_here
71
+ VARIABLY_BASE_URL=https://api.variably.tech
72
+ ```
73
+
74
+ ### 2. Add one line after your LLM call
57
75
 
58
76
  ```python
59
- from variably import VariablyClient
77
+ from variably import observe
60
78
 
61
- # Initialize the client
62
- client = VariablyClient({
63
- "api_key": "your-api-key",
64
- "base_url": "https://api.variably.com", # optional, defaults to localhost:8080
65
- "environment": "production" # optional
66
- })
79
+ # Your existing code (unchanged)
80
+ response = your_llm_call(user_query)
67
81
 
68
- # Evaluate a boolean feature flag
69
- user_context = {
70
- "user_id": "user-123",
71
- "email": "user@example.com",
72
- "country": "US"
73
- }
82
+ # Add this line:
83
+ observe(prompt=user_query, response=response)
84
+ ```
85
+
86
+ ### Auto-extract tokens & model from provider response
74
87
 
75
- is_feature_enabled = client.evaluate_flag_bool(
76
- "new-checkout-flow",
77
- False, # default value
78
- user_context
88
+ ```python
89
+ # OpenAI
90
+ from openai import OpenAI
91
+ from variably import observe
92
+
93
+ client = OpenAI()
94
+ completion = client.chat.completions.create(
95
+ model="gpt-4o",
96
+ messages=[{"role": "user", "content": user_query}],
79
97
  )
80
98
 
81
- if is_feature_enabled:
82
- # Show new checkout flow
83
- pass
99
+ observe(
100
+ prompt=user_query,
101
+ response=completion.choices[0].message.content,
102
+ provider_response=completion, # auto-extracts model, tokens
103
+ )
104
+ ```
84
105
 
85
- # Evaluate a feature gate
86
- has_access = client.evaluate_gate("premium-features", user_context)
106
+ ```python
107
+ # Anthropic
108
+ import anthropic
109
+ from variably import observe
110
+
111
+ client = anthropic.Anthropic()
112
+ message = client.messages.create(
113
+ model="claude-sonnet-4-20250514",
114
+ max_tokens=1024,
115
+ messages=[{"role": "user", "content": user_query}],
116
+ )
87
117
 
88
- # Track events
89
- client.track({
90
- "name": "button_clicked",
91
- "user_id": "user-123",
92
- "properties": {
93
- "button_name": "checkout",
94
- "page": "product-detail"
95
- }
96
- })
118
+ observe(
119
+ prompt=user_query,
120
+ response=message.content[0].text,
121
+ provider_response=message, # auto-extracts model, tokens
122
+ )
123
+ ```
124
+
125
+ ### RAG applications — grounding & hallucination scoring
97
126
 
98
- # Clean up resources
99
- client.close()
127
+ ```python
128
+ observe(
129
+ prompt=user_query,
130
+ response=llm_answer,
131
+ provider_response=completion,
132
+ reference_materials=[
133
+ {"id": "chunk-1", "content": "Retrieved text...", "source": "docs.pdf"},
134
+ {"id": "chunk-2", "content": "Another chunk...", "source": "faq.md"},
135
+ ],
136
+ retrieval_query=user_query,
137
+ )
100
138
  ```
101
139
 
140
+ ### Multi-turn chat — coherence scoring
141
+
142
+ ```python
143
+ observe(
144
+ prompt=latest_user_message,
145
+ response=llm_answer,
146
+ provider_response=completion,
147
+ conversation_history=[
148
+ {"role": "user", "content": "What is diabetes?"},
149
+ {"role": "assistant", "content": "Diabetes is a chronic condition..."},
150
+ {"role": "user", "content": "What are the symptoms?"},
151
+ ],
152
+ session_id="conv-123",
153
+ )
154
+ ```
155
+
156
+ ### RAG applications — grounding & hallucination scoring
157
+
158
+ ```python
159
+ observe(
160
+ prompt=user_query,
161
+ response=llm_answer,
162
+ provider_response=completion,
163
+ reference_materials=[
164
+ {"id": "chunk-1", "content": "Retrieved text...", "source": "docs.pdf"},
165
+ {"id": "chunk-2", "content": "Another chunk...", "source": "faq.md"},
166
+ ],
167
+ retrieval_query=user_query,
168
+ )
169
+ ```
170
+
171
+ ### Multi-turn chat — coherence scoring
172
+
173
+ ```python
174
+ observe(
175
+ prompt=latest_user_message,
176
+ response=llm_answer,
177
+ provider_response=completion,
178
+ conversation_history=[
179
+ {"role": "user", "content": "What is diabetes?"},
180
+ {"role": "assistant", "content": "Diabetes is a chronic condition..."},
181
+ {"role": "user", "content": "What are the symptoms?"},
182
+ ],
183
+ session_id="conv-123",
184
+ )
185
+ ```
186
+
187
+ | Parameter | Type | Required | Description |
188
+ |-----------|------|----------|-------------|
189
+ | `prompt` | str | Yes | The user's input / question |
190
+ | `response` | str | Yes | The LLM's generated response |
191
+ | `provider_response` | object | No | Raw OpenAI/Anthropic/Google response — auto-extracts model, tokens |
192
+ | `model` | str | No | Model name (auto-extracted if provider_response given) |
193
+ | `provider` | str | No | "openai", "anthropic", etc. (auto-detected) |
194
+ | `latency_ms` | int | No | Response generation time in milliseconds |
195
+ | `prompt_tokens` | int | No | Input token count (auto-extracted if provider_response given) |
196
+ | `completion_tokens` | int | No | Output token count (auto-extracted) |
197
+ | `cost` | float | No | Cost in USD |
198
+ | `reference_materials` | list[dict] | No | RAG chunks: `[{"id", "content", "source"}]` — enables grounding scoring |
199
+ | `retrieval_query` | str | No | Query sent to retriever — enables retrieval quality scoring |
200
+ | `conversation_history` | list[dict] | No | Prior turns: `[{"role", "content"}]` — enables coherence scoring |
201
+ | `tags` | list[str] | No | Grouping labels, e.g. `["production", "rag"]` |
202
+ | `user_id` | str | No | Your user's ID |
203
+ | `session_id` | str | No | Conversation session ID (groups multi-turn) |
204
+ | `metadata` | dict | No | Any extra key-value data |
205
+
206
+ ---
207
+
102
208
  ## Prompt Experimentation
103
209
 
104
210
  Variably provides two modes for LLM prompt experimentation:
@@ -293,7 +399,7 @@ result = client.submit_response(
293
399
  | `workflow_history[].content` | `str` | Message content |
294
400
  | `retrieval_query` | `str` (optional) | The rewritten query sent to the retriever |
295
401
 
296
- See [Context-Aware RAG Evaluation](../../docs/concepts/context-aware-rag-evaluation.md) for the full concept doc with architecture diagrams and integration examples.
402
+ See [Context-Aware RAG Evaluation](../../docs/concepts/LLM%20Evaluation/context-aware-rag-evaluation.md) for the full concept doc with architecture diagrams and integration examples.
297
403
 
298
404
  #### Integration with LangGraph / FastAPI streaming
299
405
 
@@ -1,6 +1,6 @@
1
1
  # Variably Python SDK
2
2
 
3
- Official Python SDK for Variably — feature flags, LLM experimentation, and prompt optimization.
3
+ Official Python SDK for Variably — LLM evaluation, experimentation, and prompt optimization.
4
4
 
5
5
  ## Installation
6
6
 
@@ -8,52 +8,158 @@ Official Python SDK for Variably — feature flags, LLM experimentation, and pro
8
8
  pip install variably-sdk
9
9
  ```
10
10
 
11
- ## Quick Start
11
+ For Docker/Kubernetes deployments, add to your `requirements.txt`:
12
+
13
+ ```
14
+ variably-sdk>=2.6.1
15
+ ```
16
+ ## Quick Start — Observe Mode
17
+
18
+ Add **one line** to your existing LLM app and get **multi-dimension evaluation** across 40+ metrics in 6 categories: Quality, Safety, Semantic, Grounding, Coherence, and Advanced.
19
+
20
+ No experiment setup. No prompt migration. Just log and see scores.
21
+
22
+ ### 1. Set your environment variables
23
+
24
+ ```bash
25
+ VARIABLY_API_KEY=vbl_your_key_here
26
+ VARIABLY_BASE_URL=https://api.variably.tech
27
+ ```
28
+
29
+ ### 2. Add one line after your LLM call
12
30
 
13
31
  ```python
14
- from variably import VariablyClient
32
+ from variably import observe
15
33
 
16
- # Initialize the client
17
- client = VariablyClient({
18
- "api_key": "your-api-key",
19
- "base_url": "https://api.variably.com", # optional, defaults to localhost:8080
20
- "environment": "production" # optional
21
- })
34
+ # Your existing code (unchanged)
35
+ response = your_llm_call(user_query)
22
36
 
23
- # Evaluate a boolean feature flag
24
- user_context = {
25
- "user_id": "user-123",
26
- "email": "user@example.com",
27
- "country": "US"
28
- }
37
+ # Add this line:
38
+ observe(prompt=user_query, response=response)
39
+ ```
40
+
41
+ ### Auto-extract tokens & model from provider response
29
42
 
30
- is_feature_enabled = client.evaluate_flag_bool(
31
- "new-checkout-flow",
32
- False, # default value
33
- user_context
43
+ ```python
44
+ # OpenAI
45
+ from openai import OpenAI
46
+ from variably import observe
47
+
48
+ client = OpenAI()
49
+ completion = client.chat.completions.create(
50
+ model="gpt-4o",
51
+ messages=[{"role": "user", "content": user_query}],
34
52
  )
35
53
 
36
- if is_feature_enabled:
37
- # Show new checkout flow
38
- pass
54
+ observe(
55
+ prompt=user_query,
56
+ response=completion.choices[0].message.content,
57
+ provider_response=completion, # auto-extracts model, tokens
58
+ )
59
+ ```
39
60
 
40
- # Evaluate a feature gate
41
- has_access = client.evaluate_gate("premium-features", user_context)
61
+ ```python
62
+ # Anthropic
63
+ import anthropic
64
+ from variably import observe
65
+
66
+ client = anthropic.Anthropic()
67
+ message = client.messages.create(
68
+ model="claude-sonnet-4-20250514",
69
+ max_tokens=1024,
70
+ messages=[{"role": "user", "content": user_query}],
71
+ )
42
72
 
43
- # Track events
44
- client.track({
45
- "name": "button_clicked",
46
- "user_id": "user-123",
47
- "properties": {
48
- "button_name": "checkout",
49
- "page": "product-detail"
50
- }
51
- })
73
+ observe(
74
+ prompt=user_query,
75
+ response=message.content[0].text,
76
+ provider_response=message, # auto-extracts model, tokens
77
+ )
78
+ ```
79
+
80
+ ### RAG applications — grounding & hallucination scoring
52
81
 
53
- # Clean up resources
54
- client.close()
82
+ ```python
83
+ observe(
84
+ prompt=user_query,
85
+ response=llm_answer,
86
+ provider_response=completion,
87
+ reference_materials=[
88
+ {"id": "chunk-1", "content": "Retrieved text...", "source": "docs.pdf"},
89
+ {"id": "chunk-2", "content": "Another chunk...", "source": "faq.md"},
90
+ ],
91
+ retrieval_query=user_query,
92
+ )
55
93
  ```
56
94
 
95
+ ### Multi-turn chat — coherence scoring
96
+
97
+ ```python
98
+ observe(
99
+ prompt=latest_user_message,
100
+ response=llm_answer,
101
+ provider_response=completion,
102
+ conversation_history=[
103
+ {"role": "user", "content": "What is diabetes?"},
104
+ {"role": "assistant", "content": "Diabetes is a chronic condition..."},
105
+ {"role": "user", "content": "What are the symptoms?"},
106
+ ],
107
+ session_id="conv-123",
108
+ )
109
+ ```
110
+
111
+ ### RAG applications — grounding & hallucination scoring
112
+
113
+ ```python
114
+ observe(
115
+ prompt=user_query,
116
+ response=llm_answer,
117
+ provider_response=completion,
118
+ reference_materials=[
119
+ {"id": "chunk-1", "content": "Retrieved text...", "source": "docs.pdf"},
120
+ {"id": "chunk-2", "content": "Another chunk...", "source": "faq.md"},
121
+ ],
122
+ retrieval_query=user_query,
123
+ )
124
+ ```
125
+
126
+ ### Multi-turn chat — coherence scoring
127
+
128
+ ```python
129
+ observe(
130
+ prompt=latest_user_message,
131
+ response=llm_answer,
132
+ provider_response=completion,
133
+ conversation_history=[
134
+ {"role": "user", "content": "What is diabetes?"},
135
+ {"role": "assistant", "content": "Diabetes is a chronic condition..."},
136
+ {"role": "user", "content": "What are the symptoms?"},
137
+ ],
138
+ session_id="conv-123",
139
+ )
140
+ ```
141
+
142
+ | Parameter | Type | Required | Description |
143
+ |-----------|------|----------|-------------|
144
+ | `prompt` | str | Yes | The user's input / question |
145
+ | `response` | str | Yes | The LLM's generated response |
146
+ | `provider_response` | object | No | Raw OpenAI/Anthropic/Google response — auto-extracts model, tokens |
147
+ | `model` | str | No | Model name (auto-extracted if provider_response given) |
148
+ | `provider` | str | No | "openai", "anthropic", etc. (auto-detected) |
149
+ | `latency_ms` | int | No | Response generation time in milliseconds |
150
+ | `prompt_tokens` | int | No | Input token count (auto-extracted if provider_response given) |
151
+ | `completion_tokens` | int | No | Output token count (auto-extracted) |
152
+ | `cost` | float | No | Cost in USD |
153
+ | `reference_materials` | list[dict] | No | RAG chunks: `[{"id", "content", "source"}]` — enables grounding scoring |
154
+ | `retrieval_query` | str | No | Query sent to retriever — enables retrieval quality scoring |
155
+ | `conversation_history` | list[dict] | No | Prior turns: `[{"role", "content"}]` — enables coherence scoring |
156
+ | `tags` | list[str] | No | Grouping labels, e.g. `["production", "rag"]` |
157
+ | `user_id` | str | No | Your user's ID |
158
+ | `session_id` | str | No | Conversation session ID (groups multi-turn) |
159
+ | `metadata` | dict | No | Any extra key-value data |
160
+
161
+ ---
162
+
57
163
  ## Prompt Experimentation
58
164
 
59
165
  Variably provides two modes for LLM prompt experimentation:
@@ -248,7 +354,7 @@ result = client.submit_response(
248
354
  | `workflow_history[].content` | `str` | Message content |
249
355
  | `retrieval_query` | `str` (optional) | The rewritten query sent to the retriever |
250
356
 
251
- See [Context-Aware RAG Evaluation](../../docs/concepts/context-aware-rag-evaluation.md) for the full concept doc with architecture diagrams and integration examples.
357
+ See [Context-Aware RAG Evaluation](../../docs/concepts/LLM%20Evaluation/context-aware-rag-evaluation.md) for the full concept doc with architecture diagrams and integration examples.
252
358
 
253
359
  #### Integration with LangGraph / FastAPI streaming
254
360
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "variably-sdk"
7
- version = "2.6.0"
7
+ version = "2.7.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"
@@ -464,16 +464,26 @@ class VariablyClient:
464
464
  experiment_key, dict(user_context), input_variables
465
465
  )
466
466
 
467
+ # API returns nested structure: {"experiment": {...}, "selected_variant": {...}}
468
+ experiment = response.get("experiment", {})
469
+ variant = response.get("selected_variant", {})
470
+ params = variant.get("parameters", {})
471
+
472
+ # Extract model/provider from nested parameters.llmConfig
473
+ llm_config = params.get("llmConfig", params)
474
+ model = llm_config.get("model", "")
475
+ provider = llm_config.get("provider", "")
476
+
467
477
  return PromptVariant(
468
- experiment_id=response["experiment_id"],
469
- experiment_key=response["experiment_key"],
470
- variant_id=response["variant_id"],
471
- variant_key=response["variant_key"],
472
- prompt_template=response["prompt_template"],
473
- model=response["model"],
474
- provider=response["provider"],
475
- parameters=response.get("parameters", {}),
476
- is_control=response.get("is_control", False),
478
+ experiment_id=experiment.get("experiment_id", experiment.get("id", "")),
479
+ experiment_key=experiment_key,
480
+ variant_id=variant.get("variant_id", variant.get("id", "")),
481
+ variant_key=variant.get("variant_key", variant.get("name", "")),
482
+ prompt_template=variant.get("prompt_template", variant.get("prompt", "")),
483
+ model=model,
484
+ provider=provider,
485
+ parameters=params,
486
+ is_control=variant.get("is_control", variant.get("isControl", False)),
477
487
  )
478
488
 
479
489
  def evaluate_prompt(
@@ -601,41 +611,49 @@ class VariablyClient:
601
611
  prompt: Union[str, List[Dict[str, Any]]],
602
612
  response: str,
603
613
  *,
604
- conversation_history: Optional[List[Dict[str, Any]]] = None,
605
- reference_materials: Optional[List[Dict[str, Any]]] = None,
606
- retrieval_query: Optional[str] = None,
607
- tags: Optional[List[str]] = None,
608
- user_id: Optional[str] = None,
609
- session_id: Optional[str] = None,
610
- provider: Optional[str] = None,
611
- model: Optional[str] = None,
612
- latency_ms: Optional[int] = None,
613
- prompt_tokens: Optional[int] = None,
614
- completion_tokens: Optional[int] = None,
615
- cost: Optional[float] = None,
616
- metadata: Optional[Dict[str, Any]] = None,
614
+ # RAG / Grounding context
615
+ reference_materials: Optional[List[Dict[str, Any]]] = None, # Retrieved docs for grounding scoring
616
+ retrieval_query: Optional[str] = None, # The actual search query (if rewritten/expanded)
617
+ # Multi-turn / Session context
618
+ session_id: Optional[str] = None, # Groups conversation turns into sessions
619
+ conversation_history: Optional[List[Dict[str, Any]]] = None, # Prior turns for coherence scoring
620
+ # User & organization context
621
+ user_id: Optional[str] = None, # End-user ID for per-user analytics
622
+ tags: Optional[List[str]] = None, # Grouping labels, e.g. ["production", "rag"]
623
+ metadata: Optional[Dict[str, Any]] = None, # Arbitrary key-value data
624
+ # LLM metadata
625
+ provider: Optional[str] = None, # e.g. "openai", "anthropic"
626
+ model: Optional[str] = None, # e.g. "gpt-4o", "claude-3.5-sonnet"
627
+ latency_ms: Optional[int] = None, # Response time in ms
628
+ prompt_tokens: Optional[int] = None, # Input token count
629
+ completion_tokens: Optional[int] = None, # Output token count
630
+ cost: Optional[float] = None, # Cost in USD
617
631
  ) -> LogResult:
618
- """Log a prompt/response pair for 43-dimension evaluation (Observe Mode).
632
+ """Log a prompt/response pair for multi-dimensional evaluation (Observe Mode).
619
633
 
620
634
  Zero-friction entry point — no experiment creation needed. Just log your
621
635
  prompts and responses and see quality scores in the dashboard.
622
636
 
623
637
  Args:
624
- prompt: The prompt sent to the LLM (str or messages list).
625
- response: The LLM's response text.
626
- conversation_history: Prior conversation turns for cross-turn coherence scoring.
627
- reference_materials: Retrieved RAG chunks for grounding/hallucination scoring.
628
- retrieval_query: The query sent to the retriever (for retrieval relevance scoring).
629
- tags: Optional tags for grouping observations (e.g. ["production", "chatbot"]).
630
- user_id: End-user ID for per-user analytics.
631
- session_id: Session ID for multi-turn tracking.
638
+ prompt: The user's input / question (str or messages list).
639
+ response: The AI's generated response text.
640
+ reference_materials: Retrieved docs (RAG chunks) for grounding scoring.
641
+ Enables: faithfulness, hallucination rate, attribution accuracy.
642
+ retrieval_query: The query sent to your retriever/search.
643
+ If your pipeline rewrites queries, pass the rewritten version.
644
+ session_id: Conversation/thread ID — groups turns into sessions.
645
+ conversation_history: Prior turns for coherence scoring.
646
+ Each dict: {"role": "user"/"assistant", "content": "..."}.
647
+ Not needed if session_id is provided (auto-built from stored turns).
648
+ user_id: End-user ID for per-user quality analytics.
649
+ tags: Grouping labels for filtering (e.g. ["production", "chatbot"]).
650
+ metadata: Arbitrary key-value data attached to this evaluation.
632
651
  provider: LLM provider name (e.g. "openai", "anthropic").
633
- model: Model name (e.g. "gpt-4", "claude-3-opus").
652
+ model: Model name (e.g. "gpt-4o", "claude-3.5-sonnet").
634
653
  latency_ms: LLM call latency in milliseconds.
635
654
  prompt_tokens: Number of input tokens.
636
655
  completion_tokens: Number of output tokens.
637
656
  cost: Cost in USD for this LLM call.
638
- metadata: Arbitrary key-value metadata.
639
657
 
640
658
  Returns:
641
659
  LogResult with observation_id and status.
@@ -142,58 +142,85 @@ def observe(
142
142
  prompt: str,
143
143
  response: str,
144
144
  *,
145
- # Auto-extraction from provider response object
145
+ # --- Auto-extraction ---
146
+ # Pass the raw LLM response object (OpenAI, Anthropic, Google).
147
+ # Model name, token counts, and provider are auto-detected — no manual setup needed.
146
148
  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,
149
+
150
+ # --- LLM metadata (manual overrides — take precedence over auto-extracted values) ---
151
+ model: Optional[str] = None, # e.g. "gpt-4o", "claude-3.5-sonnet"
152
+ provider: Optional[str] = None, # e.g. "openai", "anthropic", "google"
153
+ latency_ms: Optional[int] = None, # Response time in milliseconds
154
+ prompt_tokens: Optional[int] = None, # Input token count
155
+ completion_tokens: Optional[int] = None, # Output token count
156
+ cost: Optional[float] = None, # Cost in USD for this LLM call
157
+
158
+ # --- RAG / Grounding context ---
159
+ # Pass your retrieved documents to enable grounding scoring:
160
+ # faithfulness, hallucination rate, attribution accuracy, context utilization.
161
+ reference_materials: Optional[List[Dict[str, Any]]] = None, # [{"id": "chunk-1", "content": "...", "source": "docs.pdf"}]
162
+ # If your pipeline rewrites/expands queries before search, pass the actual search query here.
163
+ # Enables retrieval relevance scoring. If not provided, the raw prompt is used.
156
164
  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,
165
+
166
+ # --- Multi-turn / Session context ---
167
+ # Groups conversation turns into sessions. Enables session-level metrics,
168
+ # quality trend tracking, and automatic coherence scoring across turns.
169
+ # Use your conversation/thread ID — any unique string per conversation.
162
170
  session_id: Optional[str] = None,
163
- metadata: Optional[Dict[str, Any]] = None,
171
+ # Explicit prior turns for coherence scoring. Each dict: {"role": "user"/"assistant", "content": "..."}.
172
+ # Advanced: not needed if you pass session_id (variA/Bly auto-builds history from previous turns).
173
+ prior_turns: Optional[List[Dict[str, str]]] = None,
174
+
175
+ # --- User & organization context ---
176
+ user_id: Optional[str] = None, # Your end-user's ID — enables per-user analytics
177
+ tags: Optional[List[str]] = None, # Grouping labels, e.g. ["production", "rag", "v2"]
178
+ metadata: Optional[Dict[str, Any]] = None, # Any arbitrary key-value data for filtering
179
+
180
+ # --- Backward compatibility ---
181
+ conversation_history: Optional[List[Dict[str, str]]] = None, # Deprecated: use prior_turns instead
164
182
  ) -> Optional[Dict[str, Any]]:
165
- """Log a prompt/response pair to Variably for multi-dimension evaluation.
183
+ """Log a prompt/response pair to variA/Bly for multi-dimensional evaluation.
184
+
185
+ Add this one call after your LLM generates a response. All parameters are
186
+ optional except ``prompt`` and ``response``. The more context you provide,
187
+ the deeper the evaluation.
166
188
 
167
189
  Never raises. Returns ``{"observation_id": "...", "status": "queued"}``
168
190
  on success, or ``None`` on any error (missing API key, network failure, etc.).
169
191
 
170
192
  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.
193
+ prompt: The user's input / question — exactly what the user typed.
194
+ response: The AI's generated response text.
195
+
196
+ provider_response: Raw LLM response object (OpenAI, Anthropic, Google).
197
+ Auto-extracts model name, token counts, and provider via duck typing.
198
+
199
+ model: Model name override (e.g. ``"gpt-4o"``). Overrides auto-extracted value.
200
+ provider: Provider override (e.g. ``"openai"``). Overrides auto-detected value.
180
201
  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.
202
+ prompt_tokens: Input token count. Overrides auto-extracted value.
203
+ completion_tokens: Output token count. Overrides auto-extracted value.
185
204
  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.
205
+
206
+ reference_materials: Retrieved documents (RAG chunks) for grounding scoring.
207
+ Enables: faithfulness, hallucination rate, attribution accuracy.
208
+ Each dict should have ``"id"``, ``"content"``, and optionally ``"source"``.
209
+ retrieval_query: The query your pipeline sent to the retriever/search.
210
+ If your pipeline rewrites queries before search, pass the rewritten version.
211
+ Enables accurate retrieval relevance scoring.
212
+
213
+ session_id: Conversation/thread ID — groups multi-turn interactions into sessions.
214
+ Enables: session tracing, quality trends, degradation detection, auto-coherence.
215
+ prior_turns: Explicit prior conversation turns for coherence scoring.
216
+ Each dict: ``{"role": "user"/"assistant", "content": "..."}``.
217
+ Not needed if you pass ``session_id`` (history is auto-built from stored turns).
218
+
219
+ user_id: Your end-user's ID for per-user quality analytics.
220
+ tags: Grouping labels for filtering (e.g. ``["production", "rag"]``).
221
+ metadata: Arbitrary key-value data attached to this evaluation.
222
+
223
+ conversation_history: Deprecated — use ``prior_turns`` instead.
197
224
 
198
225
  Returns:
199
226
  Dict with ``observation_id`` and ``status`` on success, or ``None``.
@@ -217,6 +244,9 @@ def observe(
217
244
  else auto.get("completion_tokens")
218
245
  )
219
246
 
247
+ # Resolve prior_turns: prefer prior_turns, fall back to deprecated conversation_history
248
+ resolved_prior_turns = prior_turns or conversation_history
249
+
220
250
  result = client.log(
221
251
  prompt=prompt,
222
252
  response=response,
@@ -228,7 +258,7 @@ def observe(
228
258
  cost=cost,
229
259
  reference_materials=reference_materials,
230
260
  retrieval_query=retrieval_query,
231
- conversation_history=conversation_history,
261
+ conversation_history=resolved_prior_turns, # client.log still uses conversation_history internally
232
262
  tags=tags,
233
263
  user_id=user_id,
234
264
  session_id=session_id,
@@ -1,3 +1,3 @@
1
1
  """Version information for Variably SDK."""
2
2
 
3
- __version__ = "2.6.0"
3
+ __version__ = "2.6.1"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: variably-sdk
3
- Version: 2.6.0
3
+ Version: 2.7.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>
@@ -45,7 +45,7 @@ Dynamic: requires-python
45
45
 
46
46
  # Variably Python SDK
47
47
 
48
- Official Python SDK for Variably — feature flags, LLM experimentation, and prompt optimization.
48
+ Official Python SDK for Variably — LLM evaluation, experimentation, and prompt optimization.
49
49
 
50
50
  ## Installation
51
51
 
@@ -53,52 +53,158 @@ Official Python SDK for Variably — feature flags, LLM experimentation, and pro
53
53
  pip install variably-sdk
54
54
  ```
55
55
 
56
- ## Quick Start
56
+ For Docker/Kubernetes deployments, add to your `requirements.txt`:
57
+
58
+ ```
59
+ variably-sdk>=2.6.1
60
+ ```
61
+ ## Quick Start — Observe Mode
62
+
63
+ Add **one line** to your existing LLM app and get **multi-dimension evaluation** across 40+ metrics in 6 categories: Quality, Safety, Semantic, Grounding, Coherence, and Advanced.
64
+
65
+ No experiment setup. No prompt migration. Just log and see scores.
66
+
67
+ ### 1. Set your environment variables
68
+
69
+ ```bash
70
+ VARIABLY_API_KEY=vbl_your_key_here
71
+ VARIABLY_BASE_URL=https://api.variably.tech
72
+ ```
73
+
74
+ ### 2. Add one line after your LLM call
57
75
 
58
76
  ```python
59
- from variably import VariablyClient
77
+ from variably import observe
60
78
 
61
- # Initialize the client
62
- client = VariablyClient({
63
- "api_key": "your-api-key",
64
- "base_url": "https://api.variably.com", # optional, defaults to localhost:8080
65
- "environment": "production" # optional
66
- })
79
+ # Your existing code (unchanged)
80
+ response = your_llm_call(user_query)
67
81
 
68
- # Evaluate a boolean feature flag
69
- user_context = {
70
- "user_id": "user-123",
71
- "email": "user@example.com",
72
- "country": "US"
73
- }
82
+ # Add this line:
83
+ observe(prompt=user_query, response=response)
84
+ ```
85
+
86
+ ### Auto-extract tokens & model from provider response
74
87
 
75
- is_feature_enabled = client.evaluate_flag_bool(
76
- "new-checkout-flow",
77
- False, # default value
78
- user_context
88
+ ```python
89
+ # OpenAI
90
+ from openai import OpenAI
91
+ from variably import observe
92
+
93
+ client = OpenAI()
94
+ completion = client.chat.completions.create(
95
+ model="gpt-4o",
96
+ messages=[{"role": "user", "content": user_query}],
79
97
  )
80
98
 
81
- if is_feature_enabled:
82
- # Show new checkout flow
83
- pass
99
+ observe(
100
+ prompt=user_query,
101
+ response=completion.choices[0].message.content,
102
+ provider_response=completion, # auto-extracts model, tokens
103
+ )
104
+ ```
84
105
 
85
- # Evaluate a feature gate
86
- has_access = client.evaluate_gate("premium-features", user_context)
106
+ ```python
107
+ # Anthropic
108
+ import anthropic
109
+ from variably import observe
110
+
111
+ client = anthropic.Anthropic()
112
+ message = client.messages.create(
113
+ model="claude-sonnet-4-20250514",
114
+ max_tokens=1024,
115
+ messages=[{"role": "user", "content": user_query}],
116
+ )
87
117
 
88
- # Track events
89
- client.track({
90
- "name": "button_clicked",
91
- "user_id": "user-123",
92
- "properties": {
93
- "button_name": "checkout",
94
- "page": "product-detail"
95
- }
96
- })
118
+ observe(
119
+ prompt=user_query,
120
+ response=message.content[0].text,
121
+ provider_response=message, # auto-extracts model, tokens
122
+ )
123
+ ```
124
+
125
+ ### RAG applications — grounding & hallucination scoring
97
126
 
98
- # Clean up resources
99
- client.close()
127
+ ```python
128
+ observe(
129
+ prompt=user_query,
130
+ response=llm_answer,
131
+ provider_response=completion,
132
+ reference_materials=[
133
+ {"id": "chunk-1", "content": "Retrieved text...", "source": "docs.pdf"},
134
+ {"id": "chunk-2", "content": "Another chunk...", "source": "faq.md"},
135
+ ],
136
+ retrieval_query=user_query,
137
+ )
100
138
  ```
101
139
 
140
+ ### Multi-turn chat — coherence scoring
141
+
142
+ ```python
143
+ observe(
144
+ prompt=latest_user_message,
145
+ response=llm_answer,
146
+ provider_response=completion,
147
+ conversation_history=[
148
+ {"role": "user", "content": "What is diabetes?"},
149
+ {"role": "assistant", "content": "Diabetes is a chronic condition..."},
150
+ {"role": "user", "content": "What are the symptoms?"},
151
+ ],
152
+ session_id="conv-123",
153
+ )
154
+ ```
155
+
156
+ ### RAG applications — grounding & hallucination scoring
157
+
158
+ ```python
159
+ observe(
160
+ prompt=user_query,
161
+ response=llm_answer,
162
+ provider_response=completion,
163
+ reference_materials=[
164
+ {"id": "chunk-1", "content": "Retrieved text...", "source": "docs.pdf"},
165
+ {"id": "chunk-2", "content": "Another chunk...", "source": "faq.md"},
166
+ ],
167
+ retrieval_query=user_query,
168
+ )
169
+ ```
170
+
171
+ ### Multi-turn chat — coherence scoring
172
+
173
+ ```python
174
+ observe(
175
+ prompt=latest_user_message,
176
+ response=llm_answer,
177
+ provider_response=completion,
178
+ conversation_history=[
179
+ {"role": "user", "content": "What is diabetes?"},
180
+ {"role": "assistant", "content": "Diabetes is a chronic condition..."},
181
+ {"role": "user", "content": "What are the symptoms?"},
182
+ ],
183
+ session_id="conv-123",
184
+ )
185
+ ```
186
+
187
+ | Parameter | Type | Required | Description |
188
+ |-----------|------|----------|-------------|
189
+ | `prompt` | str | Yes | The user's input / question |
190
+ | `response` | str | Yes | The LLM's generated response |
191
+ | `provider_response` | object | No | Raw OpenAI/Anthropic/Google response — auto-extracts model, tokens |
192
+ | `model` | str | No | Model name (auto-extracted if provider_response given) |
193
+ | `provider` | str | No | "openai", "anthropic", etc. (auto-detected) |
194
+ | `latency_ms` | int | No | Response generation time in milliseconds |
195
+ | `prompt_tokens` | int | No | Input token count (auto-extracted if provider_response given) |
196
+ | `completion_tokens` | int | No | Output token count (auto-extracted) |
197
+ | `cost` | float | No | Cost in USD |
198
+ | `reference_materials` | list[dict] | No | RAG chunks: `[{"id", "content", "source"}]` — enables grounding scoring |
199
+ | `retrieval_query` | str | No | Query sent to retriever — enables retrieval quality scoring |
200
+ | `conversation_history` | list[dict] | No | Prior turns: `[{"role", "content"}]` — enables coherence scoring |
201
+ | `tags` | list[str] | No | Grouping labels, e.g. `["production", "rag"]` |
202
+ | `user_id` | str | No | Your user's ID |
203
+ | `session_id` | str | No | Conversation session ID (groups multi-turn) |
204
+ | `metadata` | dict | No | Any extra key-value data |
205
+
206
+ ---
207
+
102
208
  ## Prompt Experimentation
103
209
 
104
210
  Variably provides two modes for LLM prompt experimentation:
@@ -293,7 +399,7 @@ result = client.submit_response(
293
399
  | `workflow_history[].content` | `str` | Message content |
294
400
  | `retrieval_query` | `str` (optional) | The rewritten query sent to the retriever |
295
401
 
296
- See [Context-Aware RAG Evaluation](../../docs/concepts/context-aware-rag-evaluation.md) for the full concept doc with architecture diagrams and integration examples.
402
+ See [Context-Aware RAG Evaluation](../../docs/concepts/LLM%20Evaluation/context-aware-rag-evaluation.md) for the full concept doc with architecture diagrams and integration examples.
297
403
 
298
404
  #### Integration with LangGraph / FastAPI streaming
299
405
 
File without changes
File without changes