# #region PreviewExecutor [C:4] [TYPE Class] [SEMANTICS sqlalchemy, superset, llm, preview] # @BRIEF Fetch sample data from Superset and call LLM provider for preview. # @PRE Database session, config manager, and Superset client are available. # @POST Sample rows fetched, LLM called. # @SIDE_EFFECT Fetches data from Superset; makes HTTP calls to LLM provider. # @RELATION DEPENDS_ON -> [SupersetClient] # @RELATION DEPENDS_ON -> [LLMProviderService] # @RELATION DEPENDS_ON -> [LLMClient] # @RELATION DEPENDS_ON -> [preview_response_parser] # @RATIONALE LLM response parsing and hash utilities extracted to preview_response_parser module for INV_7 compliance. import json from typing import Any from sqlalchemy.orm import Session from ...core.config_manager import ConfigManager from ...core.logger import belief_scope, logger from ...core.superset_client import SupersetClient from ...models.translate import TranslationJob from ._llm_async_http import call_openai_compatible from .preview_resolve_provider import resolve_provider_model as _resolve_provider_model from .preview_response_parser import ( compute_config_hash as _compute_config_hash, compute_dict_snapshot_hash as _compute_dict_snapshot_hash, extract_data_rows as _extract_data_rows, parse_llm_response as _parse_llm_response, ) class PreviewExecutor: """Execute preview operations: fetch sample data from Superset, call LLM, parse responses.""" def __init__(self, db: Session, config_manager: ConfigManager): self.db = db self.config_manager = config_manager # region fetch_sample_rows [TYPE Function] # @PURPOSE: Fetch sample rows from Superset dataset for preview. # @SIDE_EFFECT Calls Superset chart data API. async def fetch_sample_rows( self, job: TranslationJob, sample_size: int = 10, env_id: str | None = None, ) -> list[dict[str, Any]]: with belief_scope("PreviewExecutor.fetch_sample_rows"): environments = self.config_manager.get_environments() target_env_id = env_id or job.environment_id or job.source_dialect or "" env_config = next((e for e in environments if e.id == target_env_id), None) if not env_config: if environments: env_config = environments[0] else: raise ValueError("No Superset environments configured") client = SupersetClient(env_config) dataset_detail = await client.get_dataset_detail(int(job.source_datasource_id)) query_context = client.build_dataset_preview_query_context( dataset_id=int(job.source_datasource_id), dataset_record=dataset_detail, template_params={}, effective_filters=[], ) # Build column list from dataset schema for explicit column projection column_names = [ col["name"] for col in dataset_detail.get("columns", []) if col.get("name") and col.get("is_active", True) ] queries = query_context.get("queries", []) if queries: queries[0]["row_limit"] = sample_size queries[0]["columns"] = column_names or [] queries[0]["metrics"] = [] queries[0].pop("result_type", None) query_context["result_type"] = "query" form_data = query_context.get("form_data", {}) form_data.pop("query_mode", None) try: response = await client.network.request( method="POST", endpoint="/api/v1/chart/data", data=json.dumps(query_context), headers={"Content-Type": "application/json"}, ) except Exception as e: raise ValueError(f"Failed to fetch sample data from Superset: {e}") return _extract_data_rows(response) # endregion fetch_sample_rows # region call_llm [TYPE Function] # @PURPOSE: Call the configured LLM provider with a prompt. # @SIDE_EFFECT Makes HTTP call to LLM provider. async def call_llm(self, job: TranslationJob, prompt: str, max_tokens: int = 8192) -> str: with belief_scope("PreviewExecutor.call_llm"): if not job.provider_id: raise ValueError("Job has no LLM provider configured") from ...services.llm_provider import LLMProviderService provider_svc = LLMProviderService(self.db) provider = provider_svc.get_provider(job.provider_id) if not provider: raise ValueError(f"LLM provider '{job.provider_id}' not found") api_key = provider_svc.get_decrypted_api_key(job.provider_id) if not api_key: raise ValueError(f"Could not decrypt API key for provider '{job.provider_id}'") model = provider.default_model or "gpt-4o-mini" provider_type = provider.provider_type.lower() if provider.provider_type else "openai" disable_reasoning = getattr(job, 'disable_reasoning', False) if provider_type not in ("openai", "openai_compatible", "openrouter", "kilo", "litellm"): raise ValueError(f"Unsupported provider type '{provider_type}' for preview") max_attempts = 2 for attempt in range(max_attempts): try: content_text, _ = await call_openai_compatible( base_url=provider.base_url, api_key=api_key, model=model, prompt=prompt, provider_type=provider_type, max_tokens=max_tokens * (attempt + 1), disable_reasoning=disable_reasoning or (attempt > 0), ) break except ValueError as e: if "empty content" in str(e) and attempt < max_attempts - 1: logger.explore("Empty LLM response, retrying with doubled max_tokens") continue raise return content_text # endregion call_llm # region delegation_helpers [TYPE Function] # @PURPOSE: Backward-compatible delegation to preview_response_parser module functions. @staticmethod def parse_llm_response( response_text: str, expected_count: int, target_languages: list[str] | None = None, finish_reason: str | None = None, ) -> dict[str, dict[str, str]]: return _parse_llm_response(response_text, expected_count, target_languages, finish_reason) def resolve_provider_model(self, job: TranslationJob) -> str | None: return _resolve_provider_model(self.db, job) @staticmethod def compute_config_hash(job: TranslationJob) -> str: return _compute_config_hash(job) def compute_dict_snapshot_hash(self, job_id: str) -> str: return _compute_dict_snapshot_hash(self.db, job_id) # endregion delegation_helpers # #endregion PreviewExecutor