Spaces:
Running
Running
Mike Boone Claude commited on
Commit Β·
a9a22ee
1
Parent(s): b3d59ca
feat: Improve Liveboard creation, add outlier documentation, and fix table join updates
Browse files- Refactored model column fetching to use TML export API for better reliability
- Added structured outlier documentation format to data generation prompts (DEMO_OUTLIER comments)
- Fixed table GUID handling when updating tables with joins in Phase 2
- Enhanced error handling and logging for model column retrieval
These changes improve demo quality by:
1. Making liveboard generation more robust with proper column metadata
2. Guiding AI to create intentional data patterns with demo talking points
3. Ensuring table relationships update correctly without creating duplicates
π€ Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- liveboard_creator.py +59 -36
- schema_utils.py +12 -0
- thoughtspot_deployer.py +20 -9
liveboard_creator.py
CHANGED
|
@@ -31,22 +31,40 @@ class LiveboardCreator:
|
|
| 31 |
self.openai_client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
|
| 32 |
|
| 33 |
def _fetch_model_columns(self) -> List[Dict]:
|
| 34 |
-
"""Get available columns from the model
|
| 35 |
try:
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
| 38 |
headers=self.ts_client.headers,
|
| 39 |
-
|
| 40 |
)
|
| 41 |
|
| 42 |
if response.status_code == 200:
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
else:
|
| 46 |
-
print(f"Warning: Could not fetch model
|
| 47 |
return []
|
| 48 |
except Exception as e:
|
| 49 |
print(f"Error fetching model columns: {e}")
|
|
|
|
|
|
|
| 50 |
return []
|
| 51 |
|
| 52 |
def load_reference_tml(self, reference_type: str = "simple") -> Dict:
|
|
@@ -55,16 +73,16 @@ class LiveboardCreator:
|
|
| 55 |
|
| 56 |
Args:
|
| 57 |
reference_type: 'simple' or 'demogold'
|
| 58 |
-
- simple: dev_notes/liveboard_simple/Global Retail Apparel Sales.liveboard.tml
|
| 59 |
-
- demogold: dev_notes/liveboard_demogold/rl test.liveboard.tml
|
| 60 |
|
| 61 |
Returns:
|
| 62 |
Parsed TML dictionary
|
| 63 |
"""
|
| 64 |
if reference_type == "simple":
|
| 65 |
-
tml_path = "dev_notes/liveboard_simple/Global Retail Apparel Sales.liveboard.tml"
|
| 66 |
else:
|
| 67 |
-
tml_path = "dev_notes/liveboard_demogold/rl test.liveboard.tml"
|
| 68 |
|
| 69 |
try:
|
| 70 |
with open(tml_path, 'r') as f:
|
|
@@ -324,32 +342,34 @@ Return ONLY a valid JSON object with structure:
|
|
| 324 |
Returns:
|
| 325 |
Complete Liveboard TML as YAML string
|
| 326 |
"""
|
| 327 |
-
# Generate visualization configs using AI
|
| 328 |
-
viz_configs = self.generate_visualizations_from_research(
|
| 329 |
-
company_data,
|
| 330 |
-
use_case,
|
| 331 |
-
num_visualizations
|
| 332 |
-
)
|
| 333 |
-
|
| 334 |
-
# Create visualization TML objects
|
| 335 |
visualizations = []
|
| 336 |
-
for i, viz_config in enumerate(viz_configs):
|
| 337 |
-
viz_config['id'] = f'Viz_{i+1}'
|
| 338 |
-
viz_tml = self.create_visualization_tml(viz_config)
|
| 339 |
-
visualizations.append(viz_tml)
|
| 340 |
-
|
| 341 |
-
# Create simple grid layout (2 columns)
|
| 342 |
tiles = []
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
|
| 354 |
# Assemble complete Liveboard TML
|
| 355 |
liveboard_tml = {
|
|
@@ -404,7 +424,10 @@ Return ONLY a valid JSON object with structure:
|
|
| 404 |
|
| 405 |
# Extract liveboard ID from response
|
| 406 |
liveboard_id = None
|
| 407 |
-
|
|
|
|
|
|
|
|
|
|
| 408 |
liveboard_id = result['object'][0].get('response', {}).get('header', {}).get('id')
|
| 409 |
|
| 410 |
return {
|
|
|
|
| 31 |
self.openai_client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
|
| 32 |
|
| 33 |
def _fetch_model_columns(self) -> List[Dict]:
|
| 34 |
+
"""Get available columns from the model via TML export"""
|
| 35 |
try:
|
| 36 |
+
import yaml
|
| 37 |
+
|
| 38 |
+
response = self.ts_client.session.post(
|
| 39 |
+
f"{self.ts_client.base_url}/api/rest/2.0/metadata/tml/export",
|
| 40 |
headers=self.ts_client.headers,
|
| 41 |
+
json={"metadata": [{"identifier": self.model_id}]}
|
| 42 |
)
|
| 43 |
|
| 44 |
if response.status_code == 200:
|
| 45 |
+
result = response.json()
|
| 46 |
+
if isinstance(result, list) and len(result) > 0:
|
| 47 |
+
edoc = result[0].get('edoc')
|
| 48 |
+
if edoc:
|
| 49 |
+
tml = yaml.safe_load(edoc)
|
| 50 |
+
# Get columns from model TML
|
| 51 |
+
if 'model' in tml:
|
| 52 |
+
columns = tml['model'].get('columns', [])
|
| 53 |
+
# Convert to simpler format
|
| 54 |
+
return [{
|
| 55 |
+
'name': col.get('name'),
|
| 56 |
+
'type': col.get('properties', {}).get('column_type', 'ATTRIBUTE'),
|
| 57 |
+
'column_id': col.get('column_id')
|
| 58 |
+
} for col in columns]
|
| 59 |
+
print(f"Warning: Could not parse model columns")
|
| 60 |
+
return []
|
| 61 |
else:
|
| 62 |
+
print(f"Warning: Could not fetch model TML: {response.status_code}")
|
| 63 |
return []
|
| 64 |
except Exception as e:
|
| 65 |
print(f"Error fetching model columns: {e}")
|
| 66 |
+
import traceback
|
| 67 |
+
traceback.print_exc()
|
| 68 |
return []
|
| 69 |
|
| 70 |
def load_reference_tml(self, reference_type: str = "simple") -> Dict:
|
|
|
|
| 73 |
|
| 74 |
Args:
|
| 75 |
reference_type: 'simple' or 'demogold'
|
| 76 |
+
- simple: dev_notes/tml_examples/liveboard_simple/Global Retail Apparel Sales.liveboard.tml
|
| 77 |
+
- demogold: dev_notes/tml_examples/liveboard_demogold/rl test.liveboard.tml
|
| 78 |
|
| 79 |
Returns:
|
| 80 |
Parsed TML dictionary
|
| 81 |
"""
|
| 82 |
if reference_type == "simple":
|
| 83 |
+
tml_path = "dev_notes/tml_examples/liveboard_simple/Global Retail Apparel Sales.liveboard.tml"
|
| 84 |
else:
|
| 85 |
+
tml_path = "dev_notes/tml_examples/liveboard_demogold/rl test.liveboard.tml"
|
| 86 |
|
| 87 |
try:
|
| 88 |
with open(tml_path, 'r') as f:
|
|
|
|
| 342 |
Returns:
|
| 343 |
Complete Liveboard TML as YAML string
|
| 344 |
"""
|
| 345 |
+
# Generate visualization configs using AI (skip if 0 visualizations)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
visualizations = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
tiles = []
|
| 348 |
+
|
| 349 |
+
if num_visualizations > 0:
|
| 350 |
+
viz_configs = self.generate_visualizations_from_research(
|
| 351 |
+
company_data,
|
| 352 |
+
use_case,
|
| 353 |
+
num_visualizations
|
| 354 |
+
)
|
| 355 |
+
|
| 356 |
+
# Create visualization TML objects
|
| 357 |
+
for i, viz_config in enumerate(viz_configs):
|
| 358 |
+
viz_config['id'] = f'Viz_{i+1}'
|
| 359 |
+
viz_tml = self.create_visualization_tml(viz_config)
|
| 360 |
+
visualizations.append(viz_tml)
|
| 361 |
+
|
| 362 |
+
# Create simple grid layout (2 columns)
|
| 363 |
+
for i, viz in enumerate(visualizations):
|
| 364 |
+
col = i % 2
|
| 365 |
+
row = i // 2
|
| 366 |
+
tiles.append({
|
| 367 |
+
'visualization_id': viz['id'],
|
| 368 |
+
'x': col * 6, # 12-column grid system
|
| 369 |
+
'y': row * 5,
|
| 370 |
+
'height': 5,
|
| 371 |
+
'width': 6
|
| 372 |
+
})
|
| 373 |
|
| 374 |
# Assemble complete Liveboard TML
|
| 375 |
liveboard_tml = {
|
|
|
|
| 424 |
|
| 425 |
# Extract liveboard ID from response
|
| 426 |
liveboard_id = None
|
| 427 |
+
# Handle both dict and list responses
|
| 428 |
+
if isinstance(result, list) and len(result) > 0:
|
| 429 |
+
liveboard_id = result[0].get('response', {}).get('header', {}).get('id_guid')
|
| 430 |
+
elif isinstance(result, dict) and result.get('object') and len(result['object']) > 0:
|
| 431 |
liveboard_id = result['object'][0].get('response', {}).get('header', {}).get('id')
|
| 432 |
|
| 433 |
return {
|
schema_utils.py
CHANGED
|
@@ -174,6 +174,18 @@ REQUIREMENTS:
|
|
| 174 |
5. Include realistic business scenarios and edge cases
|
| 175 |
6. Use proper data types and constraints
|
| 176 |
7. Include error handling for connection issues
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
CONNECTION TEMPLATE:
|
| 179 |
```python
|
|
|
|
| 174 |
5. Include realistic business scenarios and edge cases
|
| 175 |
6. Use proper data types and constraints
|
| 176 |
7. Include error handling for connection issues
|
| 177 |
+
8. **IMPORTANT**: Document strategic outliers with structured comments for demo purposes
|
| 178 |
+
|
| 179 |
+
OUTLIER DOCUMENTATION FORMAT:
|
| 180 |
+
For each strategic outlier or interesting pattern you inject into the data, add structured comments ABOVE the code that injects it:
|
| 181 |
+
|
| 182 |
+
# DEMO_OUTLIER: [Brief title - e.g., "High-Value Customers at Risk"]
|
| 183 |
+
# INSIGHT: [What pattern/anomaly exists - e.g., "Top 5 customers (>$50K LTV) showing declining satisfaction"]
|
| 184 |
+
# SHOW_ME: [ThoughtSpot query to find it - e.g., "Show customers where lifetime_value > 50000 and satisfaction < 3"]
|
| 185 |
+
# IMPACT: [Business impact - e.g., "$250K annual revenue at risk if these customers churn"]
|
| 186 |
+
# TALKING_POINT: [What sales person should say - e.g., "Notice how ThoughtSpot instantly surfaces your most valuable at-risk accounts"]
|
| 187 |
+
|
| 188 |
+
Create 3-5 strategic outliers that would make compelling demo talking points. Place these comments immediately BEFORE the code that injects each outlier.
|
| 189 |
|
| 190 |
CONNECTION TEMPLATE:
|
| 191 |
```python
|
thoughtspot_deployer.py
CHANGED
|
@@ -236,9 +236,13 @@ class ThoughtSpotDeployer:
|
|
| 236 |
print(f" β Relationship API call failed: {response.status_code}")
|
| 237 |
print(f" π Response: {response.text}")
|
| 238 |
|
| 239 |
-
def create_table_tml(self, table_name: str, columns: List, connection_name: str,
|
| 240 |
-
database: str, schema: str, all_tables: Dict = None) -> str:
|
| 241 |
-
"""Generate table TML matching working example structure
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
tml_columns = []
|
| 243 |
|
| 244 |
# Generate columns with proper typing
|
|
@@ -280,7 +284,7 @@ class ThoughtSpotDeployer:
|
|
| 280 |
tml_columns.append(column_def)
|
| 281 |
|
| 282 |
table_tml = {
|
| 283 |
-
'guid':
|
| 284 |
'table': {
|
| 285 |
'name': table_name.upper(),
|
| 286 |
'db': database,
|
|
@@ -1304,13 +1308,20 @@ class ThoughtSpotDeployer:
|
|
| 1304 |
log_progress("\n π Phase 2: Adding joins to tables...")
|
| 1305 |
for table_name, columns in tables.items():
|
| 1306 |
# Only add joins if the table was created successfully in Phase 1
|
| 1307 |
-
|
| 1308 |
-
|
|
|
|
| 1309 |
continue
|
| 1310 |
|
| 1311 |
-
|
| 1312 |
-
|
| 1313 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1314 |
|
| 1315 |
response = self.session.post(
|
| 1316 |
f"{self.base_url}/api/rest/2.0/metadata/tml/import",
|
|
|
|
| 236 |
print(f" β Relationship API call failed: {response.status_code}")
|
| 237 |
print(f" π Response: {response.text}")
|
| 238 |
|
| 239 |
+
def create_table_tml(self, table_name: str, columns: List, connection_name: str,
|
| 240 |
+
database: str, schema: str, all_tables: Dict = None, table_guid: str = None) -> str:
|
| 241 |
+
"""Generate table TML matching working example structure
|
| 242 |
+
|
| 243 |
+
Args:
|
| 244 |
+
table_guid: If provided, use this GUID (for updating existing tables with joins)
|
| 245 |
+
"""
|
| 246 |
tml_columns = []
|
| 247 |
|
| 248 |
# Generate columns with proper typing
|
|
|
|
| 284 |
tml_columns.append(column_def)
|
| 285 |
|
| 286 |
table_tml = {
|
| 287 |
+
'guid': table_guid, # Use provided GUID or None for new tables
|
| 288 |
'table': {
|
| 289 |
'name': table_name.upper(),
|
| 290 |
'db': database,
|
|
|
|
| 1308 |
log_progress("\n π Phase 2: Adding joins to tables...")
|
| 1309 |
for table_name, columns in tables.items():
|
| 1310 |
# Only add joins if the table was created successfully in Phase 1
|
| 1311 |
+
table_name_upper = table_name.upper()
|
| 1312 |
+
if table_name_upper not in table_guids:
|
| 1313 |
+
log_progress(f" βοΈ Skipping joins for {table_name_upper} (table creation failed)")
|
| 1314 |
continue
|
| 1315 |
|
| 1316 |
+
# Get the GUID for this table
|
| 1317 |
+
table_guid = table_guids[table_name_upper]
|
| 1318 |
+
|
| 1319 |
+
log_progress(f" π Adding joins to: {table_name_upper}...")
|
| 1320 |
+
# Create table TML WITH joins_with section AND the table GUID
|
| 1321 |
+
table_tml = self.create_table_tml(
|
| 1322 |
+
table_name, columns, connection_name, database, schema,
|
| 1323 |
+
all_tables=tables, table_guid=table_guid
|
| 1324 |
+
)
|
| 1325 |
|
| 1326 |
response = self.session.post(
|
| 1327 |
f"{self.base_url}/api/rest/2.0/metadata/tml/import",
|