semantics

This commit is contained in:
2026-05-12 20:06:16 +03:00
parent 9ea177558b
commit fe8978f716
461 changed files with 31360 additions and 25040 deletions

View File

@@ -1,12 +1,10 @@
# [DEF:backend/src/plugins/llm_analysis/service.py:Module]
# @COMPLEXITY: 3
# @SEMANTICS: service, llm, screenshot, playwright, openai
# @PURPOSE: Services for LLM interaction and dashboard screenshots.
# @LAYER: Domain
# @RELATION: DEPENDS_ON -> playwright
# @RELATION: DEPENDS_ON -> openai
# @RELATION: DEPENDS_ON -> tenacity
# @INVARIANT: Screenshots must be 1920px width and capture full page height.
# #region backend/src/plugins/llm_analysis/service.py [C:3] [TYPE Module] [SEMANTICS service, llm, screenshot, playwright, openai]
# @BRIEF Services for LLM interaction and dashboard screenshots.
# @LAYER Domain
# @INVARIANT Screenshots must be 1920px width and capture full page height.
# @RELATION DEPENDS_ON -> [playwright]
# @RELATION DEPENDS_ON -> [openai]
# @RELATION DEPENDS_ON -> [tenacity]
import asyncio
import base64
@@ -25,20 +23,20 @@ from ...core.logger import belief_scope, logger
from ...core.config_models import Environment
from ...services.llm_prompt_templates import DEFAULT_LLM_PROMPTS, render_prompt
# [DEF:ScreenshotService:Class]
# @PURPOSE: Handles capturing screenshots of Superset dashboards.
# #region ScreenshotService [TYPE Class]
# @BRIEF Handles capturing screenshots of Superset dashboards.
class ScreenshotService:
# [DEF:ScreenshotService.__init__:Function]
# @PURPOSE: Initializes the ScreenshotService with environment configuration.
# @PRE: env is a valid Environment object.
# #region ScreenshotService.__init__ [TYPE Function]
# @BRIEF Initializes the ScreenshotService with environment configuration.
# @PRE env is a valid Environment object.
def __init__(self, env: Environment):
self.env = env
# [/DEF:ScreenshotService.__init__:Function]
# #endregion ScreenshotService.__init__
# [DEF:ScreenshotService._find_first_visible_locator:Function]
# @PURPOSE: Resolve the first visible locator from multiple Playwright locator strategies.
# @PRE: candidates is a non-empty list of locator-like objects.
# @POST: Returns a locator ready for interaction or None when nothing matches.
# #region ScreenshotService._find_first_visible_locator [TYPE Function]
# @BRIEF Resolve the first visible locator from multiple Playwright locator strategies.
# @PRE candidates is a non-empty list of locator-like objects.
# @POST Returns a locator ready for interaction or None when nothing matches.
async def _find_first_visible_locator(self, candidates) -> Any:
for locator in candidates:
try:
@@ -50,12 +48,12 @@ class ScreenshotService:
except Exception:
continue
return None
# [/DEF:ScreenshotService._find_first_visible_locator:Function]
# #endregion ScreenshotService._find_first_visible_locator
# [DEF:ScreenshotService._iter_login_roots:Function]
# @PURPOSE: Enumerate page and child frames where login controls may be rendered.
# @PRE: page is a Playwright page-like object.
# @POST: Returns ordered roots starting with main page followed by frames.
# #region ScreenshotService._iter_login_roots [TYPE Function]
# @BRIEF Enumerate page and child frames where login controls may be rendered.
# @PRE page is a Playwright page-like object.
# @POST Returns ordered roots starting with main page followed by frames.
def _iter_login_roots(self, page) -> List[Any]:
roots = [page]
page_frames = getattr(page, "frames", [])
@@ -66,12 +64,12 @@ class ScreenshotService:
except Exception:
pass
return roots
# [/DEF:ScreenshotService._iter_login_roots:Function]
# #endregion ScreenshotService._iter_login_roots
# [DEF:ScreenshotService._extract_hidden_login_fields:Function]
# @PURPOSE: Collect hidden form fields required for direct login POST fallback.
# @PRE: Login page is loaded.
# @POST: Returns hidden input name/value mapping aggregated from page and child frames.
# #region ScreenshotService._extract_hidden_login_fields [TYPE Function]
# @BRIEF Collect hidden form fields required for direct login POST fallback.
# @PRE Login page is loaded.
# @POST Returns hidden input name/value mapping aggregated from page and child frames.
async def _extract_hidden_login_fields(self, page) -> Dict[str, str]:
hidden_fields: Dict[str, str] = {}
for root in self._iter_login_roots(page):
@@ -87,21 +85,21 @@ class ScreenshotService:
except Exception:
continue
return hidden_fields
# [/DEF:ScreenshotService._extract_hidden_login_fields:Function]
# #endregion ScreenshotService._extract_hidden_login_fields
# [DEF:ScreenshotService._extract_csrf_token:Function]
# @PURPOSE: Resolve CSRF token value from main page or embedded login frame.
# @PRE: Login page is loaded.
# @POST: Returns first non-empty csrf token or empty string.
# #region ScreenshotService._extract_csrf_token [TYPE Function]
# @BRIEF Resolve CSRF token value from main page or embedded login frame.
# @PRE Login page is loaded.
# @POST Returns first non-empty csrf token or empty string.
async def _extract_csrf_token(self, page) -> str:
hidden_fields = await self._extract_hidden_login_fields(page)
return str(hidden_fields.get("csrf_token") or "").strip()
# [/DEF:ScreenshotService._extract_csrf_token:Function]
# #endregion ScreenshotService._extract_csrf_token
# [DEF:ScreenshotService._response_looks_like_login_page:Function]
# @PURPOSE: Detect when fallback login POST returned the login form again instead of an authenticated page.
# @PRE: response_text is normalized HTML or text from login POST response.
# @POST: Returns True when login-page markers dominate the response body.
# #region ScreenshotService._response_looks_like_login_page [TYPE Function]
# @BRIEF Detect when fallback login POST returned the login form again instead of an authenticated page.
# @PRE response_text is normalized HTML or text from login POST response.
# @POST Returns True when login-page markers dominate the response body.
def _response_looks_like_login_page(self, response_text: str) -> bool:
normalized = str(response_text or "").strip().lower()
if not normalized:
@@ -115,23 +113,23 @@ class ScreenshotService:
'name="csrf_token"',
]
return sum(marker in normalized for marker in markers) >= 3
# [/DEF:ScreenshotService._response_looks_like_login_page:Function]
# #endregion ScreenshotService._response_looks_like_login_page
# [DEF:ScreenshotService._redirect_looks_authenticated:Function]
# @PURPOSE: Treat non-login redirects after form POST as successful authentication without waiting for redirect target.
# @PRE: redirect_location may be empty or relative.
# @POST: Returns True when redirect target does not point back to login flow.
# #region ScreenshotService._redirect_looks_authenticated [TYPE Function]
# @BRIEF Treat non-login redirects after form POST as successful authentication without waiting for redirect target.
# @PRE redirect_location may be empty or relative.
# @POST Returns True when redirect target does not point back to login flow.
def _redirect_looks_authenticated(self, redirect_location: str) -> bool:
normalized = str(redirect_location or "").strip().lower()
if not normalized:
return True
return "/login" not in normalized
# [/DEF:ScreenshotService._redirect_looks_authenticated:Function]
# #endregion ScreenshotService._redirect_looks_authenticated
# [DEF:ScreenshotService._submit_login_via_form_post:Function]
# @PURPOSE: Fallback login path that submits credentials directly with csrf token.
# @PRE: login_url is same-origin and csrf token can be read from DOM.
# @POST: Browser context receives authenticated cookies when login succeeds.
# #region ScreenshotService._submit_login_via_form_post [TYPE Function]
# @BRIEF Fallback login path that submits credentials directly with csrf token.
# @PRE login_url is same-origin and csrf token can be read from DOM.
# @POST Browser context receives authenticated cookies when login succeeds.
async def _submit_login_via_form_post(self, page, login_url: str) -> bool:
hidden_fields = await self._extract_hidden_login_fields(page)
csrf_token = str(hidden_fields.get("csrf_token") or "").strip()
@@ -188,12 +186,12 @@ class ScreenshotService:
f"[DEBUG] Direct form login fallback response: status={response_status} url={response_url} login_markup={looks_like_login_page} snippet={text_snippet!r}"
)
return not looks_like_login_page
# [/DEF:ScreenshotService._submit_login_via_form_post:Function]
# #endregion ScreenshotService._submit_login_via_form_post
# [DEF:ScreenshotService._find_login_field_locator:Function]
# @PURPOSE: Resolve login form input using semantic label text plus generic visible-input fallbacks.
# @PRE: field_name is `username` or `password`.
# @POST: Returns a locator for the corresponding input or None.
# #region ScreenshotService._find_login_field_locator [TYPE Function]
# @BRIEF Resolve login form input using semantic label text plus generic visible-input fallbacks.
# @PRE field_name is `username` or `password`.
# @POST Returns a locator for the corresponding input or None.
async def _find_login_field_locator(self, page, field_name: str) -> Any:
normalized = str(field_name or "").strip().lower()
for root in self._iter_login_roots(page):
@@ -228,12 +226,12 @@ class ScreenshotService:
return locator
return None
# [/DEF:ScreenshotService._find_login_field_locator:Function]
# #endregion ScreenshotService._find_login_field_locator
# [DEF:ScreenshotService._find_submit_locator:Function]
# @PURPOSE: Resolve login submit button from main page or embedded auth frame.
# @PRE: page is ready for login interaction.
# @POST: Returns visible submit locator or None.
# #region ScreenshotService._find_submit_locator [TYPE Function]
# @BRIEF Resolve login submit button from main page or embedded auth frame.
# @PRE page is ready for login interaction.
# @POST Returns visible submit locator or None.
async def _find_submit_locator(self, page) -> Any:
selectors = [
lambda root: root.get_by_role("button", name="Sign in", exact=False),
@@ -248,12 +246,12 @@ class ScreenshotService:
if locator:
return locator
return None
# [/DEF:ScreenshotService._find_submit_locator:Function]
# #endregion ScreenshotService._find_submit_locator
# [DEF:ScreenshotService._goto_resilient:Function]
# @PURPOSE: Navigate without relying on networkidle for pages with long-polling or persistent requests.
# @PRE: page is a valid Playwright page and url is non-empty.
# @POST: Returns last navigation response or raises when both primary and fallback waits fail.
# #region ScreenshotService._goto_resilient [TYPE Function]
# @BRIEF Navigate without relying on networkidle for pages with long-polling or persistent requests.
# @PRE page is a valid Playwright page and url is non-empty.
# @POST Returns last navigation response or raises when both primary and fallback waits fail.
async def _goto_resilient(
self,
page,
@@ -269,13 +267,13 @@ class ScreenshotService:
f"[ScreenshotService._goto_resilient] Primary navigation wait '{primary_wait_until}' failed for {url}: {primary_error}"
)
return await page.goto(url, wait_until=fallback_wait_until, timeout=timeout)
# [/DEF:ScreenshotService._goto_resilient:Function]
# #endregion ScreenshotService._goto_resilient
# [DEF:ScreenshotService.capture_dashboard:Function]
# @PURPOSE: Captures a full-page screenshot of a dashboard using Playwright and CDP.
# @PRE: dashboard_id is a valid string, output_path is a writable path.
# @POST: Returns True if screenshot is saved successfully.
# @SIDE_EFFECT: Launches a browser, performs UI login, switches tabs, and writes a PNG file.
# #region ScreenshotService.capture_dashboard [TYPE Function]
# @BRIEF Captures a full-page screenshot of a dashboard using Playwright and CDP.
# @PRE dashboard_id is a valid string, output_path is a writable path.
# @POST Returns True if screenshot is saved successfully.
# @SIDE_EFFECT Launches a browser, performs UI login, switches tabs, and writes a PNG file.
# @UX_STATE: [Navigating] -> Loading dashboard UI
# @UX_STATE: [TabSwitching] -> Iterating through dashboard tabs to trigger lazy loading
# @UX_STATE: [CalculatingHeight] -> Determining dashboard dimensions
@@ -671,15 +669,15 @@ class ScreenshotService:
await browser.close()
return True
# [/DEF:ScreenshotService.capture_dashboard:Function]
# [/DEF:ScreenshotService:Class]
# #endregion ScreenshotService.capture_dashboard
# #endregion ScreenshotService
# [DEF:LLMClient:Class]
# @PURPOSE: Wrapper for LLM provider APIs.
# #region LLMClient [TYPE Class]
# @BRIEF Wrapper for LLM provider APIs.
class LLMClient:
# [DEF:LLMClient.__init__:Function]
# @PURPOSE: Initializes the LLMClient with provider settings.
# @PRE: api_key, base_url, and default_model are non-empty strings.
# #region LLMClient.__init__ [TYPE Function]
# @BRIEF Initializes the LLMClient with provider settings.
# @PRE api_key, base_url, and default_model are non-empty strings.
def __init__(self, provider_type: LLMProviderType, api_key: str, base_url: str, default_model: str):
self.provider_type = provider_type
normalized_key = (api_key or "").strip()
@@ -717,12 +715,12 @@ class LLMClient:
default_headers=default_headers,
http_client=http_client,
)
# [/DEF:LLMClient.__init__:Function]
# #endregion LLMClient.__init__
# [DEF:LLMClient._supports_json_response_format:Function]
# @PURPOSE: Detect whether provider/model is likely compatible with response_format=json_object.
# @PRE: Client initialized with base_url and default_model.
# @POST: Returns False for known-incompatible combinations to avoid avoidable 400 errors.
# #region LLMClient._supports_json_response_format [TYPE Function]
# @BRIEF Detect whether provider/model is likely compatible with response_format=json_object.
# @PRE Client initialized with base_url and default_model.
# @POST Returns False for known-incompatible combinations to avoid avoidable 400 errors.
def _supports_json_response_format(self) -> bool:
base = (self.base_url or "").lower()
model = (self.default_model or "").lower()
@@ -737,13 +735,13 @@ class LLMClient:
if any(token in model for token in incompatible_tokens):
return False
return True
# [/DEF:LLMClient._supports_json_response_format:Function]
# #endregion LLMClient._supports_json_response_format
# [DEF:LLMClient.get_json_completion:Function]
# @PURPOSE: Helper to handle LLM calls with JSON mode and fallback parsing.
# @PRE: messages is a list of valid message dictionaries.
# @POST: Returns a parsed JSON dictionary.
# @SIDE_EFFECT: Calls external LLM API.
# #region LLMClient.get_json_completion [TYPE Function]
# @BRIEF Helper to handle LLM calls with JSON mode and fallback parsing.
# @PRE messages is a list of valid message dictionaries.
# @POST Returns a parsed JSON dictionary.
# @SIDE_EFFECT Calls external LLM API.
def _should_retry(exception: Exception) -> bool:
"""Custom retry predicate that excludes authentication errors."""
# Don't retry on authentication errors
@@ -859,13 +857,13 @@ class LLMClient:
return json.loads(json_str)
else:
raise
# [/DEF:LLMClient.get_json_completion:Function]
# #endregion LLMClient.get_json_completion
# [DEF:LLMClient.test_runtime_connection:Function]
# @PURPOSE: Validate provider credentials using the same chat completions transport as runtime analysis.
# @PRE: Client is initialized with provider credentials and default_model.
# @POST: Returns lightweight JSON payload when runtime auth/model path is valid.
# @SIDE_EFFECT: Calls external LLM API.
# #region LLMClient.test_runtime_connection [TYPE Function]
# @BRIEF Validate provider credentials using the same chat completions transport as runtime analysis.
# @PRE Client is initialized with provider credentials and default_model.
# @POST Returns lightweight JSON payload when runtime auth/model path is valid.
# @SIDE_EFFECT Calls external LLM API.
async def test_runtime_connection(self) -> Dict[str, Any]:
with belief_scope("test_runtime_connection"):
messages = [
@@ -875,13 +873,13 @@ class LLMClient:
}
]
return await self.get_json_completion(messages)
# [/DEF:LLMClient.test_runtime_connection:Function]
# #endregion LLMClient.test_runtime_connection
# [DEF:LLMClient.analyze_dashboard:Function]
# @PURPOSE: Sends dashboard data (screenshot + logs) to LLM for health analysis.
# @PRE: screenshot_path exists, logs is a list of strings.
# @POST: Returns a structured analysis dictionary (status, summary, issues).
# @SIDE_EFFECT: Reads screenshot file and calls external LLM API.
# #region LLMClient.analyze_dashboard [TYPE Function]
# @BRIEF Sends dashboard data (screenshot + logs) to LLM for health analysis.
# @PRE screenshot_path exists, logs is a list of strings.
# @POST Returns a structured analysis dictionary (status, summary, issues).
# @SIDE_EFFECT Reads screenshot file and calls external LLM API.
async def analyze_dashboard(
self,
screenshot_path: str,
@@ -947,7 +945,7 @@ class LLMClient:
"summary": f"Failed to get response from LLM: {str(e)}",
"issues": [{"severity": "UNKNOWN", "message": "LLM provider returned empty or invalid response"}]
}
# [/DEF:LLMClient.analyze_dashboard:Function]
# [/DEF:LLMClient:Class]
# #endregion LLMClient.analyze_dashboard
# #endregion LLMClient
# [/DEF:backend/src/plugins/llm_analysis/service.py:Module]
# #endregion backend/src/plugins/llm_analysis/service.py