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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/MEMPAL_GUIDE.md +1074 -0
- data/README.en.md +173 -173
- data/README.md +467 -173
- data/config/brain.yml +69 -1
- data/conversation_demo.rb +438 -438
- data/db/migrate/002_turn_events_payload.sql +9 -0
- data/db/migrate/003_tiers_and_lifecycle.sql +28 -0
- data/db/migrate/004_kg_edges.sql +30 -0
- data/db/migrate/005_domains_and_memory_scopes.sql +163 -0
- data/docs/coding_todo.md +139 -0
- data/docs/context_package.md +220 -0
- data/docs/evidence_pack.md +190 -0
- data/docs/gap_vs_mempal.md +161 -0
- data/docs/mcp.md +93 -0
- data/docs/memory_types.md +278 -0
- data/docs/multi_scope_memory_refactor_plan.md +483 -0
- data/docs/multi_scope_migration.md +65 -0
- data/docs/policies.md +308 -0
- data/docs/retrieval_plan.md +231 -0
- data/docs/smartbrain_design.md +299 -0
- data/docs/user_guide.md +546 -0
- data/example.rb +91 -91
- data/examples/01_memory_basic.rb +57 -0
- data/examples/02_governance.rb +63 -0
- data/examples/03_postgres_persistence.rb +63 -0
- data/examples/04_ollama_llm.rb +69 -0
- data/examples/05_smart_rag_integration.rb +79 -0
- data/examples/06_multi_scope_memory.rb +50 -0
- data/examples/README.md +49 -0
- data/exe/smart_brain +168 -0
- data/lib/smart_brain/adapters/smart_rag/direct_client.rb +16 -5
- data/lib/smart_brain/adapters/smart_rag/http_client.rb +16 -5
- data/lib/smart_brain/adapters/smart_rag/null_client.rb +7 -2
- data/lib/smart_brain/adapters/smart_rag/scope_filter.rb +60 -0
- data/lib/smart_brain/configuration.rb +57 -0
- data/lib/smart_brain/consolidator/working_summary.rb +80 -12
- data/lib/smart_brain/context_composer/composer.rb +40 -3
- data/lib/smart_brain/contracts/retrieval_plan.rb +10 -0
- data/lib/smart_brain/contracts/scope_context.rb +46 -0
- data/lib/smart_brain/contracts/scope_ref.rb +25 -0
- data/lib/smart_brain/db.rb +109 -0
- data/lib/smart_brain/event_store/in_memory.rb +6 -2
- data/lib/smart_brain/event_store/postgres.rb +199 -0
- data/lib/smart_brain/fusion/merger.rb +31 -2
- data/lib/smart_brain/governance/briefing.rb +146 -0
- data/lib/smart_brain/governance/fact_check.rb +110 -0
- data/lib/smart_brain/governance/knowledge_graph.rb +60 -0
- data/lib/smart_brain/governance/lifecycle.rb +225 -0
- data/lib/smart_brain/governance/tiers.rb +60 -0
- data/lib/smart_brain/memory_extractor/extractor.rb +25 -7
- data/lib/smart_brain/memory_store/in_memory.rb +202 -17
- data/lib/smart_brain/memory_store/postgres.rb +500 -0
- data/lib/smart_brain/model_provider/base.rb +87 -0
- data/lib/smart_brain/model_provider/factory.rb +49 -0
- data/lib/smart_brain/model_provider/ollama.rb +60 -0
- data/lib/smart_brain/model_provider/openai.rb +60 -0
- data/lib/smart_brain/model_provider/stub.rb +26 -0
- data/lib/smart_brain/model_provider.rb +7 -0
- data/lib/smart_brain/observability/tracker.rb +39 -1
- data/lib/smart_brain/retrievers/exact_retriever.rb +6 -0
- data/lib/smart_brain/retrievers/memory_retriever.rb +59 -5
- data/lib/smart_brain/runtime.rb +288 -16
- data/lib/smart_brain/scopes/conflict_resolver.rb +67 -0
- data/lib/smart_brain/scopes/registry.rb +133 -0
- data/lib/smart_brain/scopes/resolver.rb +32 -0
- data/lib/smart_brain/server/http_app.rb +143 -0
- data/lib/smart_brain/server/mcp_server.rb +385 -0
- data/lib/smart_brain/server/service.rb +129 -0
- data/lib/smart_brain/support/levenshtein.rb +35 -0
- data/lib/smart_brain/version.rb +5 -5
- data/lib/smart_brain.rb +80 -35
- 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
|