Python SDK for the GuruCloud Knowledge Bank API
Project description
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"):
- Embedding dimensions (
content,useful_for, …) → vector clustering (KMeans / Agglomerative / HDBSCAN over the stored vectors). - Anything else (
metadata.customer,source, atext_onlydimension) → fuzzy string grouping, so near-duplicate values ("Acme Inc" ≈ "Acme, Inc.") merge.
result = kb.cluster(
fields=["content", "metadata.customer"],
method="auto", # vector for content, fuzzy for metadata.customer
algorithm="auto", # vector: HDBSCAN when k omitted, else KMeans
similarity_threshold=0.85, # fuzzy cutoff (1.0 = exact grouping)
label=True, # opt-in names; all clusters named in ONE batched call
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"])
for group in field_result["clusters"]:
print(" ", group.get("key") or group.get("keywords"), "→", group["size"])
Cluster only the results of a search by passing the same shape as kb.search:
result = kb.cluster(
fields=["content"],
search={"dimensions": {"content": {"query_text": "billing error"}}, "k": 500},
)
Cluster request reference
| Field | Type | Notes |
|---|---|---|
fields |
[str] |
Fields to cluster (default ["content"]). 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, else fuzzy). |
algorithm |
auto | kmeans | agglomerative | hdbscan |
Vector only. kmeans/agglomerative require k. |
k |
int |
Cluster count for kmeans/agglomerative. Omit for auto/hdbscan. |
min_cluster_size |
int |
HDBSCAN minimum cluster size (default 5). |
metric |
cosine | euclidean |
Vector distance (default cosine). |
similarity_threshold |
float |
Fuzzy cutoff 0..1 (default 0.85; 1.0 = exact). |
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, all clusters of a field are named in a single batched LLM call (fast, mutually distinct), not one call per cluster. |
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]; vector
results carry a silhouette_score, fuzzy results carry each group's key and
distinct values.
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")
The MCP tools (query_knowledge_bank, report_learning) are generated from
your schema — every searchable dimension becomes a <dimension>_query
parameter automatically.
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.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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file gurucloud_kb-0.1.7.tar.gz.
File metadata
- Download URL: gurucloud_kb-0.1.7.tar.gz
- Upload date:
- Size: 32.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b5329d75479e7a1bbb5cf5c0ccaabb5698ae62b6465a88467146d27553a64035
|
|
| MD5 |
648e66c13c978e0b59740bbcb1b7b1ff
|
|
| BLAKE2b-256 |
5b34f96a954332ff97768db0b40ecef6bcad9dcb50217de4d0133f4f0f10264b
|
File details
Details for the file gurucloud_kb-0.1.7-py3-none-any.whl.
File metadata
- Download URL: gurucloud_kb-0.1.7-py3-none-any.whl
- Upload date:
- Size: 36.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7f946a59064d3a42a4651953f691983093ac0d9b4de89f60d54784a7fc6bce85
|
|
| MD5 |
1048075f27e408d0be5711d23a9b3455
|
|
| BLAKE2b-256 |
8bb7d520b16c8c0b3573875381f2c0b576f8feb8f5570f87401d4e458c807b75
|