Spaces:
Running
fix(mcp+pipeline): share demos with operator + harden data-quality consumers
Browse files- mcp_server: every MCP build shares the created model + liveboard with the operator
(MCP_SHARE_WITH, default mike.boone@thoughtspot.com) so demos are visible no matter
who ran them. Owner unchanged — the build still authenticates AS owner_email, so the
caller passed in owns the objects.
- blueprint.from_dict: drop non-numeric seasonal `monthly` params too (line 261) — the
merged author can emit a dict there like it did for params.
- engine + validator: _safe_date() tolerates a non-ISO insight window bound (e.g. the
LLM token 'relative_minus_8_months') instead of crashing on date.fromisoformat — the
second merge bug that failed Cloudscape.
Root theme: the merged blueprint author emits richer output than the parser/engine
expected; harden the consumers to tolerate it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- demoprep_app/dataset/engine.py +16 -2
- demoprep_app/dataset/validator.py +14 -2
- demoprep_app/scenario/blueprint.py +2 -1
- mcp_server.py +5 -0
|
@@ -31,6 +31,18 @@ from demoprep_app.scenario.blueprint import (
|
|
| 31 |
from demoprep_app.scenario.contract import DimensionSpec, ScenarioContract
|
| 32 |
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
class BlueprintEngine:
|
| 35 |
"""Deterministic executor for DemoBlueprint -> DatasetBundle."""
|
| 36 |
|
|
@@ -276,9 +288,11 @@ def _insight_multiplier(entries, picked: dict[str, str], d: date) -> float:
|
|
| 276 |
for i, effmult in entries:
|
| 277 |
if i.insight_type != "seasonal_spike" and picked.get(i.dimension) != i.dimension_value:
|
| 278 |
continue
|
| 279 |
-
|
|
|
|
| 280 |
continue
|
| 281 |
-
|
|
|
|
| 282 |
continue
|
| 283 |
mult *= effmult
|
| 284 |
return mult
|
|
|
|
| 31 |
from demoprep_app.scenario.contract import DimensionSpec, ScenarioContract
|
| 32 |
|
| 33 |
|
| 34 |
+
def _safe_date(value):
|
| 35 |
+
"""Parse an ISO date; return None if missing or not a valid ISO string (e.g. an
|
| 36 |
+
LLM-invented relative token like 'relative_minus_8_months'), so a bad insight
|
| 37 |
+
window bound is skipped rather than crashing dataset generation."""
|
| 38 |
+
if not value:
|
| 39 |
+
return None
|
| 40 |
+
try:
|
| 41 |
+
return date.fromisoformat(value)
|
| 42 |
+
except (ValueError, TypeError):
|
| 43 |
+
return None
|
| 44 |
+
|
| 45 |
+
|
| 46 |
class BlueprintEngine:
|
| 47 |
"""Deterministic executor for DemoBlueprint -> DatasetBundle."""
|
| 48 |
|
|
|
|
| 288 |
for i, effmult in entries:
|
| 289 |
if i.insight_type != "seasonal_spike" and picked.get(i.dimension) != i.dimension_value:
|
| 290 |
continue
|
| 291 |
+
_ws = _safe_date(i.window_start)
|
| 292 |
+
if _ws and d < _ws:
|
| 293 |
continue
|
| 294 |
+
_we = _safe_date(i.window_end)
|
| 295 |
+
if _we and d > _we:
|
| 296 |
continue
|
| 297 |
mult *= effmult
|
| 298 |
return mult
|
|
@@ -145,6 +145,18 @@ def _row_date(row: dict[str, Any]) -> date | None:
|
|
| 145 |
return None
|
| 146 |
|
| 147 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
def _check_insight(
|
| 149 |
blueprint: DemoBlueprint,
|
| 150 |
insight: PlantedInsight,
|
|
@@ -155,8 +167,8 @@ def _check_insight(
|
|
| 155 |
laggard = promised < 1.0
|
| 156 |
required = 1.0 + (promised - 1.0) * MIN_REALIZED_FRACTION # works for both directions
|
| 157 |
|
| 158 |
-
start =
|
| 159 |
-
end =
|
| 160 |
|
| 161 |
def in_window(d: date | None) -> bool:
|
| 162 |
if d is None:
|
|
|
|
| 145 |
return None
|
| 146 |
|
| 147 |
|
| 148 |
+
def _safe_date(value):
|
| 149 |
+
"""Parse an ISO date; return None if missing or not a valid ISO string (e.g. an
|
| 150 |
+
LLM-invented relative token), so a bad insight window bound is ignored rather than
|
| 151 |
+
crashing validation."""
|
| 152 |
+
if not value:
|
| 153 |
+
return None
|
| 154 |
+
try:
|
| 155 |
+
return date.fromisoformat(value)
|
| 156 |
+
except (ValueError, TypeError):
|
| 157 |
+
return None
|
| 158 |
+
|
| 159 |
+
|
| 160 |
def _check_insight(
|
| 161 |
blueprint: DemoBlueprint,
|
| 162 |
insight: PlantedInsight,
|
|
|
|
| 167 |
laggard = promised < 1.0
|
| 168 |
required = 1.0 + (promised - 1.0) * MIN_REALIZED_FRACTION # works for both directions
|
| 169 |
|
| 170 |
+
start = _safe_date(insight.window_start)
|
| 171 |
+
end = _safe_date(insight.window_end)
|
| 172 |
|
| 173 |
def in_window(d: date | None) -> bool:
|
| 174 |
if d is None:
|
|
@@ -259,7 +259,8 @@ class DemoBlueprint:
|
|
| 259 |
|
| 260 |
seas_raw = raw.get("seasonality") or {}
|
| 261 |
seasonality = Seasonality(
|
| 262 |
-
monthly={int(k): float(v) for k, v in (seas_raw.get("monthly") or {}).items()
|
|
|
|
| 263 |
trend_pct_per_year=float(seas_raw.get("trend_pct_per_year", 6.0) or 0.0),
|
| 264 |
narrative=[str(s) for s in (seas_raw.get("narrative") or [])],
|
| 265 |
)
|
|
|
|
| 259 |
|
| 260 |
seas_raw = raw.get("seasonality") or {}
|
| 261 |
seasonality = Seasonality(
|
| 262 |
+
monthly={int(k): float(v) for k, v in (seas_raw.get("monthly") or {}).items()
|
| 263 |
+
if str(k).lstrip("-").isdigit() and isinstance(v, (int, float)) and not isinstance(v, bool)},
|
| 264 |
trend_pct_per_year=float(seas_raw.get("trend_pct_per_year", 6.0) or 0.0),
|
| 265 |
narrative=[str(s) for s in (seas_raw.get("narrative") or [])],
|
| 266 |
)
|
|
@@ -366,6 +366,11 @@ def _run_build(run_id: str, brief: str, company_name: str, use_case: str, compan
|
|
| 366 |
controller.settings["model"] = controller.settings.get("model") or DEFAULT_LLM_MODEL
|
| 367 |
controller.settings["thoughtspot_url"] = ts_target_url
|
| 368 |
controller.settings["thoughtspot_trusted_auth_key"] = ts_auth_key
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
|
| 370 |
# vertical / function / use_case_config exactly as the runner does
|
| 371 |
controller.vertical, controller.function = parse_use_case(use_case or "")
|
|
|
|
| 366 |
controller.settings["model"] = controller.settings.get("model") or DEFAULT_LLM_MODEL
|
| 367 |
controller.settings["thoughtspot_url"] = ts_target_url
|
| 368 |
controller.settings["thoughtspot_trusted_auth_key"] = ts_auth_key
|
| 369 |
+
# Always share the created model + liveboard with the operator, so every demo is
|
| 370 |
+
# visible no matter who ran it. The OWNER is unchanged — whoever was passed as
|
| 371 |
+
# owner_email (the build authenticates AS that user, so they own the objects);
|
| 372 |
+
# this just adds the operator as a viewer. Override target via MCP_SHARE_WITH.
|
| 373 |
+
controller.settings["share_with"] = os.getenv("MCP_SHARE_WITH", "mike.boone@thoughtspot.com")
|
| 374 |
|
| 375 |
# vertical / function / use_case_config exactly as the runner does
|
| 376 |
controller.vertical, controller.function = parse_use_case(use_case or "")
|