GuruCloud KB SDK
Python SDK for the GuruCloud Knowledge Bank API — a multi-dimensional, semantic vector store. Every Knowledge Bank has a configurable dimension schema: each dimension is its own embedding space, and search is a weighted combination across the dimensions you choose.
pip install gurucloud-kb
Authenticate
from gurucloud_kb import GuruCloudClient
client = GuruCloudClient(api_key="kb_your_api_key") # keys start with "kb_"
API keys carry scopes: read (search, list), write (add/update entries),
admin (create/delete KBs, change the schema).
Mental model (read this first)
A Knowledge Bank is not a single vector index. It is a set of named dimensions, each of which is embedded separately:
| Concept | What it is |
|---|---|
| Dimension | A named field that gets its own embedding(s). The default KB has content, useful_for, relevant_systems, relevant_tasks. |
single dimension |
One vector per entry (e.g. content). |
multi dimension |
A list of values, each embedded; matches ANY value (e.g. relevant_systems). |
text_only dimension |
Stored (in entry metadata) but NOT embedded; non-semantic exact-match filtering of IDs / tags / enums. Searching it applies an exact-match filter rather than ranking. |
| Weighted search | You query several dimensions at once; each has a weight; scores combine into one ranked result. |
metadata_filters |
Exact (non-semantic) JSONB filtering layered on top of the semantic ranking. |
You can use the 4 default dimensions, or define your own semantic dimensions when you create a KB.
Create a KB with custom semantic dimensions
Pass a dimension_schema to create_kb. Each dimension becomes its own
embedding space and is automatically searchable.
kb = client.create_kb(
name="support-kb",
description="Resolved support tickets",
dimension_schema={
"version": 1,
"combination_mode": "weighted_sum",
"dimensions": [
# one vector per entry, required
{"name": "symptom", "display_name": "Symptom",
"description": "What the user reported",
"dimension_type": "single", "required": True, "default_weight": 1.5},
{"name": "resolution", "display_name": "Resolution",
"description": "How it was fixed",
"dimension_type": "single", "required": True},
# a list field — each value embedded, matches ANY
{"name": "products", "display_name": "Products",
"description": "Affected products",
"dimension_type": "multi", "max_items": 8},
# stored but NOT embedded — exact-match filter only (e.g. region code)
{"name": "region", "display_name": "Region",
"description": "Data-center region (exact match)",
"dimension_type": "text_only"},
],
},
)
print(kb.id, kb.name)
If you omit dimension_schema, you get the default 4-dimension schema
(content, useful_for, relevant_systems, relevant_tasks).
Embedding model. Every dimension in a KB shares one model (
text-embedding-3-small, 1536-dim). The model is not currently selectable through the SDK.
Update the name / description (and the agent-facing instructions)
A KB's description is its canonical "what is this / when should I use it"
text. It is not just metadata: it's the text agents receive at the MCP handshake
as initialize.instructions, and it's the description field returned by
get_mcp_server_definition(). Update it in place — there is no separate setter
for those agent-facing surfaces, and they refresh from this one field:
kb.update(description="Resolved support tickets. Query before triage; cite ticket IDs.")
kb.update(name="Support KB", description="...") # both at once (write scope)
client.update_kb(kb.id, description="...") # equivalent, by id
After an update, the next agent that connects sees the new text as
initialize.instructions, and get_mcp_server_definition()["description"]
returns it too. Pass description="" to clear it (the server-def description
then falls back to the KB name).
REST API.
PATCH /api/v1/kb/banks/{id}with{"description": "..."}.
Accumulate-only KBs (never overwrite or delete)
By default, deduplication may merge a near-duplicate into an existing entry
(update) or merge-and-replace conflicting entries (conflict). For an
observation / "connect-the-dots" KB — where every signal should be kept and a
later rollup should coexist with the entries it summarizes — set
allow_updates=False:
kb = client.create_kb("signals", allow_updates=False) # at creation
kb.set_allow_updates(False) # or toggle later (write scope)
With allow_updates=False the dedup LLM's update/conflict verdicts are
downgraded to new, so existing entries are never overwritten or deleted —
the KB only accumulates. Exact duplicates are still skipped (redundant). The
flag lives on the KB's schema (round-trips through get_schema()/update_schema());
the default is True, which preserves the historical merge behavior.
REST API. The same control is available without the SDK:
POST /api/v1/kb/banksaccepts"allow_updates": false, andPATCH /api/v1/kb/banks/{id}with{"allow_updates": false}toggles it.
Choose which fields the MCP tools return (response_fields)
By default a KB's MCP tools (query_knowledge_bank, narrate) return only
id + content per result, to keep agent context lean. Opt a KB into
returning extra keys on every result:
kb = client.create_kb("curated", response_fields=["useful_for", "source"]) # at creation
kb.set_response_fields(["useful_for", "relevant_systems", "source"]) # or set later (write scope)
kb.set_response_fields(None) # reset to id + content only
response_fields is additive: id + content are always present and the
listed keys are added when an entry carries them. Allowed values are the
standard result fields (useful_for, relevant_systems, relevant_tasks,
relevant_file_paths, source, metadata, combined_score, created_at,
updated_at) plus the KB's own dimension names; unknown keys are rejected. The
list lives on the KB's schema and only affects the MCP response shape — the
REST API and stored data always carry every field.
REST API.
POST /api/v1/kb/banksaccepts"mcp_response_fields": [...], andPATCH /api/v1/kb/banks/{id}with{"mcp_response_fields": [...]}(ornullto reset) toggles it.
Evolve the schema later (admin scope)
kb.get_schema() # current KBDimensionSchema
kb.validate_schema(new_schema) # returns warnings, applies nothing
kb.update_schema(new_schema) # replace the whole schema
kb.add_dimension({"name": "severity_notes", "display_name": "Severity",
"description": "Severity context", "dimension_type": "single"})
kb.remove_dimension("severity_notes")
Add entries
Provide a value for each dimension. single dimensions take a string;
multi dimensions take a list of strings.
kb.add_entry({
"dimensions": {
"symptom": "Login loops back to the sign-in page after SSO",
"resolution": "Cleared the stale SAML session cookie on the gateway",
"products": ["web app", "mobile app"],
"region": "us-east-1", # text_only — stored in metadata, exact-match only
},
"metadata": {"status": "resolved", "severity": "high"},
})
# Batch ingest (deduplicates by default)
kb.ingest([{ "dimensions": {...} }, { "dimensions": {...} }])
add_entry / ingest are synchronous — they return the stored entry (or
raise on failure), so the call itself tells you the write landed. This differs
from the agent-facing MCP report_learning tool, which queues the write
(it becomes searchable shortly after). An optional check_learning_status tool
to confirm a queued write landed is configurable per deployment (off by
default).
Search
Simple — one string against content
results = kb.search("how does authentication work?")
Multi-dimensional weighted search
Map each dimension to a query. The required per-dimension key is
query_text, and weight scales that dimension's contribution. Scores
are combined per combination_mode.
results = kb.search({
"dimensions": {
"symptom": {"query_text": "login loops after SSO", "weight": 2.0},
"products": {"query_text": "mobile app", "weight": 0.5},
},
"combination_mode": "weighted_sum",
"metadata_filters": {"status": "resolved"}, # exact, non-semantic
"k": 10,
"threshold": 0.35,
})
for r in results:
print(r["combined_score"], r["dimensions"] if "dimensions" in r else r)
Per-dimension you can also override aggregation, top_k, and
min_threshold:
"products": {"query_text": "mobile", "weight": 1.0,
"aggregation": "max", "min_threshold": 0.2}
Exact-match filtering with text_only dimensions
A text_only dimension is matched exactly, not ranked. Pass it alongside
at least one semantic (single/multi) dimension; its query_text folds into
an exact-match filter (equivalent to a metadata_filters entry) and never
contributes to the score:
results = kb.search({
"dimensions": {
"symptom": {"query_text": "login loops after SSO"},
"region": {"query_text": "us-east-1"}, # text_only → exact match
},
})
A search containing only text_only dimensions is rejected (there is
nothing to rank) — add a semantic (single/multi) dimension. The same is
true of metadata_filters (below): it is an exact post-filter layered on the
ranking, so every search still needs at least one semantic dimension. A
text_only dimension defined with searchable=False is stored but cannot be
used as a search filter.
Exact filtering with metadata_filters
metadata_filters is an exact (non-semantic) JSONB-containment filter applied
on top of the semantic ranking — only entries whose metadata contains all
the given key/values survive. Pair it with at least one semantic dimension:
results = kb.search({
"dimensions": {"observation": {"query_text": "late delivery"}},
"metadata_filters": {"order_id": "SO-1234", "type": "quality_issue"},
})
It narrows the ranked results; it does not rank on its own. To gather every
entry for a key regardless of relevance, widen k and pass a broad semantic
query alongside the filter.
Filter by time
Restrict results to a time window with a hard filter on entry timestamps
(UTC) — it removes out-of-window entries without affecting the ranking. Bounds
accept an ISO-8601 string or a datetime. For a string query they're
keyword args:
from datetime import datetime, timedelta, timezone
# Only knowledge created in the last 30 days
recent = kb.search(
"deployment pipeline",
created_after=datetime.now(timezone.utc) - timedelta(days=30),
)
For a dict query, set the same keys inline:
results = kb.search({
"dimensions": {"content": {"query_text": "deployment pipeline"}},
"created_after": "2026-05-01T00:00:00Z",
"created_before": "2026-06-01", # bare date == 00:00:00Z
"k": 10,
})
Available bounds: created_after, created_before, updated_after,
updated_before. Each result includes created_at / updated_at so you can
verify the window and sort by recency client-side.
Search request reference
| Field | Type | Notes |
|---|---|---|
dimensions |
{name: {query_text, weight, aggregation?, top_k?, min_threshold?}} |
At least one searchable dimension required. A text_only dimension here folds into an exact-match filter (and needs a single/multi dimension alongside it). |
combination_mode |
weighted_sum | weighted_product | max | min | custom |
How dimension scores combine. |
custom_formula |
str |
Required when combination_mode="custom"; SQL over <dim>_score. |
metadata_filters |
dict |
Exact JSONB containment, e.g. {"status": "resolved"}. |
category_filters |
[{tag, max_results, min_score}] |
Bucket results by metadata tag. |
created_after / created_before |
str | datetime |
Hard filter on entry creation time (UTC, ISO-8601). |
updated_after / updated_before |
str | datetime |
Hard filter on entry last-modified time (UTC, ISO-8601). |
k, threshold |
int, float |
Result count / minimum combined score. |
aggregation (for multi dimensions): max, avg, min, top_k_avg,
sum, count.
The SDK also accepts the older spellings query (per dimension) and
filters (top level) and rewrites them to query_text / metadata_filters
for you — but prefer the canonical names above.
Cluster entries
Group a KB's entries by any field. Each field is clustered independently and
returned keyed by field, so one call can group "by topic" and "by customer" at
once. The engine is chosen per field (method="auto"):
- Single embedding dimensions (
content,observation, …) → vector clustering (KMeans / Agglomerative / HDBSCAN over the stored vectors). Whenalgorithm="auto"and nokis given, HDBSCAN runs first; if it degenerates (fewer than 3 clusters or >40% noise — common on single-domain KBs) the engine automatically re-runs KMeans with a heuristic k and says so in the field'snote. - Multi-valued dimensions (tags, products, accounts, …) → entries are
grouped by their values; an entry carrying two tags appears in both
groups. Near-duplicate values merge per
similarity_threshold. - Anything else (
metadata.customer,source, atext_onlydimension) → fuzzy string grouping, so near-duplicate values ("Acme Inc" ≈ "Acme, Inc.") merge. Exception: values that look like codes or IDs (WHITE12,SKU-4471) are grouped exactly and labeled by their dominant value — fuzzy-matching identifiers would merge distinct codes. Pass an explicitsimilarity_thresholdto override.
Omit fields to cluster the KB's primary embedding dimension (its first
required single dimension) — the right default for custom-schema KBs, whose
content column has no embedding.
result = kb.cluster(
fields=["observation", "themes", "metadata.customer_no"],
method="auto", # vector / value-grouping / fuzzy per field
algorithm="auto", # vector: HDBSCAN, KMeans fallback on collapse
similarity_threshold=0.85, # fuzzy cutoff (1.0 = exact grouping)
label=True, # opt-in names; one batched LLM call per field
label_sample_size=5, # representatives per cluster fed to the namer
)
for field_result in result["results"]:
print(field_result["field"], field_result["method"], field_result["cluster_count"])
if field_result.get("note"):
print(" note:", field_result["note"]) # engine advisories land here
for group in field_result["clusters"]:
print(" ", group.get("label") or group.get("key"), "→", group["size"])
Labeling: the 30 largest clusters of a field are named in a single batched
LLM call; smaller clusters fall back to their dominant value (fuzzy) or keyword
terms (vector). The tokens spent are reported per field in
field_result["label_usage"] (model, input_tokens, output_tokens) so
labeling cost is always visible. ID-like fields skip the LLM entirely.
Cap how large any one cluster may grow with max_cluster_size (absolute entry
count) and/or max_cluster_fraction (share of the clustered scope, 0..1]) —
when both are given the stricter cap wins. Oversized vector clusters are
recursively split server-side (KMeans within the cluster) until every cluster
fits; oversized fuzzy groups split into exact-value groups (a single value
repeated past the cap cannot be split — the field's note says so); groups of
a multi-valued dimension are never split, since their size is the value's usage
count. Fields in one call are clustered concurrently server-side, so one
three-field request beats three single-field requests on wall-clock.
result = kb.cluster(
fields=["observation"],
algorithm="kmeans", k=8,
max_cluster_size=50, # no cluster larger than 50 entries…
max_cluster_fraction=0.2, # …or 20% of the scope, whichever is stricter
)
Outliers (HDBSCAN noise) don't have to stay a catch-all. outlier_strategy
controls what happens to them on vector fields: "keep" (default) leaves them
uncategorized; "reassign" absorbs each noise entry into its nearest cluster
when it lies within that cluster's own spread; "subcluster" re-clusters the
noise into new clusters flagged from_noise so nothing is left uncategorized.
Every vector cluster also reports mean_member_distance and a low_cohesion
flag marking clusters whose spread is an outlier vs their peers — likely
catch-alls worth a follow-up subcluster pass.
result = kb.cluster(fields=["observation"], outlier_strategy="subcluster")
for group in result["results"][0]["clusters"]:
if group.get("from_noise") or group.get("low_cohesion"):
print("refined:", group["size"], group.get("keywords"))
By default a cluster's returned members are its most central entries.
Pass member_sample="diverse" to get the nearest-centroid anchor plus greedy
farthest-point picks instead, so fringe sub-themes reach the sample — useful
when members feed a namer or summarizer that should see the whole cluster,
not just its dense core (vector fields only):
result = kb.cluster(
fields=["observation"],
max_members_per_cluster=12,
member_sample="diverse",
)
Cluster only the results of a search by passing the same shape as kb.search:
result = kb.cluster(
fields=["observation"],
search={"dimensions": {"observation": {"query_text": "billing error"}}, "k": 500},
)
Cluster request reference
| Field | Type | Notes |
|---|---|---|
fields |
[str] |
Fields to cluster. Omit → the KB's primary embedding dimension. metadata.<key> reads a metadata value; a bare name resolves to a dimension or metadata key. |
method |
auto | vector | fuzzy |
auto picks by field type (single embedding dim → vector, multi dim → value grouping, else fuzzy). vector on a non-embedding or multi field is a 400. |
algorithm |
auto | kmeans | agglomerative | hdbscan |
Vector only. kmeans/agglomerative require k. auto without k = HDBSCAN with automatic KMeans fallback on degenerate results; explicit hdbscan never falls back. |
k |
int |
Cluster count for kmeans/agglomerative. Omit for auto/hdbscan. |
min_cluster_size |
int |
HDBSCAN minimum cluster size (default 5). Low values fragment single-domain KBs into many tiny clusters. |
metric |
cosine | euclidean |
Vector distance (default cosine). |
similarity_threshold |
float |
Fuzzy cutoff 0..1 (default 0.85; 1.0 = exact). ID-like values force exact grouping unless you pass this explicitly. |
search |
SearchRequest |
Optional — cluster only matching entries. |
scope_limit |
int |
Max entries clustered when no search (default 2000). |
include_members / max_members_per_cluster |
bool / int |
Per-cluster member output. |
label |
bool |
Generate a short label per cluster (off by default — free & deterministic). When on, the 30 largest clusters of a field are named in ONE batched LLM call; the tail gets dominant-value/keyword labels. |
label_sample_size |
int |
When label=True, representatives per cluster fed to the namer — nearest-centroid for vector, most-distinct values for fuzzy (default 5). |
Each result is a FieldClusterResult with clusters: [ClusterGroup], plus:
note (engine advisories: algorithm fallback, exact ID grouping, un-embedded
field warnings), label_usage (LLM token accounting when labeling ran),
silhouette_score (vector), and each fuzzy group's key (dominant value) and
distinct values.
Retrieval assertions (search-quality regression tests)
Pin queries that MUST retrieve a given entry, then re-check them any time — your KB's retrieval quality becomes testable instead of vibes:
# "This query should find this entry" — baseline rank/score captured now.
kb.add_assertion(entry_id=entry["id"], query="how do we handle refunds?",
notes="core support flow")
kb.list_assertions() # all assertions + their baselines
report = kb.run_retrieval_eval() # re-run every assertion against live search
# → per-assertion current rank/score vs baseline, pass/fail, regressions
kb.get_assertion(assertion_id) # one assertion's detail
kb.delete_assertion(assertion_id) # retire it
query accepts a plain string (searches the primary dimension) or the same
per-dimension shape as kb.search. Run the eval after schema changes, bulk
ingests, or dedup sweeps to catch retrieval regressions. Requires write
scope to create/delete; read to list and evaluate.
Playbooks (procedures returned whole)
Entries are atomic facts found by semantic top-k. A playbook is the other
contract: a distinct, named, ordered procedure that is matched as a whole on
its title + when_to_use and then returned complete — every step, in
order — so an agent never runs a runbook with a step missing. A bank keeps one
playbook per task.
# Discover: rank the bank's playbooks by fit to the task at hand
hits = kb.list_playbooks("file kanban cards during the hourly PM sweep")
slug = hits["playbooks"][0]["slug"]
# Fetch the whole procedure (steps in order; cited entries inlined)
playbook = kb.get_playbook(slug)
for step in playbook["steps"]:
print(step["position"], step["title"], step["body"])
# Write / replace (versioned; every write is snapshotted)
kb.upsert_playbook(
"pm-hourly-sweep-filing",
title="PM hourly sweep: filing cards under workstreams",
when_to_use="Running the hourly PM sweep on a board and deciding which "
"workstream and milestone each unfiled card belongs to",
steps=[
{"title": "List unfiled cards", "body": "kanban_search_tasks(unfiled=true)"},
{"title": "Assign each card", "body": "kanban_assign_task_to_phase …",
"kb_entry_id": "<entry holding the filing rule>"}, # optional citation
],
change_note="initial version",
)
kb.list_playbook_versions("pm-hourly-sweep-filing") # newest first, full snapshots
kb.get_playbook_stats() # {"active": n, "draft": n, "superseded": n}
kb.delete_playbook("pm-hourly-sweep-filing") # snapshots are retained
Overlap guard. An upsert_playbook whose when_to_use scores at or above
the bank's threshold (default 0.82 cosine) against another active playbook
raises PlaybookOverlapError with .candidates. Extend the existing slug
instead, give yours a genuinely different trigger, retire the old one with
supersedes_slug="old-slug", or pass force=True when the tasks really are
distinct.
Slugs are kebab-case (2–80 chars). Step positions are assigned from list
order. The same three operations are exposed to agents as the MCP tools
list_playbooks / get_playbook / upsert_playbook, and query_knowledge_bank
names matching playbooks under matched_playbooks.
Explore a KB in the browser
The SDK ships the Knowledge Bank Explorer — the same UI the hosted app serves
at /knowledgebank/<kb_id>/explore — and can serve it locally:
export GURUCLOUD_KB_API_KEY=kb_...
gurucloud-kb ui --kb "My KB" # opens http://127.0.0.1:8765/?kb=My%20KB
gurucloud-kb ui # no --kb: pick from the banks your key can see
or from Python:
client.serve_ui("My KB") # blocks until Ctrl-C; open_browser=False to stay headless
A small stdlib server serves the bundled HTML/JS from this package and proxies
the UI's calls through this client, so the API key never reaches the browser
and nothing is installed beyond gurucloud-kb. It works against the hosted API
and against a self-hosted platform (--base-url https://<platform> with the
service token as the key). The self-hosted platform image also serves the same
UI itself at /ui.
Tabs: Entries (weighted multi-dimension search, exact-match filters for
text-only dimensions such as a business bank's kind, add / edit / delete,
"Promote to playbook"), Map (cluster the bank's embeddings with
kb.cluster() and see every entry placed by similarity, with playbooks drawn
as step chains linked to the entries they cite), Playbooks, Schema,
Quality (assertions, retrieval-eval runs), History (recent queries,
deduplication events, entry event log) and Connect (MCP config, server
definition, generated tools).
kb.list_recent_queries(limit=20) # {"queries": [{query_text, duration_ms, result_count, filters_used, query_source, created_at}, …], "limit": 20}
Manage KBs and API keys (client scope)
client.list_kbs() # every KB your key can reach → [KBInfo]
kb = client.get_kb("kb-uuid") # bind a handle (fetches info)
kb = client.get_kb("Support KB") # ...or by exact bank name (409 if ambiguous)
client.update_kb("kb-uuid", name="Support KB", description="...")
client.delete_kb("kb-uuid") # admin scope — irreversible
# API keys (admin scope): mint scoped keys for services / teammates.
key = client.create_api_key(name="reporting-bot", scopes=["read"])
client.list_api_keys()
client.delete_api_key(key["id"])
client.get_mcp_server_definition("kb-uuid") # same payload as kb.get_mcp_server_definition()
client.get_mcp_server_definition("Support KB") # by name; the payload carries kb_id + kb_name
client.close() # or use `with GuruCloudClient(...) as client:`
Use the KB as an MCP server (agent injection)
mcp_def = kb.get_mcp_server_definition()
agent_config = {
"mcpServers": {
mcp_def["server_name"]: {
"type": mcp_def["type"],
"url": mcp_def["url"],
"headers": {"Authorization": f"Bearer {api_key}"},
}
}
}
# Or mint a dedicated never-expiring token (admin scope):
pat = kb.generate_pat(token_name="My Agent")
mcp_def["available_tools"] is exactly what the server serves on
tools/list: query_knowledge_bank, report_learning, get_kb_entry,
edit_kb_entry, delete_kb_entry, list_playbooks, get_playbook,
upsert_playbook — minus whatever the server's own read-only / allow / block
configuration hides — so a gateway can pass the definition through without
hard-coding tool names. The definition also carries kb_id and kb_name.
query_knowledge_bank and report_learning are generated from your schema —
every searchable dimension becomes a <dimension>_query parameter
automatically.
Self-hosted platform
A licensed in-tenant deployment of the Knowledge Bank platform (the
kb-platform container image, >= 1.1.0) serves the same API and the same
per-bank MCP servers itself. Point the client at it with the platform's
service token — no kb_ key is involved:
client = GuruCloudClient(
api_key=platform_service_token,
base_url="https://kb-platform.<your-container-apps-domain>",
)
kb = client.get_kb("spog-desk-prod-tx") # bank NAME or UUID
mcp_def = kb.get_mcp_server_definition() # url = <platform>/kb/<id>/mcp
The definition is the platform's own MCP server for that bank: mount it as
is (the Bearer token is the same service token). Its available_tools is
exactly what the server's tools/list returns — query_knowledge_bank,
report_learning, get_kb_entry, edit_kb_entry, delete_kb_entry,
generated from the bank's schema — so a gateway that passes the definition
through never needs to hard-code tool names. Platform images >= 1.2.0 also
serve list_playbooks / get_playbook / upsert_playbook.
For a consumer that keeps one server attached and picks the bank per call,
client.get_platform_mcp_server_definition() returns the bank-addressed
server (<platform>/mcp; every tool takes kb = bank name or UUID, plus
list_knowledge_banks / get_knowledge_bank_info).
Not available on a self-hosted platform (the call raises APIError 501):
generate_pat(), API-key management, retrieval assertions / evaluation runs,
and deduplication events.
Async
The async client mirrors the sync API exactly — every method is awaitable.
from gurucloud_kb import AsyncGuruCloudClient
async with AsyncGuruCloudClient(api_key="kb_...") as client:
kb = await client.get_kb("your-kb-uuid")
results = await kb.search({
"dimensions": {"content": {"query_text": "JWT", "weight": 1.0}},
"k": 5,
})
Typed contracts
All request/response shapes are exported as TypedDicts / Literals for
editor + agent autocompletion:
from gurucloud_kb import (
DimensionConfig, DimensionSchema, DimensionType,
DimensionQuery, SearchRequest, CombinationMode, Aggregation,
CategoryFilter, EntryInput, EntryResult, KBInfo,
)
Changelog
0.2.5
- Live stats in the Knowledge Banks table on every host.
gurucloud-kb uiand the self-hosted platform/uinow feed the table's playbooks, 7d / 24h query volume, no-hit rate and recency columns: the SDK server proxies the new hostedGET /api/v1/kb/bank-stats, and the platform maps/api/kb-explorer-bank-statsonto its ownGET /api/v1/kb/bank-stats(/kb/stats-summary). The bundle reads the feed fromdata-stats-urland accepts both the bare and the{"data": ...}envelope.
0.2.4
- Knowledge Banks table: access + row actions. Bank rows carry an
accessvalue (owner/granted/admin) when the host's bank list provides one; a granted bank shows a "granted" chip and no Clear / Delete. Hosted-app extras switched on bydata-create-url,data-api-keys-urlanddata-tier-urlon#kbx-root: header buttons, a plan badge, and per-row Connect / Clear / Delete actions with confirmation dialogs. The SDK server and the platform/uiare unchanged (no attributes, no actions).
0.2.3
Cluster naming and sizing, changed after a measured evaluation on a 2,000-entry bank (an LLM judge scoring names against members the namer never saw): names fit 30% of a cluster's unseen members before, 48% after; the core of a cluster 41% before, 63% after.
- Behaviour change (service-side, affects every client on upgrade of the
platform, not of this package): with
algorithm="auto"and nok, the automatic cluster count is no longer capped at 10. It ismax(sqrt(n/2), n / target_cluster_size)withtarget_cluster_sizedefaulting to 35, so 2,000 entries give about 57 clusters instead of 10. Passk=(or a largertarget_cluster_size) to keep a coarse view. If you cache cluster titles keyed on member sets, expect one re-naming pass. kb.cluster(target_cluster_size=...)— entries per cluster the automatic count aims for.kb.cluster(label_sample="spread" | "nearest")— which members the namer sees. The service default is now"spread"(centre out toward the edge) with 8 samples;"nearest"restores the old core-only sample.label_sample_sizeis now sent only when you pass it (it used to force 5).kb.cluster(fields=["*"])— cluster across every embedding dimension, combined with the schema's search weights (the weighted-sum search score is exactly the cosine of the weighted concatenation, so "close" means the same thing it means in a query). Limited to 20,000 entries per call.- Cluster descriptions. With
label=Trueeach cluster now also carries a one-sentencedescription. This was the largest measured gain: a name alone fit 40% of a cluster's members, name plus description 55-58%. The namer also reads each sampled entry'suseful_forwhen it has one.batchsize for naming is 20 clusters per call. kb.cluster(peel_misfits=True)— sets aside entries that sit closer to another cluster than their own (negative silhouette); withoutlier_strategy="subcluster"they are regrouped and the ones that fit nowhere are left unplaced. Measured: names fit 51% of unseen members against 39%, with 22% of entries unplaced. Opt-in; the Explorer Map turns it on.- Cluster
keywordsare now distinctive terms (class-based TF-IDF across the clusters, up to 8, unigrams and bigrams) instead of raw word counts. - Limits removed:
max_members_per_clusterandlabel_sample_sizehave no upper bound; every cluster is LLM-named (the 2,000-cluster ceiling is now an optional operator setting); label length trim 60 → 120 characters. - More robust naming: labels are keyed by cluster number, so a model miscount costs one cluster its name instead of a whole 40-cluster batch.
0.2.2
- Knowledge Banks table. The explorer's bank picker (
gurucloud-kb uiwith no--kb, the platform/ui, and the hosted app) is now a sortable table instead of a card grid: filter by name / description / id, sort any column (unknown values sink to the bottom), a totals row, and banks with no entries and no playbooks hidden behind a "Show empty banks" toggle. When the host offers a per-bank live-stats feed (data-stats-url; the hosted app's/api/kb-explorer/banks/stats, backed by the KB service'sGET /kb/stats-summary) the table also shows playbooks, 7d / 24h query volume, no-hit rate and last query / last entry recency; without one it stands on the bank list's own columns.
0.2.1
- Explorer header counts. The "Total queries" tile is the bank's lifetime
query count (from
/info); the latency tiles are labelled as the KB service's last-hour window ("Avg query (1h)", "P95 query (1h)") and read "—" when nothing ran in that window instead of a misleading0.0 ms. The Entries tab caption adds the bank's live total ("… · 4,110 in bank") when more entries exist than are loaded. Pure view-model inui/js/stats_view.js.
0.2.0
- Bundled Knowledge Bank Explorer. The package now ships the explorer UI
(
gurucloud_kb/ui) and serves it locally:gurucloud-kb ui [--kb ID_OR_NAME](new console script; alsopython -m gurucloud_kb ui) orclient.serve_ui(kb). A stdlib server proxies the UI's calls through the SDK transport so the API key stays server-side. New public helpersserve_ui()/start_ui_server()(the latter binds without blocking, for embedding and tests). Works against the hosted API and self-hosted platforms; vendored Cytoscape/dagre make the Map tab work offline. kb.list_recent_queries(limit=50)(sync + async) — the bank's most recent searches with duration, result count, filters andquery_source, via the newGET /banks/{kb}/queriesendpoint. Typed asRecentQueryList.- Transport:
post/put/patch/deleteacceptparams=(used by the UI proxy).
0.1.19
- Non-destructive playbook edits —
upsert_playbook()now leavesstatusandmetadataout of the request when they areNone(the new default forstatus, previously"active"). The service keeps the stored values for an existing slug and defaults a new playbook toactive/{}, so a read → change-one-step → write round-trip no longer publishes a draft or wipes metadata. Passmetadata={}to clear it, orstatus=...to change it. Requires a platform serving the same contract (hosted, or self-hosted image ≥ 1.3.0); older services still treat the absent fields as before. delete_playbook(slug, reason=..., changed_by=...)— optional audit fields forwarded as query params; the service appends a tombstone version (deleted: true,delete_reason) solist_playbook_versions()still shows what was removed and why. The response gainstombstone_version.
0.1.18
available_toolsis the served surface — the hostedget_mcp_server_definition()now lists every tool the bank's MCP server serves ontools/list(query_knowledge_bank,report_learning,get_kb_entry,edit_kb_entry,delete_kb_entry,list_playbooks,get_playbook,upsert_playbook), honouring the server's own read-only / allow / block configuration, instead of a hard-coded two-tool list. A gateway can pass the definition through without hard-coding tool names.- Address a bank by name — every hosted
/banks/{kb}/...route (and soclient.get_kb(...),client.get_mcp_server_definition(...)and everyKnowledgeBankmethod) accepts the bank's exact name as well as itskb_id, matching the self-hosted platform contract. An ambiguous name is a 409ambiguous_bank_name(APIError) listing the candidate ids. kb_id+kb_nameon the definition —MCPServerDefinitioncarries both identifiers (hosted and self-hosted, platform image >= 1.2.1), so a registry keyed on names recovers the id from the definition itself. Requires the hosted API of 2026-09-08 or later.
0.1.17
- Playbooks —
kb.list_playbooks(query=None, status="active", limit=25, min_score=0.0),kb.get_playbook(slug),kb.upsert_playbook(slug, title=, when_to_use=, steps=, …),kb.delete_playbook(slug),kb.list_playbook_versions(slug),kb.get_playbook_stats()(sync + async). A playbook is a distinct, named, ordered procedure stored beside a bank's entries and returned whole;upsert_playbookis versioned and raises the newPlaybookOverlapError(409playbook_overlap,.candidates) when it collides with another active playbook. New typed contracts:Playbook,PlaybookList,PlaybookSummary,PlaybookStep,PlaybookStepInput,PlaybookWriteResult,PlaybookVersion,PlaybookStats,LinkedEntry,OverlapCandidate,PlaybookStatus. Requires the hosted API of 2026-09-08 or a self-hosted platform image >= 1.2.0.
0.1.16
- Self-hosted platform support —
GuruCloudClient(base_url=<your platform>, api_key=<platform service token>)works against a licensed in-tenant Knowledge Bank platform (kb-platform image >= 1.1.0). Thekb_key-prefix rule now applies only to the hostedbase_url; a self-hosted deployment authenticates with its own service token.kb.get_mcp_server_definition()returns the platform's own MCP server URL for the bank (<platform>/kb/{kb_id}/mcp), and the newclient.get_platform_mcp_server_definition()returns the bank-addressed server (<platform>/mcp, tools takekb). See "Self-hosted platform".
0.1.15
- Tunable reassign radius —
cluster()(sync + async) acceptsreassign_percentile(50-100) controlling how far outside a cluster's coreoutlier_strategy="reassign"will absorb noise (a noise entry joins its nearest cluster only within that percentile of the cluster's own member-to-centroid distances). The server default also moved from p90 to p99, measured to absorb most genuinely-belonging noise while staying shy on corpora whose noise is truly off-topic. Omitted from the request when not set, so older servers keep working.
0.1.14
- Diverse member sampling —
cluster()(sync + async) acceptsmember_sample("nearest"|"diverse")."diverse"returns the nearest-centroid anchor plus greedy farthest-point picks so fringe sub-themes reach the member sample instead of only the cluster core. Requires the 2026-09-01 server deploy; omitted from the request at its default, so older servers keep working.
0.1.13
- Outlier refinement —
cluster()(sync + async) acceptsoutlier_strategy("keep"|"reassign"|"subcluster") so vector noise can be absorbed into nearby clusters or re-clustered into its ownfrom_noiseclusters instead of staying a catch-all. Vector clusters also reportmean_member_distanceand alow_cohesionflag marking likely catch-all clusters. Requires the 2026-09-01 server deploy; the parameter is omitted from the request at its default, so older servers keep working.
0.1.12
- Cluster-size caps —
cluster()(sync + async) acceptsmax_cluster_size(absolute entries per cluster) andmax_cluster_fraction(share of the clustered scope,0..1]; stricter wins). Oversized vector clusters are recursively split server-side so the caps always hold; fuzzy groups split into exact-value groups; multi-valued dimension groups are exempt (noted). Requires the 2026-08-31 server deploy. - Multiple
fieldsin onecluster()call are now clustered concurrently server-side — one three-field request beats three single-field requests. __version__re-synced withpyproject.toml(had been stuck at 0.1.8).
0.1.11
- Batch ingest returns created entry ids —
ingest()responses carryentry_ids: one created id per input position (Nonefor rows that errored), so callers can map inputs to created entries. Typing/docs only on the client; requires the 2026-08-24 server deploy (PR #3402).
0.1.10
- Entry reads now carry custom dimension values and timestamps — the API's
list_entries(),get_entry(), andsearch()results include adimensionsobject with every non-default embedded dimension's value(s) (SINGLE → str, MULTI → list[str]; empty{}on default-schema KBs), andcreated_at/updated_atare populated on list/get responses (previously null).EntryResultdocuments the new key. No client behavior change — typing/docs only; requires the 2026-07-03 server deploy or later.
0.1.9
kb.cluster()/await kb.cluster()omitfieldswhen not given — the server now picks the KB's primary embedding dimension (first required SINGLE dimension), so custom-schema KBs no longer degrade to fuzzy grouping over a rawcontentcolumn that has no embedding. Server-side improvements shipped alongside: MULTI dimensions (tags, products, ...) are now clusterable (entries grouped by their values), degenerate auto-HDBSCAN results fall back to KMeans with a heuristic k, ID-like values are grouped exactly with dominant-value labels, and each field result carries an optionalnotepluslabel_usagetoken accounting.
0.1.8
- Removed
DimensionConfig.show_in_results— the deprecated, inert per-dimension flag (deprecated in 0.1.3) has been removed. It never controlled MCP output; useresponse_fields(mcp_response_fields) to choose the keys the MCP tools return. Stored schemas that still carry the key keep parsing — the unknown field is ignored.
0.1.7
label_sample_sizeonkb.cluster()/await kb.cluster()— whenlabel=True, how many representative entries per cluster are sent to the labeler (default 5). Server-side, cluster naming became one batched LLM call per field instead of one call per cluster, so labeled clustering no longer slows linearly with cluster count and names come out mutually distinct.
0.1.6
kb.update()/await kb.update()— update a KB's name and/or description in place (writescope; alsoclient.update_kb(kb_id, ...)). The description drives both the agent-facinginitialize.instructionsand thedescriptionreturned byget_mcp_server_definition()(see Update the name / description above) — there is no separate setter for those surfaces.- Corrected stale docstrings that claimed KB-management methods "return a token".
0.1.5
- Restore the
py.typedmarker (PEP 561) so type checkers pick up the SDK's inline types. It was inadvertently dropped in 0.1.2–0.1.4; theTyping :: Typedclassifier had been advertised without it. No API changes.
0.1.4
kb.cluster()/await kb.cluster()— group a KB's entries by any field. Embedding dimensions cluster by vector similarity (KMeans / Agglomerative / HDBSCAN); metadata/text fields cluster by fuzzy string match. Cluster the whole KB or a search result set; results are returned keyed per field. New typed contracts:ClusteringResult,FieldClusterResult,ClusterGroup,ClusterMember,ClusterScope,ClusterMethod,ClusterAlgorithm. Backed byPOST /api/v1/kb/banks/{id}/cluster.
0.1.3
response_fields/kb.set_response_fields()— choose which extra keys a KB's MCP tools return beyondid+content(see Choose which fields the MCP tools return above). Set oncreate_kb(...)or later viakb.set_response_fields([...]); needswritescope. Backed bymcp_response_fieldsonDimensionSchema.allow_updates/kb.set_allow_updates()— accumulate-only KBs (dedup never merges/overwrites).- Deprecated
DimensionConfig.show_in_results— this per-dimension flag is not enforced and never controlled MCP output; useresponse_fields(mcp_response_fields) instead. The field is kept only for backward compatibility of stored schemas.
Metadata
Release files for gurucloud-kb 0.2.5
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| gurucloud_kb-0.2.5.tar.gz | 468.1 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| gurucloud_kb-0.2.5-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 931.5 kB
Release files / gurucloud_kb-0.2.5.tar.gz
| Download URL | gurucloud_kb-0.2.5.tar.gz |
|---|---|
| Size | 468.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b321a7e8808b569cdb6c9d321e77a19245445969dcbe40784dc76671dcff0378
|
|
BLAKE2b-256 checksum How to use checksums |
30657f123f8b895d9ae7e28eaf23833c551f534452e6021a22ef3b1bdcc0f01b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Release files / gurucloud_kb-0.2.5-py3-none-any.whl
| Download URL | gurucloud_kb-0.2.5-py3-none-any.whl |
|---|---|
| Size | 463.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
820ec158cb03b446de93b91e13a7000deb833948cf8384c15169251a3bf2ae1c
|
|
BLAKE2b-256 checksum How to use checksums |
e7b54dc7df00908f076be25712cd842a19d218b513d85688d2eb10e816ea1655
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Security for gurucloud-kb 0.2.5
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.