Python Setup — SDK & Embedded Use¶
Use SLayer as a Python library — either as a client to a running server, or embedded directly in your application with no server at all.
Install¶
pip install motley-slayer # Base (SQLite works out of the box)
pip install motley-slayer[all] # Everything
For specific database drivers or optional extras, see the full list:
pip install motley-slayer[postgres] # PostgreSQL driver
pip install motley-slayer[client] # httpx + pandas for remote mode
Embedded mode (no server)¶
Use SLayer directly in your Python code — no HTTP, no server process:
from slayer.core.models import DatasourceConfig
from slayer.engine.ingestion import ingest_datasource
from slayer.engine.query_engine import SlayerQueryEngine
from slayer.storage.yaml_storage import YAMLStorage
# Set up storage
storage = YAMLStorage(base_dir="./slayer_data")
# Connect a database
ds = DatasourceConfig(
name="my_pg",
type="postgres",
host="localhost",
database="myapp",
username="analyst",
password="${DB_PASSWORD}", # resolved from env vars
)
storage.save_datasource(ds)
# Auto-generate models from schema
models = ingest_datasource(datasource=ds, schema="public")
for model in models:
storage.save_model(model)
print(f" {model.name}: {len(model.columns)} columns, {len(model.measures)} measures")
Query¶
from slayer.core.query import SlayerQuery
engine = SlayerQueryEngine(storage=storage)
result = engine.execute_sync(query=SlayerQuery(
source_model="orders",
measures=["*:count", "revenue:sum"],
dimensions=["status"],
))
for row in result.data:
print(row)
# {"orders.status": "completed", "orders._count": 42, "orders.revenue_sum": 12345.67}
The response object:
result.data # list of row dicts
result.columns # list of column names
result.row_count # number of rows
result.sql # generated SQL (when dry_run or explain is set)
result.attributes # ResponseAttributes with .dimensions and .measures dicts
Tenant scoping (row-level security)¶
Pass a policy= to the engine (or the local-mode client) to silently scope
every query to one tenant — joins, sub-queries, and sample data included. The
agent cannot read, override, or disable it:
from slayer.core.policy import SessionPolicy, ColumnFilterRule
engine = SlayerQueryEngine(storage=storage, policy=SessionPolicy(
data_filters=[ColumnFilterRule(column="organization_uuid", value="7ef3...")],
))
See Row-Level Security for the full model.
Remote mode (client → server)¶
Connect to a running SLayer server:
from slayer.client.slayer_client import SlayerClient
from slayer.core.query import SlayerQuery
# Connect to remote server
client = SlayerClient(url="http://localhost:5143")
# Query — returns SlayerResponse (same as embedded mode)
result = client.query(SlayerQuery(
source_model="orders",
measures=["*:count"],
dimensions=["status"],
))
print(result.data)
DataFrame integration¶
# With pandas (requires motley-slayer[client] extra)
df = client.query_df(SlayerQuery(
source_model="orders",
measures=["*:count", "revenue:sum"],
dimensions=["status"],
))
print(df)
SQLite storage¶
For single-file storage instead of YAML directories:
from slayer.storage.sqlite_storage import SQLiteStorage
storage = SQLiteStorage(db_path="slayer.db")
# Use exactly like YAMLStorage
Or use the factory:
from slayer.storage.base import resolve_storage
storage = resolve_storage("./slayer_data") # YAML
storage = resolve_storage("slayer.db") # SQLite
Verify it works¶
from slayer.storage.base import resolve_storage
from slayer.engine.query_engine import SlayerQueryEngine
storage = resolve_storage("./slayer_data")
engine = SlayerQueryEngine(storage=storage)
# Should list your ingested models
print(storage.list_models())
# Should return data
result = engine.execute_sync(query={"source_model": "orders", "measures": ["*:count"]})
print(f"{result.row_count} row(s), columns: {result.columns}")
Search & memories¶
SlayerClient.search, save_memory, and forget_memory are the single retrieval surface for prior notes and canonical entity discovery. All three are async; wrap them with run_sync for synchronous use. Local mode (storage=) goes through SearchService / MemoryService directly; remote mode (url=) POSTs to /search / /memories.
from slayer.async_utils import run_sync
# Save a learning so the next session inherits it
run_sync(client.save_memory(
learning="orders.is_returned in {0,1,NULL}; treat NULL as not returned",
linked_entities=["mydb.orders.is_returned"],
id="kb.returns.null-handling", # optional; auto-allocated if omitted
))
# Three retrieval channels (BM25 + Tantivy + optional dense embeddings)
# fused into one flat ranked list:
resp = run_sync(client.search(
question="What should I know about returns?",
max_results=10,
))
for hit in resp.results:
# kind: "memory" | "datasource" | "model" | "column" | "measure" | "aggregation"
# hit.query is not None marks a saved example query
print(hit.kind, hit.id, round(hit.score, 3), hit.text[:80])
client.search also accepts cypher_filter for graph-shaped narrowing — full openCypher with the advanced_search extra (LadybugDB property graph with Memory / Datasource / Model / ModelColumn / Measure / Aggregation nodes and MENTIONS / CONTAINS / JOINS edges), naive MATCH (n:Label) RETURN n.id AS id kind-filter otherwise. Without advanced_search (or a provider API key) the dense-embedding channel emits a single warning into SearchResponse.warnings and search degrades to BM25 + Tantivy. Column hits embed the structured sampled_values snapshot (top 50 by frequency, JSON-encoded) plus a Distinct count: N line on overflow; stale profiles are refreshed lazily inside search().
See Search, Memories, and the Python Client Reference for the full signature.
Embedded REST / MCP servers¶
If you're mounting SLayer's REST or MCP surface inside your own process and want models freshly ingested by the time the server starts handling requests, pass ingest_on_startup=True to the constructor:
from slayer.api.server import create_app
from slayer.mcp.server import create_mcp_server
app = create_app(storage=storage, ingest_on_startup=True)
mcp = create_mcp_server(storage=storage, ingest_on_startup=True)
The constructor runs idempotent auto-ingestion across every configured datasource before returning the app/server object. Same opt-in / continue-on-failure / read-only-drift semantics as the CLI flag. See Ingesting at Startup.
See the Python Client Reference for the full API.