Class: Insika::KnowledgeStore
- Inherits:
-
Object
- Object
- Insika::KnowledgeStore
- Defined in:
- lib/insika/knowledge_store.rb
Overview
LEARNED concepts, one per agent (+ optional tenant). Holds the complete
concept markdown (frontmatter + body) in the durable Store, the same
content/updated_at/bounded-history record shape SkillStore uses for
SKILL.md — so the Studio's editor and undo work identically.
Scoped like MemoryStore (agent/tenant baked into the scope string, not a
second constructor argument): a concept is engine-written, per-deployment
knowledge, never shared across agents the way a skill can be.
scope: "knowledge:<agent_id>" # default
scope: "knowledge:<agent_id>:<tenant>" # explicit tenant (multi-merchant)
key: "concept:<name>"
Record shape, one per concept:
{ "content" => "<entire concept markdown>", "updated_at" => iso8601,
"history" => [ { "content" =>, "at" => }, ... ] }
Layer 1 only: this is a plain upsert (last-write-wins, old version pushed into history). Dedup/merge/conflict detection across sightings is layer 2, not this store's job.
Constant Summary collapse
- SCOPE_PREFIX =
"knowledge"- KEY_PREFIX =
"concept:"- HISTORY_MAX =
20
Instance Method Summary collapse
-
#all(agent_id, tenant: nil) ⇒ Object
-> { name => content } of every concept in the scope.
-
#delete(agent_id, name, tenant: nil) ⇒ Object
-> bool (did it exist?).
-
#export_dir(agent_id, dir, tenant: nil) ⇒ Object
Writes one
<dir>/<name>.mdper concept — the storage format IS the export format, so this is a dump, not a converter: each file is the concept's content, byte for byte, directly consumable by okf-gem (OKF::Bundle) or graphify. -
#export_graphml(agent_id, tenant: nil) ⇒ Object
-> String (one GraphML document — the whole scope as a graph, §5's follow-up export shape).
-
#get(agent_id, name, tenant: nil) ⇒ Object
-> String | nil (the complete concept markdown).
-
#initialize(store:) ⇒ KnowledgeStore
constructor
A new instance of KnowledgeStore.
-
#meta(agent_id, name, tenant: nil) ⇒ Object
-> =>, "updated_at" => | nil.
-
#names(agent_id, tenant: nil) ⇒ Object
-> [String] concept names for the scope, lexicographic order.
-
#restore(agent_id, name, index, tenant: nil) ⇒ Object
Restores version
indexas the current content (a new write). -
#versions(agent_id, name, tenant: nil) ⇒ Object
-> [ { "content" =>, "at" => } ] most recent first.
-
#write(agent_id, name, content, tenant: nil) ⇒ Object
Upsert.
Constructor Details
#initialize(store:) ⇒ KnowledgeStore
Returns a new instance of KnowledgeStore.
32 33 34 |
# File 'lib/insika/knowledge_store.rb', line 32 def initialize(store:) @store = store end |
Instance Method Details
#all(agent_id, tenant: nil) ⇒ Object
-> { name => content } of every concept in the scope.
57 58 59 |
# File 'lib/insika/knowledge_store.rb', line 57 def all(agent_id, tenant: nil) names(agent_id, tenant: tenant).each_with_object({}) { |n, acc| acc[n] = get(agent_id, n, tenant: tenant) } end |
#delete(agent_id, name, tenant: nil) ⇒ Object
-> bool (did it exist?).
71 72 73 |
# File 'lib/insika/knowledge_store.rb', line 71 def delete(agent_id, name, tenant: nil) @store.delete(scope_for(agent_id, tenant), KEY_PREFIX + name.to_s) end |
#export_dir(agent_id, dir, tenant: nil) ⇒ Object
Writes one <dir>/<name>.md per concept — the storage format IS the
export format, so this is a dump, not a converter: each file is the
concept's content, byte for byte, directly consumable by okf-gem
(OKF::Bundle) or graphify. Unlike a lossy re-serialization (YAML.dump
on a curated corpus, say), re-exporting the same store is idempotent —
no force guard needed, there is nothing here to lose. -> [paths].
96 97 98 99 100 101 102 103 |
# File 'lib/insika/knowledge_store.rb', line 96 def export_dir(agent_id, dir, tenant: nil) FileUtils.mkdir_p(dir) names(agent_id, tenant: tenant).map do |name| path = File.join(dir, "#{name}.md") File.write(path, get(agent_id, name, tenant: tenant)) path end end |
#export_graphml(agent_id, tenant: nil) ⇒ Object
-> String (one GraphML document — the whole scope as a graph, §5's follow-up export shape). A record that fails to parse (a hand edit gone wrong) is skipped rather than breaking the whole export.
108 109 110 111 112 113 |
# File 'lib/insika/knowledge_store.rb', line 108 def export_graphml(agent_id, tenant: nil) concepts = names(agent_id, tenant: tenant).filter_map do |name| Knowledge::Concept.parse(get(agent_id, name, tenant: tenant)) end Knowledge::GraphmlExport.build(concepts) end |
#get(agent_id, name, tenant: nil) ⇒ Object
-> String | nil (the complete concept markdown).
37 38 39 |
# File 'lib/insika/knowledge_store.rb', line 37 def get(agent_id, name, tenant: nil) record(agent_id, name, tenant)&.fetch("content", nil) end |
#meta(agent_id, name, tenant: nil) ⇒ Object
-> =>, "updated_at" => | nil. Cheap by design (no YAML
frontmatter parse) — updated_at is the raw record's OWN timestamp,
written on every upsert, so a caller can use it as a memoization key
(Index::Scan's read cache) without re-parsing content that has not
changed since the last read.
46 47 48 49 |
# File 'lib/insika/knowledge_store.rb', line 46 def (agent_id, name, tenant: nil) rec = record(agent_id, name, tenant) rec && { "content" => rec["content"], "updated_at" => rec["updated_at"] } end |
#names(agent_id, tenant: nil) ⇒ Object
-> [String] concept names for the scope, lexicographic order.
52 53 54 |
# File 'lib/insika/knowledge_store.rb', line 52 def names(agent_id, tenant: nil) @store.list(scope_for(agent_id, tenant), KEY_PREFIX).map { |k| k.delete_prefix(KEY_PREFIX) } end |
#restore(agent_id, name, index, tenant: nil) ⇒ Object
Restores version index as the current content (a new write). -> Hash.
81 82 83 84 85 86 87 88 |
# File 'lib/insika/knowledge_store.rb', line 81 def restore(agent_id, name, index, tenant: nil) hist = versions(agent_id, name, tenant: tenant) i = Integer(index) raise Insika::NotFoundError, "concept '#{name}' not found" unless record(agent_id, name.to_s, tenant) raise Insika::ValidationError, "version #{index} does not exist" if i.negative? || i >= hist.length write(agent_id, name, hist[i]["content"], tenant: tenant) end |
#versions(agent_id, name, tenant: nil) ⇒ Object
-> [ { "content" =>, "at" => } ] most recent first.
76 77 78 |
# File 'lib/insika/knowledge_store.rb', line 76 def versions(agent_id, name, tenant: nil) record(agent_id, name, tenant)&.fetch("history", []) || [] end |
#write(agent_id, name, content, tenant: nil) ⇒ Object
Upsert. -> Hash (the stored record).
62 63 64 65 66 67 68 |
# File 'lib/insika/knowledge_store.rb', line 62 def write(agent_id, name, content, tenant: nil) key = name.to_s current = record(agent_id, key, tenant) rec = build_record(content.to_s, current) @store.set(scope_for(agent_id, tenant), KEY_PREFIX + key, rec) rec end |