Skip to content

Commit 0faf930

Browse files
esteiningerclaude
andcommitted
feat(auto-tune): SDK convenience for closing the feedback loop
Add create_interaction_from_result() and create_interactions_batch() helpers that auto-extract feature_id, position, execution_id, and feature_uri from search results — one-liner interaction creation. Add examples/auto_tune.py showing full lifecycle: create retriever with learned fusion → execute → record interaction → verify personalization. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f25e419 commit 0faf930

2 files changed

Lines changed: 369 additions & 0 deletions

File tree

examples/auto_tune.py

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
"""
2+
Mixpeek Auto-Tune Example — Learned Fusion + Interaction Feedback Loop
3+
4+
Demonstrates:
5+
1. Creating a retriever with learned fusion (multi-feature, bandit-learned weights)
6+
2. Executing a search query
7+
3. Recording user interactions to train the learned fusion model
8+
4. Verifying personalisation by re-querying
9+
10+
Before running, set your environment:
11+
12+
export MIXPEEK_API_KEY="your_api_key_here"
13+
export MIXPEEK_NAMESPACE="your_namespace_id"
14+
"""
15+
16+
from mixpeek import Mixpeek, ApiException
17+
from mixpeek_dev.interactions import create_interaction_from_result
18+
19+
20+
def main():
21+
try:
22+
client = Mixpeek()
23+
except ValueError as e:
24+
print(f"Error: {e}")
25+
print("Set MIXPEEK_API_KEY and MIXPEEK_NAMESPACE environment variables.")
26+
return
27+
28+
print("Mixpeek Auto-Tune Example\n")
29+
30+
# ----------------------------------------------------------------
31+
# 1. Create a retriever with learned fusion
32+
# ----------------------------------------------------------------
33+
print("1. Creating retriever with learned fusion...")
34+
35+
retriever_config = {
36+
"name": "auto_tune_demo",
37+
"stages": [
38+
{
39+
"stage_type": "search",
40+
"stage_id": "multi_feature_search",
41+
"config": {
42+
"queries": [
43+
{
44+
"feature_uri": "mixpeek://text-embedding@latest/description",
45+
"query_input": {"type": "text", "value": "INPUT.query"},
46+
},
47+
{
48+
"feature_uri": "mixpeek://text-embedding@latest/title",
49+
"query_input": {"type": "text", "value": "INPUT.query"},
50+
},
51+
],
52+
"parameters": {
53+
"limit": 20,
54+
"fusion_strategy": {
55+
"fusion": "learned",
56+
"learning_config": {
57+
"context_features": ["INPUT.user_id"],
58+
"reward_signal": "click",
59+
},
60+
},
61+
},
62+
},
63+
}
64+
],
65+
}
66+
67+
try:
68+
retriever = client.retrievers.create(**retriever_config)
69+
retriever_id = retriever["retriever_id"]
70+
print(f" Created retriever: {retriever_id}")
71+
except ApiException as e:
72+
print(f" Error creating retriever: {e.reason}")
73+
return
74+
75+
# ----------------------------------------------------------------
76+
# 2. Execute a search query
77+
# ----------------------------------------------------------------
78+
print("\n2. Executing search query...")
79+
80+
user_id = "user_123"
81+
session_id = "sess_abc"
82+
query = "comfortable running shoes"
83+
84+
try:
85+
results = client.retrievers.execute(
86+
retriever_id,
87+
inputs={
88+
"query": query,
89+
"user_id": user_id,
90+
"session_id": session_id,
91+
},
92+
)
93+
documents = results.get("documents", [])
94+
print(f" Got {len(documents)} results")
95+
for i, doc in enumerate(documents[:5]):
96+
score = doc.get("score", 0)
97+
doc_id = doc.get("document_id", "?")
98+
print(f" [{i}] {doc_id} (score: {score:.4f})")
99+
except ApiException as e:
100+
print(f" Search error: {e.reason}")
101+
return
102+
103+
if not documents:
104+
print(" No documents returned. Add data to your collection first.")
105+
return
106+
107+
# ----------------------------------------------------------------
108+
# 3. Record a user interaction (click on 3rd result)
109+
# ----------------------------------------------------------------
110+
print("\n3. Recording interaction (click on result #2)...")
111+
112+
clicked_index = 2 if len(documents) > 2 else 0
113+
try:
114+
interaction = create_interaction_from_result(
115+
client=client,
116+
retriever_id=retriever_id,
117+
result=documents[clicked_index],
118+
interaction_type=["click"],
119+
position=clicked_index,
120+
user_id=user_id,
121+
session_id=session_id,
122+
execution_response=results,
123+
query_snapshot={"query": query},
124+
)
125+
print(f" Recorded interaction: {interaction.get('interaction_id', '?')}")
126+
except Exception as e:
127+
print(f" Interaction error: {e}")
128+
129+
# ----------------------------------------------------------------
130+
# 4. Re-query to see personalised results
131+
# ----------------------------------------------------------------
132+
print("\n4. Re-querying with same user (weights should start adapting)...")
133+
134+
try:
135+
results2 = client.retrievers.execute(
136+
retriever_id,
137+
inputs={
138+
"query": query,
139+
"user_id": user_id,
140+
"session_id": session_id,
141+
},
142+
)
143+
documents2 = results2.get("documents", [])
144+
print(f" Got {len(documents2)} results")
145+
for i, doc in enumerate(documents2[:5]):
146+
score = doc.get("score", 0)
147+
doc_id = doc.get("document_id", "?")
148+
print(f" [{i}] {doc_id} (score: {score:.4f})")
149+
except ApiException as e:
150+
print(f" Re-query error: {e.reason}")
151+
152+
# ----------------------------------------------------------------
153+
# 5. Clean up
154+
# ----------------------------------------------------------------
155+
print("\n5. Cleaning up...")
156+
try:
157+
client.retrievers.delete(retriever_id)
158+
print(f" Deleted retriever: {retriever_id}")
159+
except ApiException as e:
160+
print(f" Cleanup error: {e.reason}")
161+
162+
print("\nAuto-Tune demo complete!")
163+
print("\nThe learned fusion model improves over time as more interactions")
164+
print("are recorded. In production, weights adapt per-user based on the")
165+
print("context_features configured in learning_config.")
166+
167+
168+
if __name__ == "__main__":
169+
main()

