lexsi-ds-agent / tests /conftest.py
adityatannaarya's picture
fixed conftest
cca512c
Raw
History Blame Contribute Delete
6.32 kB
"""Test-wide fixtures.
Two complementary fixture families live here:
**PKDD-backed fixtures (Bhavish)** — for B-owned tools that need the
bundled DuckDB and optionally a real LLM:
* `pkdd_handle` / `pkdd_ctx` — depend on the DuckDB being bootstrapped;
`pytest` skips cleanly if it's missing.
* `lexsi_env` / `pkdd_ctx_lexsi` — gated behind `SDK_ACCESS_TOKEN` +
project env vars; skip cleanly when they're absent.
**Offline-synthetic fixtures (Aditya)** — for the modeling tools
(`train_tabular_model`, `predict`, `explain_prediction`); fully
hermetic, no DuckDB / Lexsi / LLM dependency:
* `loan_context_df` / `loan_predict_df` — small PKDD-loan-shaped
DataFrames with / without labels.
* `fake_dataset_handle`, `fake_tab_project`, `ctx_with_fake`,
`ctx_offline` — context wired against `FakeTabularProject` from
`lexsi_ds.agent.testing.fakes`.
* `seed_sql_result` — helper to stash a DataFrame on `ctx.cache` the
way `run_sql` would.
No fixture-name collisions; the two families are pytest-discovered
together.
"""
from __future__ import annotations
import os
import random
from pathlib import Path
import pandas as pd
import pytest
from lexsi_ds.agent.context import AgentContext, DatasetHandle
from lexsi_ds.agent.testing import FakeTabularProject
from lexsi_ds.paths import DUCKDB_PATH
# ===========================================================================
# PKDD-backed fixtures
# ===========================================================================
@pytest.fixture(scope="session")
def pkdd_handle():
"""Load the bundled PKDD DatasetHandle. Skip if the DuckDB isn't built yet."""
if not DUCKDB_PATH.exists():
pytest.skip(
f"PKDD DuckDB not built at {DUCKDB_PATH}. Run "
f"`uv run python -m lexsi_ds.data.loader` first."
)
from lexsi_ds.agent.datasource import load_pkdd_handle
return load_pkdd_handle()
@pytest.fixture()
def pkdd_ctx(pkdd_handle):
"""Minimal AgentContext on PKDD with the stub LLM. Good for non-LLM tools."""
from lexsi_ds.agent.context import AgentContext
from lexsi_ds.llm.client import factory as llm_factory
return AgentContext(
dataset=pkdd_handle,
run_id="test",
llm=llm_factory("stub"),
)
@pytest.fixture()
def lexsi_env() -> dict[str, str]:
"""Skip the test unless the Lexsi env is fully set."""
required = ("SDK_ACCESS_TOKEN", "LEXSI_WORKSPACE_NAME", "LEXSI_TEXT_PROJECT_NAME")
missing = [v for v in required if not os.environ.get(v)]
if missing:
pytest.skip(
f"Lexsi env not configured (missing: {missing}). "
f"See README quickstart for setup."
)
return {v: os.environ[v] for v in required}
@pytest.fixture()
def pkdd_ctx_lexsi(pkdd_handle, lexsi_env):
"""AgentContext on PKDD with a real LexsiTextClient."""
from lexsi_ds.agent.context import AgentContext
from lexsi_ds.llm.client import factory as llm_factory
return AgentContext(
dataset=pkdd_handle,
run_id="test-lexsi",
llm=llm_factory("lexsi"),
)
# ===========================================================================
# Offline-synthetic fixtures
# ===========================================================================
@pytest.fixture
def loan_context_df() -> pd.DataFrame:
"""Tiny PKDD-loan-default-shaped DataFrame with KNOWN labels (context)."""
rng = random.Random(42)
n = 60
rows = []
for i in range(n):
amount = rng.randint(5_000, 200_000)
duration = rng.choice([12, 24, 36, 48, 60])
payments = round(amount / duration * rng.uniform(0.9, 1.1), 2)
# Default risk roughly proportional to amount / duration.
risk = (amount / 200_000) * (1 - duration / 60)
status = "defaulted" if rng.random() < risk else "repaid"
rows.append({
"loan_id": 1000 + i,
"amount": amount,
"duration": duration,
"payments": payments,
"status": status,
})
return pd.DataFrame(rows)
@pytest.fixture
def loan_predict_df() -> pd.DataFrame:
"""Predict-set: same shape, no status column (label-unknown)."""
rng = random.Random(7)
n = 25
rows = []
for i in range(n):
amount = rng.randint(5_000, 200_000)
duration = rng.choice([12, 24, 36, 48, 60])
payments = round(amount / duration * rng.uniform(0.9, 1.1), 2)
rows.append({
"loan_id": 9000 + i,
"amount": amount,
"duration": duration,
"payments": payments,
"status": None, # all-null target — predict tool will strip
})
return pd.DataFrame(rows)
@pytest.fixture
def fake_dataset_handle(tmp_path: Path) -> DatasetHandle:
"""A minimal in-memory DatasetHandle. We don't touch DuckDB in tests."""
return DatasetHandle(
id="test_offline",
kind="bundled",
duckdb_path=tmp_path / "nonexistent.duckdb",
tables=[],
connector_id=None,
kg=None,
)
@pytest.fixture
def fake_tab_project() -> FakeTabularProject:
return FakeTabularProject(project_name="test_proj", seed=42)
@pytest.fixture
def ctx_with_fake(
fake_dataset_handle: DatasetHandle,
fake_tab_project: FakeTabularProject,
) -> AgentContext:
"""A test context with a FakeTabularProject wired in (no LLM)."""
return AgentContext(
dataset=fake_dataset_handle,
run_id="test_run",
org=None,
text_project=None,
tab_project=fake_tab_project,
llm=None,
)
@pytest.fixture
def ctx_offline(fake_dataset_handle: DatasetHandle) -> AgentContext:
"""A test context with NO tab_project — for negative-path tests."""
return AgentContext(
dataset=fake_dataset_handle,
run_id="test_offline",
org=None,
text_project=None,
tab_project=None,
llm=None,
)
def _seed_sql_result(ctx: AgentContext, label: str, df: pd.DataFrame) -> None:
"""Helper: stash a DataFrame on ctx.cache the way run_sql would."""
ctx.cache[f"sql_result:{label}"] = df
ctx.cache["last_sql_result"] = df
@pytest.fixture
def seed_sql_result():
return _seed_sql_result