Python Client¶
The Python SDK supports both remote mode (connects to a running server) and local mode (no server needed).
Installation¶
Usage¶
Remote Mode¶
from slayer.client.slayer_client import SlayerClient
client = SlayerClient(url="http://localhost:5143")
query = {
"source_model": "orders",
"measures": ["*:count", "revenue:sum"],
"dimensions": ["status"],
"limit": 10,
}
# Get raw data
data = client.query(query)
# [{"orders.status": "completed", "orders._count": 42, ...}, ...]
# Get pandas DataFrame
df = client.query_df(query)
print(df)
Local Mode¶
No server needed — queries execute directly against the storage backend:
from slayer.client.slayer_client import SlayerClient
from slayer.storage.yaml_storage import YAMLStorage
client = SlayerClient(storage=YAMLStorage(base_dir="./slayer_data"))
# Same query API as remote mode
data = client.query(query)
df = client.query_df(query)
Accepted Input Shapes¶
client.query / query_sync / sql / sql_sync / explain / explain_sync / query_df all accept the same input union (mirroring engine.execute):
- A dict — a single query.
- A
SlayerQueryinstance. - A list of dicts or
SlayerQuery— a multi-stage DAG. Earlier stages are named sub-queries; the last entry is the root. Order doesn't matter (the engine auto-sorts). See Query Lists. - A string — runs the backing query of a query-backed model by name.
# Multi-stage DAG
client.query_sync([
{"name": "by_customer", "source_model": "orders", "measures": [{"formula": "amount:sum"}], "dimensions": [{"name": "customer_id"}]},
{"source_model": "by_customer", "measures": [{"formula": "amount_sum:avg"}]},
])
# Run-by-name (query-backed model)
client.query_sync("rev_by_region")
# Raw rows — opt out of the dim-only auto-dedup. Per-stage in DAG queries.
client.query_sync({
"source_model": "orders",
"dimensions": ["status", "amount"],
"filters": ["amount > 100"],
"limit": 100,
"distinct_dimension_values": False,
})
Other Methods¶
# List models
models = client.list_models()
# Get model definition
model = client.get_model("orders")
# Create a model
client.create_model({"name": "orders", "sql_table": "public.orders", ...})
# List datasources
datasources = client.list_datasources()
# Create a datasource
client.create_datasource({"name": "mydb", "type": "postgres", ...})
Inspect¶
inspect / inspect_sync is a point lookup (DEV-1588): the rendered detail for exactly one entity by reference + required entity_type. No fusion / ranking / bundled memories — use search for an entity in context. Same arguments as the MCP inspect tool and POST /inspect; returns the rendered string. DEV-1612: reference also accepts a list — a homogeneous-kind batch (one entity_type for every id), returning one block per id in input order with per-id error isolation.
# Compact default: schema skeleton for a model (column / measure / aggregation
# names + joins, zero DB calls); description-only for the other kinds.
print(client.inspect_sync(reference="mydb.orders", entity_type="model"))
# Full render of one column (compact=False); join paths resolve to the owner.
print(client.inspect_sync(
reference="mydb.orders.customers.region", entity_type="column",
compact=False,
))
# Batch: several same-kind columns in one round-trip (DEV-1612).
print(client.inspect_sync(
reference=["mydb.orders.amount", "mydb.orders.customer_id"],
entity_type="column", compact=False,
))
# async form: await client.inspect(reference="mydb.orders", entity_type="model")
entity_type is required (datasource / model / column / measure / aggregation / memory) and asserts the resolved kind. The model-only num_rows / show_sql / sections apply for entity_type="model"; descriptions_max_chars applies to every kind. format="json" returns a JSON string instead of Markdown.
Memories + Semantic Search¶
SlayerClient exposes the same single retrieval surface as MCP / REST. All three are async (run_sync wraps them for synchronous use); local mode (storage=) goes through SearchService / MemoryService directly, remote mode (url=) POSTs to /search and /memories. See Search and Memories.
from slayer.async_utils import run_sync
# Save a learning
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
))
# Save a query-bearing memory — pass a SlayerQuery / dict for linked_entities
run_sync(client.save_memory(
learning="Top customers by lifetime spend",
linked_entities={
"source_model": "orders",
"measures": [{"formula": "amount:sum", "name": "lifetime_spend"}],
"dimensions": ["customers.name"],
"order": [{"column": "lifetime_spend", "direction": "desc"}],
"limit": 5,
},
id="kb.top-customers",
))
# Search
resp = run_sync(client.search(
question="What should I know before comparing Brooklyn revenue to other stores?",
max_results=10,
))
for hit in resp.results:
if hit.kind == "memory":
kind = "example_query" if hit.query is not None else "learning"
print(f"[{kind}] {hit.id} score={hit.score:.3f} {hit.text[:80]}")
else:
print(f"[{hit.kind}] {hit.id} score={hit.score:.3f}")
# Forget by id (cascade-strips memory:<id> refs from other memories)
run_sync(client.forget_memory("kb.returns.null-handling"))
client.search signature (keyword-only):
async def search(
self,
*,
entities: Optional[List[str]] = None,
query: Optional[Union[SlayerQuery, Dict[str, Any]]] = None,
question: Optional[str] = None,
datasource: Optional[str] = None,
max_results: int = 10,
cypher_filter: Optional[str] = None,
) -> SearchResponse: ...
cypher_filter accepts full openCypher when the advanced_search extra is installed (LadybugDB property graph with Memory / Datasource / Model / ModelColumn / Measure / Aggregation nodes and MENTIONS / CONTAINS / JOINS edges; mutation clauses rejected). Without the extra, only the naive form MATCH (n:Label1:Label2…) RETURN n.id AS id is accepted as a label/kind filter — anything richer raises with an install hint.
SearchResponse carries a single flat ranked list. Each SearchHit has kind ("memory" / "datasource" / "model" / "column" / "measure" / "aggregation"), id, score, text, matched_entities, and query (the attached SlayerQuery for query-bearing memories, else None). Unresolved input tokens land in SearchResponse.warnings instead of raising. Column hits include the structured sampled_values snapshot (top 50 by frequency, JSON-encoded) and a Distinct count: N line when the true cardinality overflows the snapshot; stale column profiles are refreshed lazily inside search().
Direct Engine Access¶
For maximum control, use the query engine directly:
from slayer.engine.query_engine import SlayerQueryEngine
from slayer.storage.yaml_storage import YAMLStorage
storage = YAMLStorage(base_dir="./slayer_data")
engine = SlayerQueryEngine(storage=storage)
result = engine.execute(query=query)
# result.data — list of row dicts
# result.columns — list of column names
# result.attributes — ResponseAttributes with .dimensions and .measures dicts (column → FieldMetadata)
#
# client.query() returns SlayerResponse with all fields above
# client.sql(query) returns just the generated SQL string
# client.explain(query) returns SlayerResponse with EXPLAIN ANALYZE output
# result.row_count — number of rows
# result.sql — generated SQL string