Knowledge Graph Query (kl)
CLI tool for querying a spatio-temporal knowledge graph built from DingTalk workplace messages and other exported sources. It contains entities and facts, plus optional experimental multi-resolution communities. Discover capabilities before checking status or querying.
Architecture: kl CLI is a thin HTTP client talking to kl-server (port 8200). The server keeps Qdrant + SQLite warm in memory. All commands require the server to be running.
Mandatory capability discovery
Before running status or choosing any query command, call:
kl capabilities --json
On Windows use the invocation form documented below, but keep
capabilities --json as the first CLI arguments. Parse commands and use only
entries whose enabled value is true; never infer availability from this
static skill text. In particular, global-search, community, members, and
the communities search collection are experimental and normally disabled.
ask is always available and is the primary retrieval command. If discovery
fails because the server is not running, start the server, then retry
capability discovery before querying.
Invoking kl (any directory, macOS & Windows)
The examples below write kl ... / ./kl ... for brevity, but the repo can be
installed anywhere, so never hardcode a path. Discover the repo root at
runtime, then invoke kl relative to it.
Discover the repo root (call it KL_REPO):
- This skill file lives at
<repo>/skills/kl/SKILL.md, so the repo root is three directories up from this file — if you know this file's path, use its../../... - Otherwise walk up from the current directory until you find the marker files
(the
klscript +kl_cli.py):# macOS / Linux KL_REPO="$PWD" while [ "$KL_REPO" != "/" ] && [ ! -f "$KL_REPO/kl_cli.py" ]; do KL_REPO="$(dirname "$KL_REPO")" done# Windows (PowerShell) $KL_REPO = (Get-Location).Path while ($KL_REPO -and -not (Test-Path (Join-Path $KL_REPO 'kl_cli.py'))) { $KL_REPO = Split-Path $KL_REPO -Parent }
Then invoke it (works from any directory):
- macOS / Linux — the
klwrappercds into its own directory, so calling it via$KL_REPOworks from anywhere:
Equivalent direct form:"$KL_REPO/kl" status "$KL_REPO/kl" ask "...""$KL_REPO/.venv/bin/python" "$KL_REPO/kl_cli.py" status. - Portable rule of thumb: if
./klfails (wrong directory, or Windows), fall back to invoking the venv interpreter onkl_cli.pywith the same arguments — the venv Python is at.venv/bin/pythonon macOS/Linux and.venv/Scripts/python.exeon Windows. On Windows, see the dedicated section below for bash-specific syntax and encoding requirements.
Invoking on Windows (read this if you are on win32)
AIAssist agents run in bash on Windows, not PowerShell. The macOS/Linux
examples above ($KL_REPO/kl) and old PowerShell snippets (& "$KL_REPO\.venv\...") neither works in the agent's bash shell. Use the
patterns below instead.
1. Always set PYTHONUTF8=1
Windows defaults to GBK / cp1252 for console encoding. Without PYTHONUTF8=1,
any print() or log line containing Chinese characters (group names, entity
names, fact text) will crash with UnicodeEncodeError or produce garbled
output you cannot read. Set it as the first thing before any kl command:
export PYTHONUTF8=1
Or prefix every invocation inline:
PYTHONUTF8=1 "$KL_REPO/.venv/Scripts/python.exe" "$KL_REPO/kl_cli.py" status
2. Use bash syntax: forward slashes, not backslashes
The venv Python lives at .venv/Scripts/python.exe (note: Scripts, not
bin). In bash, use forward slashes throughout — backslashes are escape
characters in bash, so "$KL_REPO\.venv\Scripts\..." will break.
# Discover the repo root (agent runs in bash, not PowerShell)
KL_REPO="$PWD"
while [ "$KL_REPO" != "/" ] && [ ! -f "$KL_REPO/kl_cli.py" ]; do
KL_REPO="$(dirname "$KL_REPO")"
done
# All subsequent calls use forward slashes
PYTHONUTF8=1 "$KL_REPO/.venv/Scripts/python.exe" "$KL_REPO/kl_cli.py" status
PYTHONUTF8=1 "$KL_REPO/.venv/Scripts/python.exe" "$KL_REPO/kl_cli.py" ask "你的问题" --pretty
3. Environment variables: use export, not set
The .env file in the repo uses Windows CMD set syntax, which bash cannot
source. Instead, export each variable explicitly in bash before starting
the server or querying:
export PYTHONUTF8=1
export KL_DATA_DIR="$KL_REPO/data"
export KL_DWS_EXPORT_DIR="$KL_REPO/dataset/<your-export-dir>"
export KL_EMBED_BASE_URL=<your-embed-endpoint>/v1
export KL_EMBED_MODEL=Qwen3-Embedding-0.6B
export KL_EMBED_API_KEY=<your-key>
export KL_EMBEDDING_DIM=1024
export KL_EMBED_SEND_DIMENSIONS=0
export KL_LLM_BASE_URL=<your-llm-endpoint>
export KL_LLM_MODEL=qwen3.6-flash
export ANTHROPIC_AUTH_TOKEN=<your-token>
export DISABLE_AIOHTTP_TRANSPORT=True
export KL_SERVER_PORT=8200
4. Start the server in bash (not via ./kl)
The ./kl wrapper script may not work on Windows. Start the server directly.
Prerequisite: all environment variables from section 3 must be exported
in the same shell session first — the server needs KL_EMBED_*, KL_LLM_*,
etc. to function:
# (env vars from section 3 must already be exported in this shell)
PYTHONUTF8=1 "$KL_REPO/.venv/Scripts/python.exe" "$KL_REPO/kl_server.py" &
sleep 15 # Qdrant warmup
PYTHONUTF8=1 "$KL_REPO/.venv/Scripts/python.exe" "$KL_REPO/kl_cli.py" status
5. Quick reference: the complete pattern
# One-time per session: discover repo + set encoding
export PYTHONUTF8=1
KL_REPO="$PWD"
while [ "$KL_REPO" != "/" ] && [ ! -f "$KL_REPO/kl_cli.py" ]; do
KL_REPO="$(dirname "$KL_REPO")"
done
export KL_REPO
# Environment variables (adjust paths/values for your setup)
export KL_DATA_DIR="$KL_REPO/data"
export KL_DWS_EXPORT_DIR="$KL_REPO/dataset/<your-export-dir>"
export KL_EMBED_BASE_URL=<your-embed-endpoint>/v1
export KL_EMBED_MODEL=Qwen3-Embedding-0.6B
export KL_EMBED_API_KEY=<your-key>
export KL_EMBEDDING_DIM=1024
export KL_EMBED_SEND_DIMENSIONS=0
export KL_LLM_BASE_URL=<your-llm-endpoint>
export KL_LLM_MODEL=qwen3.6-flash
export ANTHROPIC_AUTH_TOKEN=<your-token>
export DISABLE_AIOHTTP_TRANSPORT=True
export KL_SERVER_PORT=8200
# Start server (if not already running)
PYTHONUTF8=1 "$KL_REPO/.venv/Scripts/python.exe" "$KL_REPO/kl_server.py" &
sleep 15
# Query
PYTHONUTF8=1 "$KL_REPO/.venv/Scripts/python.exe" "$KL_REPO/kl_cli.py" status
PYTHONUTF8=1 "$KL_REPO/.venv/Scripts/python.exe" "$KL_REPO/kl_cli.py" ask "session打标进度" --pretty
6. Request timeout
The CLI HTTP timeout defaults to 120 seconds (configurable via
KL_CLI_TIMEOUT). If kl ask times out, it usually means Phase-2 LLM
synthesis is slow — the server may still complete the request; check
kl status for server health. To allow more time:
export KL_CLI_TIMEOUT=180 # 3 minutes
End to End (build → ingest → query)
First-time setup from a raw DWS export to answering questions. Steps 1–4 are the one-time build; after that you only use step 5 (query), and re-run the ingest when new data arrives.
# 1. BUILD: install deps + point at your endpoints and data (env-only, no
# secrets are baked into the repo). Put the config in a local .env file
# (gitignored) and source it before starting the server:
pip install -r requirements.txt
set -a; source .env; set +a # load KL_* + ANTHROPIC_AUTH_TOKEN
The repo ships a .env at the project root (gitignored — never commit it)
with working endpoints. Nothing auto-loads it, so you must source it as above.
It sets:
# data + export
export KL_DATA_DIR=./data # where knowledge.db + qdrant land
export KL_DWS_EXPORT_DIR=/path/to/dws_export # the exported source folders
# embedding: PAI-EAS Qwen3-Embedding-0.6B (1024-dim). URL MUST end in /v1;
# KL_EMBEDDING_DIM MUST match the model (1024) — it is baked into the Qdrant
# collections, so changing models later requires wiping + re-embedding.
export KL_EMBED_BASE_URL=<your-embed-endpoint>/v1
export KL_EMBED_MODEL=Qwen3-Embedding-0.6B
export KL_EMBED_API_KEY=<embedding-api-key> # real value lives only in .env
export KL_EMBEDDING_DIM=1024
export KL_EMBED_SEND_DIMENSIONS=0
# extraction/synthesis LLM: qwen3.6-flash (Anthropic-compatible base;
# do NOT append /v1 — litellm adds /v1/messages itself).
export KL_LLM_BASE_URL=https://example.com/apps/anthropic
export KL_LLM_MODEL=qwen3.6-flash
export ANTHROPIC_AUTH_TOKEN=<api-key> # real value lives only in .env
# 2. SERVE: start the retrieval server first (ingest runs inside it).
./kl start
kl status # wait for "status": "ready"
# 3. INGEST: drive the whole build through the running server. This runs
# Phase A (chunk+embed every source) → Phase B (LLM extract + build+embed
# the graph) → the improve step (ENTITY_SIMILAR/FACT_SIMILAR edges +
# communities L0–L3), then hot-swaps the new graph in. Non-blocking.
kl ingest -d "$KL_DWS_EXPORT_DIR" # omit -d to use $KL_DWS_EXPORT_DIR
kl status # poll: "Ingest: running NN% (phase)"
# 4. COMMUNITY SUMMARIES (optional but recommended): kl ingest builds the
# community structure but NOT their summaries/vectors. These two steps add
# 1–2 sentence summaries and make `search -c communities` work.
python -m kl_graph.periodic.community_summarizer # 1–2 sentence summary/community
python scripts/embed_communities.py # embed summaries (search -c communities)
# 5. QUERY: ask questions (see Retrieval Patterns below).
kl ask "who decided to use e2b for sandbox" -k 5
kl context <fact_id> # ground the answer in source
Notes:
kl ingestupserts into the current DB (deterministic UUID5 ids mean re-ingesting never duplicates entities/facts); it has no wipe flag, so for a truly clean rebuildrm -rf "$KL_DATA_DIR"before step 2.--no-improveskips the community/PageRank step;-c Nraises extraction concurrency.- For a cold/scripted build without a running server, the offline
equivalents are
python -m scripts.ingest --fresh-db(Phase A + B,--fresh-dbwipesknowledge.db— which now holds the extraction cache, so the cache is cleared too) followed bypython -m scripts.improve --skip-llm-judge.kl ingestis the preferred path when the server is up. - Community columns (
community_L0..L3) come from the improve step; without itcommunity/membersare empty andentityshows no community labels.
Commands
# Lifecycle
kl capabilities --json # REQUIRED FIRST: live enabled/disabled query commands
kl status # Server status + DB stats (+ ingest progress)
kl start # Start kl-server (retrieval)
kl start embedding [--model P] [--dp N] [--tp N] [--port 8100] [--gpu-util 0.4]
kl stop # Stop both servers
kl stop embedding # Stop only the embedding server
kl ingest [-c N] [-d PATH] [--no-improve] # in-server background ingest (Phase A + B)
# All commands require kl-server running (kl start)
kl entity "<name>" # Entity lookup by name (substring); shows id +
# communities + top edges + facts about it (id + text)
# + ENTITY_SIMILAR neighbors ("similar", with names)
kl entity --id <id> # Same, but look up by entity id (exact or prefix).
# Add --no-similar to skip the ENTITY_SIMILAR block.
kl facts <entity_id> # Facts ABOUT an entity (id + text), confidence-sorted
kl facts --fact-id <id> # A single fact by its id (exact/prefix); minimal —
# use `kl context` for full source provenance
kl expand <entity_id> # [DEPRECATED] ENTITY_SIMILAR neighbors — same as the
# "similar" block of `kl entity --id <id>`
kl community [-l L0|L1|L2|L3] [-t entity|fact] [--id N] # capability-gated
kl members <id> [-l L1] [-t entity] # capability-gated
kl context <fact_id> # Source message + context + entities
kl timeline "<entity>" [--from YYYY-MM-DD] [--to YYYY-MM-DD]
kl stats # Detailed statistics
kl search "<query>" [-c chunks|messages|facts|entities|communities] [-k 10] # vector ANN, one collection
kl ask "<question>" [-k 10] [--phase2|--no-phase2] [--entity NAME] [--entity-type TYPE] [--fact-type TYPE] [--seed-k 6] [--radius 1] [--max-nodes 40]
kl global-search "<question>" [--user "<name>"] [--json] # experimental; use only when capability says enabled
kl hop -n <node_id> -c '<cursor_json>' # expand one node one hop deeper (no LLM/embed)
Ingest is two phases (kl ingest for in-server background ingest, or
python -m scripts.ingest for a cold/scripted build):
Phase A loads + chunks + embeds every source folder (chat + wiki/mail/
work/…) into SQLite chunks + Qdrant — no LLM, dense/BM25 recall works after
this. Phase B runs LLM entity/fact extraction over all chunks, then builds
and embeds the graph (the only LLM-billed phase; results are cached per chunk).
Watch progress with kl status (Ingest: running NN% (phase)).
Run the full ingest (Phase A + Phase B) —
kl ingestdoes both. But you don't have to wait for Phase B: the service is usable as soon as Phase A completes (search/ask work off dense + BM25 recall), so start querying while Phase B is still running. Phase B then improves performance/accuracy by adding the entity/fact graph on top — checkkl statusto see when it lands.
Offline build (needs KL_DWS_EXPORT_DIR, KL_DATA_DIR, ANTHROPIC_AUTH_TOKEN,
KL_EMBED_API_KEY in the env):
python -m scripts.ingest --fresh-db # Phase A + Phase B (fresh)
python -m scripts.improve --skip-llm-judge # ENTITY_SIMILAR/FACT_SIMILAR + communities L0–L3
python -m kl_graph.periodic.community_summarizer # 1–2 sentence summary/community
python scripts/embed_communities.py # embed summaries (search -c communities)
- With no flag,
scripts.ingestruns the full pipeline with smart resume: if Phase A is fully done (every chunk persisted and embedded) it skips to Phase B, else it re-runs Phase A.--phase-a= chunk+embed only (no LLM). --fresh-dbwipesknowledge.db+ the Qdrant dir. The extraction cache now lives in theextraction_cachetable insideknowledge.db, so--fresh-dbclears it too. To rebuild the graph without re-billing the LLM (reusing the cache), runpython -m scripts.ingest --build-onlywithout--fresh-db. To wipe everything,rm -rf "$KL_DATA_DIR"first.- Ids are deterministic UUID5s of normalized content, so re-ingesting never duplicates entities/facts.
- Community columns (
community_L0..L3) are created only byscripts.improve(orkl ingestunless--no-improve); without themcommunity/membersare empty andentityshows no community labels.
To ingest a new export into the running server (non-blocking — it keeps serving and hot-swaps the new graph in when done):
kl ingest -d /path/to/dws_export # start Phase A → B in the background
kl status # poll: "Ingest: running NN% (phase_a|phase_b)"
# -c N raises LLM extraction concurrency (default 8); --no-improve skips
# community detection/PageRank; omit -d to use $KL_DWS_EXPORT_DIR.
search returns raw nearest-neighbor hits from a single collection (default
facts; other collections: chunks/messages, entities, communities —
chunks is the unified retrieval-unit collection for all embedded source
content, and messages is a backward-compat alias for it). ask runs the full
engine over chunks+facts (dense+sparse+RRF) and walks the depth-1 graph from
the entities/facts the query extracted. Agent callers must derive the retrieval
intent themselves and pass repeatable --entity, --entity-type, and
--fact-type flags using the values advertised by capabilities; this skips
the server-side rewrite LLM while retaining server-side entity resolution.
Also pass --no-phase2 and synthesize from the returned evidence. Plain clients
may omit intent and let the server rewrite, or request a synthesized answer
with --phase2. Output is JSON by default; add --pretty for a human view, or
--json to force JSON explicitly. (The browsing commands like entity /
community also support --json.)
ask returns retrieval + a hoppable subgraph
A single kl ask "<query>" call returns both flat recall and an interactive
graph view:
items: the embedding-recalled items (dense+sparse+RRF fused facts+chunks), cut attop_k. This is the flat vector recall.answer: LLM synthesis, ornullwhen--no-phase2is set.seeds: entry nodes as{id, label}(ent:<uuid>/fact:<uuid>+ name/text).nodes: seeds (hop 0) plus the hop-1 frontier reached from them, each resolved to{id, type, score, hop, name|text, ...}.edges:{from, to, from_label, to_label, type, weight}— the walkable edges (ABOUT/INVOLVES);*_labelinline the endpoint name/text so an edge is self-describing.from/toare node ids innodes.expandable:{id, label}for nodes that still have further un-walked edges.cursor: opaque walk state — pass it tokl hopto expand a node one more hop (no LLM, no embed).
When the graph is not built the walk fields come back empty
(mode="chunks_only") and only items are populated.
To go deeper, feed an expandable id + the cursor to kl hop (pass the
.id from an expandable/seeds entry):
kl hop -n <expandable_id> -c '<cursor json from ask>' --pretty
hop returns only the newly revealed frontier (nodes/edges) plus an
updated cursor — the server is stateless between hops, so merge the frontier
into the graph you already hold and chain hop again with the new cursor. It
never re-embeds or calls the LLM.
Note the decay: each hop multiplies a node's score by λ (default 0.6) and
drops branches below mini_threshold (0.2). With these defaults real query
seeds (~0.5–0.6) reach depth 2 and a strong seed reaches depth 3 — a node
survives while score × 0.6^hop ≥ 0.2. Weak/deep nodes eventually return an
empty hop; that is the expected stop signal, not an error.
graph / hop are the interactive GraphRAG mode — see "Interactive Graph Walk"
below. The embedding server (kl start embedding) is only needed on a GPU host;
retrieval itself uses the remote embedding endpoint, so kl start alone is
enough for querying.
Command Best Practices
How to use kl well, in priority order:
- Discover capabilities first. Run
kl capabilities --jsonand use only commands whose liveenabledvalue is true. Then runkl statusto inspect health and ingestion progress. If the server is unavailable, start it and retry capabilities before selecting a query command. - Plan before you query. Think about what the user is really asking and which commands answer it; don't fire commands blindly (see the Query strategy callout under Retrieval Patterns).
- Start with
kl ask, avoidkl search.kl askis the primary entry point (hybrid retrieval + graph walk); reservekl searchfor narrow single-collection lookupskl askcan't serve. For conceptual, person-scoped "what has X been about / what are my recent tasks" questions that need aggregation rather than a single hit, usekl global-searchonly when the live capabilities report it enabled (see Retrieval Pattern 8). - Trace before you trust. Ground every claim in source: take a
fact_idfromask/entity/facts/timelineand runkl context <fact_id>to see the original message before reporting it. - Discriminate semantic relevance before drilling.
kl askandkl searchreturn nearest-neighbour hits by embedding similarity — this surfaces keyword-overlap results that may be semantically unrelated to your actual question (e.g. a query about "session 打标障碍" may return results about model evaluation timeouts or billing identification simply because they share the words "session", "超时", or "标识"). Before runningkl context, hopping the graph, or synthesizing an answer, pause and ask: does this result actually address the user's question, or does it just share vocabulary? Filter out the irrelevant hits, note which results genuinely match, and do not follow irrelevant paths intocontextorhop— that wastes the query budget and pollutes the answer with off-topic evidence. When in doubt, rephrase the query with more specific terms rather than stacking broad keywords. - Chain by id, not by name. Every id
klprints is traceable (entity→entity_id→kl facts→fact_id→kl context). Prefer ids when chaining — names aren't unique (e.g. two周强entities), so a name-basedtimeline/entitymay mix them up. - Show evidence, then conclude. Quote the raw
kloutput first, then give your synthesis (see the Answering callout under Retrieval Patterns). - Respect the budget. Cap at ~10 commands per question; stop once you have ≥3 grounded facts or two consecutive queries add nothing new (see Search Budget & Stop Criteria).
- Use
--jsonfor chaining,--prettyfor humans. Query commands emit JSON by default (easy to parse ids out of); pass--prettywhen showing a person a result.--jsonalways wins if both are given. - Widen before narrowing on empty results. If
kl askcomes back thin, try a broader phrasing orkl communityto find the right neighborhood, then drill viakl members→kl context.
Latency Profile
| Command type | Typical latency |
|---|---|
| entity, expand, context, community, members | 30-50ms server-side |
| timeline (with date filter or low-degree) | 30-50ms |
| timeline (high-degree, no filter) | auto-filtered to 90 days |
| search (single-collection ANN) | remote embed + Qdrant ANN |
| ask (hybrid; may synthesize) | dense+sparse+RRF + hop-1 graph walk; +LLM when it escalates to Phase 2 |
| global-search (map-reduce over communities) | 1 map + 1 reduce LLM call (~30s on the ok path); 0 ms / 0 LLM on no-data |
| hop (expand one node) | no embed/LLM — pure in-memory adjacency (fastest) |
| CLI total (Python startup + httpx) | +1.5s overhead |
ask is slower when it escalates to Phase-2 synthesis (an extra LLM call).
Embeddings are served by the configured remote endpoint (KL_EMBED_* env
vars); the embedding dimension is fixed at build time, so changing models
requires a full re-embed.
Retrieval Patterns
Query strategy (read first). Before running any
klcommand, think carefully about what the user is actually asking and which commands will answer it — plan the sequence, don't fire commands blindly. Always start a search withkl ask(hybrid retrieval + graph walk + optional synthesis); it is the primary entry point. Avoidkl search— it is a low-level single-collection vector ANN and should only be used for narrow, specific lookups whenkl askclearly can't serve the need.
Answering — show evidence, then conclude. To make results trustworthy, first quote the original text you got from
kl(the raw message/fact content, verified withkl contextwhere possible), then give your conclusion drawn from it. Lead with the source evidence, follow with your synthesis — never present a conclusion without the underlyingkloutput it rests on.
Semantic mismatch — filter before you drill. Embedding-based retrieval (
ask,search) ranks by vector proximity, which measures keyword overlap, not topical relevance. A result about "session 超时" in a model-benchmarking conversation is not an obstacle to "session 打标" work, even though both contain "session" and "超时". Before spendingcontextorhopcalls on any hit, ask: "Is this about the same topic the user asked about, or does it just share words?" Discard the misses silently, do not summarize them as "evidence found but not relevant" — that wastes the reader's attention. If fewer than 3 hits survive the filter, rephrase the query with more specific terms (e.g. "session 打标 进度" instead of "session 打标 障碍 超时 模型 训练") and query again, rather than padding the answer with off-topic results.
1. Direct Answer (factual questions)
kl ask "who decided to use e2b for sandbox" -k 5 \
--entity e2b --entity-type SYSTEM --fact-type DECISION --no-phase2
kl context <best_fact_id> # ground the answer in source
2. Entity Deep-Dive (about a person/project/system)
kl entity "周强" # id + "similar" (ENTITY_SIMILAR) + top facts (with fact_ids)
kl facts <entity_id> # all facts ABOUT it (fact_id + text)
kl context <fact_id> # ground a fact in its source message
kl community -l L2 --id <community_id>
kl members <community_id> -l L2 -t fact
kl timeline "周强" --from 2026-06-01
Id-driven trace-back (no name round-trip): every id kl prints is
traceable. kl entity returns an entity_id plus the facts about it; feed the
entity_id back with kl entity --id <id> (or kl facts <entity_id> for the
full list), then any fact_id to kl context for the exact source message
(or kl facts --fact-id <id> for just the fact text). The similar block also
shows each ENTITY_SIMILAR neighbor's name + full entity_id, so you can
kl entity --id/kl facts on those too. Prefer ids over names when chaining
— names aren't unique (e.g. two 周强 entities).
(To find a person/system when unsure of the exact surface form, use
kl search "<term>" -c entities for a semantic entity lookup.)
3. Broad Survey (exploratory)
kl search "sandbox architecture" -c communities -k 5 # Find relevant communities
kl members <community_id> -l L2 -t fact # Read the facts
kl context <fact_id> # Ground in source
Or browse linearly:
kl community -l L1 -t entity # Team-level (best starting point)
kl community -l L2 --id 8 # Drill into a project
kl members 8 -l L2 -t entity # Who's in it?
4. Alias Resolution
kl entity "张伟"
kl entity --id <entity_id> # the "similar" block lists ENTITY_SIMILAR links
5. Timeline
kl timeline "InkFlow" --from 2026-06-01 --to 2026-07-01
6. Topic Discovery
kl community -l L1 -t fact # What topics exist?
kl community -l L2 -t fact --id 5 # Detail on one
kl members 5 -l L2 -t fact # Read facts
7. Interactive Graph Walk (relationship / multi-hop questions)
Use ask when the question is about how things connect (who works with whom,
what a decision depends on, how a system relates to a project). A single ask
call already returns both flat recall (items) and a hoppable subgraph
(seeds/nodes/edges/expandable + a cursor) — walked from the
entities/facts the query extracted. There is no separate graph command;
the walk is built into ask.
kl ask "e2b 部署平台和谁相关" --seed-k 6 --max-nodes 40 # items + seeds + hop-1 subgraph (JSON)
kl ask "e2b 部署平台和谁相关" --pretty # human view + expandable ids
The response includes nodes, edges, an expandable list of node ids, and a
cursor object. To go deeper, expand a specific node one hop with hop,
passing the cursor back so visited state and decay carry over:
# take an id from "expandable" and the "cursor" object from the ask response
kl hop -n "ent:<id>" -c '<cursor-json-from-previous-response>'
Notes:
- Node ids are namespaced:
ent:<entity_id>andfact:<fact_id>. - If the graph isn't built,
askfalls back tomode="chunks_only"(flat vector hits) in the same response shape — check themodefield. hopdoes no embedding/LLM (pure in-memory walk) — cheap to chain.- Ground any interesting fact node with
kl context <fact_id>as usual.
8. Global Search (experimental, capability-gated)
Use this pattern only when kl capabilities --json reports
commands.global-search.enabled=true. Otherwise use ask, facts, and
timeline; do not call the disabled command.
Use kl global-search when the question is conceptual and person-scoped —
it must be aggregated over everything a person has been involved in, not
answered by any single chunk/fact. Canonical example: "我最近的任务是什么"
(what are my recent tasks?), "这个人主要负责什么" (what does this person mainly
work on?). Where ask does flat recall + a depth-1 graph walk, global-search
resolves the person to a Person entity, collects that person's community
summaries across L0–L3, and runs a GraphRAG-style map-reduce over them
(strict-JSON key points scored 0–100 → drop score-0 → importance-sort →
token-budgeted reduce → grounded markdown with [Data: Communities (...)]
citations).
kl global-search "我最近的任务是什么" --user "孙亮" # explicit person
kl global-search "这个人主要负责什么" # identity via DWS get-self
kl global-search "我最近的任务是什么" --json # full wire shape for chaining
Identity resolution precedence: --user <name> → DWS get-self (the
CLI shells out to dws contact user get-self for the logged-in user) →
otherwise the server's KL_CURRENT_USER. The response carries reason,
communities (the selected {level, community_id, member_count}), citations,
and diagnostics (map/reduce call counts, latency).
When there is no grounding — the name doesn't resolve
(reason=identity_unresolved), or the person has no community memberships
(reason=no_communities) — it returns a canned bilingual no-data answer in
~0 ms with ZERO LLM calls plus a remediation hint (e.g. run
python -m scripts.improve). It never falls back to a corpus-wide search or
errors out.
Requires the community layer to be built and summarized: kl ingest /
scripts.improve create the community_L0..L3 columns, and
python -m kl_graph.periodic.community_summarizer generates the summaries this
command reads. Without summaries every query returns no_communities.
Caveat — summaries are name-based, not task-based (today). Community summaries are built from member names/aliases, so a person whose graph neighborhood is dominated by identity/HR material may get a thin, honest "can't pin down concrete tasks" answer even on the
okpath. That is a data limitation, not a retrieval bug — preferask/facts/timelinefor specific, dateable task lookups, and useglobal-searchfor the high-level "what has this person been about" shape.
Community Hierarchy
Drill-down pattern for navigating the graph:
L0 (org-level) -> "Which team?"
L1 (team-level) -> "Which project?" <- BEST STARTING POINT
L2 (project-level) -> "Which component?"
L3 (component-level) -> "Specific facts + evidence"
Search Budget & Stop Criteria
When to STOP drilling
- You have ≥3 grounded facts (with
kl contextverification) that genuinely match the user's question and answer it - You've reached L3 community level (most granular — no further drill-down)
- Two consecutive queries returned no new semantically relevant information
- The user's question is answered with high confidence from existing results
- A query returns only keyword-overlap hits with no topically relevant result — rephrase and retry once; if still no match, report what you found rather than forcing off-topic evidence into the answer
Rejection signals — do NOT drill into these
- The result shares keywords ("session", "超时", "模型") but discusses a different topic (benchmarking, billing, deployment) than what the user asked
- The surrounding context (
kl context) reveals the conversation is about a different project or task than the question targets - A fact's
subject_entityorobject_entityis unrelated to the question's domain — the connection is coincidental keyword overlap, not topical similarity - A graph
hopexpands into entities/facts that are clearly off-topic from the seed — stop expanding that branch immediately
Query budget per question
- Simple factual: 2-3 commands (ask → context → done)
- Entity deep-dive: 4-6 commands (entity → facts → context, or entity → community → members → timeline → context)
- Broad survey: 5-8 commands (community L1 → pick → L2 → members → context × 2-3)
- Relationship / multi-hop: ask (walks the graph) → hop (× 1-2 on expandable ids) → context
- Maximum: 10 commands per question before synthesizing what you have
Confidence signals
- Fact type=DECISION with confidence≥0.9 → high reliability
- Multiple facts from different messages saying the same thing → confirmed
- Single fact from one message → report but flag as single-source
- Community summary matching your query with score≥0.8 → right neighborhood
Key Notes
- Entity names are Chinese and English: "周强", "InkFlow", "VS Code"
- Fact IDs support prefix match:
kl context 49d8370a - High-degree entities (>200 edges) get auto-filtered to last 90 days on timeline (use --from/--to to override)
- Check
kl statusfirst — if server not running, usekl start - Time range: 2025-09 to 2026-07