Class: Insika::KnowledgeStore

Inherits:
Object
  • Object
show all
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

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 meta(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