Spaces:
Sleeping
Sleeping
| """`run_sql` β execute SQL against the active dataset's DuckDB. | |
| Identical behavior across all three data sources (bundled PKDD, uploaded | |
| file, S3 connector views) β the dataset layer normalises everything to a | |
| DuckDB substrate. | |
| Results are returned as a pandas DataFrame in `payload["df"]`. The | |
| `summary` field gives the LLM a compact preview (shape + head) so it | |
| doesn't have to ingest the whole result set. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| import pandas as pd | |
| from pydantic import BaseModel, Field | |
| from lexsi_ds.agent.context import AgentContext | |
| from lexsi_ds.agent.tools.base import Tool, ToolResult, ToolSpec | |
| log = logging.getLogger(__name__) | |
| # Preview cap for the LLM observation. Full DataFrame is in payload. | |
| _PREVIEW_ROWS = 20 | |
| _PREVIEW_CHARS = 4000 | |
| class RunSqlArgs(BaseModel): | |
| sql: str = Field(..., description="DuckDB-compatible SQL to execute.") | |
| label: str | None = Field( | |
| default=None, | |
| description=( | |
| "Optional human label for this query (e.g. 'context_df' or " | |
| "'top10_risky_loans'). Used by the UI/trace; the planner can " | |
| "pass it through so later tools can reference the result by name." | |
| ), | |
| ) | |
| def _run(args: RunSqlArgs, ctx: AgentContext) -> ToolResult: | |
| sql = (args.sql or "").strip().rstrip(";") | |
| if not sql: | |
| return ToolResult(ok=False, summary="run_sql: empty SQL", error="empty_sql") | |
| con = ctx.duck() | |
| try: | |
| df = con.execute(sql).df() | |
| except Exception as e: # noqa: BLE001 β surface the SQL error to the LLM | |
| return ToolResult( | |
| ok=False, | |
| summary=f"run_sql: execution failed: {type(e).__name__}: {str(e)[:300]}", | |
| error=str(e), | |
| ) | |
| finally: | |
| con.close() | |
| # Stash on context so explainability tools can find the row set by label. | |
| if args.label: | |
| ctx.cache[f"sql_result:{args.label}"] = df | |
| ctx.cache["last_sql_result"] = df | |
| ctx.cache["last_sql"] = sql | |
| preview = _preview(df) | |
| summary = ( | |
| f"run_sql: {len(df):,} rows Γ {len(df.columns)} cols" | |
| + (f" (label: {args.label})" if args.label else "") | |
| + f"\ncolumns: {list(df.columns)}\n" | |
| + f"head:\n{preview}" | |
| ) | |
| return ToolResult( | |
| ok=True, | |
| summary=summary, | |
| payload={"df": df, "n_rows": len(df), "columns": list(df.columns), | |
| "label": args.label}, | |
| ) | |
| def _preview(df: pd.DataFrame) -> str: | |
| if df.empty: | |
| return "(empty)" | |
| head = df.head(_PREVIEW_ROWS).to_string(index=False, max_cols=12) | |
| if len(head) > _PREVIEW_CHARS: | |
| head = head[:_PREVIEW_CHARS] + "\nβ¦[truncated]" | |
| return head | |
| class _RunSqlTool: | |
| spec = ToolSpec( | |
| name="run_sql", | |
| description=( | |
| "Execute DuckDB-compatible SQL against the active dataset and " | |
| "return the result as a DataFrame. " | |
| "PREFER `text_to_sql` for any SQL involving JOINs, conditional " | |
| "aggregates, or string-value filters β that tool sees the full " | |
| "KG and is far less likely to pick the wrong join path. Use " | |
| "`run_sql` inline ONLY for trivial single-table queries (e.g. " | |
| "`SELECT AVG(amount) FROM fin_loan`) OR to execute SQL that " | |
| "`text_to_sql` returned in a previous step. " | |
| "Pass `label` to give the result a name later tools can " | |
| "reference (e.g. label='context_df' before `train_tabular_model`)." | |
| ), | |
| args_schema=RunSqlArgs, | |
| returns=( | |
| "summary: '<N> rows Γ <M> cols', columns list, and the head of " | |
| "the result. payload['df'] holds the full pandas DataFrame. " | |
| "0-row results on a string filter mean the value is wrong β " | |
| "call `sample_values` and retry." | |
| ), | |
| ) | |
| def run(self, args: RunSqlArgs, ctx: AgentContext) -> ToolResult: | |
| return _run(args, ctx) | |
| TOOL: Tool = _RunSqlTool() |