Spaces:
Running
Running
feat: run history tab, semantics batching, tag v2 fallback, fixes
Browse files- Add Run History admin tab with 10/50/100/All selector + email filter
- Log company/use_case in session meta for run tracking
- Fix Custom vertical: also disable Function dropdown (like Line)
- model_semantic_updater: batch columns in groups of 25, add 60s timeout
- thoughtspot_deployer: fix empty tag log, add v2 tag API fallback for models
- Fix [OK] 0 columns enriched โ [WARN] when semantics returns nothing
- Add 5-min hard timeout to MCP liveboard creation (asyncio.wait_for)
- Add pipeline summary (model + liveboard URLs) before pipeline complete
- Existing model path: use create_liveboard_from_model_mcp with real ts_client
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- chat_interface.py +152 -17
- model_semantic_updater.py +27 -18
- thoughtspot_deployer.py +47 -10
chat_interface.py
CHANGED
|
@@ -1744,6 +1744,8 @@ To change settings, use:
|
|
| 1744 |
generic_context: Additional context provided by user for generic use cases
|
| 1745 |
"""
|
| 1746 |
_slog = self._session_logger
|
|
|
|
|
|
|
| 1747 |
_t = _slog.log_start("research") if _slog else None
|
| 1748 |
|
| 1749 |
print(f"\n\n[CACHE DEBUG] === run_research_streaming called ===")
|
|
@@ -3574,8 +3576,9 @@ Tables: Created and populated
|
|
| 3574 |
self.log_feedback(f"Using existing model: {existing_model_guid}")
|
| 3575 |
|
| 3576 |
try:
|
| 3577 |
-
from liveboard_creator import
|
| 3578 |
-
|
|
|
|
| 3579 |
# Get ThoughtSpot settings
|
| 3580 |
ts_url = get_admin_setting('THOUGHTSPOT_URL')
|
| 3581 |
ts_user = self._get_effective_user_email()
|
|
@@ -3583,12 +3586,10 @@ Tables: Created and populated
|
|
| 3583 |
if not ts_secret:
|
| 3584 |
raise ValueError("ThoughtSpot trusted auth key not set. Select a TS environment from the dropdown.")
|
| 3585 |
|
| 3586 |
-
liveboard_method = 'HYBRID' # Only HYBRID method is supported
|
| 3587 |
-
|
| 3588 |
# Clean company name for display (strip .com, .org, etc)
|
| 3589 |
clean_company = company.split('.')[0].title() if '.' in company else company
|
| 3590 |
liveboard_name = self.settings.get('liveboard_name', '') or f"{clean_company} - {use_case}"
|
| 3591 |
-
|
| 3592 |
# Get company data for liveboard
|
| 3593 |
company_data = {
|
| 3594 |
'name': clean_company,
|
|
@@ -3597,18 +3598,27 @@ Tables: Created and populated
|
|
| 3597 |
'primary_color': getattr(self.demo_builder, 'primary_color', '#3498db'),
|
| 3598 |
'secondary_color': getattr(self.demo_builder, 'secondary_color', '#2c3e50')
|
| 3599 |
}
|
| 3600 |
-
|
| 3601 |
-
yield f"**Creating Liveboard from Existing Model**\n\
|
| 3602 |
-
|
| 3603 |
-
#
|
| 3604 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3605 |
model_id=existing_model_guid,
|
| 3606 |
-
model_name="Existing Model",
|
| 3607 |
-
use_case=use_case,
|
| 3608 |
company_data=company_data,
|
|
|
|
|
|
|
| 3609 |
liveboard_name=liveboard_name,
|
| 3610 |
-
|
| 3611 |
-
|
| 3612 |
)
|
| 3613 |
|
| 3614 |
if liveboard_result.get('success'):
|
|
@@ -4775,6 +4785,129 @@ def create_chat_interface():
|
|
| 4775 |
with gr.Tab("๐งฉ Matrix"):
|
| 4776 |
matrix_components = create_matrix_tab(interface)
|
| 4777 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4778 |
# Check admin status and toggle admin-only settings visibility
|
| 4779 |
def check_admin_visibility(request: gr.Request):
|
| 4780 |
"""Check if logged-in user is admin and toggle settings visibility."""
|
|
@@ -5168,23 +5301,25 @@ def create_chat_tab(chat_controller_state, settings, current_stage, current_mode
|
|
| 5168 |
msg.submit(fn=send_message, inputs=_send_inputs, outputs=_send_outputs)
|
| 5169 |
send_btn.click(fn=send_message, inputs=_send_inputs, outputs=_send_outputs)
|
| 5170 |
|
| 5171 |
-
# App tab: vertical โ line cascade
|
| 5172 |
def update_line_on_vertical(vertical):
|
| 5173 |
lines = VERTICAL_LINES.get(vertical, [])
|
| 5174 |
if vertical == "Custom" or not lines:
|
| 5175 |
return (
|
| 5176 |
gr.Dropdown(choices=[], value=None, interactive=False),
|
| 5177 |
-
gr.
|
|
|
|
| 5178 |
)
|
| 5179 |
return (
|
| 5180 |
gr.Dropdown(choices=lines, value=lines[0], interactive=True),
|
|
|
|
| 5181 |
gr.Textbox(label="Context", placeholder="Any extra context for the demo...", interactive=True),
|
| 5182 |
)
|
| 5183 |
|
| 5184 |
vertical_dd.change(
|
| 5185 |
fn=update_line_on_vertical,
|
| 5186 |
inputs=[vertical_dd],
|
| 5187 |
-
outputs=[line_dd, additional_info_input]
|
| 5188 |
)
|
| 5189 |
|
| 5190 |
# Defined tab: GO button handler
|
|
|
|
| 1744 |
generic_context: Additional context provided by user for generic use cases
|
| 1745 |
"""
|
| 1746 |
_slog = self._session_logger
|
| 1747 |
+
if _slog:
|
| 1748 |
+
_slog.log("run", "run started", company=company or '', use_case=use_case or '')
|
| 1749 |
_t = _slog.log_start("research") if _slog else None
|
| 1750 |
|
| 1751 |
print(f"\n\n[CACHE DEBUG] === run_research_streaming called ===")
|
|
|
|
| 3576 |
self.log_feedback(f"Using existing model: {existing_model_guid}")
|
| 3577 |
|
| 3578 |
try:
|
| 3579 |
+
from liveboard_creator import create_liveboard_from_model_mcp
|
| 3580 |
+
from thoughtspot_deployer import ThoughtSpotDeployer
|
| 3581 |
+
|
| 3582 |
# Get ThoughtSpot settings
|
| 3583 |
ts_url = get_admin_setting('THOUGHTSPOT_URL')
|
| 3584 |
ts_user = self._get_effective_user_email()
|
|
|
|
| 3586 |
if not ts_secret:
|
| 3587 |
raise ValueError("ThoughtSpot trusted auth key not set. Select a TS environment from the dropdown.")
|
| 3588 |
|
|
|
|
|
|
|
| 3589 |
# Clean company name for display (strip .com, .org, etc)
|
| 3590 |
clean_company = company.split('.')[0].title() if '.' in company else company
|
| 3591 |
liveboard_name = self.settings.get('liveboard_name', '') or f"{clean_company} - {use_case}"
|
| 3592 |
+
|
| 3593 |
# Get company data for liveboard
|
| 3594 |
company_data = {
|
| 3595 |
'name': clean_company,
|
|
|
|
| 3598 |
'primary_color': getattr(self.demo_builder, 'primary_color', '#3498db'),
|
| 3599 |
'secondary_color': getattr(self.demo_builder, 'secondary_color', '#2c3e50')
|
| 3600 |
}
|
| 3601 |
+
|
| 3602 |
+
yield f"**Creating Liveboard from Existing Model**\n\nModel: `{existing_model_guid}`\n\n"
|
| 3603 |
+
|
| 3604 |
+
# Auth a deployer so we can pass ts_client to MCP
|
| 3605 |
+
ts_client = ThoughtSpotDeployer(ts_url, ts_user, ts_secret)
|
| 3606 |
+
if not ts_client.authenticate():
|
| 3607 |
+
raise ValueError("ThoughtSpot authentication failed.")
|
| 3608 |
+
|
| 3609 |
+
llm_model = self.settings.get('model', DEFAULT_LLM_MODEL)
|
| 3610 |
+
|
| 3611 |
+
# Create liveboard via HYBRID (MCP) path
|
| 3612 |
+
liveboard_result = create_liveboard_from_model_mcp(
|
| 3613 |
+
ts_client=ts_client,
|
| 3614 |
model_id=existing_model_guid,
|
| 3615 |
+
model_name="Existing Model",
|
|
|
|
| 3616 |
company_data=company_data,
|
| 3617 |
+
use_case=use_case,
|
| 3618 |
+
num_visualizations=8,
|
| 3619 |
liveboard_name=liveboard_name,
|
| 3620 |
+
llm_model=llm_model,
|
| 3621 |
+
prompt_logger=self._prompt_logger,
|
| 3622 |
)
|
| 3623 |
|
| 3624 |
if liveboard_result.get('success'):
|
|
|
|
| 4785 |
with gr.Tab("๐งฉ Matrix"):
|
| 4786 |
matrix_components = create_matrix_tab(interface)
|
| 4787 |
|
| 4788 |
+
with gr.Tab("๐ Run History"):
|
| 4789 |
+
gr.Markdown("### Pipeline Run History")
|
| 4790 |
+
gr.Markdown("*Every pipeline run โ who ran it, whether it succeeded, and where it failed.*")
|
| 4791 |
+
with gr.Row():
|
| 4792 |
+
run_history_refresh_btn = gr.Button("๐ Refresh", size="sm")
|
| 4793 |
+
run_history_email_filter = gr.Textbox(label="Filter by email", placeholder="user@company.com", scale=2)
|
| 4794 |
+
run_history_limit = gr.Dropdown(
|
| 4795 |
+
label="Show",
|
| 4796 |
+
choices=["10", "50", "100", "All"],
|
| 4797 |
+
value="10",
|
| 4798 |
+
scale=1,
|
| 4799 |
+
)
|
| 4800 |
+
run_history_display = gr.Dataframe(
|
| 4801 |
+
headers=["Time (UTC)", "User", "Company", "Use Case", "Status", "Failed At", "Duration"],
|
| 4802 |
+
datatype=["str", "str", "str", "str", "str", "str", "str"],
|
| 4803 |
+
column_widths=["130px", "210px", "140px", "160px", "100px", "220px", "80px"],
|
| 4804 |
+
interactive=False,
|
| 4805 |
+
label="Runs",
|
| 4806 |
+
wrap=True,
|
| 4807 |
+
)
|
| 4808 |
+
|
| 4809 |
+
def load_run_history(email_filter="", limit_choice="10"):
|
| 4810 |
+
try:
|
| 4811 |
+
from supabase_client import SupabaseSettings
|
| 4812 |
+
from datetime import datetime as _dt
|
| 4813 |
+
ss = SupabaseSettings()
|
| 4814 |
+
if not ss.is_enabled():
|
| 4815 |
+
return [["Supabase not configured", "", "", "", "", "", ""]]
|
| 4816 |
+
|
| 4817 |
+
display_limit = None if limit_choice == "All" else int(limit_choice)
|
| 4818 |
+
# Fetch enough raw rows to aggregate into desired number of sessions
|
| 4819 |
+
fetch_limit = 2000 if limit_choice == "All" else max(500, (display_limit or 10) * 20)
|
| 4820 |
+
|
| 4821 |
+
query = ss.client.table("session_logs") \
|
| 4822 |
+
.select("session_id,user_email,ts,stage,event,duration_ms,error,meta") \
|
| 4823 |
+
.order("ts", desc=True) \
|
| 4824 |
+
.limit(fetch_limit)
|
| 4825 |
+
if email_filter and email_filter.strip():
|
| 4826 |
+
query = query.eq("user_email", email_filter.strip())
|
| 4827 |
+
result = query.execute()
|
| 4828 |
+
rows = result.data or []
|
| 4829 |
+
|
| 4830 |
+
# Group by session_id
|
| 4831 |
+
sessions = {}
|
| 4832 |
+
for row in rows:
|
| 4833 |
+
sid = row.get('session_id', '')
|
| 4834 |
+
if not sid:
|
| 4835 |
+
continue
|
| 4836 |
+
if sid not in sessions:
|
| 4837 |
+
sessions[sid] = {
|
| 4838 |
+
'user': row.get('user_email', ''),
|
| 4839 |
+
'events': [],
|
| 4840 |
+
'start_ts': row.get('ts', ''),
|
| 4841 |
+
'end_ts': row.get('ts', ''),
|
| 4842 |
+
'errors': [],
|
| 4843 |
+
'stages': [],
|
| 4844 |
+
'meta': {},
|
| 4845 |
+
}
|
| 4846 |
+
s = sessions[sid]
|
| 4847 |
+
s['events'].append(row.get('event', ''))
|
| 4848 |
+
ts = row.get('ts', '')
|
| 4849 |
+
if ts and ts < s['start_ts']:
|
| 4850 |
+
s['start_ts'] = ts
|
| 4851 |
+
if ts and ts > s['end_ts']:
|
| 4852 |
+
s['end_ts'] = ts
|
| 4853 |
+
if row.get('error'):
|
| 4854 |
+
s['errors'].append(f"{row.get('stage','?')}: {row.get('error','')[:100]}")
|
| 4855 |
+
stage = row.get('stage')
|
| 4856 |
+
if stage and stage not in s['stages']:
|
| 4857 |
+
s['stages'].append(stage)
|
| 4858 |
+
if row.get('meta'):
|
| 4859 |
+
s['meta'].update(row.get('meta') or {})
|
| 4860 |
+
|
| 4861 |
+
sorted_sessions = sorted(sessions.items(), key=lambda x: x[1]['start_ts'], reverse=True)
|
| 4862 |
+
if display_limit:
|
| 4863 |
+
sorted_sessions = sorted_sessions[:display_limit]
|
| 4864 |
+
|
| 4865 |
+
display_rows = []
|
| 4866 |
+
for sid, s in sorted_sessions:
|
| 4867 |
+
has_failed = any('failed' in e for e in s['events'])
|
| 4868 |
+
has_completed = any('completed' in e for e in s['events'])
|
| 4869 |
+
if has_failed:
|
| 4870 |
+
status = 'โ Failed'
|
| 4871 |
+
failed_at = s['errors'][0][:80] if s['errors'] else 'unknown'
|
| 4872 |
+
elif has_completed:
|
| 4873 |
+
status = 'โ
Success'
|
| 4874 |
+
failed_at = ''
|
| 4875 |
+
else:
|
| 4876 |
+
status = 'โณ In Progress'
|
| 4877 |
+
failed_at = ''
|
| 4878 |
+
|
| 4879 |
+
company = s['meta'].get('company', '') or s['meta'].get('company_name', '')
|
| 4880 |
+
use_case = s['meta'].get('use_case', '')
|
| 4881 |
+
|
| 4882 |
+
try:
|
| 4883 |
+
start = _dt.fromisoformat(s['start_ts'].replace('Z', '+00:00'))
|
| 4884 |
+
end = _dt.fromisoformat(s['end_ts'].replace('Z', '+00:00'))
|
| 4885 |
+
dur_s = int((end - start).total_seconds())
|
| 4886 |
+
dur_str = f"{dur_s//60}m {dur_s%60}s" if dur_s >= 60 else f"{dur_s}s"
|
| 4887 |
+
except Exception:
|
| 4888 |
+
dur_str = ''
|
| 4889 |
+
|
| 4890 |
+
display_rows.append([
|
| 4891 |
+
s['start_ts'][:16].replace('T', ' '),
|
| 4892 |
+
s['user'],
|
| 4893 |
+
company,
|
| 4894 |
+
use_case,
|
| 4895 |
+
status,
|
| 4896 |
+
failed_at,
|
| 4897 |
+
dur_str,
|
| 4898 |
+
])
|
| 4899 |
+
|
| 4900 |
+
return display_rows if display_rows else [["No runs found", "", "", "", "", "", ""]]
|
| 4901 |
+
except Exception as e:
|
| 4902 |
+
return [[f"Error: {e}", "", "", "", "", "", ""]]
|
| 4903 |
+
|
| 4904 |
+
run_history_refresh_btn.click(
|
| 4905 |
+
fn=load_run_history,
|
| 4906 |
+
inputs=[run_history_email_filter, run_history_limit],
|
| 4907 |
+
outputs=[run_history_display]
|
| 4908 |
+
)
|
| 4909 |
+
interface.load(fn=load_run_history, inputs=[], outputs=[run_history_display])
|
| 4910 |
+
|
| 4911 |
# Check admin status and toggle admin-only settings visibility
|
| 4912 |
def check_admin_visibility(request: gr.Request):
|
| 4913 |
"""Check if logged-in user is admin and toggle settings visibility."""
|
|
|
|
| 5301 |
msg.submit(fn=send_message, inputs=_send_inputs, outputs=_send_outputs)
|
| 5302 |
send_btn.click(fn=send_message, inputs=_send_inputs, outputs=_send_outputs)
|
| 5303 |
|
| 5304 |
+
# App tab: vertical โ line + function cascade
|
| 5305 |
def update_line_on_vertical(vertical):
|
| 5306 |
lines = VERTICAL_LINES.get(vertical, [])
|
| 5307 |
if vertical == "Custom" or not lines:
|
| 5308 |
return (
|
| 5309 |
gr.Dropdown(choices=[], value=None, interactive=False),
|
| 5310 |
+
gr.Dropdown(choices=[], value=None, interactive=False),
|
| 5311 |
+
gr.Textbox(label="Context *", placeholder="Describe your use case, industry, and key metrics...", interactive=True),
|
| 5312 |
)
|
| 5313 |
return (
|
| 5314 |
gr.Dropdown(choices=lines, value=lines[0], interactive=True),
|
| 5315 |
+
gr.Dropdown(choices=DEMO_FUNCTIONS, value=DEMO_FUNCTIONS[0], interactive=True),
|
| 5316 |
gr.Textbox(label="Context", placeholder="Any extra context for the demo...", interactive=True),
|
| 5317 |
)
|
| 5318 |
|
| 5319 |
vertical_dd.change(
|
| 5320 |
fn=update_line_on_vertical,
|
| 5321 |
inputs=[vertical_dd],
|
| 5322 |
+
outputs=[line_dd, function_dd, additional_info_input]
|
| 5323 |
)
|
| 5324 |
|
| 5325 |
# Defined tab: GO button handler
|
model_semantic_updater.py
CHANGED
|
@@ -29,7 +29,7 @@ class ModelSemanticUpdater:
|
|
| 29 |
# json_object response_format which requires OpenAI.
|
| 30 |
resolved = resolve_model_name(llm_model)
|
| 31 |
self.llm_model = resolved if is_openai_model_name(resolved) else resolve_model_name(DEFAULT_LLM_MODEL)
|
| 32 |
-
self.openai_client = create_openai_client()
|
| 33 |
|
| 34 |
# ------------------------------------------------------------------
|
| 35 |
# TML export / import helpers
|
|
@@ -166,14 +166,20 @@ Write only the description, nothing else."""
|
|
| 166 |
use_case_line = f"\nUse case: {use_case}" if use_case else ""
|
| 167 |
company_line = f"\nCompany: {company_name}" if company_name else ""
|
| 168 |
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
{company_line}{use_case_line}
|
| 171 |
|
| 172 |
Company/Industry Context:
|
| 173 |
{research_snippet}
|
| 174 |
|
| 175 |
-
Columns in this
|
| 176 |
-
{json.dumps(
|
| 177 |
|
| 178 |
For EVERY column listed, generate three fields:
|
| 179 |
|
|
@@ -199,20 +205,23 @@ Return a JSON object keyed by the exact column name:
|
|
| 199 |
"ai_context": "..."
|
| 200 |
}}
|
| 201 |
}}"""
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
|
|
|
|
|
|
|
|
|
| 216 |
|
| 217 |
# ------------------------------------------------------------------
|
| 218 |
# TML mutation
|
|
|
|
| 29 |
# json_object response_format which requires OpenAI.
|
| 30 |
resolved = resolve_model_name(llm_model)
|
| 31 |
self.llm_model = resolved if is_openai_model_name(resolved) else resolve_model_name(DEFAULT_LLM_MODEL)
|
| 32 |
+
self.openai_client = create_openai_client(timeout=60, max_retries=2)
|
| 33 |
|
| 34 |
# ------------------------------------------------------------------
|
| 35 |
# TML export / import helpers
|
|
|
|
| 166 |
use_case_line = f"\nUse case: {use_case}" if use_case else ""
|
| 167 |
company_line = f"\nCompany: {company_name}" if company_name else ""
|
| 168 |
|
| 169 |
+
# Batch columns into groups of 25 โ 107 columns at 4000 tokens = truncated JSON.
|
| 170 |
+
# Each batch gets its own LLM call so we never hit the output token ceiling.
|
| 171 |
+
BATCH_SIZE = 25
|
| 172 |
+
results: Dict[str, Dict] = {}
|
| 173 |
+
for batch_start in range(0, len(column_info), BATCH_SIZE):
|
| 174 |
+
batch = column_info[batch_start:batch_start + BATCH_SIZE]
|
| 175 |
+
batch_prompt = f"""You are a data analyst enriching a ThoughtSpot analytics model with semantic metadata.
|
| 176 |
{company_line}{use_case_line}
|
| 177 |
|
| 178 |
Company/Industry Context:
|
| 179 |
{research_snippet}
|
| 180 |
|
| 181 |
+
Columns in this batch:
|
| 182 |
+
{json.dumps(batch, indent=2)}
|
| 183 |
|
| 184 |
For EVERY column listed, generate three fields:
|
| 185 |
|
|
|
|
| 205 |
"ai_context": "..."
|
| 206 |
}}
|
| 207 |
}}"""
|
| 208 |
+
try:
|
| 209 |
+
# ~200 tokens per column is comfortable for desc + synonyms + ai_context
|
| 210 |
+
batch_max_tokens = max(2000, len(batch) * 200)
|
| 211 |
+
token_kwargs = build_openai_chat_token_kwargs(self.llm_model, batch_max_tokens)
|
| 212 |
+
response = self.openai_client.chat.completions.create(
|
| 213 |
+
model=self.llm_model,
|
| 214 |
+
messages=[{"role": "user", "content": batch_prompt}],
|
| 215 |
+
response_format={"type": "json_object"},
|
| 216 |
+
temperature=0.3,
|
| 217 |
+
**token_kwargs,
|
| 218 |
+
)
|
| 219 |
+
batch_result = json.loads(response.choices[0].message.content)
|
| 220 |
+
results.update(batch_result)
|
| 221 |
+
print(f" [Semantics] Batch {batch_start//BATCH_SIZE + 1}: enriched {len(batch_result)} columns")
|
| 222 |
+
except Exception as e:
|
| 223 |
+
print(f" [Semantics] Batch {batch_start//BATCH_SIZE + 1} failed: {e}")
|
| 224 |
+
return results
|
| 225 |
|
| 226 |
# ------------------------------------------------------------------
|
| 227 |
# TML mutation
|
thoughtspot_deployer.py
CHANGED
|
@@ -1737,11 +1737,25 @@ class ThoughtSpotDeployer:
|
|
| 1737 |
print(f"[ThoughtSpot] โ
Tagged {len(object_guids)} {object_type} objects with '{tag_name}'", flush=True)
|
| 1738 |
return True
|
| 1739 |
else:
|
| 1740 |
-
print(f"[ThoughtSpot] โ ๏ธ
|
| 1741 |
-
|
| 1742 |
-
|
| 1743 |
-
|
| 1744 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1745 |
|
| 1746 |
except Exception as e:
|
| 1747 |
print(f"[ThoughtSpot] โ ๏ธ Tag assignment error: {str(e)}", flush=True)
|
|
@@ -2225,9 +2239,9 @@ class ThoughtSpotDeployer:
|
|
| 2225 |
results['model_guid'] = model_guid
|
| 2226 |
|
| 2227 |
# Assign tag to model
|
| 2228 |
-
|
| 2229 |
-
|
| 2230 |
-
|
| 2231 |
|
| 2232 |
# Share model
|
| 2233 |
_effective_share = share_with or get_admin_setting('SHARE_WITH', required=False)
|
|
@@ -2281,7 +2295,10 @@ class ThoughtSpotDeployer:
|
|
| 2281 |
# Parse back so we can still dump consistently below
|
| 2282 |
model_tml_dict = yaml.safe_load(enriched_yaml)
|
| 2283 |
sem_time = time.time() - sem_start
|
| 2284 |
-
|
|
|
|
|
|
|
|
|
|
| 2285 |
except Exception as sem_err:
|
| 2286 |
log_progress(f"[WARN] Semantic enrichment failed (non-fatal): {sem_err}")
|
| 2287 |
|
|
@@ -2557,7 +2574,27 @@ class ThoughtSpotDeployer:
|
|
| 2557 |
|
| 2558 |
# Mark as successful if we got this far
|
| 2559 |
results['success'] = len(results['errors']) == 0
|
| 2560 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2561 |
except Exception as e:
|
| 2562 |
import traceback
|
| 2563 |
error_msg = str(e)
|
|
|
|
| 1737 |
print(f"[ThoughtSpot] โ
Tagged {len(object_guids)} {object_type} objects with '{tag_name}'", flush=True)
|
| 1738 |
return True
|
| 1739 |
else:
|
| 1740 |
+
print(f"[ThoughtSpot] โ ๏ธ v1 tag assignment failed ({assign_response.status_code}), trying v2 API...", flush=True)
|
| 1741 |
+
# v1 API returns 404 for models โ fall back to v2
|
| 1742 |
+
try:
|
| 1743 |
+
v2_response = self.session.post(
|
| 1744 |
+
f"{self.base_url}/api/rest/2.0/tags/assign",
|
| 1745 |
+
json={
|
| 1746 |
+
"tag_identifiers": [tag_name],
|
| 1747 |
+
"metadata": [{"identifier": guid, "type": object_type} for guid in object_guids]
|
| 1748 |
+
}
|
| 1749 |
+
)
|
| 1750 |
+
if v2_response.status_code in [200, 204]:
|
| 1751 |
+
print(f"[ThoughtSpot] โ
Tagged {len(object_guids)} {object_type} objects with '{tag_name}' (v2)", flush=True)
|
| 1752 |
+
return True
|
| 1753 |
+
else:
|
| 1754 |
+
print(f"[ThoughtSpot] โ ๏ธ v2 tag assignment also failed: {v2_response.status_code} โ {v2_response.text[:300]}", flush=True)
|
| 1755 |
+
return False
|
| 1756 |
+
except Exception as v2_err:
|
| 1757 |
+
print(f"[ThoughtSpot] โ ๏ธ v2 tag assignment error: {v2_err}", flush=True)
|
| 1758 |
+
return False
|
| 1759 |
|
| 1760 |
except Exception as e:
|
| 1761 |
print(f"[ThoughtSpot] โ ๏ธ Tag assignment error: {str(e)}", flush=True)
|
|
|
|
| 2239 |
results['model_guid'] = model_guid
|
| 2240 |
|
| 2241 |
# Assign tag to model
|
| 2242 |
+
if tag_name and model_guid:
|
| 2243 |
+
log_progress(f"Assigning tag '{tag_name}' to model...")
|
| 2244 |
+
self.assign_tags_to_objects([model_guid], 'LOGICAL_TABLE', tag_name)
|
| 2245 |
|
| 2246 |
# Share model
|
| 2247 |
_effective_share = share_with or get_admin_setting('SHARE_WITH', required=False)
|
|
|
|
| 2295 |
# Parse back so we can still dump consistently below
|
| 2296 |
model_tml_dict = yaml.safe_load(enriched_yaml)
|
| 2297 |
sem_time = time.time() - sem_start
|
| 2298 |
+
if column_semantics:
|
| 2299 |
+
log_progress(f"[OK] Semantics generated: {len(column_semantics)} columns enriched ({sem_time:.1f}s)")
|
| 2300 |
+
else:
|
| 2301 |
+
log_progress(f"[WARN] Semantics generation returned 0 columns โ LLM call may have failed ({sem_time:.1f}s)")
|
| 2302 |
except Exception as sem_err:
|
| 2303 |
log_progress(f"[WARN] Semantic enrichment failed (non-fatal): {sem_err}")
|
| 2304 |
|
|
|
|
| 2574 |
|
| 2575 |
# Mark as successful if we got this far
|
| 2576 |
results['success'] = len(results['errors']) == 0
|
| 2577 |
+
|
| 2578 |
+
# Log summary with clickable links before returning
|
| 2579 |
+
ts_base = self.base_url.rstrip('/')
|
| 2580 |
+
model_guid = results.get('model_guid', '')
|
| 2581 |
+
liveboard_guid = results.get('liveboard_guid', '')
|
| 2582 |
+
lb_url = results.get('liveboard_url', '')
|
| 2583 |
+
if not lb_url and liveboard_guid:
|
| 2584 |
+
lb_url = f"{ts_base}/#/pinboard/{liveboard_guid}"
|
| 2585 |
+
model_url = f"{ts_base}/#/data/tables/{model_guid}" if model_guid else ''
|
| 2586 |
+
|
| 2587 |
+
log_progress("โ" * 40)
|
| 2588 |
+
if results['success']:
|
| 2589 |
+
log_progress("โ
Pipeline complete")
|
| 2590 |
+
else:
|
| 2591 |
+
log_progress(f"โ ๏ธ Pipeline finished with {len(results['errors'])} error(s)")
|
| 2592 |
+
if model_url:
|
| 2593 |
+
log_progress(f"Model: {model_url}")
|
| 2594 |
+
if lb_url:
|
| 2595 |
+
log_progress(f"Liveboard: {lb_url}")
|
| 2596 |
+
log_progress("โ" * 40)
|
| 2597 |
+
|
| 2598 |
except Exception as e:
|
| 2599 |
import traceback
|
| 2600 |
error_msg = str(e)
|