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,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
require 'json'
|
|
5
|
+
require_relative 'base'
|
|
6
|
+
|
|
7
|
+
module SmartBrain
|
|
8
|
+
module ModelProvider
|
|
9
|
+
# 通用 OpenAI 兼容 provider(覆盖 silicon_flow 等任意 /v1/chat/completions 端点)。
|
|
10
|
+
class OpenAI < Base
|
|
11
|
+
include Rerankable
|
|
12
|
+
|
|
13
|
+
def initialize(model:, base_url:, api_key:, rerank_model: nil, temperature: 0.2, timeout_seconds: 30, **_)
|
|
14
|
+
@model = model
|
|
15
|
+
@rerank_model = rerank_model || model
|
|
16
|
+
@temperature = temperature
|
|
17
|
+
@timeout_seconds = timeout_seconds
|
|
18
|
+
@api_key = api_key
|
|
19
|
+
@base_url = base_url.to_s.chomp('/')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def llm?
|
|
23
|
+
true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def enabled?
|
|
27
|
+
!@api_key.to_s.empty?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def complete(prompt:, system: nil, temperature: nil, max_tokens: nil)
|
|
31
|
+
messages = []
|
|
32
|
+
messages << { 'role' => 'system', 'content' => system } if system && !system.empty?
|
|
33
|
+
messages << { 'role' => 'user', 'content' => prompt }
|
|
34
|
+
|
|
35
|
+
body = { 'model' => @model, 'messages' => messages, 'temperature' => temperature || @temperature }
|
|
36
|
+
body['max_tokens'] = max_tokens if max_tokens
|
|
37
|
+
|
|
38
|
+
response = connection.post('/v1/chat/completions') do |req|
|
|
39
|
+
req.headers['Authorization'] = "Bearer #{@api_key}" unless @api_key.to_s.empty?
|
|
40
|
+
req.body = JSON.generate(body)
|
|
41
|
+
end
|
|
42
|
+
raise "openai HTTP #{response.status}: #{response.body[0, 200]}" unless response.status.between?(200, 299)
|
|
43
|
+
|
|
44
|
+
{ text: strip_think(JSON.parse(response.body).dig('choices', 0, 'message', 'content').to_s) }
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
{ text: '', error: "#{e.class}: #{e.message}" }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def connection
|
|
52
|
+
@connection ||= Faraday.new(url: @base_url) do |f|
|
|
53
|
+
f.options.timeout = @timeout_seconds
|
|
54
|
+
f.options.open_timeout = @timeout_seconds
|
|
55
|
+
f.headers['Content-Type'] = 'application/json'
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module SmartBrain
|
|
6
|
+
module ModelProvider
|
|
7
|
+
# 确定性、零网络的默认 provider。不是真实 LLM:summary 走模板、merger 走词法。
|
|
8
|
+
class Stub < Base
|
|
9
|
+
def llm?
|
|
10
|
+
false
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def enabled?
|
|
14
|
+
true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def complete(prompt:, system: nil, temperature: nil, max_tokens: nil)
|
|
18
|
+
{ text: '' }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def rerank(query:, documents:)
|
|
22
|
+
documents.each_with_index.map { |_, i| { index: i, score: 1.0 - (i * 0.001) } }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -16,22 +16,60 @@ module SmartBrain
|
|
|
16
16
|
commit_logs << payload
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
def log_scope_promotion(payload)
|
|
20
|
+
scope_promotion_logs << payload
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def log_scope_validation_failure(error)
|
|
24
|
+
scope_validation_failures << { error: error.message, created_at: Time.now.utc.iso8601 }
|
|
25
|
+
end
|
|
26
|
+
|
|
19
27
|
def snapshot
|
|
20
28
|
{
|
|
21
29
|
compose_logs: compose_logs,
|
|
22
30
|
commit_logs: commit_logs,
|
|
31
|
+
scope_promotion_logs: scope_promotion_logs,
|
|
32
|
+
scope_validation_failures: scope_validation_failures,
|
|
23
33
|
metrics: {
|
|
24
34
|
compose_p95_ms: p95(compose_logs.map { |l| l[:took_ms] }),
|
|
25
35
|
memory_resource_ratio: memory_resource_ratio,
|
|
26
|
-
token_over_budget_rate: token_over_budget_rate
|
|
36
|
+
token_over_budget_rate: token_over_budget_rate,
|
|
37
|
+
scope_writes: scope_writes,
|
|
38
|
+
scope_recalls: scope_recalls,
|
|
39
|
+
shadowed_count: compose_logs.sum { |log| log[:shadowed_count].to_i },
|
|
40
|
+
scope_promotions: scope_promotion_logs.size,
|
|
41
|
+
scope_validation_failures: scope_validation_failures.size,
|
|
42
|
+
scope_filter_failures: compose_logs.sum { |log| Array(log[:scope_filter_warnings]).size }
|
|
27
43
|
}
|
|
28
44
|
}
|
|
29
45
|
end
|
|
30
46
|
|
|
31
47
|
private
|
|
32
48
|
|
|
49
|
+
def scope_promotion_logs
|
|
50
|
+
@scope_promotion_logs ||= []
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def scope_validation_failures
|
|
54
|
+
@scope_validation_failures ||= []
|
|
55
|
+
end
|
|
56
|
+
|
|
33
57
|
attr_reader :compose_logs, :commit_logs
|
|
34
58
|
|
|
59
|
+
def scope_writes
|
|
60
|
+
commit_logs.flat_map { |log| Array(log[:memory_items]) }.each_with_object(Hash.new(0)) do |item, counts|
|
|
61
|
+
ref = item[:scope]
|
|
62
|
+
counts[ref ? ref[:type] : 'unknown'] += 1
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def scope_recalls
|
|
67
|
+
compose_logs.flat_map { |log| Array(log[:selected_evidence]) }.each_with_object(Hash.new(0)) do |item, counts|
|
|
68
|
+
ref = item[:scope]
|
|
69
|
+
counts[ref ? ref[:type] : 'unscoped'] += 1
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
35
73
|
def p95(values)
|
|
36
74
|
values = values.compact.sort
|
|
37
75
|
return 0 if values.empty?
|
|
@@ -19,6 +19,12 @@ module SmartBrain
|
|
|
19
19
|
snippet: item[:value_json].to_s,
|
|
20
20
|
mode: 'exact',
|
|
21
21
|
score: score + (item[:confidence] || 0.5),
|
|
22
|
+
tier: item[:tier] || 'evidence',
|
|
23
|
+
memory_type: item[:type],
|
|
24
|
+
memory_key: item[:key],
|
|
25
|
+
scope: item[:scope],
|
|
26
|
+
scope_id: item[:scope_id],
|
|
27
|
+
source_session_id: item[:source_session_id] || item[:session_id],
|
|
22
28
|
ref: { memory_item_id: item[:id] }
|
|
23
29
|
}
|
|
24
30
|
end
|
|
@@ -12,19 +12,73 @@ module SmartBrain
|
|
|
12
12
|
@relational = RelationalRetriever.new(config: config)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
# Memory evidence is resolved by the store itself:
|
|
16
|
+
# - MemoryStore::Postgres#search_memory → ts_rank-ranked FTS
|
|
17
|
+
# - MemoryStore::InMemory#search_memory → ExactRetriever over active items
|
|
18
|
+
# Turn matching stays in-memory over the recent window (small), and
|
|
19
|
+
# relational matching (entities/refs) stays in-memory as before.
|
|
20
|
+
def retrieve(query:, memory_store:, session_id:, recent_turns:, entities:, refs:, scope_ids: nil, limit: nil)
|
|
21
|
+
limit ||= config.composition.fetch(:evidence_max_items, 12)
|
|
22
|
+
ids = Array(scope_ids).compact
|
|
23
|
+
memory_hits = if ids.empty?
|
|
24
|
+
memory_store.search_memory(query: query, session_id: session_id, limit: limit)
|
|
25
|
+
else
|
|
26
|
+
ids.flat_map do |scope_id|
|
|
27
|
+
memory_store.search_memory(query: query, scope_ids: [scope_id], limit: limit)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
turn_hits = exact.retrieve(query: query, memory_items: [], recent_turns: recent_turns, limit: limit)
|
|
18
31
|
relational_hits = relational.retrieve(query: query, entities: entities, refs: refs, limit: limit)
|
|
32
|
+
graph_hits = graph_evidence(
|
|
33
|
+
query: query, entities: entities, memory_store: memory_store, session_id: session_id, scope_ids: scope_ids
|
|
34
|
+
)
|
|
19
35
|
|
|
20
|
-
(
|
|
36
|
+
(memory_hits + turn_hits + relational_hits + graph_hits)
|
|
21
37
|
.sort_by { |h| -h.fetch(:score, 0.0) }
|
|
22
|
-
.first(limit)
|
|
23
38
|
end
|
|
24
39
|
|
|
25
40
|
private
|
|
26
41
|
|
|
27
42
|
attr_reader :config, :exact, :relational
|
|
43
|
+
|
|
44
|
+
# When an entity is mentioned in the query, pull its KG edges and surface
|
|
45
|
+
# the related objects as relational evidence (mode: 'graph'). Gated by
|
|
46
|
+
# config.kg.expand_retrieval (default true); no-op if the store has no
|
|
47
|
+
# kg_edges support.
|
|
48
|
+
def graph_evidence(query:, entities:, memory_store:, session_id:, scope_ids: nil)
|
|
49
|
+
return [] unless expand_retrieval?(memory_store)
|
|
50
|
+
|
|
51
|
+
terms = query.to_s.downcase
|
|
52
|
+
matched = entities.select { |e| terms.include?(e[:canonical].to_s.downcase) || terms.include?(e[:name].to_s.downcase) }
|
|
53
|
+
return [] if matched.empty?
|
|
54
|
+
|
|
55
|
+
matched.flat_map do |entity|
|
|
56
|
+
edges = memory_store.query_edges(session_id: session_id, scope_ids: scope_ids, subject: entity[:canonical])
|
|
57
|
+
edges.map do |edge|
|
|
58
|
+
{
|
|
59
|
+
id: "edge-#{edge[:id]}",
|
|
60
|
+
source: 'memory',
|
|
61
|
+
source_uri: "smartbrain://kg/#{edge[:id]}",
|
|
62
|
+
title: "#{edge[:subject]} #{edge[:predicate]} #{edge[:object]}",
|
|
63
|
+
snippet: "#{edge[:subject]} #{edge[:predicate]} #{edge[:object]}",
|
|
64
|
+
mode: 'graph',
|
|
65
|
+
score: (edge[:confidence] || 0.5) * 0.6,
|
|
66
|
+
tier: 'evidence',
|
|
67
|
+
scope: edge[:scope],
|
|
68
|
+
scope_id: edge[:scope_id],
|
|
69
|
+
source_session_id: edge[:source_session_id],
|
|
70
|
+
ref: { edge_id: edge[:id], subject_entity_id: edge[:subject_entity_id], object_entity_id: edge[:object_entity_id] }
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
rescue StandardError
|
|
75
|
+
[]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def expand_retrieval?(memory_store)
|
|
79
|
+
memory_store.respond_to?(:query_edges) &&
|
|
80
|
+
(config.kg.fetch(:expand_retrieval, true) rescue true)
|
|
81
|
+
end
|
|
28
82
|
end
|
|
29
83
|
end
|
|
30
84
|
end
|