fix(llm): add fetch-models endpoint, fix SQL Lab INSERT (client_id truncation, sync mode, target_column, timestamp normalization)
- Add POST /api/llm/providers/fetch-models route with LLMClient.fetch_models() - Add target_column to TranslationJob model/schema/service/orchestrator - Fix SQL Lab execute: truncate client_id to 11 chars (varchar(11)) - Switch SQL Lab to sync mode (runAsync: false) — no Celery workers - Fix polling: unwrap nested result from Superset query API - Fix ClickHouse timestamp: normalize float timestamps to YYYY-MM-DD
This commit is contained in:
@@ -164,12 +164,102 @@ class TranslationExecutor:
|
||||
# endregion execute_run
|
||||
|
||||
# region _fetch_source_rows [TYPE Function]
|
||||
# @PURPOSE: Fetch source rows from the accepted preview session for this job.
|
||||
# @PRE: job_id exists.
|
||||
# @POST: Returns list of dicts with source data.
|
||||
# @PURPOSE: Fetch full source dataset from Superset (via datasource) for full translation.
|
||||
# @PRE: job_id exists. Job may have source_datasource_id for full fetch.
|
||||
# @POST: Returns list of dicts with source data (all rows from the source datasource).
|
||||
# @SIDE_EFFECT: Makes HTTP call to Superset chart data API when datasource is configured.
|
||||
def _fetch_source_rows(self, job_id: str, run_id: str) -> List[Dict[str, Any]]:
|
||||
with belief_scope("TranslationExecutor._fetch_source_rows"):
|
||||
# Get the latest APPLIED preview session
|
||||
job = self.db.query(TranslationJob).filter(TranslationJob.id == job_id).first()
|
||||
|
||||
# If source_datasource_id is configured, fetch ALL rows from the Superset chart data API
|
||||
if job and job.source_datasource_id:
|
||||
try:
|
||||
logger.reason("Fetching full dataset from Superset datasource", {
|
||||
"run_id": run_id,
|
||||
"datasource_id": job.source_datasource_id,
|
||||
"environment_id": job.environment_id,
|
||||
})
|
||||
|
||||
# Determine environment
|
||||
environments = self.config_manager.get_environments()
|
||||
target_env_id = 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 and environments:
|
||||
env_config = environments[0]
|
||||
|
||||
if env_config:
|
||||
from ...core.superset_client import SupersetClient
|
||||
client = SupersetClient(env_config)
|
||||
|
||||
# Fetch dataset detail to build proper query context
|
||||
dataset_detail = client.get_dataset_detail(int(job.source_datasource_id))
|
||||
|
||||
# Build query context (same approach as preview but without row_limit)
|
||||
query_context = client.build_dataset_preview_query_context(
|
||||
dataset_id=int(job.source_datasource_id),
|
||||
dataset_record=dataset_detail,
|
||||
template_params={},
|
||||
effective_filters=[],
|
||||
)
|
||||
|
||||
# Remove row_limit to get ALL rows; use result_type="samples"
|
||||
queries = query_context.get("queries", [])
|
||||
if queries:
|
||||
queries[0].pop("row_limit", None)
|
||||
queries[0].pop("result_type", None)
|
||||
queries[0]["metrics"] = []
|
||||
query_context["result_type"] = "samples"
|
||||
form_data = query_context.get("form_data", {})
|
||||
form_data.pop("query_mode", None)
|
||||
|
||||
response = client.network.request(
|
||||
method="POST",
|
||||
endpoint="/api/v1/chart/data",
|
||||
data=json.dumps(query_context),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
# Extract rows
|
||||
rows = self._extract_chart_data_rows(response)
|
||||
|
||||
if rows:
|
||||
logger.reason(f"Fetched {len(rows)} rows from Superset datasource", {
|
||||
"run_id": run_id,
|
||||
})
|
||||
# Map rows to source_rows format
|
||||
source_rows = []
|
||||
for idx, row in enumerate(rows):
|
||||
source_data_dict = dict(row) if row else None
|
||||
source_text = str(row.get(job.translation_column, "")) if job.translation_column else json.dumps(row)
|
||||
source_rows.append({
|
||||
"row_index": str(idx),
|
||||
"source_text": source_text,
|
||||
"approved_translation": None,
|
||||
"source_object_name": f"Row {idx}",
|
||||
"source_data": source_data_dict,
|
||||
})
|
||||
return source_rows
|
||||
else:
|
||||
logger.explore("Superset datasource returned no rows", {
|
||||
"run_id": run_id,
|
||||
"datasource_id": job.source_datasource_id,
|
||||
})
|
||||
else:
|
||||
logger.explore("No environment config found for datasource fetch", {
|
||||
"env_id": target_env_id,
|
||||
})
|
||||
except Exception as e:
|
||||
logger.explore("Failed to fetch full dataset from Superset, falling back to preview", {
|
||||
"run_id": run_id,
|
||||
"error": str(e),
|
||||
})
|
||||
# Fall through to preview-based fetch
|
||||
|
||||
# Fallback: get the latest APPLIED preview session
|
||||
session = (
|
||||
self.db.query(TranslationPreviewSession)
|
||||
.filter(
|
||||
@@ -195,20 +285,48 @@ class TranslationExecutor:
|
||||
|
||||
source_rows = []
|
||||
for rec in records:
|
||||
source_data_dict = None
|
||||
if hasattr(rec, "source_data") and rec.source_data:
|
||||
source_data_dict = dict(rec.source_data)
|
||||
source_rows.append({
|
||||
"row_index": rec.source_object_id or "0",
|
||||
"source_text": rec.source_sql or "",
|
||||
"approved_translation": rec.target_sql if rec.status == "APPROVED" else None,
|
||||
"source_object_name": rec.source_object_name or "",
|
||||
"source_data": source_data_dict,
|
||||
})
|
||||
|
||||
logger.reason(f"Fetched {len(source_rows)} source rows from preview", {
|
||||
logger.reason(f"Fetched {len(source_rows)} source rows from preview fallback", {
|
||||
"run_id": run_id,
|
||||
"session_id": session.id,
|
||||
})
|
||||
return source_rows
|
||||
# endregion _fetch_source_rows
|
||||
|
||||
# region _extract_chart_data_rows [TYPE Function]
|
||||
# @PURPOSE: Extract data rows from Superset chart data API response.
|
||||
# @POST: Returns list of dicts with column-value pairs.
|
||||
@staticmethod
|
||||
def _extract_chart_data_rows(response: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
result = response.get("result")
|
||||
if isinstance(result, list):
|
||||
for item in result:
|
||||
if isinstance(item, dict):
|
||||
data = item.get("data")
|
||||
if isinstance(data, list) and data:
|
||||
return data
|
||||
if isinstance(result, dict):
|
||||
data = result.get("data")
|
||||
if isinstance(data, list) and data:
|
||||
return data
|
||||
data = response.get("data")
|
||||
if isinstance(data, list) and data:
|
||||
return data
|
||||
if isinstance(result, list):
|
||||
return result
|
||||
return []
|
||||
# endregion _extract_chart_data_rows
|
||||
|
||||
# region _process_batch [TYPE Function]
|
||||
# @PURPOSE: Process a single batch: filter dict, build prompt, call LLM, persist records.
|
||||
# @PRE: job and batch_rows are valid.
|
||||
@@ -272,6 +390,7 @@ class TranslationExecutor:
|
||||
source_object_type="table_row",
|
||||
source_object_id=row.get("row_index"),
|
||||
source_object_name=row.get("source_object_name", ""),
|
||||
source_data=row.get("source_data"),
|
||||
status="SUCCESS",
|
||||
)
|
||||
self.db.add(record)
|
||||
@@ -398,6 +517,7 @@ class TranslationExecutor:
|
||||
source_object_type="table_row",
|
||||
source_object_id=row.get("row_index"),
|
||||
source_object_name=row.get("source_object_name", ""),
|
||||
source_data=row.get("source_data"),
|
||||
status="FAILED",
|
||||
error_message=f"LLM call failed after {retries} retries: {last_error}",
|
||||
)
|
||||
@@ -425,6 +545,7 @@ class TranslationExecutor:
|
||||
source_object_type="table_row",
|
||||
source_object_id=row.get("row_index"),
|
||||
source_object_name=row.get("source_object_name", ""),
|
||||
source_data=row.get("source_data"),
|
||||
status="SKIPPED",
|
||||
error_message=f"LLM parse failure: {e}",
|
||||
)
|
||||
@@ -456,6 +577,7 @@ class TranslationExecutor:
|
||||
source_object_type="table_row",
|
||||
source_object_id=row.get("row_index"),
|
||||
source_object_name=row.get("source_object_name", ""),
|
||||
source_data=row.get("source_data"),
|
||||
status="SKIPPED",
|
||||
error_message="NULL translation returned by LLM",
|
||||
)
|
||||
@@ -474,6 +596,7 @@ class TranslationExecutor:
|
||||
source_object_type="table_row",
|
||||
source_object_id=row.get("row_index"),
|
||||
source_object_name=row.get("source_object_name", ""),
|
||||
source_data=row.get("source_data"),
|
||||
status="SKIPPED",
|
||||
error_message="Empty translation returned by LLM",
|
||||
)
|
||||
@@ -490,6 +613,7 @@ class TranslationExecutor:
|
||||
source_object_type="table_row",
|
||||
source_object_id=row.get("row_index"),
|
||||
source_object_name=row.get("source_object_name", ""),
|
||||
source_data=row.get("source_data"),
|
||||
status="SUCCESS",
|
||||
)
|
||||
self.db.add(record)
|
||||
|
||||
Reference in New Issue
Block a user