Spaces:
Sleeping
Sleeping
| # Lexsi SDK β Issues, Workarounds, and Suggested Fixes | |
| Compiled while building the Lexsi DS Agent (v0). All issues hit with | |
| `lexsi-sdk==0.1.46` (PyPI, May 2026). Repo: | |
| [Lexsi-Labs/Lexsi-sdk](https://github.com/Lexsi-Labs/Lexsi-sdk). Docs: | |
| [sdk.lexsi.ai](https://sdk.lexsi.ai/). | |
| Severity tags: | |
| - π **Bug** β reproducible code defect | |
| - π **Documentation** β code is fine, docs are wrong/misleading | |
| - π§© **Design** β works as designed but creates a UX cliff for consumers | |
| - β **Hypothesis** β observed behavior matches this but we don't have a | |
| direct confirmation from the SDK code | |
| Each issue records: where it lives in the SDK, our user-visible symptom, | |
| the workaround we landed in `lexsi_ds_agent`, and a suggested SDK-side | |
| fix where applicable. | |
| --- | |
| ## 1. π `UnboundLocalError` in `TabularProject.model_inference` when `pod=None` β β RESOLVED upstream | |
| **Status:** Fixed in [lexsi-sdk 0.1.51](https://github.com/Lexsi-Labs/Lexsi-sdk/releases/tag/0.1.51) via [PR #69](https://github.com/Lexsi-Labs/Lexsi-sdk/pull/69). The new SDK makes `pod` a required positional argument on `model_inference`, eliminating the unbound-variable code path. Our `predict` tool still passes `pod="small"` by default, but it's now a required-arg shim rather than a bug-dodge. | |
| **Severity:** Bug. **Crashed on every call** unless caller passes a non-None `pod`. | |
| **File:** [`lexsi_sdk/core/tabular.py`](https://github.com/Lexsi-Labs/Lexsi-sdk/blob/main/lexsi_sdk/core/tabular.py) β inside `TabularProject.model_inference`. | |
| **Symptom seen from agent:** | |
| ``` | |
| predict: model_inference failed: UnboundLocalError: local variable | |
| 'custom_batch_servers' referenced before assignment | |
| ``` | |
| **Root cause** β the validation block references `custom_batch_servers` | |
| in its `else` branch, but the variable is only bound inside the `if` | |
| branch: | |
| ```python | |
| if pod and self.metadata.get("modality") == "tabular": | |
| custom_batch_servers = self.api_client.get(AVAILABLE_BATCH_SERVERS_URI) | |
| available_custom_batch_servers = ( | |
| custom_batch_servers.get("details", []) | |
| + custom_batch_servers.get("available_gpu_custom_servers", []) | |
| ) | |
| Validate.value_against_list( | |
| "pod", pod, | |
| [server["instance_name"] for server in available_custom_batch_servers], | |
| ) | |
| else: | |
| Validate.value_against_list( | |
| "pod", pod, | |
| [ | |
| server["instance_name"] | |
| # β NameError: custom_batch_servers never bound on this path | |
| for server in custom_batch_servers.get("details", []) | |
| ], | |
| ) | |
| ``` | |
| When `pod=None` (the default, the most common call pattern) or when the | |
| project's modality is non-tabular, control falls into the `else` branch | |
| and Python raises `UnboundLocalError`. | |
| **Our workaround:** [`lexsi_ds/agent/tools/predict.py`](../lexsi_ds/agent/tools/predict.py) always passes `pod="small"` so we hit the `if` branch and dodge the unbound variable. See the `_DEFAULT_PREDICT_POD` constant. | |
| **Suggested fix** β move the fetch out of the conditional and only validate when the caller actually passed a `pod`: | |
| ```python | |
| if pod is not None: | |
| custom_batch_servers = self.api_client.get(AVAILABLE_BATCH_SERVERS_URI) | |
| available_servers = custom_batch_servers.get("details", []) | |
| if self.metadata.get("modality") == "tabular": | |
| available_servers = ( | |
| available_servers | |
| + custom_batch_servers.get("available_gpu_custom_servers", []) | |
| ) | |
| Validate.value_against_list( | |
| "pod", pod, | |
| [server["instance_name"] for server in available_servers], | |
| ) | |
| # else: pod is None, server uses its default, no validation needed | |
| ``` | |
| Three-line patch. Suggested PR title: `fix(tabular): UnboundLocalError when model_inference is called without pod`. | |
| --- | |
| ## 2. π `ProjectConfig.pred_label=None` crashes the server-side training pipeline | |
| **Severity:** Bug. Every `upload_data(config=ProjectConfig(...))` call with no `pred_label` lands here. | |
| **Files:** | |
| - Type declared with `Optional[str]` in [`lexsi_sdk/common/types.py`](https://github.com/Lexsi-Labs/Lexsi-sdk/blob/main/lexsi_sdk/common/types.py) (`class ProjectConfig`). | |
| - SDK forwards `config.get("pred_label")` as-is in | |
| [`lexsi_sdk/core/tabular.py`](https://github.com/Lexsi-Labs/Lexsi-sdk/blob/main/lexsi_sdk/core/tabular.py) β `upload_data` payload build. | |
| **Symptom seen from agent:** Activity Log shows | |
| ``` | |
| Started building XGBoost_default model | |
| Building Model | |
| 'pred_label' | |
| Failed while running explainability. #05-013 | |
| ``` | |
| or for foundation models: | |
| ``` | |
| Started building TabPFN_v3 model | |
| Building Model | |
| 'pred_label' | |
| Failed while building model. #05-001 | |
| ``` | |
| The bare `'pred_label'` log line is a Python `KeyError`/`NoneType` access being logged. The wrapping step labels the failure as `#05-013` (XAI) or `#05-001` (model build) depending on where the access happens. | |
| **Root cause:** the server-side handler does direct subscript access like `cfg["pred_label"]` or attribute access without first checking for `None`. Even though the type is documented as `Optional[str]`, the server can't actually accept `None`. | |
| **Our workaround:** Set `pred_label="Prediction"` unconditionally in | |
| `_build_project_config` ([`lexsi_ds/agent/tools/train_tabular_model.py`](../lexsi_ds/agent/tools/train_tabular_model.py)). Combined with workaround #3 below, training succeeds. | |
| **Suggested fix:** server-side handler should `cfg.get("pred_label")` and gate any column-name logic on a non-None value. If pred_label is genuinely required for the pipeline, change the docstring + TypedDict to remove `Optional` and make this a validation error at SDK level, not a server crash. | |
| ## 3. π§© No way to delete or update a saved project config (`update_config` is commented-out dead code) | |
| **Severity:** Design β forces consumers to use one-project-per-run. | |
| **File:** [`lexsi_sdk/core/project.py`](https://github.com/Lexsi-Labs/Lexsi-sdk/blob/main/lexsi_sdk/core/project.py) β `def update_config` exists but the entire body is commented out: | |
| ```python | |
| def update_config(self, compute_type: str, config: DataConfig) -> str: | |
| # """Update the project configurations. Accepts a config dictionary. | |
| # ... | |
| # """ | |
| # if not config: | |
| # raise Exception("Please upload config") | |
| # ... | |
| ``` | |
| **Symptom seen from agent:** After the first successful `upload_data(config=β¦)`, the project has a saved config. On the second call: | |
| ``` | |
| train_tabular_model: upload_data failed: Exception: Config already exists, please remove config | |
| ``` | |
| The SDK source confirms: | |
| ```python | |
| if project_config != "Not Found" and config: | |
| raise Exception("Config already exists, please remove config") | |
| ``` | |
| β¦and there's no method to "remove" it. Documentation says to "update" but the implementation is dead. | |
| **Our workaround:** Create a fresh per-run Lexsi project via `Workspace.create_project(name="agent<task><runid>", modality="tabular", project_type=<task>)`. Every agent run gets a clean slate. See `_get_or_create_run_project` in `train_tabular_model.py`. This works but accumulates projects on the Lexsi side that the user manually cleans up β there's no `Workspace.delete_project` (only `Project.delete_project`, which works but isn't exposed in our cleanup flow yet). | |
| **Suggested fixes:** | |
| - **Resurrect `update_config`** β finish the implementation that's commented out. The endpoint clearly existed at some point. | |
| - **OR add `Project.delete_config`** β single-purpose, "wipe my saved config so I can upload a new one". Maps to whatever DELETE endpoint exists. | |
| - **OR allow `upload_data(config=...)` to *replace* an existing config** when the caller passes `force=True` or similar. | |
| --- | |
| ## 4. π Server-side runs XAI by default even when `xai_method` is absent / empty | |
| **Severity:** Bug β or undocumented behavior, depending on intent. | |
| **Files:** SDK side is correct β `upload_data` only includes `explainability_method` in the payload when truthy: | |
| ```python | |
| if config.get("xai_method"): | |
| payload["metadata"]["explainability_method"] = config.get("xai_method") | |
| ``` | |
| So passing `xai_method=[]` or omitting the key should mean "don't run XAI." But: | |
| **Symptom seen from agent:** We tried `xai_method=[]` and the server still ran XAI and crashed (`#05-013 Failed while running explainability` with `could not convert string to float: 'class_0'` in the Activity Log). | |
| **Hypothesis:** the server-side training pipeline runs SHAP by default whenever a classification model is trained, regardless of what the upload metadata says. There's no platform-level "don't run XAI" flag we could find. | |
| **Our workaround:** [`train_tabular_model.py`](../lexsi_ds/agent/tools/train_tabular_model.py)'s `xai_method` default is `[]`. It's a no-op against the current server behavior but documents intent and is the right path forward if/when the server is fixed. | |
| **Suggested fixes:** | |
| - Make the server honor "no XAI requested" explicitly: when `explainability_method` is absent OR empty list, skip the XAI step. | |
| - Add an explicit `skip_xai: bool` flag on `ProjectConfig` for unambiguous opt-out. | |
| - OR document the current behavior in the [ProjectConfig docstring](https://github.com/Lexsi-Labs/Lexsi-sdk/blob/main/lexsi_sdk/common/types.py): "If absent or empty, server defaults to `['shap']` for classification and `['shap']` for regression." | |
| --- | |
| ## 5. π§© `xai_method=['lime']` "not available for classic models default version" β tier gate not surfaced upfront | |
| **Severity:** Design β error message is reasonable but consumers can't discover the constraint via the SDK before making the call. | |
| **Symptom seen from agent:** | |
| ``` | |
| Lime is not available for classic models default version. | |
| ``` | |
| when we tried `xai_method=["lime"]` with `model_type="XGBoost"`. | |
| **The four valid XAI methods** per [`lexsi_sdk/core/tabular.py`](https://github.com/Lexsi-Labs/Lexsi-sdk/blob/main/lexsi_sdk/core/tabular.py) validation: `["shap", "lime", "ig", "dlb"]`. But LIME requires a higher Lexsi tier when used with classic ML (XGBoost, LGBoost, etc.), and IG / DLB are deep-learning-only. So in practice on the default tier, **SHAP is the only working XAI for classic ML.** | |
| **Our workaround:** Default to `xai_method=[]` (skip training-time XAI entirely, see issue #6). | |
| **Suggested fixes:** | |
| - **Per-tier capability endpoint:** `LEXSI.available_xai_methods(model_type)` β `["shap"]` on default, `["shap", "lime"]` on premium, etc. So SDK can validate before the round-trip. | |
| - **Document the tier matrix** in `xai_method`'s docstring on `ProjectConfig`. | |
| --- | |
| ## 6. π TabPFN inference fails with CUDA-on-CPU mismatch (`#05-004`) | |
| **Severity:** Bug β server-side or compute-pod-config issue. | |
| **Symptom seen from agent:** After successfully training TabPFN on `compute_type="T4.small"` (GPU), `predict` fails with: | |
| ``` | |
| Model Inference Failed. #05-004 | |
| ``` | |
| Activity Log details: | |
| ``` | |
| Attempting to deserialize object on a CUDA device but | |
| torch.cuda.is_available() is False. If you are running on a CPU-only | |
| machine, please use torch.load with map_location=torch.device('cpu') | |
| to map your storages to the CPU. | |
| ``` | |
| **Root cause:** The TabPFN model checkpoint was saved with CUDA tensors. The inference pod Lexsi spun up doesn't have a GPU, but the loader does `torch.load(..., map_location=DEFAULT)` instead of `map_location='cpu'`. PyTorch refuses to deserialize. | |
| **Our workaround:** None really. We currently pass `pod="small"` (CPU) to `predict` to dodge the `UnboundLocalError` from issue #1. The user is steered to use XGBoost rather than TabPFN until this is fixed. | |
| **Suggested fixes:** | |
| - **Inference-side fix:** when loading a model checkpoint, use `map_location='cpu' if not torch.cuda.is_available() else None` so a CUDA-saved model can be loaded on a CPU pod. | |
| - **Or:** route TabPFN inference to a GPU pod automatically when the model was trained on one β `model_inference` could look at `models()['compute_type']` and reject CPU pods for GPU-trained models with a clearer error. | |
| --- | |
| ## 7. π `case_predict` can't parse its own response for an unlabeled (prediction) case β `CaseTabular.true_value` is required | |
| **Severity:** Bug β **breaks `explain_prediction` entirely** for prediction-tag cases (the common case: explaining a row whose outcome isn't known yet). | |
| **File:** [`lexsi_sdk/core/tabular.py`](https://github.com/Lexsi-Labs/Lexsi-sdk/blob/main/lexsi_sdk/core/tabular.py) β the `CaseTabular` pydantic model. On `lexsi-sdk==0.1.51`: | |
| ```python | |
| CaseTabular.true_value: str | int # required=True, no default | |
| CaseTabular.pred_value: str | int # required=True | |
| CaseTabular.pred_category: str | int # required=True | |
| ``` | |
| **Symptom seen from the repro script** (`case_predict(unique_identifier='5494', tag=<predict_tag>, model_name='TabICL_v1', xai=['shap'])` on a tag with no ground-truth column): | |
| ``` | |
| FAIL case_predict in 36.5s. ValidationError: 2 validation errors for CaseTabular | |
| true_value.str | |
| Input should be a valid string [input_value=None, input_type=NoneType] | |
| true_value.int | |
| Input should be a valid integer [input_value=None, input_type=NoneType] | |
| ``` | |
| (It's **one** field, not two errors β pydantic reports both arms of the `str | int` union failing against `None`.) | |
| **Root cause:** a case from a **predict tag has no observed label**, so the server legitimately returns `true_value = null`. But the response model types `true_value` as a *required* `str | int`, so pydantic raises while deserializing β `case_predict` blows up before it can return the `CaseTabular`. The agent's [`explain_prediction`](../lexsi_ds/agent/tools/explain_prediction.py) tool calls `case_predict` on exactly these unlabeled rows, so **every case fails** β `all_cases_failed`. | |
| **Our workaround (shipped):** `explain_prediction._relax_casetabular_validation()` relaxes `true_value` / `pred_value` / `pred_category` to `Optional[str | int]` at runtime (pydantic-v2 `model_fields` patch + `model_rebuild(force=True)`) before calling `case_predict`, so unlabeled prediction cases parse. It's idempotent and best-effort (guarded: if the SDK internals change, the original error surfaces). Remove once the SDK ships the fix. | |
| **Suggested fix (SDK):** make the label-dependent fields optional on the response model β | |
| `true_value: Optional[str | int] = None` (and `pred_value` / `pred_category`, which are also `null` for unscored cases). A case without ground truth should parse with `true_value=None`, not raise. | |
| **Note (attribute surface β verified, no mismatch):** `CaseTabular` *does* expose the fields `explain_prediction.py` reads β `shap_feature_importance` (`Optional[Dict]`), `pred_value`, `pred_category`, `summary` β plus methods `xai_shap()`, `xai_summary()`, `xai_similar_cases()`. (`feature_importance(feature: str) -> float` is a per-feature *method*, not the dict.) So once the validation bug above is worked around, the tool reads the right surface. | |