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,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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'model_provider/base'
4
+ require_relative 'model_provider/stub'
5
+ require_relative 'model_provider/ollama'
6
+ require_relative 'model_provider/openai'
7
+ require_relative 'model_provider/factory'
@@ -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
- def retrieve(query:, memory_items:, recent_turns:, entities:, refs:)
16
- limit = config.composition.fetch(:evidence_max_items, 12)
17
- exact_hits = exact.retrieve(query: query, memory_items: memory_items, recent_turns: recent_turns, limit: limit)
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
- (exact_hits + relational_hits)
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