Knowledge graph: sources and concepts beside the Documents
A knowledge graph is a graph the host keeps outside the Documents: ingested sources, concepts and notes with typed edges between them. The editor does not store it and does not write to it. The host passes one KnowledgeGraph; the editor adds two read routes, a client call and the Wissensgraph tab of the Explore panel.
| Piece | What it does | Port (capability) |
|---|---|---|
KnowledgeGraph | search(q, user), neighbours(id, user) and optionally origin(id, user) | knowledge |
GET /api/editor/knowledge/search?q= | the nodes q finds, best first | knowledge |
GET /api/editor/knowledge/nodes/:id/neighbours | a node, the nodes next to it and the edges between them | knowledge |
EditorBackend['knowledge'] | search(query, signal?) and neighbours(id, signal?) | knowledge |
Explore tab knowledge | the nodes found for the open Block's title; a click shows the neighbours | index and knowledge |
A node is not a Block and an edge is not an Edge of the Block index: the graph's kind (Source, Concept, …) and its edge types (References, DerivedFrom, …) are its own words, shown as written.
The port
EditorPorts.knowledge (@skaile/forge-editor/server), the shapes in @skaile/forge-editor/core:
interface KnowledgeGraph {
/** The nodes `q` finds, best first. */
search(q: string, user: object): Promise<KnowledgeHit[]>
/** The node `id` (first), the nodes next to it, and the edges that join them. An unknown id: both lists empty. */
neighbours(id: string, user: object): Promise<KnowledgeNeighbours>
/** Optional: the nodes `id` was made from (its sources). */
origin?(id: string, user: object): Promise<KnowledgeNode[]>
}
interface KnowledgeNode { id: string, label: string, kind: string }
interface KnowledgeHit extends KnowledgeNode { score: number }
interface KnowledgeEdge { from: string, to: string, kind: string }
interface KnowledgeNeighbours { nodes: KnowledgeNode[], edges: KnowledgeEdge[] }
| Type | Meaning |
|---|---|
KnowledgeNode | id is stable; label is what the node is called (its id for a node without a title); kind is what it is, in the graph's words |
KnowledgeHit | a node with the score the graph gave it. Higher is better; the scale is the graph's own and only orders the list |
KnowledgeEdge | a directed edge between two node ids; every edge of a KnowledgeNeighbours has both ends in its nodes |
origin is not served by a route yet; an add-on or a host reads it from the port.
Routes
Route (API key) | Request | Answer |
|---|---|---|
GET /api/editor/knowledge/search (knowledgeSearch) | ?q= | KnowledgeHit[] |
GET /api/editor/knowledge/nodes/:id/neighbours (knowledgeNeighbours) | none | KnowledgeNeighbours |
Both routes are served only when ports.knowledge is present, for every signed-in user: the graph is the host's, and the user the port receives is where a host that keeps per-user access applies it.
| Status | When |
|---|---|
400 | q missing, blank or longer than KNOWLEDGE_QUERY_MAX (500) characters; an id blank or longer than KNOWLEDGE_ID_MAX (200) |
401 | no user |
501 | the host has no knowledge port |
Provide a port
import { createMemoryKnowledgeGraph } from '@skaile/forge-editor/adapters/knowledge'
import type { EditorPorts } from '@skaile/forge-editor/server'
// Two nodes and one edge, for a test or a demo.
const knowledge: NonNullable<EditorPorts['knowledge']> = createMemoryKnowledgeGraph(
[
{ id: 'n1', label: 'Wissensgraph Grundlagen', kind: 'Concept' },
{ id: 'n2', label: 'Wissensgraph Quelle', kind: 'Source' },
],
[{ from: 'n1', to: 'n2', kind: 'DerivedFrom' }],
)
createMemoryKnowledgeGraph(nodes, edges) searches the words of the labels: a node is a hit when a word of its label starts with a word of the query (case and accents ignored), the score is the share of the query's words found (1 = all), the best first, at most MEMORY_KNOWLEDGE_LIMIT (20). neighbours answers the node, the nodes joined to it in either direction and every edge among them; an edge to a node that is not listed is left out. origin(id) answers the nodes id has an outgoing DerivedFrom edge to.
A real graph is the host's. The add-on @skaile/forge-editor-kgs wraps the KGS SDK.
The Wissensgraph tab
With the backend capability knowledge, exploreTabsOf(backend) lists a fourth tab, knowledge („Wissensgraph“ / “Knowledge graph”), after trail. A backend without the capability has no such tab.
The ExploreModel fills it like the other lists: when the Block changes it reads the Block's title through index.blockGet and searches the graph for it (state.query is that text; a Block without a title searches nothing and the panel says so). state.knowledge is { status, items: KnowledgeHit[] }, and each entry shows its label, its kind and its score.
Choosing a hit calls model.focusNode(hit): state.focus is then { node, status, nodes, edges } with the neighbours loading (neighbours(node.id)). The panel lists the nodes the graph joins to the focus (exploreKnowledgeLinksOf(focus): node, edge type and out or in); choosing one of them moves the focus there, and „Zurück zu den Treffern“ calls focusNode(null). A failing search or neighbourhood is failed and the other tabs stay usable. Opening a Block stays the host's open: knowledge nodes are no Blocks, so choosing one never calls it.
The KGS add-on
@skaile/forge-editor-kgs is the KnowledgeGraph over the read side of the KGS SDK (@kgs/sdk, a daemon that holds a corpus of sources and concepts). createKgsKnowledgeGraph(client, options?) takes a KgsReadClient: the three methods of the SDK's Client that it calls (get, search, neighbors), typed in the add-on. The add-on does not import @kgs/sdk (it is not published on npm); the SDK is an optional peer that the host installs.
KnowledgeGraph | KGS call | Answer |
|---|---|---|
search(q) | kgs.search | { id, label, kind, score }: the node's title, its node type, the daemon's score |
neighbours(id) | kgs.get and kgs.neighbors (both directions) | the node, the nodes next to it and the edges that join them |
origin(id) | kgs.neighbors (DerivedFrom, outgoing) | the nodes id was made from |
Deleted nodes never appear. Options: mode (the daemon's search mode, default hybrid), limit (hits, default 10), scope (a zone to search in).
import { createKgsKnowledgeGraph } from '@skaile/forge-editor-kgs'
import { Client, TcpTransport } from '@kgs/sdk'
const client = await Client.fromTransport(new TcpTransport('127.0.0.1', 7777))
const knowledge = createKgsKnowledgeGraph(client, { limit: 10 })
// provideEditorServer({ ports: { …, knowledge } })
The repository's document pages (the file-drawer layout, app/layouts/default.vue, whose Explore panel gets a fourth tab) show it behind NUXT_PUBLIC_ADDONS_KGS=1 with KGS_URL (tcp://host:port or unix:///path/to/.kgs/socket); the kitchen sink's standalone pages have no workspace index and show no knowledge graph.
Slides: decks, slide layouts and exports
The add-on @skaile/forge-editor-slides: the Block types deck and slide, the pure HTML render contract, the render targets reveal-html, pptx and marp-md, the transfer transforms that turn blocks into slides, and the slide preview in the inspector.
AI configuration and secrets
One configuration object for module options, server and browser, the Model presets and Model roles, and why API keys come from the environment only.