smart_brain 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/MEMPAL_GUIDE.md +1074 -0
  4. data/README.en.md +173 -173
  5. data/README.md +467 -173
  6. data/config/brain.yml +69 -1
  7. data/conversation_demo.rb +438 -438
  8. data/db/migrate/002_turn_events_payload.sql +9 -0
  9. data/db/migrate/003_tiers_and_lifecycle.sql +28 -0
  10. data/db/migrate/004_kg_edges.sql +30 -0
  11. data/db/migrate/005_domains_and_memory_scopes.sql +163 -0
  12. data/docs/coding_todo.md +139 -0
  13. data/docs/context_package.md +220 -0
  14. data/docs/evidence_pack.md +190 -0
  15. data/docs/gap_vs_mempal.md +161 -0
  16. data/docs/mcp.md +93 -0
  17. data/docs/memory_types.md +278 -0
  18. data/docs/multi_scope_memory_refactor_plan.md +483 -0
  19. data/docs/multi_scope_migration.md +65 -0
  20. data/docs/policies.md +308 -0
  21. data/docs/retrieval_plan.md +231 -0
  22. data/docs/smartbrain_design.md +299 -0
  23. data/docs/user_guide.md +546 -0
  24. data/example.rb +91 -91
  25. data/examples/01_memory_basic.rb +57 -0
  26. data/examples/02_governance.rb +63 -0
  27. data/examples/03_postgres_persistence.rb +63 -0
  28. data/examples/04_ollama_llm.rb +69 -0
  29. data/examples/05_smart_rag_integration.rb +79 -0
  30. data/examples/06_multi_scope_memory.rb +50 -0
  31. data/examples/README.md +49 -0
  32. data/exe/smart_brain +168 -0
  33. data/lib/smart_brain/adapters/smart_rag/direct_client.rb +16 -5
  34. data/lib/smart_brain/adapters/smart_rag/http_client.rb +16 -5
  35. data/lib/smart_brain/adapters/smart_rag/null_client.rb +7 -2
  36. data/lib/smart_brain/adapters/smart_rag/scope_filter.rb +60 -0
  37. data/lib/smart_brain/configuration.rb +57 -0
  38. data/lib/smart_brain/consolidator/working_summary.rb +80 -12
  39. data/lib/smart_brain/context_composer/composer.rb +40 -3
  40. data/lib/smart_brain/contracts/retrieval_plan.rb +10 -0
  41. data/lib/smart_brain/contracts/scope_context.rb +46 -0
  42. data/lib/smart_brain/contracts/scope_ref.rb +25 -0
  43. data/lib/smart_brain/db.rb +109 -0
  44. data/lib/smart_brain/event_store/in_memory.rb +6 -2
  45. data/lib/smart_brain/event_store/postgres.rb +199 -0
  46. data/lib/smart_brain/fusion/merger.rb +31 -2
  47. data/lib/smart_brain/governance/briefing.rb +146 -0
  48. data/lib/smart_brain/governance/fact_check.rb +110 -0
  49. data/lib/smart_brain/governance/knowledge_graph.rb +60 -0
  50. data/lib/smart_brain/governance/lifecycle.rb +225 -0
  51. data/lib/smart_brain/governance/tiers.rb +60 -0
  52. data/lib/smart_brain/memory_extractor/extractor.rb +25 -7
  53. data/lib/smart_brain/memory_store/in_memory.rb +202 -17
  54. data/lib/smart_brain/memory_store/postgres.rb +500 -0
  55. data/lib/smart_brain/model_provider/base.rb +87 -0
  56. data/lib/smart_brain/model_provider/factory.rb +49 -0
  57. data/lib/smart_brain/model_provider/ollama.rb +60 -0
  58. data/lib/smart_brain/model_provider/openai.rb +60 -0
  59. data/lib/smart_brain/model_provider/stub.rb +26 -0
  60. data/lib/smart_brain/model_provider.rb +7 -0
  61. data/lib/smart_brain/observability/tracker.rb +39 -1
  62. data/lib/smart_brain/retrievers/exact_retriever.rb +6 -0
  63. data/lib/smart_brain/retrievers/memory_retriever.rb +59 -5
  64. data/lib/smart_brain/runtime.rb +288 -16
  65. data/lib/smart_brain/scopes/conflict_resolver.rb +67 -0
  66. data/lib/smart_brain/scopes/registry.rb +133 -0
  67. data/lib/smart_brain/scopes/resolver.rb +32 -0
  68. data/lib/smart_brain/server/http_app.rb +143 -0
  69. data/lib/smart_brain/server/mcp_server.rb +385 -0
  70. data/lib/smart_brain/server/service.rb +129 -0
  71. data/lib/smart_brain/support/levenshtein.rb +35 -0
  72. data/lib/smart_brain/version.rb +5 -5
  73. data/lib/smart_brain.rb +80 -35
  74. metadata +88 -36
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sinatra/base'
4
+ require 'json'
5
+ require_relative 'service'
6
+
7
+ module SmartBrain
8
+ module Server
9
+ # Minimal JSON HTTP API over a Service. Endpoints:
10
+ # POST /commit { session_id, turn_events }
11
+ # POST /compose { session_id, user_message, agent_state? }
12
+ # POST /search { session_id, query, limit? }
13
+ # GET /status
14
+ # POST /migrate
15
+ class HttpApp < Sinatra::Base
16
+ class << self
17
+ attr_accessor :service
18
+ end
19
+
20
+ set :show_exceptions, false
21
+ set :raise_errors, false
22
+
23
+ before { content_type 'application/json' }
24
+
25
+ post '/commit' do
26
+ b = parse_body
27
+ ok service.commit(**b.slice(:domain_id, :session_id, :scope_context, :turn_events).merge(turn_events: b[:turn_events] || {}))
28
+ end
29
+
30
+ post '/compose' do
31
+ b = parse_body
32
+ ok service.compose(
33
+ session_id: b[:session_id],
34
+ user_message: b[:user_message],
35
+ agent_state: b[:agent_state] || {},
36
+ domain_id: b[:domain_id], scope_context: b[:scope_context]
37
+ )
38
+ end
39
+
40
+ post '/search' do
41
+ b = parse_body
42
+ ok service.search(**b.slice(:domain_id, :session_id, :scope_context, :query, :limit))
43
+ end
44
+
45
+ get '/status' do
46
+ ok service.status
47
+ end
48
+
49
+ post '/migrate' do
50
+ ok(ok: service.migrate)
51
+ end
52
+
53
+ post %r{/knowledge/(distill|gate|promote|demote|retract|promote-to-scope|lineage|events)} do
54
+ action = params[:captures].first
55
+ b = parse_body
56
+ result =
57
+ case action
58
+ when 'distill'
59
+ service.distill(**b.slice(:domain_id, :session_id, :scope_context, :statement, :content, :tier, :supporting_refs, :domain, :field, :reviewer, :key))
60
+ when 'gate'
61
+ service.gate(**b.slice(:memory_item_id, :domain_id, :session_id, :scope_context))
62
+ when 'promote'
63
+ service.promote(**b.slice(:memory_item_id, :domain_id, :session_id, :scope_context, :verification_refs, :reason, :reviewer, :force))
64
+ when 'demote'
65
+ service.demote(**b.slice(:memory_item_id, :domain_id, :session_id, :scope_context, :evidence_refs, :reason, :reason_type, :reviewer))
66
+ when 'retract'
67
+ service.retract(**b.slice(:memory_item_id, :domain_id, :session_id, :scope_context, :evidence_refs, :reason, :reviewer))
68
+ when 'promote-to-scope'
69
+ service.promote_to_scope(**b.slice(:memory_item_id, :target_scope, :domain_id, :session_id, :scope_context, :verification_refs, :reason, :reviewer))
70
+ when 'lineage'
71
+ service.promotion_lineage(**b.slice(:memory_item_id, :domain_id, :session_id, :scope_context))
72
+ when 'events'
73
+ service.knowledge_events(**b.slice(:memory_item_id, :domain_id, :session_id, :scope_context))
74
+ end
75
+ ok(result)
76
+ end
77
+
78
+ post %r{/kg/(add|query|timeline|invalidate|stats)} do
79
+ action = params[:captures].first
80
+ b = parse_body
81
+ result =
82
+ case action
83
+ when 'add' then service.kg_add(**b.slice(:domain_id, :session_id, :scope_context, :scope_ref, :subject, :predicate, :object, :confidence, :source_turn_id))
84
+ when 'query' then service.kg_query(**b.slice(:domain_id, :session_id, :scope_context, :subject, :predicate, :object, :include_invalid))
85
+ when 'timeline' then service.kg_timeline(**b.slice(:domain_id, :session_id, :scope_context, :subject))
86
+ when 'invalidate' then service.kg_invalidate(**b.slice(:edge_id, :reason, :domain_id, :session_id, :scope_context))
87
+ when 'stats' then service.kg_stats(**b.slice(:domain_id, :session_id, :scope_context))
88
+ end
89
+ ok(result)
90
+ end
91
+
92
+ post '/fact-check' do
93
+ b = parse_body
94
+ ok(service.fact_check(**b.slice(:domain_id, :session_id, :scope_context, :text)))
95
+ end
96
+
97
+ post '/brief' do
98
+ b = parse_body
99
+ ok(service.brief(**b.slice(:domain_id, :session_id, :scope_context, :query)))
100
+ end
101
+
102
+ post '/wake-up' do
103
+ b = parse_body
104
+ ok(service.wake_up(**b.slice(:domain_id, :session_id, :scope_context)))
105
+ end
106
+
107
+ error SmartBrain::Governance::LifecycleGateError do
108
+ status 409
109
+ ok(error: "gate failed: #{env['sinatra.error'].message}")
110
+ end
111
+
112
+ error ArgumentError do
113
+ status 400
114
+ ok(error: env['sinatra.error'].message)
115
+ end
116
+
117
+ error StandardError do
118
+ status 500
119
+ ok(error: "#{env['sinatra.error'].class}: #{env['sinatra.error'].message}")
120
+ end
121
+
122
+ error JSON::ParserError do
123
+ status 400
124
+ ok(error: "invalid JSON body: #{env['sinatra.error'].message}")
125
+ end
126
+
127
+ private
128
+
129
+ def service
130
+ self.class.service ||= Service.build
131
+ end
132
+
133
+ def parse_body
134
+ body = request.body.read
135
+ body.empty? ? {} : JSON.parse(body, symbolize_names: true)
136
+ end
137
+
138
+ def ok(payload)
139
+ JSON.generate(payload)
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,385 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative '../../smart_brain'
5
+ require_relative 'service'
6
+
7
+ module SmartBrain
8
+ module Server
9
+ # Model Context Protocol server over stdio (newline-delimited JSON-RPC 2.0).
10
+ #
11
+ # No external MCP gem dependency: implements the subset clients actually use
12
+ # (initialize / notifications/initialized / tools/list / tools/call) so that
13
+ # Claude Code, Cursor, etc. can drive SmartBrain as a native tool.
14
+ class McpServer
15
+ PROTOCOL_VERSION = '2024-11-05'
16
+
17
+ INSTRUCTIONS = <<~TXT.strip
18
+ SmartBrain memory runtime. Available tools:
19
+ - commit_turn: persist a turn (messages + structured events) and extract durable memory.
20
+ - compose_context: assemble the minimal sufficient context (summary + recent + evidence) for a user message.
21
+ - search_memory: full-text recall over the session's persistent memory.
22
+ - knowledge_distill / knowledge_gate / knowledge_promote / knowledge_demote: the Stage-1 knowledge lifecycle (evidence → candidate → promoted → demoted). Distill reusable rules at tier dao_ren or qi; promote only after enough supporting evidence; demote with a reason_type (contradicted|obsolete|superseded).
23
+ - kg_add / kg_query / kg_invalidate: knowledge-graph triples (subject-predicate-object) with temporal validity. Run fact_check before asserting entity relations to catch name/contradiction/stale issues.
24
+ - fact_check: offline, zero-LLM contradiction scan (SimilarNameConflict / RelationContradiction / StaleFact) of a text against the session's entities + KG.
25
+ - brief / wake_up: deterministic citation-first cognitive snapshot and L0/L1 resume payload.
26
+ - status: backend, metrics, counts.
27
+ Always pass a stable session_id per conversation. Prefer compose_context before answering, and commit_turn after each assistant reply.
28
+ TXT
29
+
30
+ SCOPE_PROPERTIES = {
31
+ domain_id: { type: 'string' },
32
+ scope_context: { type: 'object' }
33
+ }.freeze
34
+
35
+ TOOLS = [
36
+ {
37
+ name: 'commit_turn',
38
+ description: 'Persist a conversation turn and extract structured memory items (decisions, tasks, goals, entities, preferences).',
39
+ inputSchema: {
40
+ type: 'object',
41
+ properties: SCOPE_PROPERTIES.merge(
42
+ session_id: { type: 'string' },
43
+ turn_events: {
44
+ type: 'object',
45
+ description: 'messages: [{role, content}], plus optional tasks/decisions/goals/entities/preferences/refs arrays.'
46
+ }
47
+ ),
48
+ required: %w[session_id turn_events]
49
+ }
50
+ },
51
+ {
52
+ name: 'compose_context',
53
+ description: 'Retrieve evidence and assemble a context package for the latest user message.',
54
+ inputSchema: {
55
+ type: 'object',
56
+ properties: SCOPE_PROPERTIES.merge(
57
+ session_id: { type: 'string' },
58
+ user_message: { type: 'string' },
59
+ agent_state: { type: 'object' }
60
+ ),
61
+ required: %w[session_id user_message]
62
+ }
63
+ },
64
+ {
65
+ name: 'search_memory',
66
+ description: 'Full-text search the session\'s persistent memory; returns ranked evidence.',
67
+ inputSchema: {
68
+ type: 'object',
69
+ properties: SCOPE_PROPERTIES.merge(
70
+ session_id: { type: 'string' },
71
+ query: { type: 'string' },
72
+ limit: { type: 'integer' }
73
+ ),
74
+ required: %w[session_id query]
75
+ }
76
+ },
77
+ {
78
+ name: 'status',
79
+ description: 'SmartBrain diagnostics: storage backend, P95, memory/resource ratio, counts.',
80
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false }
81
+ },
82
+ {
83
+ name: 'knowledge_distill',
84
+ description: 'Distill a candidate knowledge item (tier dao_ren or qi) from supporting evidence refs.',
85
+ inputSchema: {
86
+ type: 'object',
87
+ properties: SCOPE_PROPERTIES.merge(
88
+ session_id: { type: 'string' },
89
+ statement: { type: 'string' },
90
+ content: { type: 'string' },
91
+ tier: { type: 'string', enum: %w[dao_ren qi] },
92
+ supporting_refs: { type: 'array', items: { type: 'string' } },
93
+ domain: { type: 'string' }, field: { type: 'string' }, reviewer: { type: 'string' }
94
+ ),
95
+ required: %w[session_id statement tier]
96
+ }
97
+ },
98
+ {
99
+ name: 'knowledge_gate',
100
+ description: 'Read-only check: is a candidate knowledge item ready for promotion?',
101
+ inputSchema: { type: 'object', properties: SCOPE_PROPERTIES.merge(memory_item_id: { type: 'string' }, session_id: { type: 'string' }), required: %w[memory_item_id] }
102
+ },
103
+ {
104
+ name: 'knowledge_promote',
105
+ description: 'Gate-enforced promotion of a candidate knowledge item to promoted.',
106
+ inputSchema: {
107
+ type: 'object',
108
+ properties: SCOPE_PROPERTIES.merge(
109
+ session_id: { type: 'string' },
110
+ memory_item_id: { type: 'string' },
111
+ verification_refs: { type: 'array', items: { type: 'string' } },
112
+ reason: { type: 'string' }, reviewer: { type: 'string' }, force: { type: 'boolean' }
113
+ ),
114
+ required: %w[memory_item_id verification_refs reason]
115
+ }
116
+ },
117
+ {
118
+ name: 'knowledge_demote',
119
+ description: 'Evidence-backed demotion of a knowledge item (reason_type: contradicted|obsolete|superseded).',
120
+ inputSchema: {
121
+ type: 'object',
122
+ properties: SCOPE_PROPERTIES.merge(
123
+ session_id: { type: 'string' },
124
+ memory_item_id: { type: 'string' },
125
+ evidence_refs: { type: 'array', items: { type: 'string' } },
126
+ reason: { type: 'string' },
127
+ reason_type: { type: 'string', enum: %w[contradicted obsolete superseded] }
128
+ ),
129
+ required: %w[memory_item_id evidence_refs reason reason_type]
130
+ }
131
+ },
132
+ {
133
+ name: 'knowledge_promote_to_scope',
134
+ description: 'Copy a verified memory item into a writable target scope while preserving provenance.',
135
+ inputSchema: {
136
+ type: 'object',
137
+ properties: SCOPE_PROPERTIES.merge(
138
+ session_id: { type: 'string' }, memory_item_id: { type: 'string' }, target_scope: { type: 'object' },
139
+ verification_refs: { type: 'array', items: { type: 'string' } }, reason: { type: 'string' }, reviewer: { type: 'string' }
140
+ ),
141
+ required: %w[domain_id session_id scope_context memory_item_id target_scope verification_refs reason]
142
+ }
143
+ },
144
+ {
145
+ name: 'knowledge_retract',
146
+ description: 'Retract a memory item with evidence and an audit event.',
147
+ inputSchema: {
148
+ type: 'object',
149
+ properties: SCOPE_PROPERTIES.merge(
150
+ session_id: { type: 'string' }, memory_item_id: { type: 'string' },
151
+ evidence_refs: { type: 'array', items: { type: 'string' } }, reason: { type: 'string' }, reviewer: { type: 'string' }
152
+ ),
153
+ required: %w[memory_item_id evidence_refs reason]
154
+ }
155
+ },
156
+ {
157
+ name: 'knowledge_lineage',
158
+ description: 'Return cross-scope promotion lineage for a memory item.',
159
+ inputSchema: {
160
+ type: 'object', properties: SCOPE_PROPERTIES.merge(session_id: { type: 'string' }, memory_item_id: { type: 'string' }),
161
+ required: %w[memory_item_id]
162
+ }
163
+ },
164
+ {
165
+ name: 'kg_add',
166
+ description: 'Add a knowledge-graph triple (subject-predicate-object) with temporal validity.',
167
+ inputSchema: {
168
+ type: 'object',
169
+ properties: SCOPE_PROPERTIES.merge(
170
+ session_id: { type: 'string' },
171
+ subject: { type: 'string' },
172
+ predicate: { type: 'string' },
173
+ object: { type: 'string' },
174
+ confidence: { type: 'number' },
175
+ scope_ref: { type: 'object' }
176
+ ),
177
+ required: %w[session_id subject predicate object]
178
+ }
179
+ },
180
+ {
181
+ name: 'kg_query',
182
+ description: 'Query KG triples (any of subject/predicate/object; include_invalid to see invalidated).',
183
+ inputSchema: {
184
+ type: 'object',
185
+ properties: SCOPE_PROPERTIES.merge(
186
+ session_id: { type: 'string' },
187
+ subject: { type: 'string' }, predicate: { type: 'string' }, object: { type: 'string' },
188
+ include_invalid: { type: 'boolean' }
189
+ ),
190
+ required: %w[session_id]
191
+ }
192
+ },
193
+ {
194
+ name: 'kg_invalidate',
195
+ description: 'Invalidate a KG triple (sets valid_to=now, status=invalidated).',
196
+ inputSchema: {
197
+ type: 'object',
198
+ properties: SCOPE_PROPERTIES.merge(
199
+ edge_id: { type: 'string' }, reason: { type: 'string' }, session_id: { type: 'string' }
200
+ ),
201
+ required: %w[edge_id]
202
+ }
203
+ },
204
+ {
205
+ name: 'kg_timeline',
206
+ description: 'Return the temporal history of KG edges for a subject across readable scopes.',
207
+ inputSchema: {
208
+ type: 'object', properties: SCOPE_PROPERTIES.merge(session_id: { type: 'string' }, subject: { type: 'string' }),
209
+ required: %w[session_id subject]
210
+ }
211
+ },
212
+ {
213
+ name: 'kg_stats',
214
+ description: 'Return KG active and invalidated counts across readable scopes.',
215
+ inputSchema: {
216
+ type: 'object', properties: SCOPE_PROPERTIES.merge(session_id: { type: 'string' }), required: %w[session_id]
217
+ }
218
+ },
219
+ {
220
+ name: 'fact_check',
221
+ description: 'Offline zero-LLM contradiction scan of text vs session entities + KG (SimilarName/Relation/StaleFact).',
222
+ inputSchema: {
223
+ type: 'object',
224
+ properties: SCOPE_PROPERTIES.merge(session_id: { type: 'string' }, text: { type: 'string' }),
225
+ required: %w[session_id text]
226
+ }
227
+ },
228
+ {
229
+ name: 'brief',
230
+ description: 'Deterministic citation-first cognitive snapshot (summary/key_facts/evidence/entities/unresolved/uncertainty/next_actions).',
231
+ inputSchema: {
232
+ type: 'object',
233
+ properties: SCOPE_PROPERTIES.merge(session_id: { type: 'string' }, query: { type: 'string' }),
234
+ required: %w[session_id]
235
+ }
236
+ },
237
+ {
238
+ name: 'wake_up',
239
+ description: 'Minimal L0/L1 importance-ordered resume payload for a session.',
240
+ inputSchema: { type: 'object', properties: SCOPE_PROPERTIES.merge(session_id: { type: 'string' }), required: %w[session_id] }
241
+ }
242
+ ].freeze
243
+
244
+ attr_reader :service
245
+
246
+ def initialize(service:, in_io: $stdin, out_io: $stdout, err_io: $stderr)
247
+ @service = service
248
+ @in = in_io
249
+ @out = out_io
250
+ @err = err_io
251
+ end
252
+
253
+ def run
254
+ @out.sync = true
255
+ @in.each_line do |raw|
256
+ line = raw.to_s.strip
257
+ next if line.empty?
258
+
259
+ request = JSON.parse(line)
260
+ handle(request)
261
+ rescue JSON::ParserError => e
262
+ log("ignoring malformed line: #{e.message}")
263
+ rescue StandardError => e
264
+ log("handler error: #{e.class}: #{e.message}")
265
+ end
266
+ end
267
+
268
+ private
269
+
270
+ def handle(req)
271
+ method_name = req['method']
272
+ id = req['id']
273
+
274
+ case method_name
275
+ when 'initialize'
276
+ respond(id, {
277
+ protocolVersion: PROTOCOL_VERSION,
278
+ capabilities: { tools: {} },
279
+ serverInfo: { name: 'smart_brain', version: SmartBrain::VERSION },
280
+ instructions: INSTRUCTIONS
281
+ })
282
+ when 'notifications/initialized'
283
+ # client-ready notification; no response expected
284
+ nil
285
+ when 'tools/list'
286
+ respond(id, { tools: TOOLS })
287
+ when 'tools/call'
288
+ respond(id, call_tool(req.fetch('params', {})))
289
+ else
290
+ respond_error(id, -32601, "method not found: #{method_name}")
291
+ end
292
+ end
293
+
294
+ def call_tool(params)
295
+ name = params['name']
296
+ # JSON arguments arrive with string keys; SmartBrain internals expect
297
+ # symbols (e.g. extractor reads events[:decisions]). Deep-symbolize.
298
+ args = deep_symbolize(params['arguments'] || {})
299
+
300
+ result =
301
+ case name
302
+ when 'commit_turn'
303
+ service.commit(**args.slice(:domain_id, :session_id, :scope_context, :turn_events).merge(turn_events: args[:turn_events] || {}))
304
+ when 'compose_context'
305
+ service.compose(**args.slice(:domain_id, :session_id, :scope_context, :user_message, :agent_state).merge(agent_state: args[:agent_state] || {}))
306
+ when 'search_memory'
307
+ service.search(**args.slice(:domain_id, :session_id, :scope_context, :query, :limit))
308
+ when 'status'
309
+ service.status
310
+ when 'knowledge_distill'
311
+ service.distill(**args.slice(:domain_id, :session_id, :scope_context, :statement, :content, :tier, :supporting_refs, :domain, :field, :reviewer, :key))
312
+ when 'knowledge_gate'
313
+ service.gate(**args.slice(:memory_item_id, :domain_id, :session_id, :scope_context))
314
+ when 'knowledge_promote'
315
+ service.promote(**args.slice(:memory_item_id, :domain_id, :session_id, :scope_context, :verification_refs, :reason, :reviewer, :force))
316
+ when 'knowledge_demote'
317
+ service.demote(**args.slice(:memory_item_id, :domain_id, :session_id, :scope_context, :evidence_refs, :reason, :reason_type, :reviewer))
318
+ when 'knowledge_promote_to_scope'
319
+ service.promote_to_scope(**args.slice(:memory_item_id, :target_scope, :domain_id, :session_id, :scope_context, :verification_refs, :reason, :reviewer))
320
+ when 'knowledge_retract'
321
+ service.retract(**args.slice(:memory_item_id, :domain_id, :session_id, :scope_context, :evidence_refs, :reason, :reviewer))
322
+ when 'knowledge_lineage'
323
+ service.promotion_lineage(**args.slice(:memory_item_id, :domain_id, :session_id, :scope_context))
324
+ when 'kg_add'
325
+ service.kg_add(**args.slice(:domain_id, :session_id, :scope_context, :scope_ref, :subject, :predicate, :object, :confidence, :source_turn_id))
326
+ when 'kg_query'
327
+ service.kg_query(**args.slice(:domain_id, :session_id, :scope_context, :subject, :predicate, :object, :include_invalid))
328
+ when 'kg_invalidate'
329
+ service.kg_invalidate(**args.slice(:edge_id, :reason, :domain_id, :session_id, :scope_context))
330
+ when 'kg_timeline'
331
+ service.kg_timeline(**args.slice(:domain_id, :session_id, :scope_context, :subject))
332
+ when 'kg_stats'
333
+ service.kg_stats(**args.slice(:domain_id, :session_id, :scope_context))
334
+ when 'fact_check'
335
+ service.fact_check(**args.slice(:domain_id, :session_id, :scope_context, :text))
336
+ when 'brief'
337
+ service.brief(**args.slice(:domain_id, :session_id, :scope_context, :query))
338
+ when 'wake_up'
339
+ service.wake_up(**args.slice(:domain_id, :session_id, :scope_context))
340
+ else
341
+ return error_result("unknown tool: #{name}")
342
+ end
343
+
344
+ { content: [{ type: 'text', text: JSON.generate(result) }] }
345
+ rescue StandardError => e
346
+ error_result("#{e.class}: #{e.message}")
347
+ end
348
+
349
+ def deep_symbolize(obj)
350
+ case obj
351
+ when Hash
352
+ obj.each_with_object({}) { |(k, v), h| h[k.to_sym] = deep_symbolize(v) }
353
+ when Array
354
+ obj.map { |v| deep_symbolize(v) }
355
+ else
356
+ obj
357
+ end
358
+ end
359
+
360
+ def respond(id, result)
361
+ return if id.nil? # notification
362
+
363
+ write(jsonrpc: '2.0', id: id, result: result)
364
+ end
365
+
366
+ def respond_error(id, code, message)
367
+ return if id.nil?
368
+
369
+ write(jsonrpc: '2.0', id: id, error: { code: code, message: message })
370
+ end
371
+
372
+ def error_result(message)
373
+ { isError: true, content: [{ type: 'text', text: message }] }
374
+ end
375
+
376
+ def write(payload)
377
+ @out.puts(JSON.generate(payload))
378
+ end
379
+
380
+ def log(message)
381
+ @err&.puts("[smart_brain mcp] #{message}")
382
+ end
383
+ end
384
+ end
385
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../smart_brain'
4
+
5
+ module SmartBrain
6
+ module Server
7
+ # Shared facade over the SmartBrain runtime. The HTTP app, MCP server, and
8
+ # CLI all go through a Service instance so behavior is identical across
9
+ # every front-end and only the transport differs.
10
+ class Service
11
+ class << self
12
+ # Configure the singleton runtime and return a Service bound to it.
13
+ def build(config_path: nil, smart_rag_client: nil)
14
+ SmartBrain.configure(config_path: config_path, smart_rag_client: smart_rag_client)
15
+ new
16
+ end
17
+ end
18
+
19
+ def initialize
20
+ raise 'SmartBrain runtime not configured; call Service.build first' unless SmartBrain.instance_variable_get(:@runtime)
21
+ end
22
+
23
+ def commit(session_id:, turn_events:, domain_id: nil, scope_context: nil)
24
+ SmartBrain.commit_turn(session_id: session_id, turn_events: turn_events, domain_id: domain_id, scope_context: scope_context)
25
+ end
26
+
27
+ def compose(session_id:, user_message:, agent_state: {}, domain_id: nil, scope_context: nil)
28
+ SmartBrain.compose_context(
29
+ session_id: session_id, user_message: user_message, agent_state: agent_state,
30
+ domain_id: domain_id, scope_context: scope_context
31
+ )
32
+ end
33
+
34
+ def search(session_id:, query:, limit: nil, domain_id: nil, scope_context: nil)
35
+ SmartBrain.search_memory(
36
+ session_id: session_id, query: query, limit: limit, domain_id: domain_id, scope_context: scope_context
37
+ )
38
+ end
39
+
40
+ def status
41
+ SmartBrain.diagnostics
42
+ end
43
+
44
+ def migrate
45
+ SmartBrain.migrate
46
+ rescue StandardError => e
47
+ { ok: false, error: "#{e.class}: #{e.message}" }
48
+ end
49
+
50
+ def backend
51
+ SmartBrain.storage_backend
52
+ end
53
+
54
+ # --- knowledge lifecycle (Stage-1) ---
55
+ def distill(session_id:, statement:, content: nil, tier:, supporting_refs: [], **opts)
56
+ SmartBrain.distill(session_id: session_id, statement: statement, content: content,
57
+ tier: tier, supporting_refs: supporting_refs, **opts)
58
+ end
59
+
60
+ def gate(memory_item_id:, **opts)
61
+ SmartBrain.gate(memory_item_id: memory_item_id, **opts)
62
+ end
63
+
64
+ def promote(memory_item_id:, verification_refs:, reason:, **opts)
65
+ SmartBrain.promote(memory_item_id: memory_item_id, verification_refs: verification_refs,
66
+ reason: reason, **opts)
67
+ end
68
+
69
+ def demote(memory_item_id:, evidence_refs:, reason:, reason_type:, **opts)
70
+ SmartBrain.demote(memory_item_id: memory_item_id, evidence_refs: evidence_refs,
71
+ reason: reason, reason_type: reason_type, **opts)
72
+ end
73
+
74
+ def retract(memory_item_id:, evidence_refs:, reason:, **opts)
75
+ SmartBrain.retract(memory_item_id: memory_item_id, evidence_refs: evidence_refs, reason: reason, **opts)
76
+ end
77
+
78
+ def promote_to_scope(**args)
79
+ SmartBrain.promote_to_scope(**args)
80
+ end
81
+
82
+ def promotion_lineage(memory_item_id:, **opts)
83
+ SmartBrain.promotion_lineage(memory_item_id: memory_item_id, **opts)
84
+ end
85
+
86
+ def knowledge_events(memory_item_id:, **opts)
87
+ SmartBrain.knowledge_events(memory_item_id: memory_item_id, **opts)
88
+ end
89
+
90
+ # --- knowledge graph ---
91
+ def kg_add(session_id:, subject:, predicate:, object:, **opts)
92
+ SmartBrain.kg_add(session_id: session_id, subject: subject, predicate: predicate, object: object, **opts)
93
+ end
94
+
95
+ def kg_query(session_id:, **opts)
96
+ SmartBrain.kg_query(session_id: session_id, **opts)
97
+ end
98
+
99
+ def kg_timeline(session_id:, subject:, **opts)
100
+ SmartBrain.kg_timeline(session_id: session_id, subject: subject, **opts)
101
+ end
102
+
103
+ def kg_invalidate(edge_id:, reason: nil, **opts)
104
+ SmartBrain.kg_invalidate(edge_id: edge_id, reason: reason, **opts)
105
+ end
106
+
107
+ def kg_stats(session_id:, **opts)
108
+ SmartBrain.kg_stats(session_id: session_id, **opts)
109
+ end
110
+
111
+ # --- fact-check / briefing ---
112
+ def fact_check(session_id:, text:, **opts)
113
+ SmartBrain.fact_check(session_id: session_id, text: text, **opts)
114
+ end
115
+
116
+ def brief(session_id:, query: nil, **opts)
117
+ SmartBrain.brief(session_id: session_id, query: query, **opts)
118
+ end
119
+
120
+ def wake_up(session_id:, **opts)
121
+ SmartBrain.wake_up(session_id: session_id, **opts)
122
+ end
123
+
124
+ def resume(query:)
125
+ SmartBrain.resume(query: query)
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmartBrain
4
+ module Support
5
+ # Pure-Ruby Levenshtein edit distance (no native dep). Used by FactCheck
6
+ # for SimilarNameConflict detection.
7
+ module Levenshtein
8
+ module_function
9
+
10
+ def distance(a, b)
11
+ a = a.to_s
12
+ b = b.to_s
13
+ return b.length if a.empty?
14
+ return a.length if b.empty?
15
+
16
+ # Iterative two-row DP.
17
+ previous = (0..b.length).to_a
18
+ current = Array.new(b.length + 1, 0)
19
+ (1..a.length).each do |i|
20
+ current[0] = i
21
+ (1..b.length).each do |j|
22
+ cost = a[i - 1] == b[j - 1] ? 0 : 1
23
+ current[j] = [
24
+ previous[j] + 1, # deletion
25
+ current[j - 1] + 1, # insertion
26
+ previous[j - 1] + cost # substitution
27
+ ].min
28
+ end
29
+ previous, current = current, previous
30
+ end
31
+ previous[b.length]
32
+ end
33
+ end
34
+ end
35
+ end