Spaces:
Running
Running
Download thoughtspot_errors.py from thoughtspot-demoprep/test-mcp: direct link, hf CLI and curl.
- Browser
- Download file 10.2 kB
-
https://huggingface.co/spaces/thoughtspot-demoprep/test-mcp/resolve/main/thoughtspot_errors.py
- Command line
-
hf download hf://spaces/thoughtspot-demoprep/test-mcp/thoughtspot_errors.py
-
curl -L -o thoughtspot_errors.py https://huggingface.co/spaces/thoughtspot-demoprep/test-mcp/resolve/main/thoughtspot_errors.py
10.2 kB
| import json | |
| from typing import Tuple | |
| from urllib.parse import urlsplit | |
| def friendly_mcp_liveboard_error(raw_error: str) -> Tuple[str, str]: | |
| """Return a user-facing MCP liveboard error and a coarse failure category.""" | |
| raw = (raw_error or "Unknown MCP error").strip() | |
| normalized = raw.lower() | |
| service_terms = [ | |
| "timed out", | |
| "timeout", | |
| "unresponsive", | |
| "gateway", | |
| "504", | |
| "503", | |
| "502", | |
| "connection refused", | |
| "connection reset", | |
| "connection aborted", | |
| "connectionerror", | |
| "server disconnected", | |
| "service unavailable", | |
| "bad gateway", | |
| "network", | |
| "agent.thoughtspot.app", | |
| ] | |
| auth_terms = [ | |
| "401", | |
| "403", | |
| "unauthorized", | |
| "forbidden", | |
| "trusted auth", | |
| "auth token", | |
| "authentication", | |
| ] | |
| answer_token_terms = [ | |
| "failed to get answers for any questions", | |
| "no valid answers with thoughtspot session/tokens", | |
| "no liveboard-compatible answers", | |
| "without session/tokens", | |
| "empty-token", | |
| "empty token", | |
| "visualization_type': 'undefined", | |
| '"visualization_type": "undefined', | |
| ] | |
| if any(term in normalized for term in auth_terms): | |
| return ( | |
| "Liveboard creation failed because ThoughtSpot MCP could not authenticate. " | |
| "Check the ThoughtSpot trusted auth key, username, and environment settings. " | |
| f"Raw MCP error: {raw}", | |
| "mcp_auth", | |
| ) | |
| if any(term in normalized for term in answer_token_terms): | |
| return ( | |
| "Liveboard creation could not retrieve MCP-compatible answer session/tokens. " | |
| "The model and data may still have deployed; creating a clearly marked backup liveboard is safe for this failure mode. " | |
| f"Raw MCP error: {raw}", | |
| "mcp_answer_tokens", | |
| ) | |
| if any(term in normalized for term in service_terms): | |
| return ( | |
| "Liveboard creation failed because the ThoughtSpot MCP service appears unavailable or timed out. " | |
| "The model and data may still have deployed; create the liveboard from the ThoughtSpot Spotter/Liveboard workflow once MCP is back. " | |
| f"Raw MCP error: {raw}", | |
| "mcp_service", | |
| ) | |
| return ( | |
| "Liveboard creation failed during the MCP liveboard generation step. " | |
| "The model and data are deployed, so the run finishes without a liveboard rather than " | |
| "falling back to another liveboard path β build a board from the model in ThoughtSpot, " | |
| "and fix the generation error (Prompt Log) before re-running. " | |
| f"Raw MCP error: {raw}", | |
| "mcp_generation", | |
| ) | |
| # --- Trusted-auth login responses (ISS-22) ----------------------------------- | |
| # | |
| # What ThoughtSpot's answer to a trusted-auth token request | |
| # (POST /api/rest/2.0/auth/token/full) says about a login, so a failure can name | |
| # its real cause instead of "ThoughtSpot authentication failed". Shapes seen | |
| # for real: | |
| # no account HTTP 400, application/json, code 10002, debug | |
| # 'Invalid parameter values: {"username":"Specified user does not exist."}' | |
| # (runs 2a1c7306af2a, b58980440961, 1a69f7ab4c7f; fake-user probes | |
| # of secloud and sebe 2026-10-14) | |
| # standby HTTP 200, text/html "ThoughtSpot - Start Instance ... Your instance | |
| # entered standby mode ... Complete the verification below to wake | |
| # up your instance" (sebe 2026-10-06, runs b22397c2cfb3, c34abed7dddb) | |
| # Outcomes that mean the build's own login WILL fail, so refusing up front loses | |
| # nothing. Every other outcome lets the build go ahead. | |
| LOGIN_BLOCKING = frozenset({"no_account", "rejected", "standby"}) | |
| _STANDBY_MARKERS = ("start instance", "standby mode") | |
| def _json_or_none(text): | |
| try: | |
| return json.loads(text) | |
| except (TypeError, ValueError): | |
| return None | |
| def _host_of_url(url: str) -> str: | |
| return urlsplit(url if "://" in url else f"https://{url}").hostname or url | |
| def is_standby_page(text: str) -> bool: | |
| """True when a ThoughtSpot response is the cluster's standby / wake-up page.""" | |
| low = (text or "").lower() | |
| return any(marker in low for marker in _STANDBY_MARKERS) | |
| def ts_error_reason(text: str) -> str: | |
| """The reason inside a ThoughtSpot REST v2 error body; "" if `text` is not one. | |
| v2 nests it as {"error": {"message": {"debug": {"code": ..., "debug": | |
| "[\\"<reason>\\"]"}}}} β the innermost debug is a JSON-encoded list of | |
| strings. A body that is an error but not in that shape comes back whole. | |
| """ | |
| body = _json_or_none(text) | |
| if not isinstance(body, dict) or not isinstance(body.get("error"), dict): | |
| return "" | |
| node = body["error"].get("message") | |
| while isinstance(node, dict): | |
| node = node.get("debug", node.get("message")) | |
| if isinstance(node, str): | |
| parsed = _json_or_none(node) | |
| if isinstance(parsed, list): | |
| return "; ".join(str(item) for item in parsed) | |
| return node | |
| return json.dumps(body["error"])[:300] | |
| def classify_trusted_login(status_code: int, content_type: str, text: str) -> Tuple[str, str]: | |
| """(outcome, reason) for a trusted-auth token response. | |
| outcome: | |
| ok 200 with a token, or 204 (session cookie) β the login works | |
| no_account ThoughtSpot says the user does not exist | |
| rejected 400/401/403 with a ThoughtSpot error body, any other reason | |
| standby the cluster served its standby / wake-up page | |
| transient 429 or 5xx β says nothing about the user; worth retrying | |
| inconclusive anything else (a 4xx that is not ThoughtSpot's, a 200 with | |
| no token). Not proof the login fails, so not a refusal. | |
| """ | |
| body = _json_or_none(text) | |
| shown_type = content_type or "none" | |
| if body is None and is_standby_page(text): | |
| return "standby", "the cluster served its 'Start Instance' standby page" | |
| if status_code == 429 or status_code >= 500: | |
| return "transient", f"HTTP {status_code}" | |
| if status_code == 204: | |
| return "ok", "" | |
| if status_code == 200: | |
| if isinstance(body, dict) and "token" in body: | |
| return "ok", "" | |
| return "inconclusive", f"HTTP 200 without a token (content-type {shown_type})" | |
| if status_code in (400, 401, 403) and isinstance(body, dict) and "error" in body: | |
| reason = ts_error_reason(text) | |
| if "user does not exist" in (text or "").lower(): | |
| return "no_account", reason | |
| return "rejected", reason | |
| return "inconclusive", f"HTTP {status_code} (content-type {shown_type})" | |
| def no_account_message(owner: str, ts_url: str) -> str: | |
| return ( | |
| f"{owner} has no ThoughtSpot account on {_host_of_url(ts_url)}. Ask a ThoughtSpot " | |
| f"admin to add them, or pass ts_url for a cluster where they have one." | |
| ) | |
| def standby_message(ts_url: str) -> str: | |
| return ( | |
| f"{_host_of_url(ts_url)} is in standby. Open {ts_url} in a browser and complete " | |
| f"the wake-up check, then retry." | |
| ) | |
| def rejected_login_message(owner: str, ts_url: str, status_code: int, reason: str) -> str: | |
| return ( | |
| f"ThoughtSpot on {_host_of_url(ts_url)} refused a trusted-auth login for {owner} " | |
| f"(HTTP {status_code}): {reason}" | |
| ) | |
| def trusted_auth_failure_message(username: str, ts_url: str, status_code, body: str) -> str: | |
| """One sentence for a failed trusted-auth login that a person can act on. | |
| Names the user, the cluster and the cause β a missing account as the plain | |
| instruction (no_account_message), a standby cluster as its wake-up | |
| instruction, anything else as ThoughtSpot's own reason with the HTTP | |
| status. deploy_all raises this, so the run's errors, the MCP result and | |
| the Slack "Failed" post all say the same thing instead of the bare | |
| "ThoughtSpot authentication failed" four SEs retried against for two days | |
| (findings triage 2026-10-15, E2). | |
| """ | |
| body = body or "" | |
| status = int(status_code or 0) | |
| outcome, reason = classify_trusted_login(status, "", body) | |
| # The body alone settles the two cases that have a plain instruction, so a | |
| # caller that lost the HTTP status (or recorded the deployer's own standby | |
| # sentence instead of the page) still gets the right answer. | |
| shown = f" (HTTP {status})" if status else "" | |
| if outcome == "no_account" or "user does not exist" in body.lower(): | |
| # The instruction first β who, which cluster, what to do β then | |
| # ThoughtSpot's own words, so the run's errors still grep as an auth | |
| # failure and still quote the response. | |
| said = ts_error_reason(body) or " ".join(body.split())[:300] | |
| http = f", HTTP {status}" if status else "" | |
| return ( | |
| f"{no_account_message(username, ts_url)} " | |
| f"(ThoughtSpot authentication failed{http}: {said})" | |
| ) | |
| if outcome == "standby" or is_standby_page(body) or body == standby_message(ts_url): | |
| return f"ThoughtSpot authentication failed for {username} on {ts_url}: {standby_message(ts_url)}" | |
| if outcome in ("rejected", "transient"): | |
| cause = reason or ts_error_reason(body) or " ".join(body.split())[:300] | |
| else: | |
| cause = ts_error_reason(body) or " ".join(body.split())[:300] | |
| return ( | |
| f"ThoughtSpot authentication failed for {username} on {ts_url}{shown}: " | |
| f"{cause or 'no detail recorded'}" | |
| ) | |
| def non_json_auth_error(ts_url: str, status_code: int, content_type: str, text: str) -> str: | |
| """The real cause behind a trusted-auth response that should have been JSON. | |
| Replaces the bare parse error ("Expecting value: line 1 column 1 (char 0)"), | |
| which is all the 2026-10-06 sebe standby runs recorded. | |
| """ | |
| if is_standby_page(text): | |
| return standby_message(ts_url) | |
| snippet = " ".join((text or "").split())[:200] | |
| return ( | |
| f"ThoughtSpot returned a non-JSON response to the trusted-auth token request " | |
| f"(HTTP {status_code}, content-type {content_type or 'none'}): {snippet!r}" | |
| ) | |