mixpeek_dev/interactions.py

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
"""Convenience helpers for recording retriever interactions.
2+
3+
These utilities auto-populate interaction fields from search results,
4+
reducing boilerplate when integrating feedback loops (Auto-Tune).
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from typing import Any, Dict, List, Optional, TYPE_CHECKING, Union
10+
11+
if TYPE_CHECKING:
12+
from mixpeek._client.client import Mixpeek
13+
14+
15+
def create_interaction_from_result(
16+
client: Mixpeek,
17+
retriever_id: str,
18+
result: Dict[str, Any],
19+
interaction_type: List[str],
20+
*,
21+
position: Optional[int] = None,
22+
user_id: Optional[str] = None,
23+
session_id: Optional[str] = None,
24+
execution_id: Optional[str] = None,
25+
query_snapshot: Optional[Dict[str, Any]] = None,
26+
metadata: Optional[Dict[str, Any]] = None,
27+
result_set_size: Optional[int] = None,
28+
execution_response: Optional[Dict[str, Any]] = None,
29+
) -> Dict[str, Any]:
30+
"""Create an interaction from a retriever result document.
31+
32+
Auto-extracts ``feature_id``, ``document_score``, and ``execution_id``
33+
from the result document and/or the full execution response so callers
34+
don't have to pluck them out manually.
35+
36+
Args:
37+
client: An initialised ``Mixpeek`` client instance.
38+
retriever_id: The retriever that produced the results.
39+
result: A single document dict from the execution response's
40+
``documents`` list. The helper looks for ``document_id``
41+
(or ``feature_id``), ``score``, and payload metadata.
42+
interaction_type: One or more interaction type strings
43+
(e.g. ``["click"]``, ``["view", "long_view"]``).
44+
position: 0-indexed position of the result. If *None* and the
45+
result dict contains a ``position`` key, that value is used.
46+
Falls back to ``0``.
47+
user_id: Persistent user identifier for personalisation.
48+
session_id: Ephemeral session identifier.
49+
execution_id: Explicit execution ID. Auto-extracted from
50+
*execution_response* or from the result's metadata when not
51+
provided.
52+
query_snapshot: The original query input dict. Recommended for
53+
training optimisation.
54+
metadata: Extra context (device, viewport, duration, etc.).
55+
The helper merges in ``feature_uri`` from the result's
56+
payload when present.
57+
result_set_size: Total results shown to the user. Auto-extracted
58+
from *execution_response* when not provided.
59+
execution_response: The full dict returned by
60+
``client.retrievers.execute()``. Used to auto-populate
61+
``execution_id`` and ``result_set_size``.
62+
63+
Returns:
64+
The API response dict (contains ``interaction_id`` and
65+
``timestamp``).
66+
67+
Example::
68+
69+
results = client.retrievers.execute(
70+
retriever_id, inputs={"query": "shoes", "user_id": "u1"}
71+
)
72+
create_interaction_from_result(
73+
client=client,
74+
retriever_id=retriever_id,
75+
result=results["documents"][2],
76+
interaction_type=["click"],
77+
user_id="u1",
78+
execution_response=results,
79+
)
80+
"""
81+
# --- feature_id --------------------------------------------------------
82+
feature_id = (
83+
result.get("feature_id")
84+
or result.get("document_id")
85+
or result.get("id")
86+
)
87+
if not feature_id:
88+
raise ValueError(
89+
"Cannot determine feature_id from result dict. "
90+
"Expected 'feature_id', 'document_id', or 'id' key."
91+
)
92+
93+
# --- position ----------------------------------------------------------
94+
if position is None:
95+
position = result.get("position", 0)
96+
97+
# --- document_score ----------------------------------------------------
98+
document_score = result.get("score")
99+
100+
# --- execution_id (from explicit arg, execution_response, or result) ---
101+
if execution_id is None and execution_response is not None:
102+
execution_id = execution_response.get("execution_id")
103+
if execution_id is None:
104+
execution_id = result.get("execution_id")
105+
106+
# --- result_set_size ---------------------------------------------------
107+
if result_set_size is None and execution_response is not None:
108+
docs = execution_response.get("documents")
109+
if docs is not None:
110+
result_set_size = len(docs)
111+
112+
# --- metadata enrichment -----------------------------------------------
113+
merged_metadata: Dict[str, Any] = {}
114+
# Pull feature_uri from payload if present
115+
payload = result.get("payload", {})
116+
if isinstance(payload, dict):
117+
feature_uri = payload.get("feature_uri")
118+
if feature_uri:
119+
merged_metadata["feature_uri"] = feature_uri
120+
if metadata:
121+
merged_metadata.update(metadata)
122+
123+
# --- build request body ------------------------------------------------
124+
body: Dict[str, Any] = {
125+
"feature_id": feature_id,
126+
"interaction_type": interaction_type,
127+
"position": position,
128+
"retriever_id": retriever_id,
129+
}
130+
if user_id is not None:
131+
body["user_id"] = user_id
132+
if session_id is not None:
133+
body["session_id"] = session_id
134+
if execution_id is not None:
135+
body["execution_id"] = execution_id
136+
if query_snapshot is not None:
137+
body["query_snapshot"] = query_snapshot
138+
if document_score is not None:
139+
body["document_score"] = document_score
140+
if result_set_size is not None:
141+
body["result_set_size"] = result_set_size
142+
if merged_metadata:
143+
body["metadata"] = merged_metadata
144+
145+
return client._request(
146+
"POST",
147+
f"/retrievers/{retriever_id}/interactions",
148+
body=body,
149+
)
150+
151+
152+
def create_interactions_batch(
153+
client: Mixpeek,
154+
retriever_id: str,
155+
results: List[Dict[str, Any]],
156+
interaction_type: List[str],
157+
*,
158+
user_id: Optional[str] = None,
159+
session_id: Optional[str] = None,
160+
execution_response: Optional[Dict[str, Any]] = None,
161+
query_snapshot: Optional[Dict[str, Any]] = None,
162+
metadata: Optional[Dict[str, Any]] = None,
163+
) -> List[Dict[str, Any]]:
164+
"""Record interactions for multiple results in one call.
165+
166+
Convenience wrapper that calls ``create_interaction_from_result`` for
167+
each result, using the list index as the position.
168+
169+
Args:
170+
client: An initialised ``Mixpeek`` client.
171+
retriever_id: The retriever that produced the results.
172+
results: List of result dicts (e.g. all results the user saw).
173+
interaction_type: Interaction type(s) to record for every result.
174+
user_id: Persistent user identifier.
175+
session_id: Ephemeral session identifier.
176+
execution_response: Full execution response (for auto-extraction).
177+
query_snapshot: Original query input dict.
178+
metadata: Extra context dict applied to every interaction.
179+
180+
Returns:
181+
List of API response dicts.
182+
"""
183+
responses = []
184+
result_set_size = len(results)
185+
for idx, result in enumerate(results):
186+
resp = create_interaction_from_result(
187+
client=client,
188+
retriever_id=retriever_id,
189+
result=result,
190+
interaction_type=interaction_type,
191+
position=idx,
192+
user_id=user_id,
193+
session_id=session_id,
194+
execution_response=execution_response,
195+
query_snapshot=query_snapshot,
196+
metadata=metadata,
197+
result_set_size=result_set_size,
198+
)
199+
responses.append(resp)
200+
return responses

0 commit comments

Comments
 (0)