Improve dashboard LLM validation UX and report flow
This commit is contained in:
@@ -30,6 +30,34 @@ from ...services.llm_prompt_templates import (
|
||||
render_prompt,
|
||||
)
|
||||
|
||||
# [DEF:_is_masked_or_invalid_api_key:Function]
|
||||
# @PURPOSE: Guards against placeholder or malformed API keys in runtime.
|
||||
# @PRE: value may be None.
|
||||
# @POST: Returns True when value cannot be used for authenticated provider calls.
|
||||
def _is_masked_or_invalid_api_key(value: Optional[str]) -> bool:
|
||||
key = (value or "").strip()
|
||||
if not key:
|
||||
return True
|
||||
if key in {"********", "EMPTY_OR_NONE"}:
|
||||
return True
|
||||
# Most provider tokens are significantly longer; short values are almost always placeholders.
|
||||
return len(key) < 16
|
||||
# [/DEF:_is_masked_or_invalid_api_key:Function]
|
||||
|
||||
# [DEF:_json_safe_value:Function]
|
||||
# @PURPOSE: Recursively normalize payload values for JSON serialization.
|
||||
# @PRE: value may be nested dict/list with datetime values.
|
||||
# @POST: datetime values are converted to ISO strings.
|
||||
def _json_safe_value(value: Any):
|
||||
if isinstance(value, datetime):
|
||||
return value.isoformat()
|
||||
if isinstance(value, dict):
|
||||
return {k: _json_safe_value(v) for k, v in value.items()}
|
||||
if isinstance(value, list):
|
||||
return [_json_safe_value(v) for v in value]
|
||||
return value
|
||||
# [/DEF:_json_safe_value:Function]
|
||||
|
||||
# [DEF:DashboardValidationPlugin:Class]
|
||||
# @PURPOSE: Plugin for automated dashboard health analysis using LLMs.
|
||||
# @RELATION: IMPLEMENTS -> backend.src.core.plugin_base.PluginBase
|
||||
@@ -70,6 +98,7 @@ class DashboardValidationPlugin(PluginBase):
|
||||
# @SIDE_EFFECT: Captures a screenshot, calls LLM API, and writes to the database.
|
||||
async def execute(self, params: Dict[str, Any], context: Optional[TaskContext] = None):
|
||||
with belief_scope("execute", f"plugin_id={self.id}"):
|
||||
validation_started_at = datetime.utcnow()
|
||||
# Use TaskContext logger if available, otherwise fall back to app logger
|
||||
log = context.logger if context else logger
|
||||
|
||||
@@ -118,11 +147,10 @@ class DashboardValidationPlugin(PluginBase):
|
||||
llm_log.debug(f"API Key decrypted (first 8 chars): {api_key[:8] if api_key and len(api_key) > 8 else 'EMPTY_OR_NONE'}...")
|
||||
|
||||
# Check if API key was successfully decrypted
|
||||
if not api_key:
|
||||
if _is_masked_or_invalid_api_key(api_key):
|
||||
raise ValueError(
|
||||
f"Failed to decrypt API key for provider {provider_id}. "
|
||||
f"The provider may have been encrypted with a different encryption key. "
|
||||
f"Please update the provider with a new API key through the UI."
|
||||
f"Invalid API key for provider {provider_id}. "
|
||||
"Please open LLM provider settings and save a real API key (not masked placeholder)."
|
||||
)
|
||||
|
||||
# 3. Capture Screenshot
|
||||
@@ -135,12 +163,15 @@ class DashboardValidationPlugin(PluginBase):
|
||||
filename = f"{dashboard_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
|
||||
screenshot_path = os.path.join(screenshots_dir, filename)
|
||||
|
||||
screenshot_started_at = datetime.utcnow()
|
||||
screenshot_log.info(f"Capturing screenshot for dashboard {dashboard_id}")
|
||||
await screenshot_service.capture_dashboard(dashboard_id, screenshot_path)
|
||||
screenshot_log.debug(f"Screenshot saved to: {screenshot_path}")
|
||||
screenshot_finished_at = datetime.utcnow()
|
||||
|
||||
# 4. Fetch Logs (from Environment /api/v1/log/)
|
||||
logs = []
|
||||
logs_fetch_started_at = datetime.utcnow()
|
||||
try:
|
||||
client = SupersetClient(env)
|
||||
|
||||
@@ -181,6 +212,7 @@ class DashboardValidationPlugin(PluginBase):
|
||||
except Exception as e:
|
||||
superset_log.warning(f"Failed to fetch logs from environment: {e}")
|
||||
logs = [f"Error fetching remote logs: {str(e)}"]
|
||||
logs_fetch_finished_at = datetime.utcnow()
|
||||
|
||||
# 5. Analyze with LLM
|
||||
llm_client = LLMClient(
|
||||
@@ -196,11 +228,13 @@ class DashboardValidationPlugin(PluginBase):
|
||||
"dashboard_validation_prompt",
|
||||
DEFAULT_LLM_PROMPTS["dashboard_validation_prompt"],
|
||||
)
|
||||
llm_call_started_at = datetime.utcnow()
|
||||
analysis = await llm_client.analyze_dashboard(
|
||||
screenshot_path,
|
||||
logs,
|
||||
prompt_template=dashboard_prompt,
|
||||
)
|
||||
llm_call_finished_at = datetime.utcnow()
|
||||
|
||||
# Log analysis summary to task logs for better visibility
|
||||
llm_log.info(f"[ANALYSIS_SUMMARY] Status: {analysis['status']}")
|
||||
@@ -218,6 +252,35 @@ class DashboardValidationPlugin(PluginBase):
|
||||
screenshot_path=screenshot_path,
|
||||
raw_response=str(analysis)
|
||||
)
|
||||
validation_finished_at = datetime.utcnow()
|
||||
|
||||
result_payload = _json_safe_value(validation_result.dict())
|
||||
result_payload["screenshot_paths"] = [screenshot_path]
|
||||
result_payload["logs_sent_to_llm"] = logs
|
||||
result_payload["logs_sent_count"] = len(logs)
|
||||
result_payload["prompt_template"] = dashboard_prompt
|
||||
result_payload["provider"] = {
|
||||
"id": db_provider.id,
|
||||
"name": db_provider.name,
|
||||
"type": db_provider.provider_type,
|
||||
"base_url": db_provider.base_url,
|
||||
"model": db_provider.default_model,
|
||||
}
|
||||
result_payload["environment_id"] = env_id
|
||||
result_payload["timings"] = {
|
||||
"validation_started_at": validation_started_at.isoformat(),
|
||||
"validation_finished_at": validation_finished_at.isoformat(),
|
||||
"validation_duration_ms": int((validation_finished_at - validation_started_at).total_seconds() * 1000),
|
||||
"screenshot_started_at": screenshot_started_at.isoformat(),
|
||||
"screenshot_finished_at": screenshot_finished_at.isoformat(),
|
||||
"screenshot_duration_ms": int((screenshot_finished_at - screenshot_started_at).total_seconds() * 1000),
|
||||
"logs_fetch_started_at": logs_fetch_started_at.isoformat(),
|
||||
"logs_fetch_finished_at": logs_fetch_finished_at.isoformat(),
|
||||
"logs_fetch_duration_ms": int((logs_fetch_finished_at - logs_fetch_started_at).total_seconds() * 1000),
|
||||
"llm_call_started_at": llm_call_started_at.isoformat(),
|
||||
"llm_call_finished_at": llm_call_finished_at.isoformat(),
|
||||
"llm_call_duration_ms": int((llm_call_finished_at - llm_call_started_at).total_seconds() * 1000),
|
||||
}
|
||||
|
||||
db_record = ValidationRecord(
|
||||
dashboard_id=validation_result.dashboard_id,
|
||||
@@ -225,7 +288,7 @@ class DashboardValidationPlugin(PluginBase):
|
||||
summary=validation_result.summary,
|
||||
issues=[issue.dict() for issue in validation_result.issues],
|
||||
screenshot_path=validation_result.screenshot_path,
|
||||
raw_response=validation_result.raw_response
|
||||
raw_response=json.dumps(result_payload, ensure_ascii=False)
|
||||
)
|
||||
db.add(db_record)
|
||||
db.commit()
|
||||
@@ -240,7 +303,7 @@ class DashboardValidationPlugin(PluginBase):
|
||||
# Final log to ensure all analysis is visible in task logs
|
||||
log.info(f"Validation completed for dashboard {dashboard_id}. Status: {validation_result.status.value}")
|
||||
|
||||
return validation_result.dict()
|
||||
return result_payload
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
@@ -328,11 +391,10 @@ class DocumentationPlugin(PluginBase):
|
||||
llm_log.debug(f"API Key decrypted (first 8 chars): {api_key[:8] if api_key and len(api_key) > 8 else 'EMPTY_OR_NONE'}...")
|
||||
|
||||
# Check if API key was successfully decrypted
|
||||
if not api_key:
|
||||
if _is_masked_or_invalid_api_key(api_key):
|
||||
raise ValueError(
|
||||
f"Failed to decrypt API key for provider {provider_id}. "
|
||||
f"The provider may have been encrypted with a different encryption key. "
|
||||
f"Please update the provider with a new API key through the UI."
|
||||
f"Invalid API key for provider {provider_id}. "
|
||||
"Please open LLM provider settings and save a real API key (not masked placeholder)."
|
||||
)
|
||||
|
||||
# 3. Fetch Metadata (US2 / T024)
|
||||
|
||||
Reference in New Issue
Block a user