Spaces:
Running
Running
feat: add Spotter Viz as second liveboard creation method
Browse filesImplements the Liveboard Creation Design Doc:
- Settings dropdown: ["HYBRID", "SPOTTER_VIZ"] replaces old 3-option menu
- SPOTTER_VIZ routing: direct REST API + TML, no MCP dependency
- Reuses LiveboardCreator.create_liveboard_tml() + deploy_liveboard()
- Gets outlier patterns from vertical×function system
- Gets model columns from ThoughtSpot model metadata
- Same enhance_mcp_liveboard() post-processing as HYBRID
- Backward compat: old "TML"/"MCP" settings mapped to "HYBRID"
- create_liveboard_from_model() extended with outliers + model_columns params
- CLAUDE.md updated with two-method system documentation
Co-authored-by: Cursor <cursoragent@cursor.com>
- CLAUDE.md +31 -16
- chat_interface.py +2 -2
- liveboard_creator.py +21 -5
- sprint_2026_02.md +10 -0
- thoughtspot_deployer.py +91 -16
CLAUDE.md
CHANGED
|
@@ -194,34 +194,49 @@ When user says "create a test for X":
|
|
| 194 |
**Working Code Paths - DO NOT BYPASS THESE:**
|
| 195 |
```
|
| 196 |
Liveboard Creation:
|
| 197 |
-
chat_interface.py → create_liveboard_from_model_mcp() → enhance_mcp_liveboard()
|
|
|
|
| 198 |
|
| 199 |
DO NOT use create_visualization_tml() directly - that's internal low-level code
|
| 200 |
```
|
| 201 |
|
| 202 |
---
|
| 203 |
|
| 204 |
-
## Liveboard Creation
|
| 205 |
|
| 206 |
-
|
|
|
|
| 207 |
|
| 208 |
-
|
| 209 |
-
|
|
|
|
|
|
|
| 210 |
|
| 211 |
-
|
|
|
|
|
|
|
|
|
|
| 212 |
|
| 213 |
-
###
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
|
|
|
|
|
|
| 217 |
|
| 218 |
-
### enhance_mcp_liveboard()
|
| 219 |
-
|
|
|
|
| 220 |
2. Classifies visualizations by type (KPI, trend, categorical)
|
| 221 |
-
3. Adds Groups
|
| 222 |
-
4.
|
| 223 |
-
5.
|
| 224 |
-
6.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
|
| 226 |
### KPI Requirements
|
| 227 |
- **For sparklines and percent change comparisons:**
|
|
|
|
| 194 |
**Working Code Paths - DO NOT BYPASS THESE:**
|
| 195 |
```
|
| 196 |
Liveboard Creation:
|
| 197 |
+
HYBRID: chat_interface.py → create_liveboard_from_model_mcp() → enhance_mcp_liveboard()
|
| 198 |
+
SPOTTER_VIZ: chat_interface.py → create_liveboard_from_model() → enhance_mcp_liveboard()
|
| 199 |
|
| 200 |
DO NOT use create_visualization_tml() directly - that's internal low-level code
|
| 201 |
```
|
| 202 |
|
| 203 |
---
|
| 204 |
|
| 205 |
+
## Liveboard Creation - Two-Method System
|
| 206 |
|
| 207 |
+
**Settings UI:** Admin tab → "Liveboard Creation Method" dropdown
|
| 208 |
+
**Two options:** HYBRID (default) and SPOTTER_VIZ
|
| 209 |
|
| 210 |
+
| Method | Speed | Dependency | Best For |
|
| 211 |
+
|--------|-------|------------|----------|
|
| 212 |
+
| **HYBRID** | ~60-90s | MCP server | AI-driven question selection |
|
| 213 |
+
| **SPOTTER_VIZ** | ~20-30s | Direct REST API only | Production demos, reliability |
|
| 214 |
|
| 215 |
+
### HYBRID Method (MCP + TML Post-Processing)
|
| 216 |
+
1. MCP creates liveboard via `agent.thoughtspot.app` (bearer auth)
|
| 217 |
+
2. TML post-processing enhances with groups, KPIs, colors, layout
|
| 218 |
+
- **Entry:** `create_liveboard_from_model_mcp()` → `enhance_mcp_liveboard()`
|
| 219 |
|
| 220 |
+
### SPOTTER_VIZ Method (Direct API + TML)
|
| 221 |
+
1. LiveboardCreator builds complete TML from outlier patterns + AI
|
| 222 |
+
2. Deploys via REST API `/metadata/tml/import`
|
| 223 |
+
3. Same TML post-processing as HYBRID (groups, KPIs, colors, layout)
|
| 224 |
+
- **Entry:** `create_liveboard_from_model()` → `enhance_mcp_liveboard()`
|
| 225 |
+
- No MCP dependency — uses same auth as table/model deployment
|
| 226 |
|
| 227 |
+
### Shared Post-Processing: enhance_mcp_liveboard()
|
| 228 |
+
Both methods share the same post-processing function:
|
| 229 |
+
1. Exports the liveboard TML
|
| 230 |
2. Classifies visualizations by type (KPI, trend, categorical)
|
| 231 |
+
3. Adds Groups with proper `group_layouts` (Golden Demo style)
|
| 232 |
+
4. KPI group always at top (y=0, full width)
|
| 233 |
+
5. Fixes KPI sparklines and comparisons
|
| 234 |
+
6. Applies brand colors (liveboard-level + group-level)
|
| 235 |
+
7. Re-imports the enhanced TML
|
| 236 |
+
|
| 237 |
+
### Backward Compatibility
|
| 238 |
+
- Old settings values "TML" or "MCP" are mapped to "HYBRID" in the router
|
| 239 |
+
- `USE_MCP_LIVEBOARD=true` env var still works (maps to HYBRID)
|
| 240 |
|
| 241 |
### KPI Requirements
|
| 242 |
- **For sparklines and percent change comparisons:**
|
chat_interface.py
CHANGED
|
@@ -4133,9 +4133,9 @@ def create_settings_tab():
|
|
| 4133 |
|
| 4134 |
liveboard_method = gr.Dropdown(
|
| 4135 |
label="Liveboard Creation Method",
|
| 4136 |
-
choices=["HYBRID", "
|
| 4137 |
value="HYBRID",
|
| 4138 |
-
info="HYBRID
|
| 4139 |
)
|
| 4140 |
|
| 4141 |
# Existing Model Section
|
|
|
|
| 4133 |
|
| 4134 |
liveboard_method = gr.Dropdown(
|
| 4135 |
label="Liveboard Creation Method",
|
| 4136 |
+
choices=["HYBRID", "SPOTTER_VIZ"],
|
| 4137 |
value="HYBRID",
|
| 4138 |
+
info="HYBRID: MCP + TML polish (AI-driven). SPOTTER_VIZ: Direct API + TML (faster, no MCP dependency)."
|
| 4139 |
)
|
| 4140 |
|
| 4141 |
# Existing Model Section
|
liveboard_creator.py
CHANGED
|
@@ -2505,12 +2505,15 @@ def create_liveboard_from_model(
|
|
| 2505 |
use_case: str,
|
| 2506 |
num_visualizations: int = 6,
|
| 2507 |
liveboard_name: str = None,
|
| 2508 |
-
llm_model: str = None
|
|
|
|
|
|
|
| 2509 |
) -> Dict:
|
| 2510 |
"""
|
| 2511 |
-
|
| 2512 |
|
| 2513 |
-
This is the
|
|
|
|
| 2514 |
|
| 2515 |
Args:
|
| 2516 |
ts_client: Authenticated ThoughtSpotDeployer instance
|
|
@@ -2521,6 +2524,8 @@ def create_liveboard_from_model(
|
|
| 2521 |
num_visualizations: Number of visualizations to create
|
| 2522 |
liveboard_name: Optional custom name for the liveboard
|
| 2523 |
llm_model: LLM model to use for AI-driven viz generation
|
|
|
|
|
|
|
| 2524 |
|
| 2525 |
Returns:
|
| 2526 |
Deployment result dictionary with:
|
|
@@ -2529,13 +2534,24 @@ def create_liveboard_from_model(
|
|
| 2529 |
- liveboard_name: str
|
| 2530 |
- liveboard_url: str
|
| 2531 |
"""
|
| 2532 |
-
print(f"🚀 Starting
|
| 2533 |
print(f" - Model: {model_name}", flush=True)
|
| 2534 |
print(f" - Use case: {use_case}", flush=True)
|
| 2535 |
print(f" - Visualizations: {num_visualizations}", flush=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2536 |
|
| 2537 |
creator = LiveboardCreator(ts_client, model_id, model_name, llm_model)
|
| 2538 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2539 |
|
| 2540 |
# Deploy and get result
|
| 2541 |
result = creator.deploy_liveboard(liveboard_tml)
|
|
|
|
| 2505 |
use_case: str,
|
| 2506 |
num_visualizations: int = 6,
|
| 2507 |
liveboard_name: str = None,
|
| 2508 |
+
llm_model: str = None,
|
| 2509 |
+
outliers: Optional[List[Dict]] = None,
|
| 2510 |
+
model_columns: Optional[List[Dict]] = None
|
| 2511 |
) -> Dict:
|
| 2512 |
"""
|
| 2513 |
+
Create and deploy a Liveboard via TML (Spotter Viz path).
|
| 2514 |
|
| 2515 |
+
This is the direct API approach — builds complete TML and imports it.
|
| 2516 |
+
Used by both the legacy TML path and the new SPOTTER_VIZ method.
|
| 2517 |
|
| 2518 |
Args:
|
| 2519 |
ts_client: Authenticated ThoughtSpotDeployer instance
|
|
|
|
| 2524 |
num_visualizations: Number of visualizations to create
|
| 2525 |
liveboard_name: Optional custom name for the liveboard
|
| 2526 |
llm_model: LLM model to use for AI-driven viz generation
|
| 2527 |
+
outliers: Optional list of outlier pattern dicts for targeted viz generation
|
| 2528 |
+
model_columns: Optional list of column metadata from TS model
|
| 2529 |
|
| 2530 |
Returns:
|
| 2531 |
Deployment result dictionary with:
|
|
|
|
| 2534 |
- liveboard_name: str
|
| 2535 |
- liveboard_url: str
|
| 2536 |
"""
|
| 2537 |
+
print(f"🚀 Starting Spotter Viz liveboard creation...", flush=True)
|
| 2538 |
print(f" - Model: {model_name}", flush=True)
|
| 2539 |
print(f" - Use case: {use_case}", flush=True)
|
| 2540 |
print(f" - Visualizations: {num_visualizations}", flush=True)
|
| 2541 |
+
if outliers:
|
| 2542 |
+
print(f" - Outlier patterns: {len(outliers)}", flush=True)
|
| 2543 |
+
if model_columns:
|
| 2544 |
+
print(f" - Model columns: {len(model_columns)}", flush=True)
|
| 2545 |
|
| 2546 |
creator = LiveboardCreator(ts_client, model_id, model_name, llm_model)
|
| 2547 |
+
|
| 2548 |
+
# If external model_columns provided, override the auto-fetched ones
|
| 2549 |
+
if model_columns:
|
| 2550 |
+
creator.model_columns = model_columns
|
| 2551 |
+
|
| 2552 |
+
liveboard_tml = creator.create_liveboard_tml(
|
| 2553 |
+
company_data, use_case, num_visualizations, liveboard_name, outliers
|
| 2554 |
+
)
|
| 2555 |
|
| 2556 |
# Deploy and get result
|
| 2557 |
result = creator.deploy_liveboard(liveboard_tml)
|
sprint_2026_02.md
CHANGED
|
@@ -89,6 +89,16 @@
|
|
| 89 |
### Done
|
| 90 |
|
| 91 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
#### Feb 6, 2026 - Gradio Compat + Hybrid Liveboard Layout Fix
|
| 93 |
- [x] **Gradio 4.44.1 + pydantic 2.12 + httpx 0.28 compatibility** ✅
|
| 94 |
- Monkey-patch `url_ok()` for localhost check bypass
|
|
|
|
| 89 |
### Done
|
| 90 |
|
| 91 |
|
| 92 |
+
#### Feb 6, 2026 - Spotter Viz Method Added (Design Doc Implementation)
|
| 93 |
+
- [x] **Settings dropdown updated**: `["HYBRID", "SPOTTER_VIZ"]` replaces old 3-option dropdown ✅
|
| 94 |
+
- [x] **SPOTTER_VIZ routing in deployer**: Direct TML path with outliers + model_columns ✅
|
| 95 |
+
- [x] **Backward compatibility**: old "TML"/"MCP" values mapped to "HYBRID" ✅
|
| 96 |
+
- [x] **Shared post-processing**: Both methods use `enhance_mcp_liveboard()` ✅
|
| 97 |
+
- [x] **create_liveboard_from_model() extended**: accepts `outliers` and `model_columns` params ✅
|
| 98 |
+
- [x] **CLAUDE.md updated**: Two-method system documented ✅
|
| 99 |
+
- [ ] **To test**: Run SPOTTER_VIZ end-to-end with Retail Sales
|
| 100 |
+
|
| 101 |
+
|
| 102 |
#### Feb 6, 2026 - Gradio Compat + Hybrid Liveboard Layout Fix
|
| 103 |
- [x] **Gradio 4.44.1 + pydantic 2.12 + httpx 0.28 compatibility** ✅
|
| 104 |
- Monkey-patch `url_ok()` for localhost check bypass
|
thoughtspot_deployer.py
CHANGED
|
@@ -2123,9 +2123,13 @@ class ThoughtSpotDeployer:
|
|
| 2123 |
use_mcp = os.getenv('USE_MCP_LIVEBOARD', 'false').lower() == 'true'
|
| 2124 |
method = 'MCP' if use_mcp else 'HYBRID'
|
| 2125 |
|
| 2126 |
-
# Normalize method name
|
| 2127 |
method = method.upper()
|
| 2128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2129 |
log_progress(f"[WARN] Unknown liveboard method '{method}', defaulting to HYBRID")
|
| 2130 |
method = 'HYBRID'
|
| 2131 |
|
|
@@ -2140,22 +2144,93 @@ class ThoughtSpotDeployer:
|
|
| 2140 |
'use_case': use_case or 'General Analytics'
|
| 2141 |
}
|
| 2142 |
|
| 2143 |
-
if method == '
|
| 2144 |
-
#
|
| 2145 |
-
from liveboard_creator import create_liveboard_from_model
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2146 |
|
| 2147 |
-
|
| 2148 |
-
|
| 2149 |
-
|
| 2150 |
-
|
| 2151 |
-
|
| 2152 |
-
|
| 2153 |
-
|
| 2154 |
-
|
| 2155 |
-
|
| 2156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2157 |
|
| 2158 |
-
elif method
|
| 2159 |
# MCP and HYBRID both use MCP for creation
|
| 2160 |
# HYBRID adds TML post-processing enhancement
|
| 2161 |
from liveboard_creator import create_liveboard_from_model_mcp, enhance_mcp_liveboard
|
|
|
|
| 2123 |
use_mcp = os.getenv('USE_MCP_LIVEBOARD', 'false').lower() == 'true'
|
| 2124 |
method = 'MCP' if use_mcp else 'HYBRID'
|
| 2125 |
|
| 2126 |
+
# Normalize method name and backward compatibility
|
| 2127 |
method = method.upper()
|
| 2128 |
+
# Map old values to current options
|
| 2129 |
+
if method in ('TML', 'MCP'):
|
| 2130 |
+
log_progress(f"[INFO] Mapping legacy method '{method}' → HYBRID")
|
| 2131 |
+
method = 'HYBRID'
|
| 2132 |
+
if method not in ['HYBRID', 'SPOTTER_VIZ']:
|
| 2133 |
log_progress(f"[WARN] Unknown liveboard method '{method}', defaulting to HYBRID")
|
| 2134 |
method = 'HYBRID'
|
| 2135 |
|
|
|
|
| 2144 |
'use_case': use_case or 'General Analytics'
|
| 2145 |
}
|
| 2146 |
|
| 2147 |
+
if method == 'SPOTTER_VIZ':
|
| 2148 |
+
# Spotter Viz: Direct REST API + TML — no MCP dependency
|
| 2149 |
+
from liveboard_creator import create_liveboard_from_model, enhance_mcp_liveboard
|
| 2150 |
+
|
| 2151 |
+
# Get actual column names from ThoughtSpot model
|
| 2152 |
+
model_columns = self.get_model_columns(model_guid)
|
| 2153 |
+
if not model_columns:
|
| 2154 |
+
log_progress(f" ⚠️ Could not get model columns, falling back to DDL")
|
| 2155 |
+
model_columns = []
|
| 2156 |
+
for table_name, columns_list in tables.items():
|
| 2157 |
+
for col in columns_list:
|
| 2158 |
+
model_columns.append(col)
|
| 2159 |
|
| 2160 |
+
# Get outlier patterns from the vertical×function system
|
| 2161 |
+
outlier_dicts = []
|
| 2162 |
+
try:
|
| 2163 |
+
from outlier_system import get_outliers_for_use_case
|
| 2164 |
+
from demo_personas import parse_use_case
|
| 2165 |
+
uc_vertical, uc_function = parse_use_case(use_case or '')
|
| 2166 |
+
if uc_vertical or uc_function:
|
| 2167 |
+
outlier_config = get_outliers_for_use_case(
|
| 2168 |
+
uc_vertical or "Generic",
|
| 2169 |
+
uc_function or "Generic"
|
| 2170 |
+
)
|
| 2171 |
+
for op in outlier_config.required:
|
| 2172 |
+
outlier_dicts.append({
|
| 2173 |
+
'title': op.name,
|
| 2174 |
+
'insight': op.viz_talking_point,
|
| 2175 |
+
'viz_type': op.viz_type,
|
| 2176 |
+
'show_me_query': op.viz_question,
|
| 2177 |
+
'kpi_companion': True,
|
| 2178 |
+
'spotter_questions': op.spotter_questions,
|
| 2179 |
+
})
|
| 2180 |
+
for op in outlier_config.optional[:2]:
|
| 2181 |
+
outlier_dicts.append({
|
| 2182 |
+
'title': op.name,
|
| 2183 |
+
'insight': op.viz_talking_point,
|
| 2184 |
+
'viz_type': op.viz_type,
|
| 2185 |
+
'show_me_query': op.viz_question,
|
| 2186 |
+
'kpi_companion': False,
|
| 2187 |
+
'spotter_questions': op.spotter_questions,
|
| 2188 |
+
})
|
| 2189 |
+
if outlier_dicts:
|
| 2190 |
+
log_progress(f" [SPOTTER] Using {len(outlier_dicts)} outlier patterns from {uc_vertical}×{uc_function}")
|
| 2191 |
+
except Exception as outlier_err:
|
| 2192 |
+
log_progress(f" [SPOTTER] Outlier loading skipped: {outlier_err}")
|
| 2193 |
+
|
| 2194 |
+
log_progress(f" [SPOTTER] Model: {model_name}, GUID: {model_guid}")
|
| 2195 |
+
log_progress(f" [SPOTTER] Using {len(model_columns)} columns from ThoughtSpot model")
|
| 2196 |
+
log_progress(f" Step 1/2: Building liveboard via TML...")
|
| 2197 |
+
|
| 2198 |
+
try:
|
| 2199 |
+
liveboard_result = create_liveboard_from_model(
|
| 2200 |
+
ts_client=self,
|
| 2201 |
+
model_id=model_guid,
|
| 2202 |
+
model_name=model_name,
|
| 2203 |
+
company_data=company_data,
|
| 2204 |
+
use_case=use_case or 'General Analytics',
|
| 2205 |
+
num_visualizations=8,
|
| 2206 |
+
liveboard_name=liveboard_name,
|
| 2207 |
+
llm_model=llm_model,
|
| 2208 |
+
outliers=outlier_dicts if outlier_dicts else None,
|
| 2209 |
+
model_columns=model_columns
|
| 2210 |
+
)
|
| 2211 |
+
except Exception as spotter_error:
|
| 2212 |
+
import traceback
|
| 2213 |
+
error_trace = traceback.format_exc()
|
| 2214 |
+
log_progress(f" [SPOTTER ERROR] {type(spotter_error).__name__}: {str(spotter_error)}")
|
| 2215 |
+
liveboard_result = {'success': False, 'error': str(spotter_error), 'traceback': error_trace}
|
| 2216 |
+
|
| 2217 |
+
# Spotter Viz: Add TML enhancement (same as Hybrid post-processing)
|
| 2218 |
+
if liveboard_result.get('success') and liveboard_result.get('liveboard_guid'):
|
| 2219 |
+
log_progress(f" Step 2/2: Enhancing with TML post-processing...")
|
| 2220 |
+
enhance_result = enhance_mcp_liveboard(
|
| 2221 |
+
liveboard_guid=liveboard_result['liveboard_guid'],
|
| 2222 |
+
company_data=company_data,
|
| 2223 |
+
ts_client=self,
|
| 2224 |
+
add_groups=True,
|
| 2225 |
+
fix_kpis=True,
|
| 2226 |
+
apply_brand_colors=True
|
| 2227 |
+
)
|
| 2228 |
+
if enhance_result.get('success'):
|
| 2229 |
+
log_progress(f" [OK] Enhancement applied: {', '.join(enhance_result.get('enhancements', []))}")
|
| 2230 |
+
else:
|
| 2231 |
+
log_progress(f" [WARN] Enhancement partial: {enhance_result.get('message', '')[:80]}")
|
| 2232 |
|
| 2233 |
+
elif method == 'HYBRID':
|
| 2234 |
# MCP and HYBRID both use MCP for creation
|
| 2235 |
# HYBRID adds TML post-processing enhancement
|
| 2236 |
from liveboard_creator import create_liveboard_from_model_mcp, enhance_mcp_liveboard
|