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>

Files changed (3) hide show
  1. liveboard_creator.py +59 -36
  2. schema_utils.py +12 -0
  3. 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 for search query generation"""
35
  try:
36
- response = self.ts_client.session.get(
37
- f"{self.ts_client.base_url}/api/rest/2.0/metadata/details",
 
 
38
  headers=self.ts_client.headers,
39
- params={"id": self.model_id}
40
  )
41
 
42
  if response.status_code == 200:
43
- data = response.json()
44
- return data.get("storables", [{}])[0].get("columns", [])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  else:
46
- print(f"Warning: Could not fetch model columns: {response.text}")
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
- for i, viz in enumerate(visualizations):
344
- col = i % 2
345
- row = i // 2
346
- tiles.append({
347
- 'visualization_id': viz['id'],
348
- 'x': col * 6, # 12-column grid system
349
- 'y': row * 5,
350
- 'height': 5,
351
- 'width': 6
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
- if result.get('object') and len(result['object']) > 0:
 
 
 
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': None,
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
- if table_name.upper() not in table_guids:
1308
- log_progress(f" ⏭️ Skipping joins for {table_name.upper()} (table creation failed)")
 
1309
  continue
1310
 
1311
- log_progress(f" πŸ”— Adding joins to: {table_name.upper()}...")
1312
- # Create table TML WITH joins_with section
1313
- table_tml = self.create_table_tml(table_name, columns, connection_name, database, schema, all_tables=tables)
 
 
 
 
 
 
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",