livepilot 1.9.22 → 1.9.24
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.
- package/.claude-plugin/marketplace.json +3 -3
- package/.mcpbignore +40 -0
- package/AGENTS.md +3 -3
- package/CHANGELOG.md +84 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +141 -72
- package/bin/livepilot.js +135 -0
- package/livepilot/.Codex-plugin/plugin.json +2 -2
- package/livepilot/.claude-plugin/plugin.json +2 -2
- package/livepilot/agents/livepilot-producer/AGENT.md +13 -0
- package/livepilot/commands/arrange.md +42 -23
- package/livepilot/commands/mix.md +34 -19
- package/livepilot/commands/perform.md +31 -19
- package/livepilot/commands/sounddesign.md +38 -25
- package/livepilot/skills/livepilot-arrangement/SKILL.md +2 -1
- package/livepilot/skills/livepilot-composition-engine/references/transition-archetypes.md +2 -2
- package/livepilot/skills/livepilot-core/SKILL.md +60 -4
- package/livepilot/skills/livepilot-core/references/device-atlas/distortion-and-character.md +11 -11
- package/livepilot/skills/livepilot-core/references/device-atlas/drums-and-percussion.md +25 -25
- package/livepilot/skills/livepilot-core/references/device-atlas/dynamics-and-punch.md +21 -21
- package/livepilot/skills/livepilot-core/references/device-atlas/eq-and-filtering.md +13 -13
- package/livepilot/skills/livepilot-core/references/device-atlas/midi-tools.md +13 -13
- package/livepilot/skills/livepilot-core/references/device-atlas/movement-and-modulation.md +5 -5
- package/livepilot/skills/livepilot-core/references/device-atlas/space-and-depth.md +16 -16
- package/livepilot/skills/livepilot-core/references/device-atlas/spectral-and-weird.md +40 -40
- package/livepilot/skills/livepilot-core/references/m4l-devices.md +3 -3
- package/livepilot/skills/livepilot-core/references/overview.md +4 -4
- package/livepilot/skills/livepilot-evaluation/SKILL.md +12 -8
- package/livepilot/skills/livepilot-evaluation/references/memory-promotion.md +2 -2
- package/livepilot/skills/livepilot-mix-engine/SKILL.md +1 -1
- package/livepilot/skills/livepilot-mix-engine/references/mix-moves.md +2 -2
- package/livepilot/skills/livepilot-mixing/SKILL.md +3 -1
- package/livepilot/skills/livepilot-notes/SKILL.md +2 -1
- package/livepilot/skills/livepilot-release/SKILL.md +29 -15
- package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +2 -2
- package/livepilot/skills/livepilot-wonder/SKILL.md +62 -0
- package/livepilot.mcpb +0 -0
- package/manifest.json +91 -0
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/creative_constraints/__init__.py +6 -0
- package/mcp_server/creative_constraints/engine.py +277 -0
- package/mcp_server/creative_constraints/models.py +75 -0
- package/mcp_server/creative_constraints/tools.py +341 -0
- package/mcp_server/experiment/__init__.py +6 -0
- package/mcp_server/experiment/engine.py +213 -0
- package/mcp_server/experiment/models.py +120 -0
- package/mcp_server/experiment/tools.py +263 -0
- package/mcp_server/hook_hunter/__init__.py +5 -0
- package/mcp_server/hook_hunter/analyzer.py +365 -0
- package/mcp_server/hook_hunter/models.py +58 -0
- package/mcp_server/hook_hunter/tools.py +588 -0
- package/mcp_server/memory/taste_graph.py +328 -0
- package/mcp_server/memory/tools.py +99 -0
- package/mcp_server/mix_engine/critics.py +2 -2
- package/mcp_server/mix_engine/models.py +1 -1
- package/mcp_server/mix_engine/state_builder.py +2 -2
- package/mcp_server/musical_intelligence/__init__.py +8 -0
- package/mcp_server/musical_intelligence/detectors.py +434 -0
- package/mcp_server/musical_intelligence/phrase_critic.py +163 -0
- package/mcp_server/musical_intelligence/tools.py +224 -0
- package/mcp_server/persistence/__init__.py +1 -0
- package/mcp_server/persistence/base_store.py +82 -0
- package/mcp_server/persistence/project_store.py +106 -0
- package/mcp_server/persistence/taste_store.py +122 -0
- package/mcp_server/preview_studio/__init__.py +5 -0
- package/mcp_server/preview_studio/engine.py +280 -0
- package/mcp_server/preview_studio/models.py +74 -0
- package/mcp_server/preview_studio/tools.py +466 -0
- package/mcp_server/runtime/capability.py +66 -0
- package/mcp_server/runtime/capability_probe.py +118 -0
- package/mcp_server/runtime/execution_router.py +139 -0
- package/mcp_server/runtime/remote_commands.py +82 -0
- package/mcp_server/runtime/session_kernel.py +96 -0
- package/mcp_server/runtime/tools.py +90 -1
- package/mcp_server/semantic_moves/__init__.py +13 -0
- package/mcp_server/semantic_moves/compiler.py +116 -0
- package/mcp_server/semantic_moves/mix_compilers.py +291 -0
- package/mcp_server/semantic_moves/mix_moves.py +157 -0
- package/mcp_server/semantic_moves/models.py +46 -0
- package/mcp_server/semantic_moves/performance_compilers.py +208 -0
- package/mcp_server/semantic_moves/performance_moves.py +81 -0
- package/mcp_server/semantic_moves/registry.py +32 -0
- package/mcp_server/semantic_moves/resolvers.py +126 -0
- package/mcp_server/semantic_moves/sound_design_compilers.py +266 -0
- package/mcp_server/semantic_moves/sound_design_moves.py +78 -0
- package/mcp_server/semantic_moves/tools.py +205 -0
- package/mcp_server/semantic_moves/transition_compilers.py +222 -0
- package/mcp_server/semantic_moves/transition_moves.py +76 -0
- package/mcp_server/server.py +10 -0
- package/mcp_server/services/__init__.py +1 -0
- package/mcp_server/services/motif_service.py +67 -0
- package/mcp_server/session_continuity/__init__.py +6 -0
- package/mcp_server/session_continuity/models.py +86 -0
- package/mcp_server/session_continuity/tools.py +230 -0
- package/mcp_server/session_continuity/tracker.py +263 -0
- package/mcp_server/song_brain/__init__.py +6 -0
- package/mcp_server/song_brain/builder.py +504 -0
- package/mcp_server/song_brain/models.py +136 -0
- package/mcp_server/song_brain/tools.py +312 -0
- package/mcp_server/stuckness_detector/__init__.py +5 -0
- package/mcp_server/stuckness_detector/detector.py +400 -0
- package/mcp_server/stuckness_detector/models.py +66 -0
- package/mcp_server/stuckness_detector/tools.py +195 -0
- package/mcp_server/tools/_conductor.py +104 -6
- package/mcp_server/tools/analyzer.py +1 -1
- package/mcp_server/tools/devices.py +34 -0
- package/mcp_server/wonder_mode/__init__.py +6 -0
- package/mcp_server/wonder_mode/diagnosis.py +84 -0
- package/mcp_server/wonder_mode/engine.py +493 -0
- package/mcp_server/wonder_mode/session.py +114 -0
- package/mcp_server/wonder_mode/tools.py +290 -0
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/remote_script/LivePilot/browser.py +4 -1
- package/remote_script/LivePilot/devices.py +29 -0
- package/remote_script/LivePilot/tracks.py +11 -4
- package/scripts/generate_tool_catalog.py +131 -0
- package/scripts/sync_metadata.py +132 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""Preview Studio engine — pure computation, zero I/O.
|
|
2
|
+
|
|
3
|
+
Creates, compares, and ranks preview variants using the creative triptych
|
|
4
|
+
pattern (safe / strong / unexpected).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import time
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from .models import PreviewSet, PreviewVariant
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ── In-memory store ───────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
_preview_sets: dict[str, PreviewSet] = {}
|
|
20
|
+
_MAX_PREVIEW_SETS = 20
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_preview_set(set_id: str) -> Optional[PreviewSet]:
|
|
24
|
+
return _preview_sets.get(set_id)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def store_preview_set(ps: PreviewSet) -> None:
|
|
28
|
+
_preview_sets[ps.set_id] = ps
|
|
29
|
+
# Evict oldest sets if over limit
|
|
30
|
+
while len(_preview_sets) > _MAX_PREVIEW_SETS:
|
|
31
|
+
oldest_key = next(iter(_preview_sets))
|
|
32
|
+
del _preview_sets[oldest_key]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ── Creation ──────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def create_preview_set(
|
|
39
|
+
request_text: str,
|
|
40
|
+
kernel_id: str,
|
|
41
|
+
strategy: str = "creative_triptych",
|
|
42
|
+
available_moves: Optional[list[dict]] = None,
|
|
43
|
+
song_brain: Optional[dict] = None,
|
|
44
|
+
taste_graph: Optional[dict] = None,
|
|
45
|
+
) -> PreviewSet:
|
|
46
|
+
"""Create a preview set with variant slots.
|
|
47
|
+
|
|
48
|
+
For creative_triptych, generates 3 variants: safe, strong, unexpected.
|
|
49
|
+
Each variant gets a move_id from available_moves ranked by novelty.
|
|
50
|
+
"""
|
|
51
|
+
set_id = _compute_set_id(request_text, kernel_id)
|
|
52
|
+
now = int(time.time() * 1000)
|
|
53
|
+
|
|
54
|
+
moves = available_moves or []
|
|
55
|
+
song_brain = song_brain or {}
|
|
56
|
+
taste_graph = taste_graph or {}
|
|
57
|
+
|
|
58
|
+
if strategy == "creative_triptych":
|
|
59
|
+
variants = _build_triptych(request_text, moves, song_brain, taste_graph, set_id, now)
|
|
60
|
+
elif strategy == "binary":
|
|
61
|
+
variants = _build_binary(request_text, moves, song_brain, set_id, now)
|
|
62
|
+
else:
|
|
63
|
+
variants = _build_triptych(request_text, moves, song_brain, taste_graph, set_id, now)
|
|
64
|
+
|
|
65
|
+
ps = PreviewSet(
|
|
66
|
+
set_id=set_id,
|
|
67
|
+
request_text=request_text,
|
|
68
|
+
strategy=strategy,
|
|
69
|
+
source_kernel_id=kernel_id,
|
|
70
|
+
variants=variants,
|
|
71
|
+
created_at_ms=now,
|
|
72
|
+
)
|
|
73
|
+
store_preview_set(ps)
|
|
74
|
+
return ps
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _build_triptych(
|
|
78
|
+
request_text: str,
|
|
79
|
+
moves: list[dict],
|
|
80
|
+
song_brain: dict,
|
|
81
|
+
taste_graph: dict,
|
|
82
|
+
set_id: str,
|
|
83
|
+
now: int,
|
|
84
|
+
) -> list[PreviewVariant]:
|
|
85
|
+
"""Build safe / strong / unexpected variants."""
|
|
86
|
+
identity = song_brain.get("identity_core", "")
|
|
87
|
+
sacred = [e.get("description", "") for e in song_brain.get("sacred_elements", [])]
|
|
88
|
+
sacred_text = ", ".join(sacred[:3]) if sacred else "core elements"
|
|
89
|
+
|
|
90
|
+
profiles = [
|
|
91
|
+
{
|
|
92
|
+
"label": "safe",
|
|
93
|
+
"novelty": 0.2,
|
|
94
|
+
"intent": f"Close to current identity, minimal risk. {request_text}",
|
|
95
|
+
"identity_effect": "preserves",
|
|
96
|
+
"what_preserved": f"Preserves {sacred_text}",
|
|
97
|
+
"why_it_matters": "Low risk — good when identity is fragile",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"label": "strong",
|
|
101
|
+
"novelty": 0.5,
|
|
102
|
+
"intent": f"Musically assertive approach. {request_text}",
|
|
103
|
+
"identity_effect": "evolves",
|
|
104
|
+
"what_preserved": f"Maintains {sacred_text} while pushing forward",
|
|
105
|
+
"why_it_matters": "Best balance of impact and safety",
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"label": "unexpected",
|
|
109
|
+
"novelty": 0.8,
|
|
110
|
+
"intent": f"Surprising but taste-filtered. {request_text}",
|
|
111
|
+
"identity_effect": "contrasts",
|
|
112
|
+
"what_preserved": f"Respects {sacred_text} but reframes context",
|
|
113
|
+
"why_it_matters": "High novelty — may unlock a new direction",
|
|
114
|
+
},
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
variants = []
|
|
118
|
+
for i, profile in enumerate(profiles):
|
|
119
|
+
# Pick a move if available
|
|
120
|
+
move_id = ""
|
|
121
|
+
compiled_plan = None
|
|
122
|
+
if moves and i < len(moves):
|
|
123
|
+
move_id = moves[i].get("move_id", "")
|
|
124
|
+
compiled_plan = moves[i].get("compile_plan")
|
|
125
|
+
|
|
126
|
+
variants.append(PreviewVariant(
|
|
127
|
+
variant_id=f"{set_id}_{profile['label']}",
|
|
128
|
+
label=profile["label"],
|
|
129
|
+
intent=profile["intent"],
|
|
130
|
+
novelty_level=profile["novelty"],
|
|
131
|
+
identity_effect=profile["identity_effect"],
|
|
132
|
+
what_preserved=profile["what_preserved"],
|
|
133
|
+
why_it_matters=profile["why_it_matters"],
|
|
134
|
+
move_id=move_id,
|
|
135
|
+
compiled_plan=compiled_plan,
|
|
136
|
+
taste_fit=_estimate_taste_fit(profile["novelty"], taste_graph),
|
|
137
|
+
created_at_ms=now,
|
|
138
|
+
))
|
|
139
|
+
|
|
140
|
+
return variants
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _build_binary(
|
|
144
|
+
request_text: str,
|
|
145
|
+
moves: list[dict],
|
|
146
|
+
song_brain: dict,
|
|
147
|
+
set_id: str,
|
|
148
|
+
now: int,
|
|
149
|
+
) -> list[PreviewVariant]:
|
|
150
|
+
"""Build simple A/B comparison."""
|
|
151
|
+
return [
|
|
152
|
+
PreviewVariant(
|
|
153
|
+
variant_id=f"{set_id}_a",
|
|
154
|
+
label="option_a",
|
|
155
|
+
intent=f"Primary approach: {request_text}",
|
|
156
|
+
novelty_level=0.3,
|
|
157
|
+
identity_effect="preserves",
|
|
158
|
+
move_id=moves[0].get("move_id", "") if moves else "",
|
|
159
|
+
created_at_ms=now,
|
|
160
|
+
),
|
|
161
|
+
PreviewVariant(
|
|
162
|
+
variant_id=f"{set_id}_b",
|
|
163
|
+
label="option_b",
|
|
164
|
+
intent=f"Alternative approach: {request_text}",
|
|
165
|
+
novelty_level=0.6,
|
|
166
|
+
identity_effect="evolves",
|
|
167
|
+
move_id=moves[1].get("move_id", "") if len(moves) > 1 else "",
|
|
168
|
+
created_at_ms=now,
|
|
169
|
+
),
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ── Comparison ────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def compare_variants(
|
|
177
|
+
preview_set: PreviewSet,
|
|
178
|
+
criteria: Optional[dict] = None,
|
|
179
|
+
) -> dict:
|
|
180
|
+
"""Compare variants within a preview set and rank them."""
|
|
181
|
+
criteria = criteria or {}
|
|
182
|
+
weight_taste = criteria.get("taste_weight", 0.3)
|
|
183
|
+
weight_novelty = criteria.get("novelty_weight", 0.2)
|
|
184
|
+
weight_identity = criteria.get("identity_weight", 0.5)
|
|
185
|
+
|
|
186
|
+
rankings = []
|
|
187
|
+
for v in preview_set.variants:
|
|
188
|
+
# Score components
|
|
189
|
+
taste_score = v.taste_fit
|
|
190
|
+
novelty_score = 1.0 - abs(v.novelty_level - 0.5) * 2 # bell curve around 0.5
|
|
191
|
+
identity_score = _identity_effect_score(v.identity_effect)
|
|
192
|
+
|
|
193
|
+
composite = (
|
|
194
|
+
taste_score * weight_taste
|
|
195
|
+
+ novelty_score * weight_novelty
|
|
196
|
+
+ identity_score * weight_identity
|
|
197
|
+
)
|
|
198
|
+
v.score = round(composite, 3)
|
|
199
|
+
|
|
200
|
+
rankings.append({
|
|
201
|
+
"variant_id": v.variant_id,
|
|
202
|
+
"label": v.label,
|
|
203
|
+
"score": v.score,
|
|
204
|
+
"taste_fit": v.taste_fit,
|
|
205
|
+
"novelty_level": v.novelty_level,
|
|
206
|
+
"identity_effect": v.identity_effect,
|
|
207
|
+
"summary": v.intent,
|
|
208
|
+
"what_preserved": v.what_preserved,
|
|
209
|
+
"why_it_matters": v.why_it_matters,
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
rankings.sort(key=lambda r: r["score"], reverse=True)
|
|
213
|
+
|
|
214
|
+
comparison = {
|
|
215
|
+
"rankings": rankings,
|
|
216
|
+
"recommended": rankings[0]["variant_id"] if rankings else "",
|
|
217
|
+
"criteria_used": {
|
|
218
|
+
"taste_weight": weight_taste,
|
|
219
|
+
"novelty_weight": weight_novelty,
|
|
220
|
+
"identity_weight": weight_identity,
|
|
221
|
+
},
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
preview_set.comparison = comparison
|
|
225
|
+
preview_set.status = "compared"
|
|
226
|
+
return comparison
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def commit_variant(preview_set: PreviewSet, variant_id: str) -> Optional[PreviewVariant]:
|
|
230
|
+
"""Mark a variant as committed and discard others."""
|
|
231
|
+
chosen = None
|
|
232
|
+
for v in preview_set.variants:
|
|
233
|
+
if v.variant_id == variant_id:
|
|
234
|
+
v.status = "committed"
|
|
235
|
+
chosen = v
|
|
236
|
+
else:
|
|
237
|
+
v.status = "discarded"
|
|
238
|
+
|
|
239
|
+
if chosen:
|
|
240
|
+
preview_set.committed_variant_id = variant_id
|
|
241
|
+
preview_set.status = "committed"
|
|
242
|
+
|
|
243
|
+
return chosen
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def discard_set(set_id: str) -> bool:
|
|
247
|
+
"""Discard an entire preview set."""
|
|
248
|
+
ps = _preview_sets.pop(set_id, None)
|
|
249
|
+
if ps:
|
|
250
|
+
ps.status = "discarded"
|
|
251
|
+
for v in ps.variants:
|
|
252
|
+
v.status = "discarded"
|
|
253
|
+
return True
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# ── Helpers ───────────────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _compute_set_id(request_text: str, kernel_id: str) -> str:
|
|
261
|
+
seed = json.dumps({"request": request_text, "kernel": kernel_id}, sort_keys=True)
|
|
262
|
+
return "ps_" + hashlib.sha256(seed.encode()).hexdigest()[:10]
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _estimate_taste_fit(novelty: float, taste_graph: dict) -> float:
|
|
266
|
+
"""Estimate how well a novelty level fits user taste."""
|
|
267
|
+
boldness = taste_graph.get("transition_boldness", 0.5)
|
|
268
|
+
# Users who like boldness prefer higher novelty
|
|
269
|
+
fit = 1.0 - abs(novelty - boldness) * 0.5
|
|
270
|
+
return round(max(0.0, min(1.0, fit)), 3)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _identity_effect_score(effect: str) -> float:
|
|
274
|
+
"""Score identity effects — preserves is safest."""
|
|
275
|
+
return {
|
|
276
|
+
"preserves": 0.9,
|
|
277
|
+
"evolves": 0.7,
|
|
278
|
+
"contrasts": 0.4,
|
|
279
|
+
"resets": 0.2,
|
|
280
|
+
}.get(effect, 0.5)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Preview Studio data models — pure dataclasses, zero I/O."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import asdict, dataclass, field
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class PreviewVariant:
|
|
12
|
+
"""One creative option in a preview set."""
|
|
13
|
+
|
|
14
|
+
variant_id: str = ""
|
|
15
|
+
label: str = "" # "safe", "strong", "unexpected"
|
|
16
|
+
intent: str = "" # what this variant is trying to achieve
|
|
17
|
+
novelty_level: float = 0.0 # 0=conservative, 1=radical
|
|
18
|
+
songbrain_delta: str = "" # what changed vs identity
|
|
19
|
+
taste_fit: float = 0.5 # 0-1 how well it matches user taste
|
|
20
|
+
render_ref: str = "" # reference to cached render
|
|
21
|
+
summary: str = "" # one-line musical explanation
|
|
22
|
+
|
|
23
|
+
# What changed, why it matters, what it preserves
|
|
24
|
+
what_changed: str = ""
|
|
25
|
+
why_it_matters: str = ""
|
|
26
|
+
what_preserved: str = ""
|
|
27
|
+
|
|
28
|
+
# Move / plan data
|
|
29
|
+
move_id: str = ""
|
|
30
|
+
compiled_plan: Optional[dict] = None
|
|
31
|
+
|
|
32
|
+
# Scoring
|
|
33
|
+
score: float = 0.0
|
|
34
|
+
identity_effect: str = "preserves" # preserves, evolves, contrasts, resets
|
|
35
|
+
|
|
36
|
+
# State
|
|
37
|
+
status: str = "pending" # pending, rendered, committed, discarded
|
|
38
|
+
preview_mode: str = "" # audible_preview, metadata_only_preview, analytical_preview
|
|
39
|
+
created_at_ms: int = 0
|
|
40
|
+
|
|
41
|
+
def to_dict(self) -> dict:
|
|
42
|
+
d = asdict(self)
|
|
43
|
+
# Remove None compiled_plan for cleaner output
|
|
44
|
+
if d.get("compiled_plan") is None:
|
|
45
|
+
d.pop("compiled_plan", None)
|
|
46
|
+
return d
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class PreviewSet:
|
|
51
|
+
"""A set of variants tied to one user request."""
|
|
52
|
+
|
|
53
|
+
set_id: str = ""
|
|
54
|
+
request_text: str = ""
|
|
55
|
+
strategy: str = "creative_triptych" # creative_triptych, binary, custom
|
|
56
|
+
source_kernel_id: str = ""
|
|
57
|
+
variants: list[PreviewVariant] = field(default_factory=list)
|
|
58
|
+
comparison: Optional[dict] = None
|
|
59
|
+
committed_variant_id: str = ""
|
|
60
|
+
status: str = "pending" # pending, compared, committed, discarded
|
|
61
|
+
created_at_ms: int = field(default_factory=lambda: int(time.time() * 1000))
|
|
62
|
+
|
|
63
|
+
def to_dict(self) -> dict:
|
|
64
|
+
return {
|
|
65
|
+
"set_id": self.set_id,
|
|
66
|
+
"request_text": self.request_text,
|
|
67
|
+
"strategy": self.strategy,
|
|
68
|
+
"source_kernel_id": self.source_kernel_id,
|
|
69
|
+
"variants": [v.to_dict() for v in self.variants],
|
|
70
|
+
"comparison": self.comparison,
|
|
71
|
+
"committed_variant_id": self.committed_variant_id,
|
|
72
|
+
"status": self.status,
|
|
73
|
+
"variant_count": len(self.variants),
|
|
74
|
+
}
|